ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

emWin 5.26入门与实战:窗口、内存设备与控件开发

emWin 5.26入门与实战:窗口、内存设备与控件开发 简介《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.262014 年 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_ShowWindow。WM_DefaultProc是把未处理消息交给系统默认处理漏掉这一步会导致按键、触摸、聚焦等消息链断裂。这里我建议把GUI_SetBkColor和GUI_Clear放在WM_PAINT开头而不是在创建窗口时执行因为窗口尺寸变化或被遮挡后需要重新清底。2.2 无效区域机制与 WM_InvalidateRect另一个容易踩的坑是「主动重绘」和「被动重绘」的混淆。窗口不会因为外部变量改变而自动刷新必须先调用WM_InvalidateRect或WM_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 新增 APIV5.26 的版本记录里窗口管理器部分新增了WM_GetScrollbarH、WM_GetScrollbarV、WM_SetModalLayer、WM_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_CreateEx的GUI_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内存设备却创建为 32bppGUI_MEMDEV_CopyToLCD时颜色转换虽然是自动的但旋转和 Alpha 混合的输出格式可能会不符合预期。3.3 旋转、Alpha 与 Dithering按场景选择V5.26 手册第 14 章新增了GUI_MEMDEV_FadeOutDevices、GUI_MEMDEV_RotateAlpha、GUI_MEMDEV_RotateHQAlpha、GUI_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肉眼观感会好很多。代价是它会修改内存设备内部的数据格式不能翻转操作顺序先Dither32再RotateHQAlpha和先旋转再抖动最终效果和耗时都不同。4. Widget 实战BUTTON、LISTVIEW 与 MULTIPAGE 的配置顺序4.1 BUTTON 回调与 WM_NOTIFY_PARENTWidget 的消息不再靠轮询而是通过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_EnableCellSelect、LISTVIEW_GetItemRect、LISTVIEW_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_SetTabHeight和SetTabWidth的值要结合字体高度来计算默认字体下 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_GetEvent、GUI_MTOUCH_IsEmpty、GUI_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。本文还有配套的精品资源点击获取
返回列表