1. 项目概述从手册到实战深度解析emWin三大核心控件在嵌入式GUI开发这条路上我见过太多开发者抱着一本厚厚的用户手册对着里面密密麻麻的API函数原型和参数列表发愁。手册是必要的但它更像一本字典告诉你每个“单词”是什么意思却很少教你如何用这些“单词”写出一篇流畅的文章。今天我们就以SEGGER emWin图形库中的三个高频控件——进度条PROGBAR、单选按钮RADIO和滚动条SCROLLBAR为例抛开手册式的平铺直叙从一线开发的实战视角深入剖析它们的API设计哲学、使用陷阱以及那些手册里不会写的“骚操作”。为什么是这三个控件因为它们是构建一个基础且完整交互界面的基石。进度条负责向用户传递明确的进程反馈是提升用户体验信任感的关键单选按钮处理非此即彼的互斥选择是配置类界面的常客滚动条则是应对有限显示区域与海量内容矛盾的唯一解。掌握它们你就能搭建出80%的常见嵌入式交互界面。本文将基于emWin V5.18的用户手册内容但绝不局限于翻译和罗列我会结合多年的踩坑经验带你理解每个API背后的“为什么”并给出可直接“抄作业”的代码范例和避坑指南。2. 控件核心机制与设计思想解析在深入每个控件之前我们必须先统一思想emWin的控件Widget本质上是什么你可以把它理解为一个“智能的窗口”。它不仅仅是一块有颜色的显示区域更是一个封装了特定状态、行为回调函数和视觉渲染逻辑的对象。这种封装带来了两大好处一是开发效率你无需从零开始绘制一个按钮并处理它的点击、高亮、禁用状态二是一致性确保整个应用乃至不同项目间的同类控件行为与外观统一。2.1 事件驱动与消息传递模型所有emWin控件的交互都建立在窗口管理器WM的事件驱动模型之上。当用户点击一个单选按钮或者程序调用PROGBAR_SetValue更新进度时内部发生了什么输入事件传递触摸或键盘事件先由WM捕获然后根据坐标或焦点传递给正确的窗口控件。控件内部处理控件收到事件后会根据自身逻辑更新内部状态。例如RADIO被点击后它会将自身Value设为对应索引并自动清除同组其他按钮的选中状态。通知父窗口状态变更后控件会向其父窗口发送通知消息Notification Code例如WM_NOTIFICATION_VALUE_CHANGED。这是你作为开发者介入控件行为的关键钩子。重绘请求状态改变后控件会标记自身为“无效”WM随后安排重绘调用控件的绘制回调函数更新屏幕显示。理解这个流程至关重要。它意味着你通常不需要直接“画”控件而是通过API设置其状态然后响应其通知来更新你的应用程序逻辑。例如在滚动条的WM_NOTIFY_PARENT消息处理中你才能知道用户滚动了然后去更新列表框的内容。2.2 皮肤Skinning与自定义绘制手册中提到这几个控件都支持“Skinning”。这不仅仅是换个颜色那么简单它代表了emWin控件系统的可扩展性。默认情况下控件使用内置的绘制函数呈现一种标准风格。但你可以通过WIDGET_SetDefaultEffect或为特定控件设置回调函数完全接管其绘制过程。实操心得对于资源极度紧张的MCU自定义绘制禁用Skinning使用经典风格往往比默认的皮肤渲染更节省资源。因为皮肤可能涉及抗锯齿、渐变等效果。在项目初期如果发现界面刷新慢可以尝试将控件风格切回经典模式WIDGET_SetDefaultEffect(WIDGET_Effect_None)看看性能提升。2.3 对象句柄与资源管理每个控件创建后都会返回一个WM_HWIN类型的句柄。这个句柄是你后续操作该控件的唯一凭证。emWin内部有一套完整的内存管理机制来维护这些窗口对象。你需要关注的是生命周期控件窗口必须在父窗口被删除之前删除通常通过在父窗口的WM_DELETE消息中调用WM_DeleteWindow()来实现。虽然现代emWin版本垃圾回收更完善但主动管理仍是好习惯能避免内存泄漏和野指针。3. 进度条PROGBAR控件不仅仅是“进度”进度条看似简单但用好它能极大提升界面的专业度。它不仅是等待时的动画更是数据可视化的轻量级工具。3.1 创建与基础配置PROGBAR_CreateEx的学问手册推荐使用PROGBAR_CreateEx替代旧的Create函数这是有深层次原因的。CreateEx提供了更精细的控制。PROGBAR_Handle hProgbar; hProgbar PROGBAR_CreateEx(50, // x0: 父窗口坐标系下的X起始坐标 100, // y0: 父窗口坐标系下的Y起始坐标 200, // xsize: 宽度水平进度条 30, // ysize: 高度 hParent, // 父窗口句柄0则为桌面 WM_CF_SHOW, // 窗口标志立即显示 PROGBAR_CF_HORIZONTAL, // 扩展标志水平方向 GUI_ID_PROGBAR0); // 控件ID关键参数解析ExFlags这里使用了PROGBAR_CF_HORIZONTAL。另一个选项是PROGBAR_CF_VERTICAL。这里有个坑如果你不指定任何一个默认是水平还是垂直手册没明说但根据实践和源码不指定ExFlags或设为0默认创建的是水平进度条。如果你需要垂直进度条必须显式指定PROGBAR_CF_VERTICAL。WM_CF_SHOW这个标志让控件创建后立即可见。在动态创建界面时我习惯先不加这个标志等所有控件创建、配置完毕最后统一调用WM_ShowWindow()显示这样可以避免屏幕在布局完成前闪烁。3.2 核心数值控制PROGBAR_SetMinMax与PROGBAR_SetValue这是进度条的灵魂。SetMinMax定义了数值范围SetValue设置当前值。// 设置范围表示从 0 到 500 PROGBAR_SetMinMax(hProgbar, 0, 500); // 设置当前值为 250进度条将显示在50%的位置 PROGBAR_SetValue(hProgbar, 250);重要细节与避坑默认范围如果不调用SetMinMax范围默认为0-100。如果你的进度逻辑是0-100%可以省略此调用。数值计算进度条的填充比例计算公式为填充比例 (当前值 - 最小值) / (最大值 - 最小值)。内部使用整数运算注意避免除零错误最大值等于最小值。越界处理SetValue传入的值如果超出[Min, Max]范围会被自动钳制在边界。即小于Min按Min算大于Max按Max算。这既是安全特性也意味着你不会因为计算错误而看到“爆表”的图形。动态文本如果不调用PROGBAR_SetText设置自定义文本进度条会自动在中央显示百分比。这个百分比是基于Min/Max计算出来的公式手册已给出p 100% * (v-Min)/(Max-Min)。注意这个文本渲染可能会在低端MCU上成为性能瓶颈如果不需要务必设置为空字符串。3.3 高级视觉定制颜色、字体与对齐进度条支持双色渐变和文本独立着色这为创造更丰富的视觉效果提供了可能。// 设置进度条本身的颜色左侧颜色和右侧颜色用于水平渐变 PROGBAR_SetBarColor(hProgbar, 0, GUI_BLUE); // 左侧颜色 PROGBAR_SetBarColor(hProgbar, 1, GUI_CYAN); // 右侧颜色 // 设置进度条上文本的颜色 PROGBAR_SetTextColor(hProgbar, 0, GUI_WHITE); // 左侧文本色覆盖在蓝色部分上 PROGBAR_SetTextColor(hProgbar, 1, GUI_BLACK); // 右侧文本色覆盖在青色部分上 // 设置字体 PROGBAR_SetFont(hProgbar, GUI_Font16B_ASCII); // 微调文本位置单位像素 PROGBAR_SetTextPos(hProgbar, 5, 2); // 向右偏移5像素向下偏移2像素颜色设置逻辑Index参数为0和1分别对应进度条“已填充”部分的左/上侧和右/下侧。对于水平进度条0是左端1是右端对于垂直进度条0是顶端1是底端。文本颜色的Index同理它决定了文本在对应区域显示的颜色。这个设计允许文本在进度条填充过程中自动变色以适应背景提升可读性。3.4 实战案例创建动态更新的下载进度条让我们结合一个常见场景——文件下载来综合运用上述API。// 假设在一个对话框的回调函数中 static PROGBAR_Handle hDownloadProgbar; static int s_totalFileSize 0; static int s_downloadedSize 0; case WM_INIT_DIALOG: // 创建进度条 hDownloadProgbar PROGBAR_CreateEx(20, 50, 260, 25, hDlg, WM_CF_SHOW, 0, GUI_ID_PROGBAR0); // 设置范围假设文件大小为102400字节 s_totalFileSize 102400; PROGBAR_SetMinMax(hDownloadProgbar, 0, s_totalFileSize); // 设置自定义文本格式例如“250KB/1MB” PROGBAR_SetText(hDownloadProgbar, ); // 先显示初始文本 _UpdateProgbarText(hDownloadProgbar, 0); break; // 在下载数据到达的回调中 void OnDownloadDataReceived(int chunkSize) { s_downloadedSize chunkSize; // 更新进度条值 PROGBAR_SetValue(hDownloadProgbar, s_downloadedSize); // 更新自定义文本 _UpdateProgbarText(hDownloadProgbar, s_downloadedSize); } // 辅助函数更新进度条文本 static void _UpdateProgbarText(PROGBAR_Handle hObj, int current) { char buffer[32]; // 格式化文本例如 “256KB / 1MB” sprintf(buffer, %dKB / %dKB, current/1024, s_totalFileSize/1024); PROGBAR_SetText(hObj, buffer); }注意事项频繁调用PROGBAR_SetText和PROGBAR_SetValue会触发重绘。在高速更新的场景如实时传感器数据这可能导致界面卡顿。一个优化技巧是节流更新例如每接收100毫秒的数据或每增加1%的进度才更新一次UI而不是来一个字节就更新一次。4. 单选按钮RADIO控件实现互斥选择单选按钮的核心在于“一组”和“唯一”。emWin的RADIO控件天然管理一组内的互斥逻辑这比手动管理一堆BUTTON控件要可靠得多。4.1 创建与项管理理解NumItems和Spacing创建单选按钮组时最关键的两个参数是NumItems项数和Spacing项间距。RADIO_Handle hRadio; hRadio RADIO_CreateEx(10, 10, 150, 0, // 高度设为0控件会根据项数自动计算 hParent, WM_CF_SHOW, 0, GUI_ID_RADIO0, 3, // NumItems: 3个选项 25); // Spacing: 每个选项占25像素高自动计算高度如上例将创建时的ysize高度参数设为0是一个常用技巧。控件会根据NumItems * Spacing自动计算所需高度。如果你手动指定了一个过小的高度底部的选项可能显示不全或被裁剪。4.2 文本与状态管理RADIO_SetText和RADIO_SetValue创建后需要为每个选项设置文本并可以获取或设置当前选中项。// 设置选项文本索引从0开始 RADIO_SetText(hRadio, 选项 A, 0); RADIO_SetText(hRadio, 选项 B, 1); RADIO_SetText(hRadio, 选项 C, 2); // 设置默认选中第二项索引为1 RADIO_SetValue(hRadio, 1); // 在回调函数中获取用户选择 case WM_NOTIFY_PARENT: Id WM_GetId(pMsg-hWinSrc); // 获取触发通知的控件ID NCode pMsg-Data.v; // 获取通知代码 if (Id GUI_ID_RADIO0 NCode WM_NOTIFICATION_VALUE_CHANGED) { int selectedIndex RADIO_GetValue(hRadio); printf(用户选择了第 %d 项\n, selectedIndex); } break;4.3 高级功能分组RADIO_SetGroupId与自定义图像分组功能是RADIO控件一个强大但易被忽略的特性。它允许你将多个独立的RADIO控件逻辑上绑定为一组。RADIO_Handle hRadioGroup1[2]; // 创建两个独立的RADIO控件各有2个选项 hRadioGroup1[0] RADIO_CreateEx(10, 10, 80, 0, hParent, WM_CF_SHOW, 0, GUI_ID_RADIO0, 2, 20); RADIO_SetText(hRadioGroup1[0], 男, 0); RADIO_SetText(hRadioGroup1[0], 女, 1); hRadioGroup1[1] RADIO_CreateEx(100, 10, 80, 0, hParent, WM_CF_SHOW, 0, GUI_ID_RADIO1, 2, 20); RADIO_SetText(hRadioGroup1[1], 公开, 0); RADIO_SetText(hRadioGroup1[1], 私密, 1); // 将它们设置为同一组GroupId 非0 RADIO_SetGroupId(hRadioGroup1[0], 1); RADIO_SetGroupId(hRadioGroup1[1], 1); // 现在这4个按钮中同时只能有一个被选中这个功能非常适合需要将不同位置的选项归为一组的复杂布局例如一个设置界面中“性别”和“可见性”虽然是不同标签但你可能希望它们共用一组单选逻辑虽然这例子不太恰当但说明了可能性。自定义图像通过RADIO_SetDefaultImage或RADIO_SetImage你可以替换掉默认的“圆圈”和“圆点”图案。这在需要特殊风格如方形选择框、勾选标记的界面中非常有用。你需要准备三张位图未激活状态外框、激活状态外框、选中标记。4.4 常见问题排查焦点与键盘导航焦点矩形不显示如果为单选按钮设置了文本焦点矩形会围绕文本绘制如果没设置文本则围绕按钮图形绘制。确保控件可以通过WM_SetFocus()获得焦点并且RADIO_SetFocusColor设置了可见的颜色非透明色。键盘上下键无效确保控件拥有焦点WM_SetFocus并且其父窗口能正确传递键盘消息。有时在对话框资源表中需要明确设置控件的TAB停靠顺序。动态修改选项文本直接调用RADIO_SetText更新即可但要注意索引不能越界。更新后可能需要手动调用WM_InvalidateWindow(hRadio)来触发重绘。5. 滚动条SCROLLBAR控件内容导航的核心滚动条通常不单独使用而是作为其他控件如列表框LISTBOX、多行文本MULTIEDIT的一部分。emWin提供了独立创建和附着创建两种方式。5.1 独立创建 vs 附着创建CreateEx与CreateAttached独立创建(SCROLLBAR_CreateEx)你将得到一个可以放在任何位置的独立滚动条控件。你需要手动处理它的所有消息并与其他控件联动。这种方式更灵活但更复杂。// 创建一个独立的垂直滚动条 hScrollbar SCROLLBAR_CreateEx(250, 0, 20, 200, hParent, WM_CF_SHOW, SCROLLBAR_CF_VERTICAL, GUI_ID_SCROLLBAR0); // 必须手动设置范围和页面大小 SCROLLBAR_SetNumItems(hScrollbar, 100); // 总共100项 SCROLLBAR_SetPageSize(hScrollbar, 10); // 一页显示10项附着创建(SCROLLBAR_CreateAttached)这是更常用、更便捷的方式。滚动条会自动附着到指定父窗口的一侧右或下并与其内容联动。// 先创建一个列表框 hListbox LISTBOX_CreateEx(50, 50, 150, 100, hParent, WM_CF_SHOW, 0, 0); // ... 向列表框中添加项目 ... // 然后为其创建一个附着式垂直滚动条 hScrollbar SCROLLBAR_CreateAttached(hListbox, SCROLLBAR_CF_VERTICAL); // 完成了滚动条会自动获取列表框的项目数并处理滚动逻辑。核心优势附着式滚动条会自动从父窗口获取WM_GET_NUM_ITEMS和WM_GET_VISIBLE_ITEMS等消息来确定总项数和页面大小无需你手动调用SetNumItems和SetPageSize。滚动条的值变化时也会向父窗口发送WM_VSCROLL或WM_HSCROLL消息父窗口只需响应这些消息来更新显示内容即可。这大大简化了开发。5.2 关键参数详解NumItems,PageSize,Value这三个参数共同定义了滚动条的数学模型。NumItems可滚动内容的总项数。例如一个文本文件有500行NumItems就是500。PageSize一屏或一个视图能显示的项数。例如列表框高度只能显示20行PageSize就是20。Value当前滚动位置表示可见区域顶部所对应的内容项索引。范围是[0, NumItems - PageSize]。拇指Thumb大小计算滚动条上可拖动的滑块拇指大小不是固定的它直观反映了PageSize与NumItems的比例拇指大小 (滚动条长度 - 箭头按钮大小) * (PageSize / NumItems)同时受SCROLLBAR_SetThumbSizeMin设置的最小值限制。这个比例关系是良好用户体验的基础。5.3 消息处理与同步让滚动条“动起来”对于独立滚动条或需要自定义滚动逻辑的附着滚动条你需要处理消息循环。// 在父窗口的回调函数中 case WM_NOTIFY_PARENT: Id WM_GetId(pMsg-hWinSrc); NCode pMsg-Data.v; if (Id GUI_ID_SCROLLBAR0) { switch (NCode) { case WM_NOTIFICATION_VALUE_CHANGED: // 滚动条的值被改变了拖动拇指或点击箭头 int currentPos SCROLLBAR_GetValue(hScrollbar); // 根据currentPos更新你的内容显示例如更新列表起始行 _UpdateContentView(currentPos); break; case WM_NOTIFICATION_SCROLLBAR_ADDED: // 滚动条刚被添加进行初始化通常用于独立滚动条 SCROLLBAR_SetNumItems(hScrollbar, GetTotalItemsCount()); SCROLLBAR_SetPageSize(hScrollbar, GetItemsPerPage()); break; } } break;同步问题一个常见的Bug是内容滚动和滚动条位置不同步。确保在两种情况下更新滚动条值1. 用户操作滚动条时更新内容显示2. 用户通过其他方式如鼠标滚轮、键盘滚动内容时主动调用SCROLLBAR_SetValue更新滚动条位置。5.4 性能优化与视觉定制避免频繁重绘在快速连续滚动时如快速拖动拇指WM_NOTIFICATION_VALUE_CHANGED可能会被高频触发。如果_UpdateContentView操作很重如刷新大量文本会导致界面卡顿。解决方案是在消息处理中只记录目标位置在WM_PAINT或一个低优先级的定时器任务中执行实际的内容重绘。自定义颜色通过SCROLLBAR_SetColor可以修改滚动条轨道SCROLLBAR_CI_SHAFT、拇指SCROLLBAR_CI_THUMB和箭头SCROLLBAR_CI_ARROW的颜色使其符合你的UI主题。调整宽度默认滚动条宽度可能不适合你的设计。使用SCROLLBAR_SetWidth可以调整独立滚动条的宽度。对于附着滚动条其宽度由SCROLLBAR_SetDefaultWidth设置的全局默认值决定通常在初始化阶段配置。6. 综合实战构建一个简单的设备配置界面现在我们将三个控件组合起来创建一个模拟的设备配置对话框包含网络质量指示进度条、协议选择单选按钮和一个带滚动条的日志显示区域。// 定义控件句柄 static PROGBAR_Handle hSignalStrength; static RADIO_Handle hProtocolRadio; static MULTIEDIT_Handle hLogEdit; static SCROLLBAR_Handle hLogScrollbar; // 在窗口初始化中创建控件 case WM_INIT_DIALOG: // 1. 信号强度指示进度条 hSignalStrength PROGBAR_CreateEx(20, 20, 200, 20, hDlg, WM_CF_SHOW, 0, GUI_ID_PROGBAR0); PROGBAR_SetMinMax(hSignalStrength, -90, -50); // 假设dBm值 PROGBAR_SetValue(hSignalStrength, -65); PROGBAR_SetText(hSignalStrength, 信号强度); PROGBAR_SetFont(hSignalStrength, GUI_Font13_1); // 2. 协议选择单选按钮 hProtocolRadio RADIO_CreateEx(20, 60, 120, 0, hDlg, WM_CF_SHOW, 0, GUI_ID_RADIO0, 3, 25); RADIO_SetText(hProtocolRadio, TCP, 0); RADIO_SetText(hProtocolRadio, UDP, 1); RADIO_SetText(hProtocolRadio, MQTT, 2); RADIO_SetValue(hProtocolRadio, 0); // 默认选择TCP // 3. 日志显示区域多行编辑框附着滚动条 hLogEdit MULTIEDIT_CreateEx(20, 130, 250, 150, hDlg, WM_CF_SHOW, 0, GUI_ID_MULTIEDIT0); MULTIEDIT_SetText(hLogEdit, 系统启动...\n); MULTIEDIT_SetFont(hLogEdit, GUI_Font8x16); MULTIEDIT_SetWrapMode(hLogEdit, MULTIEDIT_WRAP_WORD); // 自动换行 // 为多行编辑框创建附着垂直滚动条 hLogScrollbar SCROLLBAR_CreateAttached(hLogEdit, SCROLLBAR_CF_VERTICAL); // MULTIEDIT控件会自动与附着滚动条协作无需额外配置 // 启动一个定时器模拟信号变化和日志增加 WM_CreateTimer(hDlg, GUI_ID_TIMER0, 1000, 0); // 1秒周期 break; // 定时器回调模拟动态更新 case WM_TIMER: if(pMsg-Data.v GUI_ID_TIMER0) { // 随机更新信号强度 int signal rand() % 41 - 90; // -90 到 -50 PROGBAR_SetValue(hSignalStrength, signal); // 向日志添加一行 char logMsg[64]; sprintf(logMsg, [%lu] 信号更新: %d dBm\n, GUI_GetTime(), signal); MULTIEDIT_AddText(hLogEdit, logMsg); // 滚动到底部 int numLines MULTIEDIT_GetNumLines(hLogEdit); MULTIEDIT_SetCursorAtChar(hLogEdit, 0, numLines-1); } break; // 处理协议选择变化 case WM_NOTIFY_PARENT: Id WM_GetId(pMsg-hWinSrc); NCode pMsg-Data.v; if (Id GUI_ID_RADIO0 NCode WM_NOTIFICATION_VALUE_CHANGED) { int proto RADIO_GetValue(hProtocolRadio); const char* protoStr[] {TCP, UDP, MQTT}; char logMsg[64]; sprintf(logMsg, 协议已切换至: %s\n, protoStr[proto]); MULTIEDIT_AddText(hLogEdit, logMsg); } break;这个例子展示了如何将控件联动起来定时器驱动进度条更新单选按钮触发日志记录而多行编辑框与附着滚动条自动配合无需手动管理滚动逻辑。这种“各司其职事件驱动”的模式正是高效开发emWin GUI应用的核心。7. 调试技巧与常见问题实录即使理解了API实际开发中仍会遇到各种问题。以下是我总结的一些常见坑点及解决方案。问题1控件创建失败句柄为0。排查首先检查内存是否充足。emWin动态创建控件需要从内存设备中分配。确保GUI_ALLOC_SIZE在GUIConf.h中设置得足够大。其次检查坐标和尺寸是否合理是否在父窗口的客户区内。工具使用GUI_GetUsedMem()函数打印当前内存使用量辅助判断。问题2进度条/滚动条数值更新了但屏幕没刷新。原因控件被其他窗口覆盖或者所在窗口的无效区域没有被正确合并。解决手动调用WM_InvalidateWindow(hObj)强制将该控件标记为无效请求重绘。更根本的方法是确保你的主任务循环中正确调用了GUI_Exec()或GUI_Delay()它们负责处理WM的重绘消息。问题3单选按钮组行为异常可以多选。确认确保这些按钮属于同一个RADIO控件通过RADIO_CreateEx创建的一个实例包含多个项而不是多个独立的RADIO控件。如果是多个独立控件需要互斥必须使用RADIO_SetGroupId将它们设为同一组。检查在回调中打印RADIO_GetValue的返回值确认每次点击后之前选中的项索引是否变为-1未选中。问题4附着滚动条不出现或不起作用。步骤确认父窗口如LISTBOX的内容确实超出了其显示区域。如果总项数小于等于一页可显示数滚动条可能自动隐藏。检查父窗口是否正确处理了WM_GET_NUM_ITEMS等消息。对于标准控件如LISTBOX、MULTIEDIT它们已内部处理。对于自定义窗口你需要自己响应这些消息。确保在创建附着滚动条后没有调用SCROLLBAR_SetNumItems等函数覆盖了自动计算的值。问题5界面响应卡顿尤其是在更新控件时。优化策略批量操作在初始化时先创建所有控件不加WM_CF_SHOW配置所有属性最后再统一WM_ShowWindow。禁用自动重绘在连续进行多次Set操作前可以调用WM_DisableWindow(hObj)临时禁用控件操作完成后再WM_EnableWindow(hObj)并WM_InvalidateWindow。使用内存设备对于复杂的、需要频繁更新的区域可以考虑将其创建在内存设备WM_CreateMemoryDevice上先在内存中完成所有绘制再一次性拷贝到前台能有效减少闪烁和提升速度。掌握emWin控件关键在于从“会用API”上升到“理解其设计模式”。进度条、单选按钮、滚动条这三个控件完美体现了事件驱动、状态封装和父子窗口通信这些核心概念。当你再遇到手册上其他控件时这套方法论依然适用先理解它是什么用途再看它如何创建和配置初始化接着是核心状态如何改变API最后是它如何通知应用程序消息。沿着这个思路任何emWin控件都将不再神秘。