1. LISTWHEEL控件:嵌入式GUI中的“滚轮选择器”
在嵌入式设备的用户界面开发里,我们经常需要让用户从一个列表里选东西,比如设置闹钟时选小时和分钟,或者在一个产品型号列表里做选择。你可能会想到用下拉列表(DROPDOWN),但在触摸屏上,尤其是需要快速、连续滚动浏览大量选项时,下拉列表的交互体验并不算最好。这时候,一个更直观、更符合触控直觉的控件就派上用场了——那就是emWin图形库中的LISTWHEEL控件。
你可以把它想象成一个手机上的时间选择器,或者老式 iPod 上的经典点击轮。LISTWHEEL 在屏幕上呈现为一个垂直的列表区域,用户可以上下拖动或滑动,列表会像物理滚轮一样惯性滚动并最终“咔哒”一声停在某个选项上,这个停住的位置我们叫它“吸附位置”(Snap Position)。它完美融合了直观的视觉反馈和精准的输入控制,是构建现代化嵌入式人机界面(HMI)的利器。无论是智能家居的中控面板选择房间,还是工业设备上设置工艺参数,LISTWHEEL 都能提供流畅且专业的交互体验。
这篇文章,我就结合自己多年在STM32、NXP等MCU平台上折腾emWin的经验,带你彻底吃透LISTWHEEL控件。我们不只停留在手册里的函数原型,我会重点拆解每个API背后的设计意图、实际应用中的关键细节,以及那些手册里不会写、但能让你少走弯路的“坑”和技巧。从最基础的创建、添加数据,到高级的自定义绘制和动态行为控制,让你不仅能“会用”,更能“用好”这个控件。
2. 核心设计思路与工作机制拆解
在动手写代码之前,理解LISTWHEEL控件内部是怎么工作的,能让你在遇到问题时更快地定位和解决。它不是一个简单的静态列表,而是一个有状态、可交互的动态窗口对象。
2.1 控件本质与消息循环
LISTWHEEL本质上是一个emWin的窗口对象(Widget)。这意味着它有自己的窗口句柄(
WM_HWIN
),完全融入emWin的消息驱动架构中。当用户触摸或点击控件时,底层输入驱动(比如触摸屏驱动)会生成消息,emWin的核心窗口管理器(WM)会将这些消息派发给LISTWHEEL控件窗口。
控件内部的消息处理函数(Callback)会解析这些消息,比如
WM_TOUCH
消息里的坐标变化,然后计算出滚动的速度和方向。接着,它会启动一个内部定时器(默认25ms周期,可通过
LISTWHEEL_SetTimerPeriod
调整),在定时器中断里不断更新列表的绘制位置,从而实现平滑的动画效果。当滚动速度低于某个阈值时,控件会自动计算距离最近的那个数据项,并让列表滑动到使该项对准“吸附位置”的地方,完成一次选择。
这个机制带来的一个关键特性是:
LISTWHEEL的交互是异步的
。你调用
LISTWHEEL_SetVelocity
让它滚动后,不需要自己写循环去更新位置,控件自己会在后台处理好动画和最终定位。这大大简化了应用层逻辑。
2.2 数据管理与渲染分离
LISTWHEEL采用了典型的数据与视图分离设计。控件内部维护一个字符串指针数组(或更通用的数据项数组),用来存储所有可选项的文本。而如何将这些文本画到屏幕上,则是另一套机制。
默认情况下,控件使用内置的
LISTWHEEL_OwnerDraw
函数进行绘制。这个函数会根据当前字体、颜色、对齐方式等属性,将字符串绘制在每一行对应的矩形区域内。但是,emWin提供了强大的
LISTWHEEL_SetOwnerDraw
接口,允许你用一个自定义的函数替换默认的绘制逻辑。这就打开了自定义绘制的大门——你不仅可以画文字,还可以画图标、进度条、不同颜色的背景,甚至任何你能用emWin绘图API画出来的东西。
这种分离的好处是显而易见的:数据层(有哪些选项)和表现层(选项长什么样)可以独立变化。你可以动态更换列表内容而不影响绘制风格,也可以彻底重绘界面而不必改动数据源。
2.3 “吸附”机制与状态管理
“吸附”(Snap)是LISTWHEEL交互体验的核心。
LISTWHEEL_SetSnapPosition
函数设置了一个Y坐标(相对于控件顶部),滚动停止时,总会有一个数据项的中心线(或顶部,取决于你的逻辑)对齐到这个位置。
这里有一个容易混淆的概念:
当前选中项(Selected Item)
和
吸附位置项(Snapped Item)
。在用户快速滚动时,这两个可能不同。
LISTWHEEL_GetSel
返回的是通过
LISTWHEEL_SetSel
以编程方式设置的“选中”项,它更像一个高亮标记。而
LISTWHEEL_GetPos
返回的则是当前实际停在吸附位置的那个项的索引。在大多数直接滚选的应用中,我们更关心
GetPos
的结果。控件通过
WM_NOTIFICATION_SEL_CHANGED
通知码上报的,也是吸附位置发生变化的事件。
理解这些核心机制后,我们再去看那些API函数,就不会觉得它们是一堆孤立的命令,而是一个有机整体中各司其职的模块。
3. 从零到一:创建、配置与基础交互
让我们从创建一个最基本的LISTWHEEL开始,这是所有工作的起点。我会用一个“星期选择器”作为贯穿本章的例子。
3.1 控件的创建:三种方式及其适用场景
emWin提供了三种创建LISTWHEEL的方法,适用于不同的工程模式。
3.1.1 直接创建:LISTWHEEL_CreateEx
这是最常用、最直接的方式。你需要指定控件的位置、大小、父窗口、标志位、ID和初始数据。
static const GUI_CONST_STORAGE char * _apWeekdays[] = {
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
"Sunday",
NULL // 必须!用于标识数组结束
};
WM_HWIN hListWheel;
void CreateListWheel(void) {
hListWheel = LISTWHEEL_CreateEx(
50, // x0: 父窗口坐标系下的左上角X坐标
100, // y0: 父窗口坐标系下的左上角Y坐标
200, // xSize: 控件宽度
150, // ySize: 控件高度,决定了可见区域能显示几行
WM_HBKWIN, // hParent: 父窗口句柄,WM_HBKWIN是桌面窗口
WM_CF_SHOW, // WinFlags: 窗口创建标志,WM_CF_SHOW表示创建后立即显示
0, // ExFlags: 保留位,必须为0
GUI_ID_LISTWHEEL0, // Id: 控件ID,用于在回调函数中识别该控件
_apWeekdays // ppText: 初始字符串数组
);
}
关键细节与避坑指南 :
- NULL终止符 :字符串数组
_apWeekdays的最后一个元素 必须是NULL。这是emWin许多控件遍历数组的约定,忘记它会导致控件在读取数据时越界,可能引发硬件错误(HardFault)。- 内存存储 :使用
GUI_CONST_STORAGE修饰符将字符串数组声明在常量区(通常是Flash),可以节省宝贵的RAM。如果你的选项需要动态改变,则需存储在RAM中。- 控件高度与行高 :控件高度并不直接等于“行数×行高”。控件内部会计算可滚动区域。行高默认由字体决定,也可用
LISTWHEEL_SetLineHeight自定义。确保高度足以清晰显示至少3行(当前项、上一项、下一项),体验会更好。- ID的作用 :
GUI_ID_LISTWHEEL0是一个预定义的ID(范围是0x200-0x209)。在父窗口的回调函数中,当收到来自控件的WM_NOTIFY_PARENT消息时,可以通过pMsg->Data.v(或Id字段)来判断是哪个控件发来的消息。这对于一个窗口内有多个同类控件时至关重要。
3.1.2 间接创建:LISTWHEEL_CreateIndirect
这种方式常用于配合emWin的GUIBuilder工具或资源表(Resource Table)。你先定义一个
GUI_WIDGET_CREATE_INFO
结构体数组来描述界面,然后在运行时通过
GUI_CreateDialogBox
等函数一次性创建整个对话框及其所有控件。
static const GUI_WIDGET_CREATE_INFO _aDialogCreate[] = {
{ WINDOW_CreateIndirect, "MyDialog", 0, 0, 0, 320, 240, 0, 0, 0 },
{ LISTWHEEL_CreateIndirect, NULL, GUI_ID_LISTWHEEL0, 50, 100, 200, 150, 0, 0, (const void*)_apWeekdays },
// ... 其他控件
};
这种方式将UI布局与逻辑代码分离,便于管理和修改。
LISTWHEEL_CreateIndirect
的内部实现,最终也是调用了
LISTWHEEL_CreateEx
。
3.1.3 带用户数据的创建:LISTWHEEL_CreateUser
这个函数是
LISTWHEEL_CreateEx
的扩展,允许你在创建控件时,额外分配一块用户数据内存(Extra Bytes)并关联到控件句柄上。这块内存可以通过
LISTWHEEL_SetUserData
和
LISTWHEEL_GetUserData
来存取。
什么时候用它? 假设你的每个列表项不是一个简单的字符串,而是一个结构体,包含ID、图标索引、状态标志等。你可以在这块用户数据里存储一个指向你自定义数据结构的指针。在自定义绘制函数中,通过句柄获取这个指针,然后根据当前绘制项索引,从你的数据结构中取出丰富的信息进行绘制。
typedef struct {
U16 id;
const char* text;
const GUI_BITMAP* pIcon;
} MY_ITEM_DATA;
MY_ITEM_DATA myData[10];
// ... 初始化 myData
// 假设通过某种方式(如修改控件Class)分配了额外4字节(一个指针的大小)
hListWheel = LISTWHEEL_CreateUser(50, 100, 200, 150, hParent, WM_CF_SHOW, 0, ID_LISTWHEEL, _apDummyText);
LISTWHEEL_SetUserData(hListWheel, (void*)myData);
3.2 外观与行为的基础配置
创建好控件后,我们通常需要调整它的外观和行为,以符合UI设计。
3.2.1 字体、颜色与边框
// 设置字体 - 使用emWin内置字体或自定义字体
LISTWHEEL_SetFont(hListWheel, &GUI_Font16B_1); // 设置为16点阵粗体
// 设置颜色 - 区分选中和未选中状态
LISTWHEEL_SetTextColor(hListWheel, LISTWHEEL_CI_UNSEL, GUI_BLACK); // 未选中项文字颜色
LISTWHEEL_SetTextColor(hListWheel, LISTWHEEL_CI_SEL, GUI_WHITE); // 选中项文字颜色
LISTWHEEL_SetBkColor(hListWheel, LISTWHEEL_CI_UNSEL, GUI_LIGHTGRAY); // 未选中项背景色
LISTWHEEL_SetBkColor(hListWheel, LISTWHEEL_CI_SEL, GUI_BLUE); // 选中项背景色
// 设置文本对齐方式
LISTWHEEL_SetTextAlign(hListWheel, GUI_TA_HCENTER | GUI_TA_VCENTER); // 水平垂直居中
// 设置左右边距,让文字不紧贴边框
LISTWHEEL_SetLBorder(hListWheel, 10); // 左边距10像素
LISTWHEEL_SetRBorder(hListWheel, 10); // 右边距10像素
3.2.2 行高与吸附位置
行高决定了每一项的视觉高度。如果不设置,控件会自动使用当前字体的高度加上一些内部间距。
// 获取当前字体高度
int FontHeight = GUI_GetYSizeOfFont(LISTWHEEL_GetFont(hListWheel));
// 设置行高为字体高度+10像素,获得更宽松的视觉效果
LISTWHEEL_SetLineHeight(hListWheel, FontHeight + 10);
// 设置吸附位置为控件垂直中心。这是最常用的设置,让选中项停在正中间。
int WidgetHeight;
WM_GetWindowSizeEx(hListWheel, NULL, &WidgetHeight);
LISTWHEEL_SetSnapPosition(hListWheel, WidgetHeight / 2);
实操心得:吸附位置的“陷阱” 新手常犯的一个错误是忘记设置吸附位置,或者设得不对。默认吸附位置是0(控件顶部)。如果你有一个高150像素的控件,行高30像素,那么当第一项(索引0)吸附在顶部时,它只有上半部分在控件内,体验很奇怪。 通常应将吸附位置设置为
控件高度/2,让选中项在控件中央高亮,上下项部分可见,这是最符合物理滚轮隐喻和用户心理预期的。
3.2.3 动态内容管理
列表内容并非一成不变。
// 1. 追加新项
LISTWHEEL_AddString(hListWheel, "New Day");
// 2. 完全替换所有项
static const char* _apNewList[] = {"Jan", "Feb", "Mar", ..., NULL};
LISTWHEEL_SetText(hListWheel, _apNewList); // 这会清除旧列表!
// 3. 获取当前项文本
char CurrentText[50];
int SelIndex = LISTWHEEL_GetSel(hListWheel); // 或 LISTWHEEL_GetPos(hListWheel)
LISTWHEEL_GetItemText(hListWheel, SelIndex, CurrentText, sizeof(CurrentText));
// 4. 获取总项数
int TotalItems = LISTWHEEL_GetNumItems(hListWheel);
3.2.4 控制滚动行为
这是让LISTWHEEL拥有“灵魂”的关键——模拟真实的物理滚动感。
// 设置减速值。值越大,滚动停止得越快。默认是15。
LISTWHEEL_SetDeceleration(hListWheel, 20); // 比默认更“粘手”,快速停止
// 设置定时器周期,影响滚动动画的刷新率。默认25ms(40FPS)。
LISTWHEEL_SetTimerPeriod(hListWheel, 20); // 提高到50FPS,动画更平滑,但更耗CPU
// 以编程方式启动滚动(模拟用户快速滑动)
// Velocity值可正可负,代表方向。绝对值越大,初始速度越快,滚动时间越长。
LISTWHEEL_SetVelocity(hListWheel, 50);
// 直接跳转到指定项(无动画)
LISTWHEEL_SetPos(hListWheel, 3); // 直接跳到第4项(索引从0开始)
// 滚动到指定项(带动画)
LISTWHEEL_MoveToPos(hListWheel, 3); // 滚动到第4项
LISTWHEEL_MoveToPos
会智能地选择最短路径。例如,当前在第2项(索引1),要跳到第7项(索引6),如果总共有10项,它会向后滚动经过3,4,5,6到达7,而不是向前滚动经过0,9,8再到7。
4. 高级应用:自定义绘制与深度交互处理
当默认的文本列表无法满足你的UI设计时,自定义绘制(Owner Draw)功能就闪亮登场了。这是LISTWHEEL控件最强大、最灵活的特性。
4.1 理解Owner Draw机制
自定义绘制的核心是
LISTWHEEL_SetOwnerDraw
函数。你提供一个自定义的回调函数,控件在需要绘制每一项、或者获取某项尺寸时,就会调用你这个函数,而不是调用默认的
LISTWHEEL_OwnerDraw
。
回调函数的原型是:
int (*WIDGET_DRAW_ITEM_FUNC)(const WIDGET_ITEM_DRAW_INFO *pDrawItemInfo);
WIDGET_ITEM_DRAW_INFO
结构体是关键,它包含了本次绘制任务的所有信息:
typedef struct {
int Cmd; // 命令:绘制、获取尺寸等
WM_HWIN hWin; // 控件窗口句柄
int ItemIndex; // 当前需要绘制的项索引
int x0, y0, x1, y1; // 绘制区域的坐标
GUI_RECT Rect; // 同上,矩形结构
const void* p; // 附加数据指针,对于LISTWHEEL,这是字符串指针
GUI_DRAW_STATE DrawState; // 绘制状态(选中、按下、禁用等)
} WIDGET_ITEM_DRAW_INFO;
4.2 实现一个带图标的列表项
假设我们要做一个音乐播放器的歌曲列表,每一项左侧有一个图标(播放/暂停),然后是歌曲名和艺术家。
首先,定义我们的数据结构和图标资源:
// 自定义数据结构
typedef struct {
const char* songName;
const char* artist;
int isPlaying; // 0: 未播放,1: 播放中
} SongItem;
SongItem myPlaylist[] = {
{"Song A", "Artist 1", 0},
{"Song B", "Artist 2", 1}, // 当前正在播放
{"Song C", "Artist 3", 0},
// ...
};
// 假设已有播放和暂停的位图资源
extern GUI_CONST_STORAGE GUI_BITMAP bmPlay;
extern GUI_CONST_STORAGE GUI_BITMAP bmPause;
然后,编写自定义绘制函数:
static int _cbOwnerDraw(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) {
SongItem* pSong;
int TextHeight, IconWidth;
char dispText[60];
switch (pDrawItemInfo->Cmd) {
case WIDGET_ITEM_GET_YSIZE:
// 当控件需要知道每一项的高度时调用。
// 我们返回一个固定高度,比如40像素。
return 40;
case WIDGET_ITEM_DRAW:
// 这是最主要的绘制命令。
// 1. 获取当前项的用户数据(这里我们简化处理,直接通过索引访问全局数组)
// 在实际项目中,你可能通过LISTWHEEL_GetUserData获取一个数据结构指针。
int index = pDrawItemInfo->ItemIndex;
if (index < 0 || index >= NUM_ITEMS) {
return 0; // 索引无效,交给默认处理或直接返回
}
pSong = &myPlaylist[index];
// 2. 根据状态设置颜色
if (pDrawItemInfo->DrawState & GUI_DRAW_STATE_SELECTED) {
GUI_SetColor(GUI_BLUE);
GUI_SetTextColor(GUI_WHITE);
GUI_SetBkColor(GUI_BLUE);
} else {
GUI_SetColor(GUI_LIGHTGRAY);
GUI_SetTextColor(GUI_BLACK);
GUI_SetBkColor(GUI_LIGHTGRAY);
}
// 填充项背景
GUI_FillRect(pDrawItemInfo->x0, pDrawItemInfo->y0,
pDrawItemInfo->x1, pDrawItemInfo->y1);
// 3. 绘制图标 (假设图标宽高24x24,左边距5像素)
const GUI_BITMAP* pIcon = (pSong->isPlaying) ? &bmPause : &bmPlay;
GUI_DrawBitmap(pIcon, pDrawItemInfo->x0 + 5,
pDrawItemInfo->y0 + (40 - 24) / 2); // 垂直居中
// 4. 绘制文本
// 组合歌曲名和艺术家
sprintf(dispText, "%s - %s", pSong->songName, pSong->artist);
GUI_SetFont(&GUI_Font13_1);
// 文本区域从图标右侧开始,右边留一些空间
GUI_DispStringInRect(dispText,
&(GUI_Rect){pDrawItemInfo->x0 + 5 + 24 + 5,
pDrawItemInfo->y0,
pDrawItemInfo->x1 - 5,
pDrawItemInfo->y1},
GUI_TA_LEFT | GUI_TA_VCENTER);
break;
default:
// 对于不处理的命令(如WIDGET_ITEM_DRAW_BACKGROUND等),
// 调用默认的绘制函数来处理,比如绘制聚焦框等。
return LISTWHEEL_OwnerDraw(pDrawItemInfo);
}
return 0; // 成功处理
}
最后,在创建控件后设置这个自定义绘制函数:
// 创建控件时,可以传一个空数组或占位数组,因为内容将由我们自定义
static const char* _dummyText[] = {NULL};
hListWheel = LISTWHEEL_CreateEx(..., _dummyText);
LISTWHEEL_SetOwnerDraw(hListWheel, _cbOwnerDraw);
// 必须手动设置行高,因为默认的GET_YSIZE不会生效了(除非你在回调里处理了)
LISTWHEEL_SetLineHeight(hListWheel, 40);
深度解析与避坑 :
WIDGET_ITEM_GET_YSIZE命令必须处理 :如果你不处理这个命令,或者你的自定义函数总是返回0,那么控件将无法知道每一项有多高,导致布局计算错误,可能无法正常滚动或显示。 务必根据你的绘制内容返回一个准确的像素高度 。GUI_DRAW_STATE的运用 :pDrawItemInfo->DrawState包含了该项的视觉状态,如GUI_DRAW_STATE_SELECTED(选中)、GUI_DRAW_STATE_FOCUS(焦点)、GUI_DRAW_STATE_GRAYED(禁用)等。根据这些状态来改变颜色、图标,是实现交互反馈的关键。- 默认函数的调用 :在
default分支中调用LISTWHEEL_OwnerDraw(pDrawItemInfo)是一个好习惯。这确保了那些你不打算自定义处理的命令(比如WIDGET_ITEM_DRAW_BACKGROUND用于绘制默认背景,或者WIDGET_ITEM_DRAW_OVERLAY用于绘制叠加层)仍然有默认行为,保持控件的完整性。- 性能考量 :自定义绘制函数会在滚动动画的每一帧被频繁调用。避免在函数内部进行复杂计算或资源加载(如从SD卡读取图片)。所有位图、字体等资源应在初始化阶段就加载到内存(或能够快速访问的存储区)。
4.3 处理用户交互与通知
LISTWHEEL通过向父窗口发送
WM_NOTIFY_PARENT
消息来报告交互事件。你需要在父窗口(也就是创建LISTWHEEL时传入的
hParent
)的回调函数中处理这些消息。
static void _cbParentWindow(WM_MESSAGE * pMsg) {
WM_HWIN hWin = pMsg->hWin;
int NCode, Id;
switch (pMsg->MsgId) {
case WM_NOTIFY_PARENT:
Id = WM_GetId(pMsg->hWinSrc); // 获取发送通知的控件ID
NCode = pMsg->Data.v; // 通知代码
if (Id == GUI_ID_LISTWHEEL0) { // 判断是否是我们的LISTWHEEL
switch (NCode) {
case WM_NOTIFICATION_CLICKED:
// 控件被点击(按下)。可以在这里改变控件视觉反馈,比如变暗。
break;
case WM_NOTIFICATION_RELEASED:
// 控件被释放。这是完成“点击”操作的地方。
// 但注意,对于LISTWHEEL,释放不代表最终选择。
break;
case WM_NOTIFICATION_SEL_CHANGED:
// **最重要的通知!** 当有项吸附到Snap位置时发送。
{
int currentPos = LISTWHEEL_GetPos(pMsg->hWinSrc);
char selectedText[50];
LISTWHEEL_GetItemText(pMsg->hWinSrc, currentPos, selectedText, sizeof(selectedText));
// 更新UI其他部分,显示当前选择:selectedText
printf("Selected: %s (Index: %d)\n", selectedText, currentPos);
}
break;
case WM_NOTIFICATION_MOVED_OUT:
// 用户按下后,将手指/指针移出了控件范围然后释放。
// 通常用于取消点击操作。
break;
case WM_NOTIFICATION_VALUE_CHANGED:
// LISTWHEEL通常不发送这个。它是给SLIDER等控件用的。
break;
}
}
break;
// ... 处理其他消息
default:
WM_DefaultProc(pMsg); // 调用默认窗口过程处理其他消息
}
}
交互逻辑的精髓 : 很多初学者会误在
WM_NOTIFICATION_RELEASED里获取最终选择。但对于LISTWHEEL,用户释放手指时,滚动可能还在惯性运动,并未停止。 真正的最终选择事件是WM_NOTIFICATION_SEL_CHANGED。它会在滚动完全停止,且吸附位置上的项索引发生变化时触发。这是你更新应用状态(比如改变当前选中的日期、时间)的正确时机。
5. 实战技巧、性能优化与疑难排查
掌握了基础API和高级绘制后,我们来看看如何在实际项目中用好LISTWHEEL,并解决那些棘手的问题。
5.1 实战技巧:打造更佳用户体验
1. 动态加载长列表:
LISTWHEEL理论上可以支持非常多的项,但一次性加载成千上万个字符串会消耗大量内存。对于超长列表(如通讯录),可以采用“窗口”技术,只维护当前可见项及前后缓冲区的少量数据。当滚动时,动态替换
LISTWHEEL_SetText
的内容。这需要更复杂的逻辑,监听滚动位置(可通过定时器轮询
LISTWHEEL_GetPos
或计算项索引),并预加载数据。
2. 与键盘/编码器配合:
虽然手册说LISTWHEEL不直接响应键盘,但你可以在父窗口的
WM_KEY
消息处理中模拟滚动。
case WM_KEY:
switch (((WM_KEY_INFO*)(pMsg->Data.p))->Key) {
case GUI_KEY_UP:
// 模拟向上滚动:设置一个负速度,或直接MoveToPos到上一项
LISTWHEEL_SetVelocity(hListWheel, -30);
break;
case GUI_KEY_DOWN:
// 模拟向下滚动
LISTWHEEL_SetVelocity(hListWheel, 30);
break;
case GUI_KEY_ENTER:
// 确认选择,可以模拟一个“点击”或直接获取当前项
int sel = LISTWHEEL_GetPos(hListWheel);
// ... 处理选择
break;
}
break;
对于旋转编码器,可以将编码器的脉冲计数转换为速度或直接的位置变化,调用
LISTWHEEL_SetVelocity
或
LISTWHEEL_MoveToPos
。
3. 视觉增强:渐变与动画
在自定义绘制函数中,你可以根据项距离吸附位置的远近,实现字体大小、颜色透明度的渐变,营造出更立体的3D滚轮效果。这需要你在
WIDGET_ITEM_DRAW
命令中,计算当前绘制项相对于控件中心的Y坐标偏移量,然后根据这个偏移量插值计算颜色或缩放比例。
5.2 性能优化要点
嵌入式设备资源紧张,优化至关重要。
-
启用存储设备(Memory Device):
如果LISTWHEEL滚动时有明显的闪烁或撕裂,启用存储设备是最有效的解决方案。在创建控件前,使用
WM_SetCreateFlags(WM_CF_MEMDEV)为其父窗口或整个桌面窗口启用存储设备。这会将控件绘制到内存缓冲区,然后一次性刷屏,杜绝闪烁。 -
谨慎使用透明和Alpha混合:
在自定义绘制中,避免频繁使用
GUI_SetAlpha或绘制带Alpha通道的位图,这会给软件渲染器带来巨大负担。如果必须用,考虑使用硬件加速的emWin版本,或者将带透明度的效果做成静态位图资源。 -
优化绘制函数:
-
将
GUI_SetFont,GUI_SetColor等状态设置放在switch之外(如果所有项共用),减少重复调用。 -
对于固定图标,使用
GUI_DrawBitmap而不是GUI_DrawBitmapExp或GUI_DrawBitmapMag,除非你需要缩放。 -
避免在绘制函数内进行字符串格式化(如
sprintf),尽量在数据准备阶段完成。
-
将
-
调整定时器周期:
LISTWHEEL_SetTimerPeriod调小(如10ms)会让动画更流畅,但CPU占用更高。调大(如50ms)会节省CPU,但动画卡顿。需要根据你的MCU主频和整体UI复杂度找到平衡点。通常25ms-40ms(40-25 FPS)是一个不错的起点。
5.3 常见问题排查实录
下面是我在项目中遇到的一些典型问题及解决方法,整理成了速查表:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 控件完全不显示 |
1. 创建失败,句柄为0。
2. 父窗口不可见或已删除。 3. 坐标在屏幕外。 |
1. 检查
LISTWHEEL_CreateEx
返回值。确保字符串数组以
NULL
结尾。
2. 确认父窗口
hParent
有效且已显示(
WM_ShowWindow
)。
3. 检查x0, y0, xSize, ySize参数是否在屏幕范围内。 |
| 列表项显示为乱码或空白 |
1. 字符串编码问题(非ASCII)。
2. 字体设置错误或未包含相应字库。 3. 文本颜色与背景色相同。 |
1. 确保字符串是emWin支持的编码(如UTF-8,需开启
GUI_SUPPORT_UNICODE
)。
2. 调用
LISTWHEEL_GetFont
检查当前字体,并用
GUI_DispString
在屏幕其他地方测试该字体是否能显示你的文本。
3. 用
LISTWHEEL_SetTextColor
设置一个对比明显的颜色。
|
| 触摸滚动无反应 |
1. 触摸屏驱动未正确初始化或校准。
2. 控件未获得焦点。 3. 父窗口或控件本身被禁用。 |
1. 在其他控件(如BUTTON)上测试触摸是否正常。
2. 使用
WM_SetFocus
将焦点设置到LISTWHEEL控件。
3. 检查是否调用了
WM_DisableWindow
。
|
| 滚动动画卡顿、闪烁 |
1. 未使用存储设备(MEMDEV)。
2. 自定义绘制函数过于复杂耗时。 3. 系统其他任务阻塞太久。 |
1. 为窗口启用
WM_CF_MEMDEV
。
2. 优化绘制代码,见上一节。使用性能分析工具定位耗时点。 3. 检查是否在中断或高优先级任务中执行了耗时GUI操作。确保GUI任务有足够的执行时间。 |
WM_NOTIFICATION_SEL_CHANGED
不触发
|
1. 吸附位置设置不当,导致没有项能“吸附”上去。
2. 滚动从未停止(如速度设置极高,减速值极低)。 3. 通知被父窗口回调函数错误地处理或忽略。 |
1. 打印或调试
LISTWHEEL_GetSnapPosition
和控件高度,确保吸附位置在控件内部。通常设为高度/2。
2. 检查
LISTWHEEL_SetDeceleration
的值,太小的值会导致滚动几乎不停。设为合理值(10-30)。
3. 在父窗口回调的
WM_NOTIFY_PARENT
case中,确保没有提前
return
,并且正确判断了控件ID。
|
| 自定义绘制项高度不对 |
自定义绘制函数未处理或错误处理
WIDGET_ITEM_GET_YSIZE
命令。
|
在自定义绘制函数的
switch
中,必须为
WIDGET_ITEM_GET_YSIZE
命令返回一个准确的像素高度值。这个值应与你在
WIDGET_ITEM_DRAW
中绘制的实际高度一致。同时,也需要调用
LISTWHEEL_SetLineHeight
设置这个高度。
|
| 内存占用过大 |
1. 列表项字符串全部存储在RAM中。
2. 自定义绘制加载了未压缩的大位图。 |
1. 将不变的字符串用
GUI_CONST_STORAGE
存储在Flash中。
2. 对位图资源使用emWin的位图转换器生成C数组,并启用压缩格式(如RLE4, RLE8)。在绘制时使用
GUI_DrawBitmap
即可自动解压。
|
最后一个小技巧:调试利器
GUI_DEBUG_LOG
emWin提供了一个调试日志功能
GUI_DEBUG_LOG
。在复杂的自定义绘制或消息处理调试中,可以在关键位置添加日志输出,帮助理解函数调用顺序和参数状态。记得在
GUIConf.h
中启用
GUI_DEBUG_LEVEL
和
GUI_DEBUG_LOG
。

1020


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



