1. 项目概述与控件定位
在嵌入式GUI开发领域,尤其是资源受限的MCU平台上,一个高效、灵活且易于使用的图形库是产品成功的关键。emWin作为SEGGER公司推出的嵌入式图形用户界面解决方案,以其卓越的性能和丰富的控件集著称。在众多控件中, LISTVIEW (列表视图)和 LISTWHEEL (列表滚轮)是构建复杂数据展示和交互界面的核心组件。它们远不止是简单的文本列表,而是承载着数据管理、用户交互和视觉呈现三大功能的微型应用框架。
LISTVIEW控件,你可以把它想象成一个微型的电子表格或文件管理器视图。它能够以行和列的形式组织数据,支持选择、高亮、滚动以及为每一行关联用户自定义数据。这对于需要展示配置项、日志记录、文件列表或任何结构化数据的场景至关重要。而LISTWHEEL控件则提供了另一种独特的交互范式——模拟物理滚轮的触控体验。用户通过上下滑动来“旋转”选项列表,松开手指后选项会自动“吸附”到指定位置,这种交互方式在日期时间选择、数值调节等场景下非常直观,能极大提升触摸屏设备的操作体验。
这两个控件的技术价值,核心在于它们将底层复杂的绘图、消息处理和状态管理封装成了简洁的API。开发者无需关心如何绘制每一行文本、如何处理触摸事件的物理模拟、如何管理选中状态的重绘,只需调用诸如
LISTVIEW_SetTextColor
或
LISTWHEEL_SetBkColor
这样的函数,即可实现精细的视觉控制。这种封装在嵌入式开发中意义重大,它让开发者能将精力集中在业务逻辑上,而非底层图形细节,同时保证了界面响应的实时性和内存使用的可控性。无论是工业HMI面板、医疗设备操作界面,还是智能家居中控屏,你都能看到它们的身影。
2. LISTVIEW控件深度解析与API实战
2.1 控件创建与基础配置
LISTVIEW的创建是使用的起点。
LISTVIEW_CreateEx
函数是创建控件最常用的方法,它提供了最大的灵活性。其原型如下:
LISTVIEW_Handle LISTVIEW_CreateEx(int x0, int y0, int xSize, int ySize,
WM_HWIN hParent, int WinFlags,
int ExFlags, int Id);
这里有几个关键参数需要仔细考量。
x0
和
y0
决定了控件在父窗口坐标系中的位置,而
xSize
和
ySize
则定义了控件的初始尺寸。在实际项目中,我通常不会写死这些数值,而是根据屏幕分辨率或父窗口大小动态计算,例如
xSize = LCD_GetXSize() - 20
,以确保布局的适应性。
WinFlags
参数通常设置为
WM_CF_SHOW
,让控件创建后立即可见,避免额外的显示调用。
创建完成后,一个空的列表视图是没用的,我们需要向其中添加列和行。添加列使用
LISTVIEW_AddColumn
函数,你需要指定列的宽度、对齐方式和标题文本。这里有一个常见的“坑”:列宽的计算。如果你使用自动宽度(
GUI_TA_AUTO
),控件会根据内容动态调整,但在嵌入式环境中,频繁的文本测量可能影响性能。我的经验是,在界面初始化时,根据最长的预期字符串长度,使用
GUI_GetStringDistX()
函数预先计算并设置固定列宽,这样效率更高。
添加行数据则使用
LISTVIEW_AddRow
和
LISTVIEW_SetItemText
。一个高效的做法是,先一次性添加所有空行,再批量设置文本,而不是添加一行设置一次,这样可以减少内部重绘次数。每一行都可以通过
LISTVIEW_SetUserData
关联一个32位的用户数据(
U32 UserData
),这个功能极其强大。你可以把该行数据在应用中的索引、指针或其他状态信息存储在这里。当用户选中某一行触发回调时,你可以直接从这个
UserData
中取出关键信息进行处理,无需再去全局数组中查找,大大提升了事件处理效率。
2.2 视觉定制与状态管理
LISTVIEW的强大之处在于其精细的视觉控制。通过一系列
Set
函数,你可以完全掌控控件在不同状态下的外观。
颜色管理
是定制的核心。
LISTVIEW_SetTextColor
函数允许你为不同状态的文本设置不同的颜色:
void LISTVIEW_SetTextColor(LISTVIEW_Handle hObj, unsigned int Index, GUI_COLOR Color);
参数
Index
是一个关键枚举,它定义了颜色应用的场景:
-
LISTVIEW_CI_UNSEL: 未选中项的文字颜色。通常设置为深灰色(GUI_GRAY)或黑色(GUI_BLACK)。 -
LISTVIEW_CI_SEL: 已选中但未获得焦点的项的文字颜色。为了清晰区分,我常设为白色(GUI_WHITE)。 -
LISTVIEW_CI_SELFOCUS: 已选中且获得焦点的项的文字颜色。可以设置为更醒目的颜色,如亮蓝色(GUI_BLUE)。
背景色的设置同理,使用
LISTVIEW_SetBkColor
函数。一个专业的技巧是,选中项的背景色与未选中项形成高对比度,而获得焦点的选中项可以再用一个更亮的色系,形成视觉层次。例如:未选中背景为浅灰(
GUI_LIGHTGRAY
),选中背景为蓝色(
GUI_BLUE
),焦点选中背景为深蓝。
字体与对齐
同样重要。
LISTVIEW_SetFont
可以为整个控件或特定列设置字体。在资源紧张的系统中,应避免使用过多字体,通常一个清晰的无衬线体足矣。文本对齐通过
LISTVIEW_SetColumnTextAlign
设置,对于数字列,右对齐(
GUI_TA_RIGHT
)更符合阅读习惯;对于文本列,左对齐(
GUI_TA_LEFT
)是标准做法。
网格线
的显示由
LISTVIEW_SetGridVis
控制。在显示大量数据时,启用网格线(
GUI_TRUE
)可以显著提高可读性。你可以通过
LISTVIEW_SetGridColor
来设置网格线颜色,通常设为比背景稍深的颜色,如
GUI_DARKGRAY
。
2.3 通知机制与用户交互
LISTVIEW不是静态的展示控件,它需要响应用户操作。这是通过
窗口管理器(WM)的通知消息
实现的。当用户在列表上进行点击、选择等操作时,控件会向其父窗口发送
WM_NOTIFY
消息。
你需要在父窗口的回调函数中处理这些消息。核心的消息类型有:
-
WM_NOTIFICATION_CLICKED: 控件被点击。 -
WM_NOTIFICATION_SEL_CHANGED: 选中项发生改变。这是最常用的事件,用于触发详情更新或加载关联操作。 -
WM_NOTIFICATION_RELEASED: 控件上的点击被释放。
处理示例如下:
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; // 通知代码
switch (NCode) {
case WM_NOTIFICATION_SEL_CHANGED: {
if (Id == GUI_ID_LISTVIEW0) { // 判断是哪个列表
int selRow = LISTVIEW_GetSel(pMsg->hWinSrc);
U32 userData = LISTVIEW_GetUserData(pMsg->hWinSrc, selRow);
// 根据userData更新界面其他部分或执行操作
_UpdateDetailView(userData);
}
break;
}
// ... 处理其他通知
}
break;
}
// ... 处理其他消息
}
}
这里的关键是
LISTVIEW_GetSel
获取当前选中行,再通过
LISTVIEW_GetUserData
取出之前关联的业务数据,从而实现界面与逻辑的解耦。
3. LISTWHEEL控件详解与高级应用
3.1 控件特性与创建
LISTWHEEL提供了一种截然不同的交互模型。它模拟了物理滚轮或老式iPod点击轮的体验,通过触摸滑动来滚动选项列表,释放后选项会“吸附”到预设的捕捉位置(Snap Position)。这种交互在有限的屏幕空间内选择有限选项(如日期、时间、预设模式)时,既直观又节省空间。
创建LISTWHEEL同样使用
CreateEx
函数族。一个典型的创建示例如下:
GUI_CONST_STORAGE char * _apWeekdays[] = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", NULL};
hListWheel = LISTWHEEL_CreateEx(50, 100, 80, 120, hParent, WM_CF_SHOW, 0, GUI_ID_LISTWHEEL0, _apWeekdays);
注意,字符串数组必须以
NULL
指针结尾,这是emWin许多可变参数列表的约定。创建后,列表会循环显示,即滚动到底部后会从顶部重新开始,形成“滚轮”的视觉闭环。
3.2 核心API与行为控制
LISTWHEEL的API围绕滚动、选择和显示进行控制。
滚动与捕捉控制
是LISTWHEEL的灵魂。
LISTWHEEL_SetSnapPosition
函数设置“吸附点”的Y坐标(相对于控件顶部)。默认是0,即顶部对齐。你可以将其设置为控件垂直中心,这样选中的项目会始终停留在屏幕中央,体验更佳:
LISTWHEEL_SetSnapPosition(hObj, ySize / 2)
。
LISTWHEEL_GetPos
和
LISTWHEEL_SetPos
用于获取和设置当前“吸附”住的项索引。
LISTWHEEL_MoveToPos
则会让滚轮以动画方式滚动到目标项,比直接
SetPos
更具动感。
视觉定制
方面,LISTWHEEL同样强大。
LISTWHEEL_SetTextColor
和
LISTWHEEL_SetBkColor
分别控制文本和背景色,其
Index
参数通常只有两个状态:
LISTWHEEL_CI_UNSEL
(未选中)和
LISTWHEEL_CI_SEL
(选中)。为了突出选中项,我通常会将选中项的文本加粗(通过设置不同字体)或使用高对比度颜色。
一个高级技巧是使用
LISTWHEEL_SetLineHeight
。默认行高由字体决定,但在某些设计中,你可能希望行间有更大的间距。设置一个固定的行高(如
LISTWHEEL_SetLineHeight(hObj, 30)
)可以让布局更规整。同时,通过
LISTWHEEL_SetLBorder
和
LISTWHEEL_SetRBorder
可以设置文本左右的边距,实现文本在滚轮内的水平居中或留有呼吸空间。
3.3 所有者绘制(Owner Draw)实现深度定制
当默认的文本显示无法满足需求时,LISTWHEEL的 所有者绘制(Owner Draw) 功能提供了终极的灵活性。你可以接管每个项目的绘制过程,绘制任何图形、图标或复杂文本。
启用所有者绘制的步骤如下:
-
定义一个绘制回调函数,其原型必须匹配
WIDGET_DRAW_ITEM_FUNC。 -
在回调函数中,处理不同的绘制命令(
Cmd)。 -
使用
LISTWHEEL_SetOwnerDraw将回调函数注册到控件。
下面是一个绘制带图标的项目的示例:
static int _DrawCustomItem(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) {
const char* pText;
int x, y;
switch (pDrawItemInfo->Cmd) {
case WIDGET_ITEM_GET_YSIZE:
// 告诉控件我们需要的项目高度是40像素
return 40;
case WIDGET_ITEM_DRAW:
// 获取当前项目的文本
LISTWHEEL_GetItemText(pDrawItemInfo->hWin, pDrawItemInfo->ItemIndex, acBuffer, sizeof(acBuffer));
pText = acBuffer;
// 根据是否选中设置颜色
if (pDrawItemInfo->Sel) {
GUI_SetColor(GUI_WHITE);
GUI_SetBkColor(GUI_BLUE);
GUI_FillRect(pDrawItemInfo->x0, pDrawItemInfo->y0,
pDrawInfo->x1, pDrawInfo->y1); // 填充选中背景
GUI_SetColor(GUI_WHITE);
} else {
GUI_SetColor(GUI_BLACK);
GUI_SetBkColor(GUI_LIGHTGRAY);
GUI_FillRect(pDrawInfo->x0, pDrawInfo->y0,
pDrawInfo->x1, pDrawInfo->y1); // 填充未选中背景
GUI_SetColor(GUI_BLACK);
}
// 绘制图标(假设有图标数组)
x = pDrawItemInfo->x0 + 5;
y = pDrawItemInfo->y0 + (pDrawItemInfo->y1 - pDrawItemInfo->y0 - 16) / 2; // 垂直居中
GUI_DrawBitmap(&_apIcons[pDrawItemInfo->ItemIndex], x, y);
// 绘制文本
x += 20; // 图标右侧留空
y = pDrawItemInfo->y0 + (pDrawItemInfo->y1 - pDrawItemInfo->y0 - GUI_GetFontSizeY()) / 2;
GUI_DispStringAt(pText, x, y);
break;
default:
// 对于不处理的命令,调用默认绘制函数,这是一个好习惯
return LISTWHEEL_OwnerDraw(pDrawItemInfo);
}
return 0;
}
// 在初始化代码中注册回调
LISTWHEEL_SetOwnerDraw(hListWheel, _DrawCustomItem);
通过所有者绘制,你可以实现诸如渐变背景、圆角矩形选中框、动态图标等复杂效果,让LISTWHEEL完全融入你的产品视觉设计语言。
4. 实战应用:构建一个日期时间选择器
理解了单个控件的API后,我们将它们组合起来解决一个实际问题:构建一个触摸屏上常见的日期时间选择器。这通常需要三个LISTWHEEL控件分别代表年、月、日(或时、分、秒)。
4.1 架构设计与数据联动
首先,我们需要创建三个LISTWHEEL控件,并排或上下排列。年的列表可以预设一个范围(如2000-2030),月的列表固定为1-12。关键在于 日的列表 ,它需要根据当前选中的年和月进行动态更新,因为不同月份的天数不同,闰年二月还有29天。
实现数据联动的核心是在处理
WM_NOTIFICATION_SEL_CHANGED
通知时,更新依赖的列表。例如,当年或月的选择改变时,必须重新计算并设置日的列表内容。
static void _UpdateDayList(LISTWHEEL_Handle hWheelYear,
LISTWHEEL_Handle hWheelMonth,
LISTWHEEL_Handle hWheelDay) {
int year, month, days_in_month;
char* apDays[32] = {0}; // 最多31天+NULL结束符
char aDay[4];
year = LISTWHEEL_GetSel(hWheelYear) + 2000; // 假设年份从2000开始
month = LISTWHEEL_GetSel(hWheelMonth) + 1; // 获取月份索引(0-11)并转为1-12
// 计算当月天数
days_in_month = _GetDaysInMonth(year, month);
// 构建天的字符串数组
for (int i = 0; i < days_in_month; i++) {
sprintf(aDay, "%d", i+1);
apDays[i] = GUI_malloc(strlen(aDay)+1);
strcpy(apDays[i], aDay);
}
apDays[days_in_month] = NULL; // 数组必须以NULL结尾
// 更新日的LISTWHEEL
LISTWHEEL_SetText(hWheelDay, (const GUI_CONST_CHAR**)apDays);
// 释放临时分配的内存(简化示例,实际需管理内存)
for (int i = 0; i < days_in_month; i++) {
GUI_free(apDays[i]);
}
}
_GetDaysInMonth
是一个辅助函数,根据年份和月份返回正确的天数,需要处理闰年判断。
4.2 视觉统一与交互优化
为了让三个滚轮看起来是一个整体,我们需要进行视觉统一设置:
-
统一尺寸与字体
:确保三个控件的
xSize,ySize和通过LISTWHEEL_SetFont设置的字体完全一致。 -
对齐捕捉位置
:使用
LISTWHEEL_SetSnapPosition将三个控件的捕捉点设置在同一水平线上(通常是控件垂直中心),这样选中项才能对齐。 -
同步滚动效果
:虽然三个滚轮独立操作,但可以通过
LISTWHEEL_SetBkColor和LISTWHEEL_SetTextColor设置相同的配色方案,保持视觉一致性。
交互上,可以增加一个“确认”按钮。当用户滑动选择完成后,点击按钮,程序通过
LISTWHEEL_GetSel
从三个控件中分别获取选中的索引,转换为实际的年、月、日数值,供后续逻辑使用。
4.3 性能考量与内存管理
在嵌入式环境中,动态更新列表内容(如日的列表)需要注意性能。
-
避免频繁更新
:只在年或月真正改变时才触发日的列表更新。可以在通知回调中记录上一次的年月值,仅当变化时才调用
_UpdateDayList。 -
使用常量字符串
:对于固定列表(如月份),应使用
GUI_CONST_STORAGE将字符串数组存储在Flash中,而非RAM,以节省宝贵的内存。GUI_CONST_STORAGE char * _apMonths[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", NULL}; -
合理分配内存
:像上面
_UpdateDayList示例中动态构建字符串数组的方式,在频繁调用时会产生内存碎片。对于嵌入式系统,更好的做法是预先分配一个足够大的静态二维字符数组,或者使用内存池来管理这些临时字符串。
5. 常见问题排查与调试技巧
在实际开发中,你一定会遇到各种控件表现不符合预期的情况。下面是我总结的一些常见问题及其解决方法。
5.1 控件不显示或显示异常
- 问题 :LISTVIEW/LISTWHEEL创建后看不到,或者只显示一部分。
-
排查步骤
:
-
检查父窗口
:确保
hParent参数是有效的窗口句柄,并且该窗口本身是可见的。如果父窗口被隐藏或未创建,子控件也不会显示。 -
检查坐标和尺寸
:确认
(x0, y0)是否在父窗口的客户区内,并且(x0+xSize, y0+ySize)没有超出父窗口边界。超出部分不会被绘制。 -
确认WinFlags
:创建时是否包含了
WM_CF_SHOW标志?如果没有,需要手动调用WM_ShowWindow(hObj)。 -
检查重绘
:确保在界面初始化完成后,调用了
WM_Exec()或GUI_Exec()来执行窗口管理器的任务,包括重绘。如果你的程序是裸机轮询,必须定期调用这些函数。 -
内存不足
:在创建控件前,使用
GUI_ALLOC_GetNumFreeBytes()检查可用内存。控件创建失败可能静默返回0。
-
检查父窗口
:确保
5.2 触摸/点击无响应
- 问题 :可以看见控件,但点击或滑动没有反应。
-
排查步骤
:
-
输入设备是否启用
:确认在
main函数或初始化阶段已经调用了GUI_PID_StoreState等相关函数来启用触摸屏输入,并且触摸坐标数据被正确送入emWin。 -
消息回调是否正确
:控件的父窗口必须正确设置回调函数(通过
WM_SetCallback),并且在该回调中处理了WM_NOTIFY_PARENT消息。 -
控件是否被禁用
:检查是否意外调用了
WM_DisableWindow禁用了该控件或其父窗口。 -
层叠顺序(Z-order)
:是否有其他透明窗口或控件覆盖在了LISTVIEW/LISTWHEEL之上,拦截了触摸事件?使用
WM_SelectWindow和WM_BringToTop可以调整窗口层次。
-
输入设备是否启用
:确认在
5.3 文本显示乱码、裁剪或对齐错误
- 问题 :列表中的文字显示为乱码、显示不全或没有按设定的对齐方式显示。
-
排查步骤
:
-
字体编码
:确保你使用的字体(
GUI_FONT)包含了你所显示字符的编码。中文等宽字体与ASCII字体不同。使用GUI_UC_SetEncodeUTF8()来设置UTF-8编码(如果字体支持)。 -
列宽/控件宽度不足
:文字被裁剪通常是因为分配给文本显示的区域宽度不够。对于LISTVIEW,检查通过
LISTVIEW_AddColumn设置的列宽;对于LISTWHEEL,检查控件本身的xSize以及通过LISTWHEEL_SetLBorder/SetRBorder设置的边距。使用GUI_GetStringDistX()计算字符串像素宽度作为参考。 -
对齐标志
:
LISTVIEW_SetColumnTextAlign和LISTWHEEL_SetTextAlign设置的对齐方式,是相对于 文本单元格 而言的,而不是整个控件。确保你理解GUI_TA_LEFT,GUI_TA_HCENTER,GUI_TA_RIGHT的含义。 -
内存覆盖
:如果提供文本的字符串数组是临时变量,确保其在控件整个生命周期内有效。如果文本是动态生成的,确保字符串以
\0结尾。
-
字体编码
:确保你使用的字体(
5.4 滚动卡顿或动画不流畅
-
问题
:LISTWHEEL滚动时卡顿,或
LISTWHEEL_MoveToPos的动画效果不流畅。 -
排查步骤
:
-
帧率与
GUI_Exec:emWin的动画和触摸反馈依赖于GUI_Exec()或WM_Exec()的定期调用。确保在主循环中以稳定的频率(如每秒30-60次)调用它。如果系统繁忙导致调用间隔过长,动画就会卡顿。 -
重绘区域过大
:检查是否在滚动过程中有不必要的全屏重绘。确保其他窗口或控件的
WM_PAINT消息处理函数效率足够高。 -
使用存储设备
:对于复杂的LISTWHEEL(如启用了所有者绘制),启用存储设备(Memory Device)可以极大提升滚动流畅度。在创建控件前,使用
WM_SetCreateFlags(WM_CF_MEMDEV)为其父窗口或控件本身启用存储设备,绘图操作将在内存中进行,然后一次性复制到屏幕,避免闪烁和提升速度。 -
优化所有者绘制回调
:你的
WIDGET_ITEM_DRAW回调函数执行速度是否过快?避免在其中进行复杂的计算或磁盘访问。尽量使用预计算好的值、位图和简单的图形操作。
-
帧率与
5.5 调试工具与技巧
- 使用模拟器 :SEGGER提供的emWin模拟器(Windows版)是强大的调试工具。你可以在PC上快速验证布局、交互逻辑和视觉效果,无需反复烧录硬件。
- 日志输出 :在关键API调用前后、消息回调中加入简单的日志输出(通过串口或SEGGER的RTT技术),可以清晰了解程序执行流程。
-
检查返回值
:养成习惯,检查
CreateEx等函数的返回值是否为0(NULL句柄)。0通常意味着创建失败。 -
参考官方示例
:emWin安装包中的
Sample、Tutorial、Application文件夹包含了大量针对每个控件的示例代码(如WIDGET_ListView.c,WIDGET_ListWheel.c)。当遇到问题时,首先对照官方示例的用法,这是最权威的参考。



被折叠的 条评论
为什么被折叠?



