1. 项目概述在嵌入式GUI开发领域emWin以其高效、稳定和丰富的控件库而著称是许多嵌入式设备人机交互界面的首选。无论是工业控制面板上的一个急停按钮还是智能家居中控屏上的一个模式选择开关其背后都离不开BUTTON和CHECKBOX这类基础控件的支撑。然而仅仅调用BUTTON_CreateEx或CHECKBOX_CreateEx创建一个默认样式的控件往往难以满足产品对UI美观度和交互一致性的高要求。这时深入理解控件的底层机制尤其是其自定义绘制能力就显得至关重要。本文将从一线开发者的视角带你深入emWin的BUTTON与CHECKBOX控件内部。我们不仅会梳理其完整的创建、配置API更会聚焦于两个高级主题如何通过WIDGET_SetEffect为控件赋予不同的视觉风格如3D、简约效果以及如何通过实现WIDGET_DRAW_ITEM_FUNC回调函数彻底接管控件的绘制过程实现从颜色、形状到动态效果的完全自定义。掌握这些技能意味着你能让界面控件摆脱库的默认“皮肤”真正服务于你的产品设计语言。2. 控件核心原理与窗口管理器基础在深入具体控件之前我们必须先理解emWin GUI的运作基石——窗口管理器。你可以把整个GUI界面想象成一个桌面而每个窗口包括对话框、控件都是这个桌面上的一个“图层”。窗口管理器负责管理这些图层的创建、销毁、显示、隐藏、焦点切换以及用户输入触摸、按键的分发。2.1 窗口与控件的关系在emWin中几乎所有可见的元素都是窗口。控件是一种特殊类型的窗口它预定义了特定的外观和行为。例如一个BUTTON控件本质上是一个具有按钮行为的窗口对象。当你在一个对话框本身也是一个窗口中创建一个按钮时这个按钮窗口就成为对话框窗口的子窗口。这种父子关系形成了窗口树输入事件会沿着这棵树进行传递和处理。创建控件时你会获得一个窗口句柄WM_HWIN或具体的BUTTON_Handle。这个句柄是你后续操作该控件的唯一凭证无论是设置文本、改变颜色还是订阅其通知消息。2.2 消息与通知机制emWin采用消息驱动模型。用户的任何操作如触摸按下、释放键盘按下都会被系统转化为消息发送给相应的窗口。控件在内部处理这些消息并据此改变自身状态如按钮按下时变暗。同时控件还会向它的父窗口发送“通知”告知父窗口“我身上发生了某事”。对于BUTTON和CHECKBOX最重要的通知是WM_NOTIFICATION_CLICKED点击和WM_NOTIFICATION_RELEASED释放。CHECKBOX还有一个独有的WM_NOTIFICATION_VALUE_CHANGED值改变。父窗口通过重写WM_NOTIFY_PARENT消息的处理函数就能捕获这些通知从而执行相应的业务逻辑例如点击按钮后打开一个新界面或勾选复选框后更新配置。理解了这个基础我们就能明白控件的“创建”本质上是向窗口管理器注册一个具有特定功能的子窗口而“配置”和“自定义绘制”则是通过API或回调函数去修改这个窗口的属性和其内部的绘制行为。3. BUTTON控件从创建到深度定制按钮是交互的起点一个响应灵敏、反馈清晰的按钮能极大提升用户体验。emWin的BUTTON控件提供了从简单文本按钮到复杂位图按钮的全套支持。3.1 创建按钮现代API与资源表创建控件有多个函数但BUTTON_CreateEx是目前推荐使用的、功能最全的创建函数。它提供了比旧版BUTTON_Create更精细的控制。BUTTON_Handle BUTTON_CreateEx(int x0, int y0, int xsize, int ysize, WM_HWIN hParent, int WinFlags, int ExFlags, int Id);x0, y0, xsize, ysize: 定义了按钮在父窗口坐标系中的位置和大小。这里有个实操细节如果你计划为按钮设置位图最好将按钮大小设置为与位图尺寸一致或者考虑好位图在按钮中的对齐方式否则可能出现拉伸或裁剪。hParent: 父窗口句柄。如果设为0则按钮将成为桌面顶级窗口的直接子窗口。在对话框应用中通常传入对话框的句柄。WinFlags: 窗口创建标志。最常用的是WM_CF_SHOW创建后立即显示。其他如WM_CF_MEMDEV可用于内存设备支持以优化闪烁。ExFlags: 扩展标志当前版本保留未用设为0即可。Id: 控件ID。当按钮被点击时这个ID会随着WM_NOTIFY_PARENT消息发送给父窗口用于区分是哪个按钮触发了事件。对于大型项目更优雅的方式是使用“资源表”配合BUTTON_CreateIndirect。资源表是一个结构体数组以声明式的方式定义界面中的所有控件及其属性。这种方式将UI布局与业务逻辑代码分离便于管理和修改。3.2 基础属性配置文本、颜色与位图创建按钮后我们可以通过一系列Set函数来配置其外观。文本与字体BUTTON_SetText设置按钮文字BUTTON_SetFont设置字体。默认字体是GUI_Font13_1。注意事项如果按钮大小固定而文本过长超出的部分会被裁剪。务必在设计阶段考虑文本长度或使用GUI_GetStringDistX函数动态计算文本宽度来调整按钮大小。颜色设置这是定制按钮视觉的关键。BUTTON_SetBkColor和BUTTON_SetTextColor函数都接受一个Index参数用于区分不同状态。BUTTON_CI_UNPRESSED: 未按下状态。BUTTON_CI_PRESSED: 按下状态。BUTTON_CI_DISABLED: 禁用状态。 通过为不同状态设置不同的颜色可以直观地向用户反馈按钮的当前状态。例如按下时背景色变深禁用时变为灰色。位图按钮使用BUTTON_SetBitmapEx可以为按钮的不同状态设置不同的位图。这对于创建图标按钮、带纹理的按钮非常有用。Index参数同样对应BUTTON_BI_UNPRESSED、BUTTON_BI_PRESSED和BUTTON_BI_DISABLED。x和y参数可以微调位图在按钮中的位置。3.3 高级特性触摸响应模式与焦点BUTTON_REACT_ON_LEVEL配置选项或BUTTON_SetReactOnLevel函数控制着按钮对触摸事件的响应逻辑这是一个容易忽略但影响体验的重要细节。默认模式BUTTON_REACT_ON_LEVEL 0按钮对每个触摸消息都做出反应。手指按下WM_PID_STATE_CHANGED消息时按钮立即变为按下状态手指在屏幕上滑动时如果进入按钮区域按钮也会变为按下状态离开则恢复。这种模式在触摸屏上能提供连续的视觉反馈。电平模式BUTTON_REACT_ON_LEVEL 1按钮只在触摸“电平”变化时反应。即只有当手指在按钮区域内按下并释放按钮状态才会改变一次。在手指按下状态下滑过按钮按钮不会有反应。这种模式可以避免一些误触特别是在有重叠窗口或复杂手势的场景下。例如一个弹出对话框的关闭按钮覆盖在主界面的某个功能按钮上关闭弹出框时手指滑过电平模式可以防止误触发底层按钮。另一个细节是焦点。默认情况下按钮是可以接收输入焦点的通过Tab键切换。你可以用BUTTON_SetFocussable(hObj, 0)来禁止某个按钮获取焦点。当按钮拥有焦点时会显示一个焦点矩形颜色可通过BUTTON_SetFocusColor设置这对于纯键盘操作的设备如某些工业面板是必要的视觉提示。4. CHECKBOX控件状态、样式与三态支持复选框用于表示二元或三元选择状态在设置界面中极为常见。emWin的CHECKBOX控件不仅支持标准的勾选/未勾选还支持一个“第三态”例如“部分选中”。4.1 创建与基础状态管理CHECKBOX的创建函数CHECKBOX_CreateEx参数与BUTTON类似。一个关键点是xsize和ysize参数如果设为0控件将使用默认的勾选框位图大小11x11像素加上效果尺寸作为自身大小。如果你需要显示旁边的文本或者使用自定义的大尺寸图标务必指定足够大的尺寸。状态管理是CHECKBOX的核心CHECKBOX_SetState(hObj, state): 设置状态state为0未选、1选中或2第三态。CHECKBOX_GetState(hObj): 获取当前状态。CHECKBOX_IsChecked(hObj): 这是一个便捷函数只返回是否选中1或0忽略第三态。CHECKBOX_SetNumStates(hObj, 3): 启用第三态支持。在启用前state传入2是无效的。实操心得在处理CHECKBOX的WM_NOTIFICATION_VALUE_CHANGED通知时不要简单地根据CHECKBOX_IsChecked来判断而应该使用CHECKBOX_GetState因为你的业务逻辑可能需要处理“部分选中”这个中间状态。例如在一个文件管理器中用于“全选”的复选框当只有部分文件被选中时就应处于第三态。4.2 外观定制图片、颜色与文本CHECKBOX的外观由几个部分构成左侧的勾选框本质上是位图、右侧的文本标签、以及整个控件的背景。自定义勾选框图片这是深度定制CHECKBOX外观最强大的方式。通过CHECKBOX_SetImage函数你可以为6种状态分别设置位图CHECKBOX_BI_ACTIV_UNCHECKED: 启用未选中CHECKBOX_BI_ACTIV_CHECKED: 启用选中CHECKBOX_BI_ACTIV_3STATE: 启用第三态CHECKBOX_BI_INACTIV_*: 对应上述三种状态的禁用版本。 这意味着你可以用任何你设计的图形来替代默认的“方框对勾”比如圆形开关、自定义图标等。重要提示你提供的位图必须完全填充CHECKBOX控件为它分配的“框”区域。如果控件创建时大小是20x20你的位图也应该是20x20否则会拉伸或留白。颜色设置CHECKBOX的颜色控制比BUTTON更细分。CHECKBOX_SetBkColor: 设置控件整体的背景色。如果设置为GUI_INVALID_COLOR则控件背景透明会显示其下层窗口的内容。CHECKBOX_SetBoxBkColor: 设置勾选框区域的背景色。这个颜色只有在使用的位图是透明背景或者没有设置位图时才会显现。默认的勾选框位图是透明的所以设置这个颜色会改变框内的底色。CHECKBOX_SetTextColor: 设置右侧文本的颜色。文本与布局CHECKBOX_SetText设置文本。CHECKBOX_SetSpacing控制勾选框与文本之间的像素距离默认是4像素。CHECKBOX_SetTextAlign可以设置文本相对于勾选框的对齐方式默认是左对齐、垂直居中(GUI_TA_LEFT | GUI_TA_VCENTER)。5. 视觉效果的灵魂WIDGET_SetEffect详解WIDGET_SetEffect函数是emWin赋予控件统一视觉风格的利器。它通过一个WIDGET_EFFECT结构体定义了控件在绘制其3D边框、背景等元素时所使用的“效果”。5.1 效果类型与结构体emWin内置了三种效果WIDGET_EFFECT_NONE无效果扁平风格、WIDGET_EFFECT_SIMPLE简单凸起/凹陷和WIDGET_EFFECT_3D更强的3D立体感。WIDGET_EFFECT结构体内部包含了绘制函数指针、颜色参数等但通常我们不需要自己构造它而是使用库提供的预定义效果。// 为指定控件设置3D效果 WIDGET_SetEffect(hButton, WIDGET_Effect_3D); // 为指定控件设置简单效果 WIDGET_SetEffect(hButton, WIDGET_Effect_Simple); // 为指定控件取消效果扁平化 WIDGET_SetEffect(hButton, WIDGET_Effect_None);这个设置是全局性的会影响控件的边框、按下时的偏移等所有基于效果的绘制。例如设置了3D效果的按钮在按下时其内部的文本或位图会根据BUTTON_3D_MOVE_X/Y的配置默认为1产生一个像素的偏移模拟出被按下的物理感。5.2 效果的应用场景与限制在实际项目中WIDGET_SetEffect常用于快速统一整个应用程序的控件风格。你可以在程序初始化时为所有BUTTON和CHECKBOX甚至其他支持效果的控件设置同一种效果。注意事项效果与自绘的冲突如果你为控件设置了完全的自定义绘制回调Owner Drawing那么WIDGET_SetEffect设置的效果很可能不会被使用因为绘制逻辑已经完全由你的回调函数接管。效果系统是控件默认绘制流水线的一部分。性能考量3D效果通常比无效果需要更多的计算来绘制光影。在资源极其有限的低端MCU上如果界面刷新感到卡顿可以尝试将效果改为WIDGET_EFFECT_SIMPLE或WIDGET_EFFECT_NONE这是一个简单的优化手段。皮肤系统emWin的Skinning皮肤系统是比WIDGET_SetEffect更高级、更全面的视觉定制方案。皮肤可以定义控件各个部分如按钮的上边框、下边框、左上角、右下角等的绘制方式。当你需要极其复杂的定制时应该研究皮肤系统。WIDGET_SetEffect可以看作是一个轻量级的“皮肤”。6. 终极自由基于WIDGET_DRAW_ITEM_FUNC的自定义绘制当你觉得通过设置颜色、位图、效果仍然无法实现设计稿上的那个炫酷开关或异形按钮时自定义绘制Owner Drawing是你的终极武器。它允许你为控件注册一个回调函数当控件需要绘制自身时将调用你的函数来执行所有绘制操作。6.1 回调函数机制与参数解析支持Owner Drawing的控件如LISTBOX, BUTTON等会在创建时或通过特定API启用此模式。对于BUTTON虽然没有直接的BUTTON_SetOwnerDraw但我们可以通过理解其作为“窗口对象”的通用性在某些高级用法中结合窗口回调实现类似效果。更典型的例子是LISTBOX的每一项。其核心是WIDGET_DRAW_ITEM_FUNC类型的回调函数int MyOwnerDrawCallback(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo);WIDGET_ITEM_DRAW_INFO结构体是这个回调函数的灵魂它携带了本次绘制任务的所有上下文信息hWin: 需要绘制的控件窗口句柄。Cmd:最重要的成员指示本次回调需要执行什么操作。它决定了你的函数应该如何响应。ItemIndex: 要绘制的项目索引对于多项目控件如列表。x0, y0: 绘制区域的起始坐标窗口坐标系。6.2 响应绘制命令的两种策略回调函数必须正确处理Cmd参数。主要有两种策略策略一完全接管。你的函数处理所有Cmd包括计算项目大小(WIDGET_ITEM_GET_XSIZE/YSIZE)和执行绘制(WIDGET_ITEM_DRAW)。这给了你最大的自由度但工作量也最大。策略二混合绘制。这是更常见且推荐的方式。你只处理你关心的部分通常是WIDGET_ITEM_DRAW对于其他命令如获取尺寸则调用控件库提供的默认处理函数。这能保证控件的基础行为如尺寸计算正确同时允许你自定义外观。例如为一个LISTBOX项目实现自定义绘制int MyListBoxDraw(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) { switch (pDrawItemInfo-Cmd) { case WIDGET_ITEM_GET_XSIZE: 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_BLUE); GUI_FillRect(x, y, x99, y19); // 假设项目宽100高20 // 2. 绘制文本 GUI_SetColor(GUI_WHITE); GUI_SetFont(GUI_Font16_1); GUI_DispStringAt(Custom Item, x5, y2); // 3. 如果需要还可以绘制边框、图标等 GUI_SetColor(GUI_LIGHTGRAY); GUI_DrawRect(x, y, x99, y19); return 0; } default: // 其他命令如WIDGET_DRAW_BACKGROUND交给默认处理 return LISTBOX_OwnerDraw(pDrawItemInfo); } }关键原则在WIDGET_ITEM_DRAW命令中你必须填满(x0, y0)到(x0 width -1, y0 height -1)的整个矩形区域不能留空。因为emWin在调用你的函数前已经设置了裁剪区域留空会导致该区域显示为未定义内容可能是上一次绘制残留。6.3 在BUTTON和CHECKBOX上应用自绘虽然官方文档更强调LISTBOX的自绘但BUTTON和CHECKBOX作为窗口对象其原理是相通的。一种高级用法是创建一个自定义的“皮肤”或“效果”在这个效果的回调函数中实现复杂的绘制逻辑然后通过WIDGET_SetEffect应用到控件上。这需要你深入理解并可能修改WIDGET_EFFECT结构体。更直接但更底层的方式是子类化控件窗口。通过WM_SetCallback函数替换控件的窗口回调函数在你的回调函数中拦截WM_PAINT消息。在WM_PAINT消息处理中你可以先调用原来的默认窗口过程以保持控件的焦点、边框等逻辑然后再进行额外的绘制或者完全自己重绘整个控件。这种方法更复杂但能力最强可以实现任何你能想象到的动态效果如渐变、动画。避坑指南性能第一自绘函数会被频繁调用滚动、焦点变化、按下状态变化时。函数内部应避免复杂的计算或内存分配。预先计算好颜色、坐标等值。状态管理在自绘函数中你需要通过控件API如BUTTON_IsPressed或查询窗口状态来获取控件的当前状态按下、禁用、获得焦点等并据此决定绘制内容。兼容性使用“混合绘制”策略调用默认的WIDGET_*_OwnerDraw函数来处理你不打算修改的命令这能最大程度保证与未来emWin版本的兼容性。7. 实战创建一个自定义风格的开关按钮理论说得再多不如动手一试。让我们综合运用上述知识创建一个自定义的开关按钮它拥有圆角矩形外观、平滑的颜色过渡并且按下时有缩放动画感通过改变绘制区域模拟。7.1 设计目标与实现思路目标创建一个类似iOS风格的开关按钮未按下时是灰色圆角矩形内部有一个白色圆形滑块在左侧按下开启时背景变为绿色白色滑块滑动到右侧。思路我们将使用Owner Drawing。由于标准BUTTON的自绘支持不如LISTBOX直接我们将采用一种变通方法创建一个普通的BUTTON但将其背景色设置为透明(GUI_INVALID_COLOR)并禁用默认效果。然后我们在这个BUTTON的父窗口或按钮自身的WM_PAINT处理中或者通过一个覆盖在按钮上方的自定义窗口来绘制我们想要的开关外观。为了简化这里我们阐述在按钮的WM_PAINT消息中拦截并自绘的核心逻辑需要子类化。7.2 关键代码实现步骤首先我们创建一个按钮并设置其回调函数。// 创建按钮位置大小根据你的UI设计设定 hSwitchBtn BUTTON_CreateEx(50, 50, 60, 30, hParent, WM_CF_SHOW, 0, GUI_ID_SWITCH); // 设置按钮文本为空因为我们完全自绘 BUTTON_SetText(hSwitchBtn, ); // 设置背景透明这样我们绘制的背景才能完全显示 BUTTON_SetBkColor(hSwitchBtn, BUTTON_CI_UNPRESSED, GUI_INVALID_COLOR); BUTTON_SetBkColor(hSwitchBtn, BUTTON_CI_PRESSED, GUI_INVALID_COLOR); // 禁用默认效果避免干扰 WIDGET_SetEffect(hSwitchBtn, WIDGET_Effect_None); // 设置窗口回调接管绘制 WM_SetCallback(hSwitchBtn, _cbSwitchButton);接下来实现回调函数_cbSwitchButtonstatic void _cbSwitchButton(WM_MESSAGE * pMsg) { switch (pMsg-MsgId) { case WM_PAINT: { BUTTON_Handle hObj pMsg-hWin; // 1. 调用原始按钮的绘制处理焦点矩形等基础元素可选这里我们完全自绘可以不调用 // WM_DefaultProc(pMsg); // 2. 获取按钮的当前状态和客户区 int Pressed BUTTON_IsPressed(hObj); GUI_RECT Rect; WM_GetClientRectEx(hObj, Rect); int width Rect.x1 - Rect.x0 1; int height Rect.y1 - Rect.y0 1; int radius height / 2 - 2; // 圆角半径 // 3. 开始绘制 GUI_SetBkColor(GUI_INVALID_COLOR); GUI_Clear(); // 清除背景透明 // 绘制背景轨道未按下灰色按下绿色 GUI_COLOR trackColor Pressed ? GUI_GREEN : GUI_GRAY; GUI_SetColor(trackColor); GUI_AA_FillRoundedRect(Rect.x0, Rect.y0, Rect.x1, Rect.y1, radius); // 绘制滑块白色圆形 GUI_SetColor(GUI_WHITE); int sliderRadius height - 4; int sliderX; if (Pressed) { // 开启状态滑块在右侧 sliderX Rect.x1 - sliderRadius - 2; } else { // 关闭状态滑块在左侧 sliderX Rect.x0 2; } int sliderY Rect.y0 (height - sliderRadius) / 2; GUI_AA_FillCircle(sliderX sliderRadius/2, sliderY sliderRadius/2, sliderRadius/2); // 4. 如果需要可以绘制一个很细的边框 GUI_SetColor(GUI_DARKGRAY); GUI_AA_DrawRoundedRect(Rect.x0, Rect.y0, Rect.x1, Rect.y1, radius); break; } case WM_TOUCH: // 如果需要处理触摸可以在这里添加逻辑但BUTTON本身已处理 default: // 将其他所有消息交给默认窗口过程处理以保证按钮的点击、焦点等行为正常 BUTTON_Callback(pMsg); break; } }7.3 状态同步与通知处理我们的自绘按钮外观依赖于BUTTON_IsPressed的状态。这个状态是由BUTTON控件自身在接收到WM_TOUCH等消息时更新的。因此我们不需要在回调函数中处理触摸逻辑只需依赖BUTTON的默认行为。当按钮状态因用户点击而改变时会触发WM_PAINT消息我们的绘制代码就会被调用从而更新外观。父窗口仍然可以像处理普通按钮一样监听来自这个自定义按钮的WM_NOTIFICATION_CLICKED通知以执行开关切换对应的业务逻辑如开启某个功能。注意事项抗锯齿示例中使用了GUI_AA_*函数绘制圆角和圆形这会使图形更平滑但也会消耗更多CPU资源。如果性能紧张可以使用GUI_FillCircle和GUI_FillRoundedRect等非抗锯齿函数或者使用预先制作好的圆角矩形位图。动态效果上述代码实现的是状态切换的“跳变”。如果你想实现滑块平滑移动的动画需要在WM_PAINT中根据一个动画进度变量来计算滑块位置并在一个定时器GUI_TIMER中不断更新这个变量并触发重绘(WM_InvalidateWindow)。这涉及到更复杂的状态机和消息处理。代码复用如果你需要在多个地方使用这种开关最好将创建和回调函数封装成一个独立的模块或函数通过参数化来控制大小、颜色等属性。通过这个实战案例你可以看到结合控件的标准API和自定义绘制我们几乎可以创造出任何符合产品需求的UI元素。这打破了GUI库自带控件样式的限制将设计的主动权完全交还给了开发者。