1. 项目概述与核心价值在嵌入式GUI开发的世界里列表控件就像是我们日常生活中的菜单或者目录是用户与设备进行信息交互最直接、最频繁的窗口之一。无论是工业触摸屏上的参数设置列表还是智能家居中控面板上的设备选择菜单亦或是医疗仪器上的功能选项其背后往往都离不开一个高效、稳定的列表控件在支撑。今天我们就来深入聊聊emWin图形库中两个至关重要的列表控件LISTBOX列表框和LISTVIEW列表视图。很多刚接触emWin的朋友可能会觉得官方手册里的API列表有些冰冷和抽象不知道从何下手或者仅仅停留在“知道有这个函数”的层面。实际上真正用好这些控件关键在于理解其设计哲学、掌握关键API的“组合拳”并能在实际项目中灵活应对各种边界情况。本文将基于官方手册结合我多年在STM32、NXP等MCU平台上使用emWin的经验为你拆解这两个控件的核心API并通过具体的应用实践让你不仅能“会用”更能“用好”。2. LISTBOX控件从基础创建到高级定制LISTBOX即列表框是emWin中最基础的列表控件用于呈现一个垂直的单列项目列表用户通常从中选择一项或多项。它的核心价值在于其简洁性和高效性非常适合选项不多、交互逻辑清晰的场景。2.1 核心创建函数与初始化策略创建LISTBOX有多种方式选择哪种取决于你的窗口管理策略。LISTBOX_CreateEx: 这是目前最推荐、功能最全的创建函数。它提供了最大的灵活性允许你指定父窗口、窗口标志、扩展标志和控件ID。LISTBOX_Handle LISTBOX_CreateEx(int x0, int y0, int xsize, int ysize, WM_HWIN hParent, int WinFlags, int ExFlags, int Id, const GUI_ConstString * ppText);参数精讲:x0, y0, xsize, ysize: 控件的位置和尺寸。这里有一个非常重要的细节如果你将ysize设置为0控件会自动调整其高度以恰好容纳所有列表项这对于实现“自适应高度”的列表非常有用。同样xsize如果设置得比实际内容所需宽度大也会被自动缩减。hParent: 父窗口句柄。设置为0则创建为桌面窗口的子窗口顶级窗口。WinFlags: 窗口创建标志最常用的是WM_CF_SHOW用于创建后立即显示控件。ppText: 这是一个指向GUI_ConstString通常是const char*数组的指针数组的每个元素是一个字符串指针代表了列表的初始项。数组必须以NULL指针结尾这是emWin识别列表结束的约定。一个完整的创建示例:static const GUI_ConstString _aListContent[] { “项目一”, “项目二”, “项目三”, “项目四”, NULL // 必须以此结束 }; hListbox LISTBOX_CreateEx(10, 50, 200, 150, hParent, WM_CF_SHOW, 0, GUI_ID_LISTBOX0, _aListContent);实操心得在资源紧张的嵌入式系统中将列表内容字符串定义为static const并存储在Flash中而非RAM可以节省宝贵的RAM空间。GUI_ConstString正是为此设计。LISTBOX_CreateAsChild和LISTBOX_Create是更早期的API功能相对较少在新项目中建议统一使用LISTBOX_CreateEx。2.2 动态内容管理与状态控制创建静态列表只是第一步动态增删改查才是灵魂。添加与插入项:LISTBOX_AddString(hObj, “新项目”): 在列表末尾追加一项。LISTBOX_InsertString(hObj, “插入项”, 2): 在指定索引从0开始处插入一项。例如索引2则新项会成为第三项。删除与修改项:LISTBOX_DeleteItem(hObj, 1): 删除索引为1的项。LISTBOX_SetString(hObj, “修改后的文本”, 0): 修改索引0项的显示文本。获取与设置选择状态:int sel LISTBOX_GetSel(hObj): 获取当前选中项的索引。单选模式下返回选中索引多选模式下返回具有焦点的项索引。无选中时返回-1。LISTBOX_SetSel(hObj, 3): 设置索引3的项为选中状态滚动到可见区域并高亮。LISTBOX_SetMulti(hObj, 1):启用多选模式。在此模式下结合LISTBOX_SetItemSel和LISTBOX_GetItemSel可以独立控制每一项的选中状态。这在需要批量操作的场景如文件管理器中选择多个文件中非常有用。禁用项:LISTBOX_SetItemDisabled(hObj, 2, 1): 将索引2的项禁用。被禁用的项会显示为灰色取决于主题并且用户无法通过键盘或触摸选中它在滚动时也会被自动跳过。这个功能常用于根据当前上下文动态禁用某些选项提升交互逻辑的严谨性。2.3 视觉样式深度定制默认的黑白灰样式显然无法满足产品化的UI需求。emWin提供了丰富的API进行视觉定制。颜色设置: LISTBOX有三类颜色状态通过Index参数区分LISTBOX_CI_UNSEL: 未选中项的背景/文字色。LISTBOX_CI_SEL: 已选中但控件无焦点时的背景/文字色。LISTBOX_CI_SELFOCUS: 已选中且控件有焦点时的背景/文字色通常最醒目。LISTBOX_CI_DISABLED: 禁用项的颜色仅背景色有效。// 设置选中且有焦点时的背景为蓝色文字为白色 LISTBOX_SetBkColor(hListbox, LISTBOX_CI_SELFOCUS, GUI_BLUE); LISTBOX_SetTextColor(hListbox, LISTBOX_CI_SELFOCUS, GUI_WHITE); // 设置全局默认颜色影响之后创建的所有LISTBOX LISTBOX_SetDefaultBkColor(LISTBOX_CI_UNSEL, GUI_LIGHTGRAY); LISTBOX_SetDefaultTextColor(LISTBOX_CI_UNSEL, GUI_DARKGRAY);字体与对齐:LISTBOX_SetFont(hObj, GUI_Font16_1): 切换字体。字体大小直接影响每行的高度。LISTBOX_SetTextAlign(hObj, GUI_TA_LEFT | GUI_TA_VCENTER): 设置文本对齐方式。GUI_TA_VCENTER垂直居中通常能获得更好的视觉效果但需注意其生效的前提是行高足够。默认行高仅等于字高垂直居中看起来没变化。这就需要用到下一个API。行高与间距:LISTBOX_SetItemSpacing(hObj, 5): 在每个列表项下方增加5个像素的额外间距。这是实现美观排版的关键增加了间距后行高 字体高度 额外间距此时再设置垂直居中对齐文本就会在变高的行内完美居中。滚动条:LISTBOX_SetAutoScrollV(hObj, 1): 启用垂直滚动条。当列表项总高度超过控件可视区域时自动出现滚动条。LISTBOX_SetAutoScrollH(hObj, 1): 启用水平滚动条。当某项文本宽度超过控件宽度时自动出现。LISTBOX_SetScrollbarWidth(hObj, 15): 自定义滚动条的宽度以适应不同的UI风格。2.4 高级功能所有者绘制Owner Draw当默认的文本项无法满足需求时比如你想在列表项前加个图标或者让某一项用特殊颜色和背景渲染LISTBOX_SetOwnerDraw就是你的终极武器。所有者绘制原理你提供一个自定义的绘制函数pfDrawItememWin在需要绘制每一项、计算某项尺寸时都会回调这个函数。你在这个函数里拥有完全的绘制控制权。关键步骤编写所有者绘制函数。使用LISTBOX_SetOwnerDraw注册该函数。在绘制函数中通常需要调用LISTBOX_OwnerDraw来处理你不想自定义的部分比如获取默认的文本尺寸或绘制文本。static int _MyDrawItem(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) { switch (pDrawItemInfo-Cmd) { case WIDGET_ITEM_GET_XSIZE: // 告诉控件我的项宽度是默认文本宽度加上一个图标的宽度比如30像素 return LISTBOX_OwnerDraw(pDrawItemInfo) 30; case WIDGET_ITEM_GET_YSIZE: // 告诉控件我的项高度是默认行高 return LISTBOX_OwnerDraw(pDrawItemInfo); case WIDGET_ITEM_DRAW: // 实际绘制 int x pDrawItemInfo-x0; int y pDrawItemInfo-y0; // 1. 绘制自定义背景例如根据数据状态改变颜色 GUI_SetColor(GUI_LIGHTBLUE); GUI_FillRect(x, y, x pDrawItemInfo-xSize-1, y pDrawItemInfo-ySize-1); // 2. 绘制一个图标假设图标宽高24x24 GUI_DrawBitmap(_bmIcon, x3, y (pDrawItemInfo-ySize-24)/2); // 3. 绘制文本向右偏移图标宽度间距 GUI_SetColor(GUI_BLACK); GUI_SetFont(pDrawItemInfo-pFont); // 注意pDrawItemInfo-pText 指向当前项的字符串 GUI_DispStringAt(pDrawItemInfo-pText, x30, y (pDrawItemInfo-ySize - GUI_GetFontSizeY(pDrawItemInfo-pFont))/2); return 0; // 已处理返回0 } // 对于未处理的命令交给默认函数处理 return LISTBOX_OwnerDraw(pDrawItemInfo); } // 在初始化时注册 LISTBOX_SetOwnerDraw(hListbox, _MyDrawItem);注意事项使用所有者绘制后通过LISTBOX_SetBkColor等API设置的颜色可能失效因为绘制逻辑完全由你的函数接管。你需要在自己的绘制函数中实现所有的颜色和样式逻辑。同时性能上会比默认绘制稍差因为每个项的绘制都涉及一次函数调用和你的自定义代码执行。3. LISTVIEW控件构建多列数据表格LISTVIEW可以看作是LISTBOX的增强版它引入了“列”的概念非常适合展示结构化的数据比如文件列表文件名、大小、修改日期、传感器数据记录时间、数值、状态等。3.1 核心结构与创建要点LISTVIEW内部包含一个HEADER表头控件来管理列。因此其创建和配置分为两部分创建LISTVIEW本身以及配置其HEADER。创建LISTVIEW的API与LISTBOX的CreateEx类似hListview LISTVIEW_CreateEx(10, 10, 300, 200, hParent, WM_CF_SHOW, 0, GUI_ID_LISTVIEW0, 0, 0);注意最后两个参数与LISTBOX不同通常设为0。创建后你得到一个空的、没有列的列表视图。3.2 配置表头与列管理列的定义和管理通过LISTVIEW_AddColumn函数和HEADER相关的API完成。// 添加列 LISTVIEW_AddColumn(hListview, 80, “姓名”, GUI_TA_LEFT); // 第一列宽80像素左对齐 LISTVIEW_AddColumn(hListview, 60, “年龄”, GUI_TA_HCENTER); // 第二列宽60像素居中 LISTVIEW_AddColumn(hListview, 100, “部门”, GUI_TA_LEFT); // 第三列 // 可以通过HEADER句柄进一步精细控制表头 HEADER_Handle hHeader LISTVIEW_GetHeader(hListview); HEADER_SetDragLimit(hHeader, 10); // 设置列宽最小拖动限制为10像素 HEADER_SetFont(hHeader, GUI_Font13B_1); // 设置表头字体加粗列宽调整用户通常可以通过拖动表头列之间的分隔线来调整列宽。HEADER_SetDragLimit可以设置一个最小宽度限制防止用户拖得太窄导致内容无法显示。3.3 数据填充与操作与LISTBOX使用字符串数组初始化不同LISTVIEW的数据需要逐行、逐列地添加。添加行与设置单元格内容:// 添加一行返回行索引 int rowIndex LISTVIEW_AddRow(hListview, NULL); // 第二个参数已废弃传NULL即可 // 设置该行各列的内容 LISTVIEW_SetItemText(hListview, rowIndex, 0, “张三”); // 第0列 LISTVIEW_SetItemText(hListview, rowIndex, 1, “28”); // 第1列 LISTVIEW_SetItemText(hListview, rowIndex, 2, “研发部”); // 第2列 // 可以继续添加更多行... rowIndex LISTVIEW_AddRow(hListview, NULL); LISTVIEW_SetItemText(hListview, rowIndex, 0, “李四”); LISTVIEW_SetItemText(hListview, 1, 1, “35”); LISTVIEW_SetItemText(hListview, rowIndex, 2, “市场部”);插入与删除行:LISTVIEW_InsertRow(hListview, 1, NULL): 在索引1的位置插入一个新行。LISTVIEW_DeleteRow(hListview, 0): 删除索引0的行。3.4 视觉样式与网格线LISTVIEW的视觉定制API与LISTBOX类似但多了网格线的控制。颜色与字体使用LISTVIEW_SetBkColor,LISTVIEW_SetTextColor,LISTVIEW_SetFont等函数其颜色索引LISTVIEW_CI_UNSEL等含义与LISTBOX一致。网格线这是LISTVIEW的特色功能能让数据看起来更规整。LISTVIEW_SetGridVis(hListview, 1); // 显示网格线 LISTVIEW_SetGridColor(hListview, GUI_GRAY); // 设置网格线颜色行高通过LISTVIEW_SetRowHeight统一设置所有行的高度。如果内容高度超过行高默认会被裁剪。要支持自动行高需要更复杂的所有者绘制。3.5 LISTVIEW的所有者绘制LISTVIEW同样支持所有者绘制功能更强大因为你需要处理多个列。其回调函数的结构与LISTBOX类似但WIDGET_ITEM_DRAW_INFO结构体中的Column成员会告诉你当前正在绘制哪一列。static int _ListViewDrawItem(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) { int r; switch (pDrawItemInfo-Cmd) { case WIDGET_ITEM_DRAW: // 根据行索引(pDrawItemInfo-ItemIndex)和列索引(pDrawItemInfo-Column)决定如何绘制 if (pDrawItemInfo-Column 1) { // 例如特殊绘制第二列年龄 char buf[10]; // 假设我们从某个数据源获取该单元格的数值 int age _GetDataAge(pDrawItemInfo-ItemIndex); sprintf(buf, “%d”, age); if (age 30) { GUI_SetColor(GUI_RED); // 年龄大于30标红 } else { GUI_SetColor(GUI_BLACK); } GUI_DispStringInRect(buf, (pDrawItemInfo-Rect), GUI_TA_HCENTER | GUI_TA_VCENTER); return 0; } // 其他列交给默认绘制 break; } // 未处理的情况调用默认函数 r LISTVIEW_OwnerDraw(pDrawItemInfo); return r; } // 注册 LISTVIEW_SetOwnerDraw(hListview, _ListViewDrawItem);4. 实战应用构建一个可配置的设备参数列表让我们结合一个常见的嵌入式应用场景——工业HMI的设备参数设置界面来综合运用LISTBOX和LISTVIEW。需求一个界面分为左右两栏。左栏LISTBOX显示可配置的参数类别如“通信参数”、“运动参数”、“安全设置”。点击左栏任一类别右栏LISTVIEW动态显示该类别下的具体参数项参数名、当前值、单位并允许对“当前值”列进行编辑通过弹出键盘对话框。4.1 界面布局与控件创建首先在窗口的WM_PAINT消息或初始化函数中创建控件。// 假设 hWin 是当前窗口句柄 // 1. 创建左侧分类LISTBOX static const GUI_ConstString _aParamCategories[] {“通信参数”, “运动参数”, “安全设置”, “系统信息”, NULL}; hListboxCat LISTBOX_CreateEx(10, 10, 150, 220, hWin, WM_CF_SHOW, 0, GUI_ID_LISTBOX_CAT, _aParamCategories); LISTBOX_SetFont(hListboxCat, GUI_Font16_1); LISTBOX_SetItemSpacing(hListboxCat, 8); // 增加行间距 LISTBOX_SetBkColor(hListboxCat, LISTBOX_CI_SELFOCUS, GUI_BLUE); // 2. 创建右侧参数LISTVIEW hListviewParam LISTVIEW_CreateEx(170, 10, 300, 220, hWin, WM_CF_SHOW, 0, GUI_ID_LISTVIEW_PARAM, 0, 0); LISTVIEW_AddColumn(hListviewParam, 120, “参数名”, GUI_TA_LEFT); LISTVIEW_AddColumn(hListviewParam, 100, “当前值”, GUI_TA_HCENTER); LISTVIEW_AddColumn(hListviewParam, 60, “单位”, GUI_TA_LEFT); LISTVIEW_SetGridVis(hListviewParam, 1); // 显示网格 LISTVIEW_SetRowHeight(hListviewParam, 25); // 设置行高4.2 实现交互逻辑消息处理交互的核心在于处理WM_NOTIFY_PARENT消息。当用户在LISTBOX中选择不同类别时我们需要更新LISTVIEW的内容。static void _cbCallback(WM_MESSAGE * pMsg) { switch (pMsg-MsgId) { case WM_NOTIFY_PARENT: { int Id WM_GetId(pMsg-hWinSrc); // 获取触发消息的控件ID int NCode pMsg-Data.v; // 获取通知代码 if (Id GUI_ID_LISTBOX_CAT) { if (NCode WM_NOTIFICATION_SEL_CHANGED) { // 左栏LISTBOX选择已改变 int selCat LISTBOX_GetSel(pMsg-hWinSrc); _UpdateParamListView(selCat); // 更新右栏LISTVIEW } } else if (Id GUI_ID_LISTVIEW_PARAM) { if (NCode WM_NOTIFICATION_CLICKED) { // 获取点击的行和列 int row, col; LISTVIEW_GetItemPos(pMsg-hWinSrc, row, col); if (row 0 col 1) { // 如果点击的是“当前值”列 _EditParamValue(pMsg-hWinSrc, row); // 弹出编辑对话框 } } } } break; // ... 处理其他消息 } }4.3 动态更新LISTVIEW数据_UpdateParamListView函数根据选中的类别清空LISTVIEW并填充对应的参数数据。static void _UpdateParamListView(int categoryIndex) { // 1. 清空现有所有行 int numRows LISTVIEW_GetNumRows(hListviewParam); for (int i numRows - 1; i 0; i--) { LISTVIEW_DeleteRow(hListviewParam, i); } // 2. 根据类别索引从数据模型如结构体数组中获取数据并添加 const ParamItem * pParams _GetParamListByCategory(categoryIndex); // 假设的函数返回参数数组 int paramCount _GetParamCountByCategory(categoryIndex); for (int i 0; i paramCount; i) { int row LISTVIEW_AddRow(hListviewParam, NULL); LISTVIEW_SetItemText(hListviewParam, row, 0, pParams[i].name); // 将数值转换为字符串显示 char valStr[20]; sprintf(valStr, “%.2f”, pParams[i].currentValue); LISTVIEW_SetItemText(hListviewParam, row, 1, valStr); LISTVIEW_SetItemText(hListviewParam, row, 2, pParams[i].unit); } }4.4 实现单元格编辑_EditParamValue函数负责弹出编辑界面例如一个包含数字键盘的对话框并将修改后的值写回LISTVIEW和数据模型。static void _EditParamValue(LISTVIEW_Handle hLV, int row) { // 1. 获取当前显示的值 char curValText[20]; LISTVIEW_GetItemText(hLV, row, 1, curValText, sizeof(curValText)); // 2. 创建模态对话框内含编辑框和键盘 // ... (此处省略具体的对话框创建代码可使用emWin的对话框管理器或手动创建窗口) // 假设通过对话框获取到新的字符串 newValText // 3. 更新LISTVIEW显示 LISTVIEW_SetItemText(hLV, row, 1, newValText); // 4. 更新底层数据模型 float newValue atof(newValText); _UpdateParamDataModel(category, row, newValue); // 假设的函数 }5. 性能优化与常见问题排查在资源受限的嵌入式设备上使用这些控件性能是需要重点考虑的问题。5.1 性能优化技巧避免频繁重绘在批量更新列表内容如_UpdateParamListView前可以使用WM_DisableWindow临时禁用窗口更新所有操作完成后再用WM_EnableWindow启用并调用WM_InvalidateWindow触发一次重绘而不是每增加一行就重绘一次。使用所有者绘制的权衡所有者绘制提供了最大灵活性但每个项的绘制都涉及一次函数调用和你的自定义代码会比默认绘制慢。如果列表项很多如超过50项且滚动频繁需要评估自定义绘制的复杂度。尽量在绘制函数中避免复杂的计算或资源加载。合理设置滚动步进LISTBOX_SetScrollStepH和LISTVIEW_SetScrollStepH可以设置水平滚动的像素步长。默认值可能不适合你的字体大小调整到一个合适的值如字符平均宽度可以使滚动更平滑。谨慎使用透明效果如果列表控件背景非纯色或者与底层窗口有复杂的叠加重绘开销会增大。在低端MCU上应尽量避免。5.2 常见问题与解决方案下面将开发中常见的问题、可能原因及解决方法整理成表格方便快速排查。问题现象可能原因解决方案LISTBOX/LISTVIEW创建后不显示1. 创建标志WinFlags未包含WM_CF_SHOW。2. 控件被其他窗口遮挡。3. 父窗口未有效显示或无效。1. 确保创建时传入了WM_CF_SHOW。2. 检查Z序确保控件在最上层或父窗口已显示。3. 检查父窗口句柄hParent是否正确。列表项点击无高亮反馈1. 控件未获得焦点。2. 颜色设置错误选中色与背景色相同。3. 触摸或输入设备消息未正确传递到控件。1. 调用WM_SetFocus将焦点设置到控件。2. 检查LISTBOX_CI_SELFOCUS等状态的颜色设置。3. 确保触摸屏校准正确且WM_TOUCH消息被发送到正确的窗口。滚动条不出现或行为异常1. 未启用自动滚动功能SetAutoScrollV/H。2. 控件尺寸设置过大足以显示所有内容。3. 所有者绘制函数返回的项尺寸计算错误。1. 调用LISTBOX_SetAutoScrollV(hObj, 1)启用。2. 检查内容总尺寸是否大于控件可视区尺寸。3. 在WIDGET_ITEM_GET_XSIZE/YSIZE命令中返回正确的尺寸。所有者绘制项显示错乱或闪烁1. 绘制函数未正确处理所有Cmd命令。2. 在WIDGET_ITEM_DRAW命令中未根据ItemIndex区分绘制不同项。3. 绘制操作超出了pDrawItemInfo-Rect规定的区域。1. 确保未处理的Cmd最终调用了LISTBOX_OwnerDraw或LISTVIEW_OwnerDraw。2. 使用pDrawItemInfo-ItemIndex和pDrawItemInfo-Column来获取当前项数据。3. 所有GUI_DrawXXX或GUI_FillXXX操作应限制在给定的矩形内。LISTVIEW表头无法拖动调整列宽默认就是可拖动的如果不行可能是消息循环被阻塞或者HEADER控件本身被禁用。检查是否在对话框循环或自定义消息循环中正确处理了所有消息。确保没有长时间阻塞GUI任务的操作。内存占用过大1. 列表项字符串全部存储在RAM中。2. 列表项数量极多且使用了所有者绘制等复杂功能。1. 尽量使用const字符串将其存储在Flash中。2. 对于超长列表考虑实现“虚拟列表”只创建和渲染可视区域附近的项。emWin本身不直接支持虚拟列表需要自己管理数据并动态更新控件内容。5.3 调试心得使用模拟器在PC端的emWin模拟器上进行前期开发和调试效率远高于在目标板上下载调试。可以快速验证布局、颜色和交互逻辑。关注返回值很多API函数都有返回值如Create返回句柄GetSel返回索引养成检查这些返回值的习惯例如句柄是否为0能快速定位创建失败或状态异常的问题。理解消息流emWin是消息驱动系统。列表控件的滚动、点击、选择变化都是通过WM_NOTIFY_PARENT等消息通知父窗口的。在回调函数中设置断点观察消息的传递和处理流程是解决交互问题的关键。通过以上对LISTBOX和LISTVIEW控件从API解析到实战应用再到问题排查的全面梳理相信你已经对如何在emWin中高效使用列表控件有了深入的理解。记住控件是工具理解其内在机制结合具体业务需求灵活运用才能打造出既流畅又美观的嵌入式用户界面。