emWin 5.26入门与实战:窗口、内存设备与控件开发
2026/9/17 16:30:56 网站建设 项目流程

简介:《emWin5.26指导手册》是SEGGER官方发布的emWin图形库用户与参考指南,版本V5.26,文档修订版1,主要面向基于微控制器的嵌入式GUI应用开发。手册系统覆盖窗口管理、控件操作、2D图形库、触摸输入处理等核心模块,并针对MULTIPAGE等控件的新增函数和版本修订细节进行了说明,内容编排循序渐进,适合嵌入式软件工程师、电子爱好者以及相关专业学生系统查阅。压缩包内仅包含1个PDF文件,文件大小14.46MB,体积适中便于离线阅读和按需检索。目前已有141人学习浏览,作为官方一手资料,它能帮助读者快速理解emWin的架构设计、主要API用法以及GUI界面开发流程,尤其适合在STM32、ARM Cortex-M等资源受限平台上构建人机交互界面的场景,是一份实用且权威的参考手册。

1. 为一块 320×240 的屏,先读懂 emWin 5.26 的能力边界

手里有一块 320×240 或 800×480 的 TFT 屏,主控是 STM32F4 或其它 Cortex-M 系列的 MCU。在这个配置下,很多人会从点灯、画线开始刷像素,等界面牵涉到多窗口切换、控件交互、触摸输入时,代码会很快失控。SEGGER 的 emWin 5.26 指导手册解决的就是这个问题:它把窗口管理、2D 图形库、内存设备、控件库、触摸驱动组织成一套可复用的框架。这份文档对应的软件版本是 V5.26,2014 年 8 月发布,涵盖 WM、MEMDEV、Widget、MultiTouch、显示驱动等章节。对于刚接触 emWin 的开发者,它是最直观的入门依据;对于已经在使用的团队,版本记录里那些新增 API 才是翻手册的重点。

2. WM 回调与 2D 图形库:重绘时机决定闪烁

2.1 窗口不是画布,是有生命周期的对象

很多从裸机刷屏转过来的开发者,第一次看到WM_CreateWindow会误以为它只是「把一块区域标记为可画」。实际上 emWin 的窗口管理器维护着一棵窗口树,每个窗口都有回调函数,系统通过消息驱动它创建、移动、重绘、销毁。绘制代码不应该散落在主循环里,而是放进WM_PAINT分支内,由窗口管理器在合适的时机调用。

下面是一个最简窗口回调整体结构:

static void _cbWindow(WM_MESSAGE * pMsg) { switch (pMsg->MsgId) { case WM_PAINT: GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetColor(GUI_BLUE); GUI_FillRect(10, 10, 310, 230); GUI_DrawLine(0, 120, 320, 120); break; default: WM_DefaultProc(pMsg); } } WM_HWIN hWin; hWin = WM_CreateWindowAsChild(0, 0, 320, 240, hParent, WM_CF_SHOW, _cbWindow, 0);

WM_CreateWindowAsChild的最后一个0是额外参数,回调里可以通过pMsg->Data.v取到。WM_CF_SHOW决定窗口创建后立即可见,如果不加,需要手动调用WM_ShowWindowWM_DefaultProc是把未处理消息交给系统默认处理,漏掉这一步会导致按键、触摸、聚焦等消息链断裂。这里我建议把GUI_SetBkColorGUI_Clear放在WM_PAINT开头,而不是在创建窗口时执行,因为窗口尺寸变化或被遮挡后需要重新清底。

2.2 无效区域机制与 WM_InvalidateRect

另一个容易踩的坑是「主动重绘」和「被动重绘」的混淆。窗口不会因为外部变量改变而自动刷新,必须先调用WM_InvalidateRectWM_InvalidateWindow把区域标记为「失效」,窗口管理器才会在下一轮GUI_Exec时向回调发送WM_PAINT

WM_InvalidateRect(hWin, NULL); /* 整窗重绘 */ GUI_RECT rUpdate = {0, 0, 319, 100}; /* 只重绘顶部区域 */ WM_InvalidateRect(hWin, &rUpdate); /* 局部重绘 */

NULL表示全窗口无效,适合对话框初始化;局部刷新适合数值变化、进度条推进这类场景。WM_InvalidateRect的最大价值是减少像素写入量:在一块 320×240 的 16bpp 屏上,全屏重绘要写 153600 字节,而只刷新一个 100×30 的区域只需 6000 字节,对低速 SPI 屏的帧率影响非常明显。注意GUI_RECT是闭区间,{0, 0, 319, 100}实际宽度是 320,别在循环里多写一个像素。

2.3 4 个容易被忽略的 V5.26 新增 API

V5.26 的版本记录里,窗口管理器部分新增了WM_GetScrollbarHWM_GetScrollbarVWM_SetModalLayerWM_GetModalLayer。前两个是获取窗口关联的水平、垂直滚动条句柄,在自定义皮肤时很有用;后两个是模态层级控制,比普通WM_DisableWindow更精细。

WM_SetModalLayer(hDialog, 1); /* 将该窗口放入模态层,阻隔同层其它窗口输入 */

模态层的作用是限制输入而不销毁窗口树:弹窗所在的模态层开启后,同一层级的兄弟窗口收不到触摸和按键事件,但上一层窗口不受影响,这在多层菜单设计中比逐个禁用窗口省事得多。WM_GetModalLayer通常在调试时用来确认某个窗口到底属于哪一层,返回值配合WM_IsWindow可以快速定位事件路由问题。

下表是我在移植时常用的 WM 相关 API 速查:

API作用典型场景
WM_CreateWindowAsChild创建子窗口并绑定回调自定义控件的容器
WM_InvalidateRect使指定矩形区域失效局部刷新、滑动条重绘
WM_SetModalLayer设置模态层级弹窗、遮罩层
WM_GetScrollbarH/V获取窗口滚动条句柄联动滚动条位置
WM_DefaultProc默认消息处理每个窗口回调的最后兜底

使用WM_GetScrollbarH时注意返回值可能是 0,表示窗口从未创建过滚动条,直接拿返回值去调SCROLLBAR_SetValue会导致空指针。我一般会先判空再做后续操作,这一点在手册的示例代码里虽然没强调,但实际工程中很常见。

3. MEMDEV 内存设备:从防闪烁到旋转与 Alpha 混合

3.1 闪烁的根源:直接刷屏的代价

闪烁的本质是「绘制过程被用户看到」:画背景、画边框、画文字分多次写入 LCD,每次写入之间屏幕停留在中间状态,人眼就感知到了闪动。解决思路很简单——先画到一块内存缓冲区,全部画完再一次拷贝到 LCD。这就是内存设备(Memory Device)的核心思想。emWin 里对应的对象是GUI_MEMDEV_Handle,所有绘图函数都可以指向它,绘制完成后通过GUI_MEMDEV_CopyToLCD或者GUI_MEMDEV_Write输出。

下面是一段完整的创建、使用、销毁流程:

GUI_MEMDEV_Handle hMem; hMem = GUI_MEMDEV_CreateEx(0, 0, 320, 240, GUI_MEMDEV_NOTRANS); if (hMem) { GUI_MEMDEV_Select(hMem); /* 后续绘图都画进内存设备 */ GUI_SetBkColor(GUI_WHITE); GUI_Clear(); GUI_SetColor(GUI_BLACK); GUI_FillCircle(160, 120, 50); /* 复杂图形先在内存中完成 */ GUI_MEMDEV_Select(0); /* 切回 LCD,必须配对 */ GUI_MEMDEV_CopyToLCD(hMem); /* 一次性整块输出 */ GUI_MEMDEV_Delete(hMem); /* 释放内存 */ }

GUI_MEMDEV_CreateExGUI_MEMDEV_NOTRANS表示不启用透明通道,减少一半内存占用;如果需要窗口叠加和 Alpha 混合,改用GUI_MEMDEV_HASTRANS或者直接使用带 Alpha 的 32bpp 格式。GUI_MEMDEV_Select(0)这一步最容易遗漏,遗漏后后续所有绘图函数会继续写入内存设备,导致 LCD 画面静止不动。GUI_MEMDEV_CopyToLCD只适合整块显示区域相同的场景,如果内存设备尺寸和窗口尺寸不一致,显示偏移是常见问题,建议用GUI_MEMDEV_Write并传入目标坐标。

3.2 GUI_MEMDEV_Select 配对与异常排查

GUI_MEMDEV_Select本质上是把设备上下文切换到内存设备,它内部的LCD_SetDevFunc会把底层驱动函数指针替换成内存读写版本。因此两个多任务同时调用绘图函数时,必须用GUI_MEMDEV_Select前后的临界区保护,否则函数指针被改写时另一个任务正在绘屏,轻则花屏重则 HardFault。

我排查这类问题的固定路径是:先确认GUI_MEMDEV_Select(0)有没有被调用;再确认内存设备句柄有没有被意外覆盖;最后检查创建时传入的色深是否和 LCD 配置一致。比如 LCD 配置为 16bpp,内存设备却创建为 32bpp,GUI_MEMDEV_CopyToLCD时颜色转换虽然是自动的,但旋转和 Alpha 混合的输出格式可能会不符合预期。

3.3 旋转、Alpha 与 Dithering:按场景选择

V5.26 手册第 14 章新增了GUI_MEMDEV_FadeOutDevicesGUI_MEMDEV_RotateAlphaGUI_MEMDEV_RotateHQAlphaGUI_MEMDEV_Dither32。从函数名就能看出分工:FadeOutDevices处理多设备整体淡出;RotateAlpha做标准旋转并保留 Alpha 通道;RotateHQAlpha是高画质版本,内部做了双线性插值,适合图标和图片旋转;Dither32才是真正容易被忽视的一个。

GUI_MEMDEV_Handle hRot; hRot = GUI_MEMDEV_RotateHQAlpha(hMem, 900, 0); /* 900 = 90.0 度,0 表示旋转轴心 */ if (hRot) { GUI_MEMDEV_CopyToLCD(hRot); GUI_MEMDEV_Delete(hRot); }

角度参数的手册单位是 0.1 度,900 就是 90 度,我在第一次使用时直接传了 90,结果旋转了 9 度,排查半天。RotateHQAlpha的高画质模式对 CPU 开销不小,在 Cortex-M4 上旋转一张 320×240 的图片大约需要几十毫秒,不要在触摸回调里同步调用,否则界面卡顿会非常明显。下面是三个旋转相关 API 的选择表格:

API画质内存占用适用场景
GUI_MEMDEV_RotateAlpha标准90/180/270 度的固定旋转
GUI_MEMDEV_RotateHQAlpha高(插值)任意角度旋转,图标、照片
GUI_MEMDEV_RotateHQHR高(保持纵横比)需要缩放且不能变形的场景

GUI_MEMDEV_Dither32的用途是把 32bpp 的平滑渐变降到 16bpp 或 8bpp 色深时,通过颜色抖动减少色阶断层。如果你的屏是 18bpp RGB 面板但驱动配置成 16bpp,天空渐变和水波纹会出现明显的横向色带,这时候在拷贝前调用一次GUI_MEMDEV_Dither32,肉眼观感会好很多。代价是它会修改内存设备内部的数据格式,不能翻转操作顺序,先Dither32RotateHQAlpha和先旋转再抖动,最终效果和耗时都不同。

4. Widget 实战:BUTTON、LISTVIEW 与 MULTIPAGE 的配置顺序

4.1 BUTTON 回调与 WM_NOTIFY_PARENT

Widget 的消息不再靠轮询,而是通过WM_NOTIFY_PARENT通知父窗口。按下、释放、聚焦变化都走这个入口,区别只在pMsg->Data.v里的通知码。以 BUTTON 为例,最常用的是WM_NOTIFICATION_CLICKED(按下)和WM_NOTIFICATION_RELEASED(释放后抬起)。

static void _cbDialog(WM_MESSAGE * pMsg) { int Id, NCode; switch (pMsg->MsgId) { case WM_NOTIFY_PARENT: Id = WM_GetId(pMsg->hWinSrc); NCode = pMsg->Data.v; if (NCode == WM_NOTIFICATION_RELEASED) { if (Id == GUI_ID_BUTTON0) { /* 处理按钮事件,例如切换页面 */ } } break; default: WM_DefaultProc(pMsg); } }

WM_GetId(pMsg->hWinSrc)取到的是触发通知的子窗口 ID,而不是父窗口 ID,两者别混。如果多个按钮共用一个回调,务必先判断Id再判断NCode。触摸屏场景下,我建议关注WM_NOTIFICATION_CLICKED而不是RELEASED:物理按键需要在释放时确认,触摸屏用户期望「按下即响应」,这两者的差异在交互体验上非常明显。BUTTON_SetReactOnTouch这个 API 在 5.16 引入,就是用来把响应时机从释放改为按下的。

4.2 LISTVIEW:单元格选择与排序的边界

LISTVIEW 是嵌入式 UI 里最容易被写坏的控件:列宽、行高、排序、单元格选择,每一层都需要显式配置。5.26 里新增了LISTVIEW_EnableCellSelectLISTVIEW_GetItemRectLISTVIEW_SetItemTextSorted,这三个组合起来正好覆盖「高亮选中格 + 拖动滚动 + 点击表头排序」的完整交互。

LISTVIEW_Handle hList; hList = LISTVIEW_CreateEx(10, 40, 300, 160, hWin, WM_CF_SHOW, 0, GUI_ID_LIST0, 4, 0); LISTVIEW_AddColumn(hList, 60, "名称", 0); LISTVIEW_AddColumn(hList, 80, "数值", 1); LISTVIEW_EnableCellSelect(hList, 1); /* 启用单元格级选中 */ LISTVIEW_SetItemTextSorted(hList, 0, 1); /* 第 0 列按文本排序 */

LISTVIEW_EnableCellSelect启用后,聚焦框以单元格为单位而不是整行,这在小屏设备上能明显提高选中项的辨识度。LISTVIEW_SetItemTextSorted的第三个参数表示是否立即排序,如果数据行数超过几十条,建议先关闭自动排序,全部插入完再一次性打开,否则每插入一行都会触发布局重算。

LISTVIEW_GetItemRect可以获取某一项的屏幕坐标,适合做右键菜单定位或者 ToolTip 弹出。注意它返回的是列表客户区坐标,使用时需要配合WM_Screen2hWin转换成屏幕坐标,这一点在手册的「The Window Manager」章节里提到过,但示例代码里没展开。

4.3 MULTIPAGE:先调整页签再添加页面

MULTIPAGE 是页签容器,它的消息逻辑和 BUTTON 完全不同:子页面切换通过WM_NOTIFICATION_SEL_CHANGED通知父窗口,而不是在每个页面里单独处理。配置顺序上,我建议先设置页签尺寸和滚动条,再添加页面,避免页签布局在运行时反复重排:

hMulti = MULTIPAGE_CreateEx(0, 0, 320, 220, hWin, WM_CF_SHOW, 0, GUI_ID_MULTIPAGE0); MULTIPAGE_SetTabHeight(hMulti, 28); /* 页签高度 */ MULTIPAGE_SetTabWidth(hMulti, 80); /* 页签宽度 */ MULTIPAGE_EnableScrollBar(hMulti, 1, 1); /* 水平+垂直滚动条 */ MULTIPAGE_AddPage(hMulti, "参数", GUI_ID_USER + 1); MULTIPAGE_AddPage(hMulti, "曲线", GUI_ID_USER + 2);

MULTIPAGE_SetTabHeightSetTabWidth的值要结合字体高度来计算:默认字体下 28 像素的页签高度比较协调,小于 20 像素时文本会被裁剪。MULTIPAGE_EnableScrollBar的第 2、3 个参数分别控制水平和垂直滚动条,如果不需要滚动条,传 0 而不是删除调用,这样页签栏始终可见。MULTIPAGE_SetBitmap可以为每个页签设置图标,注意图标尺寸不能超过页签宽高,否则绘制时会被截断,这是我在移植时踩过的一个坑。

下面对应 V5.26 中 MULTIPAGE 新增的几个 API 的使用场景:

API使用场景注意事项
MULTIPAGE_SetTabHeight调整页签高度以适配大字体值过小会裁剪文字
MULTIPAGE_SetTabWidth统一页签宽度固定宽度下文字居中
MULTIPAGE_SetBitmap页签加图标图标尺寸要小于页签区域
MULTIPAGE_EnableScrollBar页签数量超出屏幕时开启滚动两轴可独立控制
MULTIPAGE_SetDefaultBorderSizeX调整页签和内容区水平间距SetBorderSizeY配合使用

5. GUI_TIMER 驱动周期刷新,配合 MTOUCH 合并触摸事件

5.1 GUI_TIMER 的创建与重启

GUI_TIMER_Create是窗口管理器内置的定时器,不是硬件定时器,它依赖GUI_Exec的周期性调用来计时。注意它的回调签名不是普通的void (*)(void),而是带WM_MESSAGE *参数,内部通过WM_TIMER消息分发。创建后调用GUI_TIMER_Restart可以重新计满一个周期,这个特性特别适合做周期性的数据采样刷新:

WM_HWIN hTimer; hTimer = GUI_TIMER_Create(_cbTimer, 0, 100, 0, 0); static void _cbTimer(WM_MESSAGE * pMsg) { switch (pMsg->MsgId) { case WM_TIMER: /* 周期性刷新曲线、数值或动画帧 */ GUI_TIMER_Restart(hTimer); /* 重新计时 */ break; } }

GUI_TIMER_Create的第三个参数是周期,单位毫秒。100ms 对应 10Hz 刷新率,对温度、电压这类缓变数据绰绰有余;动画则要看帧率需求,我一般会把动画计时设成 33ms 配合 MEMDEV 旋转,低于 20ms 时 CPU 占用会急剧上升。GUI_TIMER_Restart必须在WM_TIMER分支里调用,否则定时器只触发一次就停了。

5.2 MTOUCH 事件读取与坐标合并

5.24 引入的 MultiTouch 章节提供了GUI_MTOUCH_GetEventGUI_MTOUCH_IsEmptyGUI_MTOUCH_StoreEvent等接口。它和普通GUI_PID_GetState的区别在于缓存了多个触摸事件,不会因为主循环处理不及时而丢失中间状态。

GUI_MTOUCH_INPUT evt; while (!GUI_MTOUCH_IsEmpty()) { GUI_MTOUCH_GetEvent(&evt); if (evt.x >= 0 && evt.y >= 0) { xPos = evt.x; yPos = evt.y; } }

GUI_MTOUCH_GetEvent每次取一个事件,IsEmpty判断队列是否清空。evt.x / evt.y是物理触摸坐标,可能包含触摸屏的硬件偏移,需要先做校准。常见做法是把触摸坐标映射到 LCD 坐标后再进入逻辑层:先交换轴、再缩放、最后平移。GUI_MTOUCH_StoreEvent允许向队列里注入合成事件,这在模拟器调试和自动化测试里非常有用,可以绕过真实触摸屏直接驱动手势逻辑——我把这看作验证交互逻辑最快的一条路径。触摸层和显示层的坐标系不一致时,先做轴对齐和缩放,再进入GUI_MTOUCH_StoreEvent

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询