嵌入式GUI开发:emWin控件API设计哲学与CHECKBOX、DROPDOWN实战详解
1. 控件API设计哲学与emWin的实践
在嵌入式GUI开发里摸爬滚打了十几年,我越来越觉得,一套好的控件API,就像是给开发者的一把趁手的瑞士军刀。它不能太笨重,否则在资源受限的MCU上跑不起来;也不能太简陋,否则实现个稍微复杂点的界面就得自己造轮子。emWin在这方面做得相当不错,它把面向对象的思想巧妙地融入了C语言的环境里,核心就是那个“句柄”(Handle)。
你可以把句柄理解为一个控件的“身份证”或者“遥控器”。在emWin里,你创建一个CHECKBOX或者DROPDOWN,系统会在内部为这个控件分配一块内存,记录它的所有信息——位置、大小、颜色、状态、文字等等。然后,它会返回给你一个唯一的句柄,通常就是一个整型数值。之后你对这个控件的所有操作,比如改颜色、设文字、查状态,都是通过这个句柄来告诉系统:“嘿,我要操作的是身份证号为XXX的那个控件”。这种方式完美地封装了控件内部的复杂性,你不需要知道控件内部是怎么画出来的、状态机是怎么流转的,你只需要通过API这个清晰的接口去下达指令。
这种设计带来的最大好处就是可维护性和模块化。你的界面初始化代码可以很清晰:创建窗口、创建一堆控件、拿到它们的句柄存起来。在事件回调函数里,根据不同的消息和控件句柄,调用相应的API去更新界面。代码结构一目了然。另一个好处是动态性,你可以在运行时根据条件创建、销毁、修改控件属性,这让实现动态界面(比如根据用户选择显示不同配置项)变得非常自然。
emWin的API命名也很有规律,基本遵循<控件名>_<动作>_<对象>的模式,比如CHECKBOX_SetTextColor、DROPDOWN_GetSel。看到函数名,你大概就能猜出它是干什么的,这大大降低了学习和查阅手册的成本。接下来,我们就深入CHECKBOX和DROPDOWN这两个最常用也最具代表性的控件,看看它们的API到底能玩出什么花样。
2. CHECKBOX控件:不只是打勾那么简单
很多人觉得复选框(CHECKBOX)就是个简单的二选一开关,但在工业HMI或者复杂设备设置里,它的需求可能很细致。emWin的CHECKBOX API提供了从外观到行为的全方位控制。
2.1 创建与基础状态管理
创建控件是所有操作的起点。虽然手册里提到了CHECKBOX_Create,但那个已经被标记为“废弃”(deprecated)了。现在更推荐使用CHECKBOX_CreateEx,它提供了更多的控制参数。
CHECKBOX_Handle hCheckbox; hCheckbox = CHECKBOX_CreateEx(50, // x 位置 100, // y 位置 150, // 宽度 25, // 高度(注意是包含文本区域的总高) hParent, // 父窗口句柄 WM_CF_SHOW, // 创建后立即显示 0, // 扩展标志,通常为0 GUI_ID_CHECKBOX0); // 控件ID if (hCheckbox == 0) { // 创建失败处理,可能是内存不足 }注意:这里的
ySize(高度参数)需要特别注意。它指的是整个CHECKBOX控件(包括左侧的选框和右侧的文本标签)的总体高度。这个高度必须足够容纳你所设置的字体。如果高度太小,文本可能会显示不全。一个安全的做法是使用GUI_GetFontSizeY()获取当前字体高度,然后加上一些裕量(比如4-8个像素)作为控件高度。
创建好后,最核心的操作就是获取和设置它的选中状态。CHECKBOX_IsChecked()返回1表示选中,0表示未选中。而设置状态推荐使用CHECKBOX_SetState(),因为它能统一处理二态和三态。
// 判断是否选中 int isChecked = CHECKBOX_IsChecked(hCheckbox); if (isChecked) { printf(“复选框已被选中\n”); } // 设置为选中状态 CHECKBOX_SetState(hCheckbox, 1); // 第二个参数:0-未选,1-选中,2-第三态这里就引出了CHECKBOX的一个高级特性:三态支持。普通的复选框只有勾选/未勾选。但在某些场景,比如“全选”功能,或者表示一个“不确定”状态(例如,一个文件夹里部分文件被选中),就需要第三态。通过CHECKBOX_SetNumStates(hCheckbox, 3)启用三态后,复选框就会在三种状态间循环:空框、灰色勾选(或方框内一个点)、实心勾选。这在表示层级选择时非常有用。
2.2 视觉定制:从颜色、字体到图片
默认的CHECKBOX样式可能和你的UI设计风格不搭。emWin允许你进行深度的视觉定制。
颜色定制:你可以分别设置背景色、文本颜色和焦点框颜色。背景色设置有个小技巧:如果你想实现透明背景,让复选框浮现在一张图片或者其他窗口上,可以将背景色设置为GUI_INVALID_COLOR。
// 设置背景色为浅灰色,或者透明 CHECKBOX_SetBkColor(hCheckbox, GUI_GRAY_LIGHT); // 或者设置为透明 CHECKBOX_SetBkColor(hCheckbox, GUI_INVALID_COLOR); // 设置文本颜色为蓝色 CHECKBOX_SetTextColor(hCheckbox, GUI_BLUE); // 设置获得焦点时的矩形框颜色为红色 CHECKBOX_SetFocusColor(hCheckbox, GUI_RED);字体与文本:通过CHECKBOX_SetFont可以更换显示文本的字体。这在多语言界面或者需要突出显示时很重要。文本对齐方式通过CHECKBOX_SetTextAlign控制,比如你可以让文本相对于选框左对齐、右对齐或者居中(水平和垂直)。默认是左对齐且垂直居中(GUI_TA_LEFT | GUI_TA_VCENTER)。
// 设置为16像素的字体 CHECKBOX_SetFont(hCheckbox, &GUI_Font16_1); // 设置文本右对齐,垂直居中 CHECKBOX_SetTextAlign(hCheckbox, GUI_TA_RIGHT | GUI_TA_VCENTER); // 设置或更改复选框旁边的文字 CHECKBOX_SetText(hCheckbox, “启用高级选项”);实操心得:
CHECKBOX_SetTextAlign的对齐基准点是整个控件的矩形区域。如果你设置了右对齐,文本会紧贴控件右边界,可能会和选框离得很远。这时候就需要配合CHECKBOX_SetSpacing()来调整选框和文本之间的间距。默认间距是4像素,你可以根据视觉效果增大或减小。
终极自定义:图片替换。如果你觉得默认的“√”图标不好看,完全可以用自己的位图来替换。emWin允许你为复选框的每一种状态(未选中/选中/第三态,以及每种状态下的激活/禁用)设置不同的图片。这通过CHECKBOX_SetImage()函数并配合一系列索引宏来实现。
GUI_BITMAP bmUnchecked, bmChecked, bmIndeterminate; // ... 此处加载或定义你的位图 ... // 设置未选中状态下的激活态图片 CHECKBOX_SetImage(hCheckbox, &bmUnchecked, CHECKBOX_BI_ACTIV_UNCHECKED); // 设置选中状态下的激活态图片 CHECKBOX_SetImage(hCheckbox, &bmChecked, CHECKBOX_BI_ACTIV_CHECKED); // 设置第三态下的激活态图片 CHECKBOX_SetImage(hCheckbox, &bmIndeterminate, CHECKBOX_BI_ACTIV_3STATE);重要警告:当你使用自定义图片时,必须确保创建复选框时指定的控件宽度和高度足够大,能够完整显示你的位图以及文本(如果存在)。否则图片会被裁剪。一个稳妥的做法是,在设置图片前,先用
GUI_GetBitmapSize()获取位图尺寸,然后动态调整控件大小(通过WM_SetSize),或者一开始就创建足够大的控件。
2.3 “Default”系列API的妙用:统一风格管理
你可能已经注意到了,很多设置函数都有一个对应的“SetDefault”版本,比如CHECKBOX_SetDefaultFont、CHECKBOX_SetDefaultTextColor。这些函数是做什么的呢?
它们的作用是设置后续新创建的CHECKBOX控件的默认属性。这在你需要统一整个应用程序中所有复选框风格时,简直是神器。你不需要在每个创建控件的地方都重复写一遍设置代码。
// 在程序初始化阶段,统一设置所有未来创建的复选框的样式 CHECKBOX_SetDefaultFont(&GUI_Font16B_1); // 默认加粗字体 CHECKBOX_SetDefaultTextColor(GUI_DARKBLUE); // 默认深蓝色文字 CHECKBOX_SetDefaultBkColor(GUI_INVALID_COLOR); // 默认透明背景 CHECKBOX_SetDefaultSpacing(8); // 默认间距8像素 // 之后在代码任何地方创建的CHECKBOX,都会自动继承这些样式 hCheckbox1 = CHECKBOX_CreateEx(...); hCheckbox2 = CHECKBOX_CreateEx(...); // hCheckbox1和hCheckbox2已经具有了上面设置的默认样式这极大地提升了开发效率和一致性。如果你想单独修改某个控件的样式,之后再对其调用具体的Set函数(如CHECKBOX_SetFont)覆盖默认值即可。这种全局默认值加局部覆盖的模式,是管理复杂界面风格的优秀实践。
3. DROPDOWN控件:高效列表选择器的实现
下拉列表(DROPDOWN)是节省界面空间的利器,特别适合选项较多但不需要同时展示的场景。emWin的DROPDOWN本质上是一个“触发器”加一个“弹出式LISTBOX”的组合体。
3.1 创建、列表项管理与交互
创建DROPDOWN时,ySize参数有特殊含义:它指的是下拉列表展开时的高度。控件在折叠状态下的高度是由字体自动决定的。
DROPDOWN_Handle hDropdown; hDropdown = DROPDOWN_CreateEx(50, 100, 200, 150, // 展开后列表高150像素 hParent, WM_CF_SHOW, 0, GUI_ID_DROPDOWN0);创建后的第一件事就是添加选项。DROPDOWN_AddString()用于在末尾追加,DROPDOWN_InsertString()则可以在指定位置插入。
// 添加选项 DROPDOWN_AddString(hDropdown, “选项一”); DROPDOWN_AddString(hDropdown, “选项二”); DROPDOWN_AddString(hDropdown, “选项三”); // 在索引1的位置(即“选项二”之前)插入一个新选项 DROPDOWN_InsertString(hDropdown, “新增的选项”, 1);注意:
DROPDOWN_InsertString的索引是从0开始的。如果指定的索引大于当前项数,字符串会被追加到末尾,这可以作为一个安全的“追加”操作来使用。
获取当前选中项是核心操作。DROPDOWN_GetSel()返回当前选中项的索引(从0开始)。如果用户没有选择或列表为空,默认返回0。结合DROPDOWN_GetItemText()可以获取选中项的文本内容。
int selIndex = DROPDOWN_GetSel(hDropdown); char buffer[50]; if (DROPDOWN_GetItemText(hDropdown, selIndex, buffer, sizeof(buffer)) == 0) { printf(“当前选中的是:%s\n”, buffer); }通过DROPDOWN_SetSel()可以编程式地设置选中项,这在根据配置恢复界面状态时非常有用。DROPDOWN_IncSel()和DROPDOWN_DecSel()则可以在不展开列表的情况下,模拟“上下键”切换选中项,适合纯键盘操作的场景。
3.2 展开、折叠与列表控制
DROPDOWN的交互逻辑是:点击控件(或按空格键)展开列表,选择一项后(或失去焦点)列表折叠,显示选中项。你可以通过API手动控制这个过程。
// 手动展开下拉列表 DROPDOWN_Expand(hDropdown); // 手动折叠下拉列表 DROPDOWN_Collapse(hDropdown);实操心得:在触摸屏设备上,用户点击下拉框外部区域,DROPDOWN会自动折叠。但在某些复杂窗口层级下,自动折叠可能不灵敏。一个可靠的实践是,在父窗口的
WM_NOTIFY_PARENT消息处理中,监听其他控件获得焦点的通知(WM_NOTIFICATION_SEL_CHANGED等),然后主动调用DROPDOWN_Collapse()来关闭任何可能打开的下拉列表,避免多个下拉列表同时展开的混乱局面。
展开后的列表本质上是一个LISTBOX控件。你可以通过DROPDOWN_GetListbox()获取它的句柄,从而进行更精细的控制(但这属于高级用法,通常DROPDOWN自身的API已足够)。DROPDOWN_SetListHeight()可以动态调整展开列表的高度。如果选项很多,你可能需要启用滚动条。
3.3 滚动条与视觉高级定制
当列表项的总高度超过DROPDOWN_SetListHeight设置的高度时,就需要滚动条。DROPDOWN_SetAutoScroll(hDropdown, 1)可以启用自动滚动条。启用后,emWin会自动判断是否需要显示滚动条。
你可以进一步定制这个滚动条:
// 设置滚动条宽度(默认可能较宽,在小屏上需要调窄) DROPDOWN_SetScrollbarWidth(hDropdown, 12); // 设置为12像素宽 // 设置滚动条颜色(例如,设置拇指为蓝色) DROPDOWN_SetScrollbarColor(hDropdown, GUI_SCROLLBAR_CI_THUMB, GUI_BLUE); // GUI_SCROLLBAR_CI_THUMB 是滚动条拇指的索引,还可以设置背景色等DROPDOWN的视觉定制同样丰富。你可以分别设置未选中状态、选中但无焦点状态、选中且有焦点状态这三种情况下的背景色和文字颜色。这是通过DROPDOWN_SetBkColor和DROPDOWN_SetTextColor函数,并配合不同的颜色索引来实现的。
// 设置不同状态下的背景色 DROPDOWN_SetBkColor(hDropdown, 0, GUI_WHITE); // 索引0:未选中状态背景 DROPDOWN_SetBkColor(hDropdown, 1, GUI_GRAY); // 索引1:选中无焦点背景 DROPDOWN_SetBkColor(hDropdown, 2, GUI_BLUE); // 索引2:选中且有焦点背景 // 设置不同状态下的文字颜色 DROPDOWN_SetTextColor(hDropdown, 0, GUI_BLACK); // 未选中文字 DROPDOWN_SetTextColor(hDropdown, 1, GUI_BLACK); // 选中无焦点文字 DROPDOWN_SetTextColor(hDropdown, 2, GUI_WHITE); // 选中有焦点文字(蓝色背景上配白色字)这种设计使得下拉列表在不同交互状态下的视觉反馈非常清晰。同样,DROPDOWN也有一整套SetDefault函数(如DROPDOWN_SetDefaultFont),用于统一设置后续创建的所有下拉列表的默认样式。
一个有用的细节是DROPDOWN_SetTextHeight,它用于设置下拉框在折叠状态下显示选中文本的矩形区域高度。如果字体较大,默认高度可能不够,导致文本垂直方向显示不全,这时就需要手动调大这个值。
4. 消息处理与用户数据:让控件“活”起来
控件画出来只是第一步,让它能响应用户操作并更新数据,才是GUI的核心。emWin通过窗口管理器(WM)的消息机制来实现。
4.1 通知码(Notification Codes)处理
当用户与CHECKBOX或DROPDOWN交互时,控件会向它的父窗口发送WM_NOTIFY_PARENT消息。我们需要在父窗口的回调函数里处理这些消息。
static void _cbDialog(WM_MESSAGE * pMsg) { switch (pMsg->MsgId) { case WM_NOTIFY_PARENT: { int Id = WM_GetId(pMsg->hWinSrc); // 获取触发消息的控件ID int NCode = pMsg->Data.v; // 通知码 switch (Id) { case GUI_ID_CHECKBOX0: // 我们的复选框ID switch (NCode) { case WM_NOTIFICATION_CLICKED: // 复选框被点击了(鼠标按下或触摸按下) break; case WM_NOTIFICATION_RELEASED: // 复选框被释放了(状态可能已改变) { int state = CHECKBOX_IsChecked(pMsg->hWinSrc); // 根据state更新应用逻辑 } break; case WM_NOTIFICATION_VALUE_CHANGED: // 复选框的值(选中状态)发生了改变(这是最常用的) // 对于CHECKBOX,RELEASED和VALUE_CHANGED通常一起处理 break; } break; case GUI_ID_DROPDOWN0: // 我们的下拉列表ID switch (NCode) { case WM_NOTIFICATION_CLICKED: // 下拉框被点击,可能即将展开 break; case WM_NOTIFICATION_SEL_CHANGED: // 下拉列表的选中项发生了改变!!! { int sel = DROPDOWN_GetSel(pMsg->hWinSrc); char buf[50]; DROPDOWN_GetItemText(pMsg->hWinSrc, sel, buf, sizeof(buf)); printf(“用户选择了:%s (索引:%d)\n”, buf, sel); // 这里更新与这个选择相关的其他界面或变量 } break; } break; } } break; // ... 处理其他消息 ... } }对于CHECKBOX,我们最关心的是WM_NOTIFICATION_RELEASED或WM_NOTIFICATION_VALUE_CHANGED,在这里获取最新状态。对于DROPDOWN,WM_NOTIFICATION_SEL_CHANGED是黄金消息,它意味着用户做出了新的选择。pMsg->hWinSrc就是触发消息的控件句柄,可以直接用它来调用API,这比用之前存储的句柄更通用,尤其是在动态创建控件的场景下。
4.2 UserData的妙用:将数据与控件绑定
在复杂的界面中,一个窗口可能有几十个控件。当收到一个WM_NOTIFICATION_SEL_CHANGED消息时,如何快速知道是哪个DROPDOWN,以及这个DROPDOWN代表什么配置项?除了用控件ID做一大串switch-case,还有一个更优雅的方法:UserData。
每个emWin控件都可以关联一段用户自定义数据(一个32位的无符号整数)。你可以用它存储任何对你有意义的信息,比如一个枚举值、一个数组索引,甚至是一个指针(需谨慎转换)。
// 假设我们用一个枚举来定义不同的配置项 typedef enum { CONFIG_BAUDRATE, CONFIG_DATABITS, CONFIG_PARITY, } ConfigItem_t; // 创建下拉列表并绑定UserData hDropdownBaud = DROPDOWN_CreateEx(..., GUI_ID_DROPDOWN0); DROPDOWN_SetUserData(hDropdownBaud, (WM_HandleUserData)CONFIG_BAUDRATE); hDropdownParity = DROPDOWN_CreateEx(..., GUI_ID_DROPDOWN1); DROPDOWN_SetUserData(hDropdownParity, (WM_HandleUserData)CONFIG_PARITY); // 在消息回调中 case WM_NOTIFICATION_SEL_CHANGED: { ConfigItem_t item = (ConfigItem_t)DROPDOWN_GetUserData(pMsg->hWinSrc); int sel = DROPDOWN_GetSel(pMsg->hWinSrc); switch (item) { case CONFIG_BAUDRATE: g_config.baudrate = baudrateValues[sel]; // 从映射表取值 break; case CONFIG_PARITY: g_config.parity = parityValues[sel]; break; // ... } break; }通过UserData,我们将控件的“身份”信息直接绑在了控件自身上。消息处理函数不再需要依赖固定的控件ID顺序,代码更清晰,也更易于扩展。当新增一个配置项时,只需要创建控件并设置对应的UserData,然后在处理函数里加一个case即可。CHECKBOX控件同样有CHECKBOX_SetUserData和CHECKBOX_GetUserData函数,用法完全一样。
5. 实战技巧与避坑指南
结合我多年的项目经验,这里分享一些使用CHECKBOX和DROPDOWN API时容易踩的坑和提升效率的技巧。
5.1 内存与性能考量
在资源紧张的嵌入式系统(比如只有几十KB RAM的Cortex-M0)中使用这些控件要格外小心。
字符串存储:
DROPDOWN_AddString添加的字符串,emWin会在内部为其分配内存进行存储。如果列表项是固定的,考虑使用DROPDOWN_AddString的变种DROPDOWN_AddStringEx(如果支持)或直接使用DROPDOWN_SetString数组,或者更根本的,使用内存占用更小的字体。对于非常长的列表(比如国家选择),如果RAM吃紧,可能需要实现动态加载——只显示可见项,滚动时再加载新的项文本,但这需要自己实现一个虚拟列表,emWin标准DROPDOWN不直接支持。位图资源:自定义CHECKBOX图片会显著增加Flash占用。务必使用适合屏幕尺寸的位图,并考虑使用emWin的位图转换工具生成高压缩比的C数组格式。对于简单的颜色变化,有时用
CHECKBOX_SetTextColor和背景色搭配就能达到效果,不必动用位图。频繁刷新:避免在循环中频繁调用
CHECKBOX_SetState或DROPDOWN_SetSel。每次设置都会触发控件的重绘。如果需要批量更新界面,可以先调用WM_DisableWindow()禁用窗口更新,所有更新操作完成后再调用WM_EnableWindow()并手动WM_InvalidateWindow()触发一次整体重绘,这样效率高得多。
5.2 用户体验细节打磨
禁用状态:通过
WM_DisableWindow(hControl)可以禁用整个控件(变灰,不响应输入)。对于DROPDOWN,还可以用DROPDOWN_SetItemDisabled(hDropdown, index, 1)禁用单个列表项,这在某些选项因条件不满足而不可用时非常有用。被禁用的项会显示为灰色且无法被选中。键盘导航:如果你的设备带物理键盘,要确保Tab键焦点移动顺序正确。这通常通过
WM_SetFocusOnNextChild()或创建控件时指定WM_CF_TA_BEFOR/WM_CF_TA_AFTER样式来控制。DROPDOWN在获得焦点时,按空格键展开,上下键在展开的列表中移动,回车键确认选择,这个行为是内置的,但你需要确保窗口管理器能正确接收键盘消息。触摸反馈:在电阻屏或反应较慢的电容屏上,点击CHECKBOX或DROPDOWN后,如果没有视觉上的即时反馈(如颜色变化),用户会以为没点到。确保为控件设置了清晰的不同状态颜色(如
DROPDOWN_SetBkColor索引1和2)。对于CHECKBOX,可以考虑在WM_NOTIFICATION_CLICKED消息里立刻调用CHECKBOX_SetState切换状态,而不是等WM_NOTIFICATION_RELEASED,这样反馈更及时。
5.3 常见问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| CHECKBOX/DROPDOWN创建失败,返回0 | 内存不足;父窗口句柄无效;坐标/尺寸非法。 | 1. 检查系统剩余内存。 2. 确认父窗口 hParent已创建且有效。3. 检查创建坐标是否在父窗口客户区内,尺寸是否>0。 |
| CHECKBOX文本显示不全或重叠 | 控件宽度不足;字体设置过大;间距(Spacing)设置不当。 | 1. 增加CHECKBOX_CreateEx中的xSize(宽度)。2. 使用 GUI_GetStringDistX()计算文本所需宽度。3. 调整 CHECKBOX_SetSpacing。 |
| DROPDOWN展开后列表项不显示或显示不全 | 展开列表高度(ySize)设置过小;未添加列表项。 | 1. 增大DROPDOWN_CreateEx中的ySize参数(这是展开高度)。2. 确认已调用 DROPDOWN_AddString添加了项。3. 检查父窗口或对话框是否有裁剪区域。 |
| DROPDOWN选中项改变,但界面无反应 | 未正确处理WM_NOTIFICATION_SEL_CHANGED消息。 | 在父窗口回调函数的WM_NOTIFY_PARENT分支中,为对应控件ID添加WM_NOTIFICATION_SEL_CHANGEDcase处理。 |
| 自定义CHECKBOX图片不显示 | 位图资源未正确链接或初始化;控件尺寸小于位图尺寸。 | 1. 确认位图数组已链接到工程,且用GUI_BITMAP结构正确声明。2. 用 WM_GetWindowSize获取控件尺寸,与位图尺寸对比,确保控件足够大。 |
| 控件对触摸/点击无反应 | 控件被禁用(WM_DisableWindow);父窗口未启用输入;消息循环未运行。 | 1. 检查控件是否被意外禁用。 2. 确认父窗口创建时包含 WM_CF_SHOW和WM_CF_HASTRANS(如果需要)。3. 确保 GUI_Exec()或WM_Exec()在主循环中被定期调用。 |
使用SetDefault系列函数无效 | 调用顺序错误:在创建控件之后才设置默认值。 | 务必在程序初始化阶段、创建任何控件之前,调用CHECKBOX_SetDefaultFont等函数。 |
| DROPDOWN滚动条不出现 | 未启用自动滚动;列表高度足够显示所有项。 | 1. 调用DROPDOWN_SetAutoScroll(hObj, 1)。2. 确认列表项总高度(项数*字体行高)大于 DROPDOWN_SetListHeight设置的高度。 |
掌握这些API和技巧,你就能让CHECKBOX和DROPDOWN这两个基础控件在嵌入式界面上发挥出强大的效用。它们不仅仅是简单的UI元素,更是你和用户进行清晰、高效交互的桥梁。关键在于理解其背后的设计逻辑——通过句柄进行面向对象式的控制,通过消息机制进行事件驱动,再结合灵活的属性设置,便能构建出既稳定又美观的嵌入式人机界面。
