嵌入式GUI窗口管理器:消息驱动、坐标系统与触摸交互实战

发布时间:2026/6/26 12:59:56
嵌入式GUI窗口管理器:消息驱动、坐标系统与触摸交互实战 1. 窗口管理器核心架构与消息驱动模型在嵌入式GUI开发领域窗口管理器Window Manager 简称WM扮演着整个用户界面系统的“中枢神经”角色。它远不止于管理窗口的堆叠和显示更核心的职责是构建一个有序、高效的事件驱动架构。emWin的窗口管理器正是这一理念的典范其设计哲学深深植根于经典的桌面窗口系统但针对嵌入式系统的资源约束做了大量精妙的优化。1.1 消息机制GUI交互的基石消息机制是窗口管理器的灵魂。你可以把它想象成一个高度组织化的邮局系统。当用户在触摸屏上点击、滑动或者系统内部状态发生变化如定时器到期、窗口需要重绘时都会产生一封封“信件”即消息。窗口管理器作为邮局负责将这些信件精准地投递到对应的“收件人”——也就是各个窗口的回调函数中。每个窗口在创建时都可以也应该指定一个回调函数。这个函数本质上是一个巨大的switch-case语句专门用于处理投递来的各种消息。例如当用户点击一个按钮时系统会生成WM_TOUCH消息并发送给按钮窗口的回调函数回调函数识别到这个消息后就可以执行改变按钮状态、触发业务逻辑等操作。这种“事件-响应”模式将用户输入、系统事件与具体的处理逻辑解耦使得程序结构清晰易于维护和扩展。在资源受限的嵌入式环境中这种机制的优势尤为突出。它避免了轮询带来的CPU空转损耗只有当事件真正发生时才执行相应的代码极大地提高了系统的能效比。同时消息队列的引入可以平滑处理短时间内爆发的多个输入事件防止系统因来不及响应而卡死。1.2 窗口树与坐标系统管理的层次与空间窗口管理器以树形结构组织所有窗口。最底层是桌面窗口Desktop Window它是所有窗口的根父窗口。在此之上创建的窗口要么是桌面窗口的直接子窗口要么是其他窗口的子窗口从而形成一棵窗口树。这个树形结构带来了两个核心概念父子关系与裁剪子窗口的显示区域被严格限制在其父窗口的客户区Client Area内。如果子窗口试图绘制到父窗口边界之外这部分内容会被自动裁剪掉不会显示。这为创建复杂的嵌套界面如对话框中的按钮组提供了天然的隔离和管理机制。坐标系统emWin使用两套坐标系统来精确定位。桌面坐标以物理显示屏的左上角为原点(0,0)向右为X轴正方向向下为Y轴正方向。这是绝对的坐标参考系WM_CreateWindow函数使用的就是桌面坐标。窗口坐标以每个窗口自身客户区的左上角为原点(0,0)。这在窗口的回调函数内部处理绘制和触摸事件时非常方便因为你无需关心窗口在屏幕上的绝对位置。例如在WM_PAINT消息处理中传递给你的无效区域矩形GUI_RECT就是窗口坐标。理解并正确运用这两套坐标系是避免出现控件错位、触摸点不准等问题的关键。一个常见的技巧是使用WM_GetWindowRectEx获取窗口的桌面坐标范围再结合触摸事件的桌面坐标通过坐标转换来判断触摸点是否落在该窗口内。1.3 无效区域与重绘机制性能优化的关键嵌入式设备的图形绘制通常是性能瓶颈。窗口管理器通过“无效区域”机制来最小化重绘操作避免不必要的图形渲染这是其高效性的核心秘密。当一个窗口的内容需要更新时例如文本改变、位置移动我们并不直接调用绘图函数而是告诉窗口管理器“我窗口的某块区域现在无效了需要重画”。这是通过WM_InvalidateWindow或WM_InvalidateRect函数实现的。窗口管理器会将这些无效区域记录到一个列表中。随后在系统的主循环中通常由GUI_Exec()或WM_Exec()驱动窗口管理器会检查这个列表。对于每一个无效的窗口它会向其发送WM_PAINT消息。窗口的回调函数收到此消息后才进行实际的绘制操作。更重要的是在发送WM_PAINT消息前WM会自动将当前绘图上下文Context的裁剪区Clip Rect设置为窗口的无效区域与窗口自身区域的交集。这意味着你的绘图代码只会在这个精确的、需要更新的小区域内执行大大提升了绘制效率。实操心得理解“脏矩形”无效区域管理常被称为“脏矩形”算法。在复杂的动画或频繁更新的界面中主动管理无效区域而非无效化整个窗口能带来显著的性能提升。例如一个模拟仪表的指针转动你只需要无效化指针扫过的新旧两个扇形区域而不是整个表盘。emWin的WM_InvalidateRect函数正是为此而生。2. 系统定义消息深度解析与处理实践系统定义的消息是窗口管理器与应用程序窗口通信的标准“协议”。深入理解每条消息的触发时机、携带的数据以及处理方式是编写健壮GUI应用的基础。2.1 窗口生命周期消息WM_CREATE 与 WM_DELETEWM_CREATE和WM_DELETE消息标志着窗口生命的开始与结束它们为窗口提供了初始化和清理的机会。WM_CREATE在窗口对象被成功创建、但尚未显示之前发送。这是进行窗口初始化的黄金时机。典型的操作包括为窗口分配额外的动态内存如果创建时指定了NumExtraBytes可以在此初始化这块内存。创建并初始化该窗口的子窗口或控件Widget。例如在一个自定义的对话框窗口的WM_CREATE处理中创建按钮、文本框等子控件。设置窗口的初始状态或加载资源。static WM_RESULT _cbMyWindow(WM_MESSAGE * pMsg) { switch (pMsg-MsgId) { case WM_CREATE: // 创建子控件 _hButton BUTTON_CreateEx(10, 10, 80, 30, pMsg-hWin, 0, 0, GUI_ID_BUTTON0); // 初始化额外数据 MY_WINDOW_DATA* pData (MY_WINDOW_DATA*)WM_GetUserData(pMsg-hWin); pData-counter 0; break; // ... 处理其他消息 } return WM_OK; }WM_DELETE在窗口对象被从内存中移除之前发送。这是进行资源清理的唯一安全场所。你必须在此释放所有在WM_CREATE或窗口生命周期内分配的资源否则会导致内存泄漏。删除所有由该窗口创建的子窗口注意WM会自动删除子窗口但如果你有特殊的子对象需要处理应在此进行。释放动态分配的内存、图形资源如图片、字体等。断开与外部模块的连接。重要注意事项窗口删除的连锁反应调用WM_DeleteWindow(hWin)时WM会首先向hWin发送WM_DELETE消息然后递归地删除其所有子窗口每个子窗口也会收到自己的WM_DELETE。最后它会向hWin的父窗口发送一个WM_NOTIFICATION_CHILD_DELETED通知。因此切忌在WM_DELETE中再次尝试删除子窗口也不要在父窗口的WM_DELETE中删除父窗口自身这会导致递归死循环。2.2 绘制消息序列WM_PRE_PAINT, WM_PAINT, WM_POST_PAINT这一系列消息构成了窗口内容更新的完整流程。WM_PRE_PAINT在第一个WM_PAINT消息发送之前触发。它适用于需要在任何绘制发生前进行的全局设置例如准备一个覆盖整个窗口的渐变背景色。在实际应用中使用频率较低。WM_PAINT核心的绘制消息。当窗口的任何部分被标记为无效后WM就会发送此消息。回调函数应在此消息中完成所有必要的绘图操作以将窗口内容恢复到正确状态。pMsg-Data.p指向一个GUI_RECT结构它描述了在窗口坐标系下需要重绘的矩形区域无效区域。高效的绘制代码应利用这个信息进行局部更新而不是盲目重绘整个窗口。WM在发送此消息前已自动将绘图上下文切换到该窗口并设置了正确的裁剪区。case WM_PAINT: { GUI_RECT Rect; WM_GetInvalidRect(pMsg-hWin, Rect); // 获取无效区域窗口坐标 // 仅重绘无效区域内的内容提升性能 GUI_SetBkColor(GUI_WHITE); GUI_ClearRect(Rect.x0, Rect.y0, Rect.x1, Rect.y1); // ... 其他绘制操作 break; }WM_POST_PAINT在最后一个WM_PAINT消息处理完成之后发送。适用于那些必须在所有基础绘制完成后才能进行的操作比如在已经绘制好的内容上叠加一层高亮的装饰线、或者绘制一个总在最顶层的自定义光标。使用它需要谨慎避免覆盖掉WM_PAINT中绘制的内容。2.3 输入焦点消息WM_SET_FOCUS 与 WM_NOTIFICATION_GOT/LOST_FOCUS焦点管理对于键盘操作或具有输入焦点的控件如编辑框至关重要。WM_SET_FOCUS当窗口管理器试图将输入焦点移交给某个窗口时发送。pMsg-Data.v的值为1表示获得焦点为0表示失去焦点。一个窗口可以通过在响应此消息时将Data.v设为0来拒绝获得焦点例如一个仅用于显示的静态文本控件。WM_NOTIFICATION_GOT_FOCUS / LOST_FOCUS这些是通知码通常由控件Widget在自身焦点状态改变时通过WM_NotifyParent()发送给其父窗口。父窗口可以据此更新界面例如当编辑框获得焦点时父窗口可以高亮其边框。// 在父窗口的回调函数中 case WM_NOTIFY_PARENT: { int Id WM_GetId(pMsg-hWinSrc); // 获取触发通知的子窗口ID int NCode pMsg-Data.v; // 通知码 switch (NCode) { case WM_NOTIFICATION_GOT_FOCUS: if (Id GUI_ID_EDIT0) { // 高亮ID为GUI_ID_EDIT0的编辑框 } break; case WM_NOTIFICATION_LOST_FOCUS: // 失去焦点时的处理 break; } break; }2.4 定时器消息WM_TIMERWM_TIMER使得窗口可以基于时间触发动作是实现动画、周期性数据更新等功能的核心。使用WM_CreateTimer()创建一个定时器需要指定窗口句柄、定时周期毫秒和一个定时器ID。当定时器到期时WM会向创建定时器的窗口发送WM_TIMER消息pMsg-Data.v中包含了到期定时器的句柄。通过WM_GetTimerId()可以将定时器句柄转换回创建时设置的ID以便区分多个定时器。使用WM_RestartTimer()可以重置定时器WM_DeleteTimer()用于删除定时器。// 创建定时器 WM_HTIMER hTimer WM_CreateTimer(pMsg-hWin, /* 窗口句柄 */ 1000, /* 1000ms */ GUI_ID_TIMER0); // 处理定时器消息 case WM_TIMER: { WM_HTIMER hTimer pMsg-Data.v; int TimerId WM_GetTimerId(hTimer); if (TimerId GUI_ID_TIMER0) { // 每秒执行一次的任务例如更新时钟显示 // ... 更新界面 ... WM_InvalidateWindow(pMsg-hWin); // 触发重绘 } break; }3. 指针输入设备PID消息与触摸交互实现在触摸屏设备上流畅、准确的触摸体验是GUI的灵魂。emWin的窗口管理器通过一套精细的PID消息序列来模拟完整的触摸交互过程。3.1 触摸事件流WM_PID_STATE_CHANGED 与 WM_TOUCH这是理解触摸处理的关键。当用户手指接触屏幕时并非只产生一个事件而是一个有序的事件流。按下PressWM_PID_STATE_CHANGED:State从0变为1StatePrev为0。这个消息最先到达纯粹告知“按压状态已改变”。WM_TOUCH:Pressed为1。紧随其后携带具体的坐标信息。移动Move 手指保持按压并滑动仅发送WM_TOUCH消息且Pressed保持为1。不会发送WM_PID_STATE_CHANGED因为按压状态没有改变。释放ReleaseWM_PID_STATE_CHANGED:State从1变为0StatePrev为1。WM_TOUCH:Pressed变为0。这种分离的设计状态改变 vs. 具体事件提供了极大的灵活性。例如你可以只在WM_PID_STATE_CHANGED中处理按钮的“按下”和“释放”状态切换改变颜色而在WM_TOUCH中处理滑动操作如滚动列表。数据结构解析 两个消息都通过pMsg-Data.p传递一个结构体指针。WM_PID_STATE_CHANGED指向WM_PID_STATE_CHANGED_INFO主要关注状态变迁。WM_TOUCH指向GUI_PID_STATE包含当前坐标(x,y)和详细的按压状态(Pressed)。对于鼠标Pressed的每个位代表不同的鼠标按键这为支持多键鼠标提供了可能。3.2 高级触摸特性WM_TOUCH_CHILD 与 WM_MOUSEOVERWM_TOUCH_CHILD当触摸事件发生在子窗口上时除了子窗口会收到WM_TOUCH其父窗口也会收到WM_TOUCH_CHILD消息。pMsg-Data.p指向的是发送给子窗口的那个GUI_PID_STATE结构体。这允许父窗口监听所有子控件的触摸事件实现全局手势识别或事件过滤。例如一个对话框可以在父窗口层实现“滑动关闭”手势而不干扰内部按钮的点击。WM_MOUSEOVER / WM_MOUSEOVER_END这两个消息用于实现鼠标悬停效果需要GUI_SUPPORT_MOUSE启用。当鼠标光标进入窗口区域时发送WM_MOUSEOVER离开时发送WM_MOUSEOVER_END。这对于桌面模拟或需要丰富鼠标交互的应用非常有用。注意GUI_PID_STATE中的Pressed在WM_MOUSEOVER中始终为0按压事件仍需通过WM_TOUCH处理。3.3 输入捕获WM_SetCapture 与 WM_ReleaseCapture默认情况下触摸消息会发送给位于触摸点最顶层的可见窗口。但在某些交互场景下我们需要让一个窗口“独占”触摸输入即使手指移动到了它的区域之外。这就是输入捕获的用途。WM_SetCapture(hWin, AutoRelease): 调用后所有后续的PID消息都将被定向到hWin窗口直到捕获被释放。AutoRelease参数若为1则当用户释放触摸收到Pressed0的WM_TOUCH时自动释放捕获若为0则必须手动调用WM_ReleaseCapture()。典型应用场景是滑块控件或窗口拖动。用户按下滑块按钮后即使手指快速滑出按钮区域滑块仍应继续跟随手指移动这就需要设置捕获。case WM_TOUCH: { const GUI_PID_STATE* pState (const GUI_PID_STATE*)pMsg-Data.p; if (pState-Pressed) { // 首次按下开始捕获 WM_SetCapture(pMsg-hWin, 1); // 记录起始位置开始拖动逻辑 _StartDrag(pState-x, pState-y); } else { // 释放捕获会因AutoRelease1而自动结束 _EndDrag(); } break; }4. 核心API函数分类详解与应用场景emWin窗口管理器的API函数数量众多但按其功能可以清晰地分为几个大类。掌握每一类函数的用途和配合方式是灵活操控GUI界面的前提。4.1 窗口生命周期管理API这类API负责窗口的“生老病死”和基本属性控制。创建与销毁WM_CreateWindow/WM_CreateWindowAsChild: 创建窗口的核心函数。区别在于坐标系桌面坐标 vs. 父窗口坐标和父窗口的指定。创建标志Style参数尤为重要例如WM_CF_MEMDEV可以自动启用存储设备实现无闪烁重绘WM_CF_HASTRANS用于声明透明窗口。WM_DeleteWindow: 安全删除窗口及其所有子窗口。务必在窗口回调函数的WM_DELETE消息中处理资源释放。显示与隐藏WM_ShowWindow/WM_HideWindow: 控制窗口可见性。需要注意的是调用这些函数后窗口并不会立即重绘需要依赖WM_Exec或手动调用WM_Paint来更新显示。WM_IsVisible: 查询窗口当前可见状态。启用与禁用WM_EnableWindow/WM_DisableWindow: 启用或禁用窗口。被禁用的窗口不会接收WM_TOUCH、WM_PID_STATE_CHANGED等输入消息且控件通常会呈现灰色外观。WM_IsEnabled用于查询状态。4.2 窗口几何与层级管理API管理窗口的位置、大小和前后顺序。位置与尺寸WM_MoveTo/WM_MoveChildTo: 移动窗口。前者使用桌面坐标后者使用父窗口坐标。WM_GetWindowRectEx/WM_GetClientRectEx: 获取窗口在桌面坐标下的整个矩形区域或客户区矩形窗口坐标原点为0。WM_SetSize/WM_ResizeWindow: 设置窗口绝对大小或相对调整大小。改变尺寸会触发WM_SIZE消息。WM_GetWindowSizeX/Y: 快速获取窗口宽高。层级关系WM_BringToTop/WM_BringToBottom: 将窗口置于其兄弟窗口的最顶层或最底层。WM_GetFirstChild/WM_GetNextSibling/WM_GetPrevSibling: 遍历窗口树。这在需要批量操作子窗口时非常有用。WM_GetParent: 获取父窗口句柄。WM_AttachWindow/WM_DetachWindow: 动态改变窗口的父子关系。例如将一个控件从一个对话框移动到另一个对话框。焦点与模态WM_SetFocus/WM_GetFocussedWindow/WM_HasFocus: 管理输入焦点。WM_MakeModal: 创建一个模态窗口。模态窗口会阻塞输入所有PID消息只会发送给该窗口或其子窗口这是实现对话框必须等待用户响应的基础。4.3 消息与绘制控制API直接控制消息流和绘制流程。消息发送WM_SendMessage: 向指定窗口发送一个完整的WM_MESSAGE结构体消息用于自定义消息或复杂数据传递。WM_SendMessageNoPara: 仅发送消息ID效率更高适用于简单的信号通知。WM_BroadcastMessage: 向所有现存窗口广播消息慎用性能开销大。WM_NotifyParent: 子窗口通知父窗口的标准方法通常携带一个通知码如WM_NOTIFICATION_CLICKED。绘制控制WM_InvalidateWindow/WM_InvalidateRect: 标记窗口或窗口内某一矩形区域为“无效”请求重绘。这是触发WM_PAINT消息的标准方式。WM_ValidateWindow/WM_ValidateRect: 与Invalidate相反标记区域为“有效”。通常由WM内部管理手动调用需非常小心可能导致该区域无法重绘。WM_Paint:立即执行指定窗口的绘制回调处理WM_PAINT。WM_Update则是立即绘制该窗口的无效区域。WM_PaintWindowAndDescs和WM_UpdateWindowAndDescs会递归处理所有子窗口。WM_SelectWindow: 切换当前活动的绘图窗口。所有后续的GUI绘图函数如GUI_DrawPoint,GUI_DispString都将作用于此窗口的客户区。在窗口回调函数外部进行自定义绘制时常用。管理器执行WM_Exec/WM_Exec1: 驱动窗口管理器的主循环。WM_Exec1执行一次重绘任务一个窗口WM_Exec循环执行直到所有无效窗口都被重绘。在无操作系统的应用中通常将其放在while(1)主循环或定时中断中。更常见的做法是调用GUI_Exec()它内部会调用WM_Exec并处理其他GUI任务。4.4 高级功能与工具API存储设备支持WM_EnableMemdev/WM_DisableMemdev。启用后窗口的绘制会先在内存中完成再一次性拷贝到显示设备能有效消除闪烁但会消耗更多RAM。用户数据WM_SetUserData/WM_GetUserData。允许为每个窗口关联一小块自定义数据在创建窗口时通过NumExtraBytes指定大小。这是实现面向对象设计的关键可以将窗口的实例数据如文本缓冲、状态变量存储在这里在回调函数中通过窗口句柄访问。裁剪区设置WM_SetUserClipRect。临时限制当前窗口的绘制区域。常用于实现进度条不同颜色部分、或者复杂控件的局部优化绘制如前面手册示例中的进度指示器。遍历函数WM_ForEachDesc。对指定窗口的所有后代窗口执行一个回调函数。适用于批量设置皮肤、查找特定类型的子窗口等场景。5. 实战构建一个自定义控件与消息传递范例理论最终需要服务于实践。让我们通过构建一个简单的“可拖动标签”自定义控件来串联消息处理、API调用和用户数据的使用。5.1 控件设计目标我们要创建一个DraggableLabel控件它具有以下功能显示一段文本。可以通过触摸拖动来改变其在父窗口中的位置。拖动开始时控件颜色变化拖动结束时恢复原色。拖动结束后通知父窗口新的位置。5.2 数据结构与创建函数首先定义控件的数据结构。我们将使用窗口的“额外字节”来存储这些实例数据。// DraggableLabel 的数据结构 typedef struct { GUI_COLOR BkColor; // 背景色 GUI_COLOR TextColor; // 文字颜色 GUI_COLOR DragColor; // 拖动时的背景色 char Text[32]; // 显示的文本 int IsDragging; // 拖动状态标志 int StartX, StartY; // 拖动起始点窗口坐标 } DRAGGABLE_LABEL_DATA; // 创建控件的函数 WM_HWIN CreateDraggableLabel(int x0, int y0, int width, int height, WM_HWIN hParent, const char* pText) { WM_HWIN hWin; // 创建窗口指定额外字节数为数据结构的大小 hWin WM_CreateWindowAsChild(x0, y0, width, height, hParent, WM_CF_SHOW, _cbDraggableLabel, sizeof(DRAGGABLE_LABEL_DATA)); if (hWin) { DRAGGABLE_LABEL_DATA Data; // 初始化默认数据 Data.BkColor GUI_GRAY; Data.TextColor GUI_WHITE; Data.DragColor GUI_BLUE; Data.IsDragging 0; strncpy(Data.Text, pText, sizeof(Data.Text)-1); Data.Text[sizeof(Data.Text)-1] \0; // 将初始化数据设置到窗口 WM_SetUserData(hWin, Data, sizeof(Data)); // 设置窗口ID方便父窗口识别可选 WM_SetId(hWin, GUI_ID_USER 0); } return hWin; }5.3 回调函数实现处理消息这是控件的核心处理绘制、触摸和自定义逻辑。static WM_RESULT _cbDraggableLabel(WM_MESSAGE * pMsg) { DRAGGABLE_LABEL_DATA * pData; pData (DRAGGABLE_LABEL_DATA*)WM_GetUserData(pMsg-hWin, NULL, 0); // 获取用户数据指针 switch (pMsg-MsgId) { case WM_PAINT: { GUI_RECT Rect; GUI_COLOR CurrentBkColor; WM_GetClientRect(pMsg-hWin, Rect); // 根据拖动状态选择颜色 CurrentBkColor pData-IsDragging ? pData-DragColor : pData-BkColor; GUI_SetBkColor(CurrentBkColor); GUI_SetColor(pData-TextColor); GUI_Clear(); // 居中显示文本 GUI_SetTextMode(GUI_TM_TRANS); // 透明文本模式 GUI_DispStringInRect(pData-Text, Rect, GUI_TA_HCENTER | GUI_TA_VCENTER); break; } case WM_TOUCH: { const GUI_PID_STATE * pState (const GUI_PID_STATE *)pMsg-Data.p; if (pState-Pressed) { // 手指按下开始拖动 if (!pData-IsDragging) { pData-IsDragging 1; // 记录起始触摸点相对于窗口自身 pData-StartX pState-x; pData-StartY pState-y; // 捕获输入确保手指移出控件区域仍能接收消息 WM_SetCapture(pMsg-hWin, 1); // 使窗口无效触发重绘以改变颜色 WM_InvalidateWindow(pMsg-hWin); } } else { // 手指释放结束拖动 if (pData-IsDragging) { pData-IsDragging 0; // 捕获会因WM_SetCapture的AutoRelease1而自动释放 WM_InvalidateWindow(pMsg-hWin); // 恢复颜色 // 通知父窗口拖动结束并传递新的位置信息 WM_MESSAGE Msg; Msg.MsgId WM_NOTIFY_PARENT; Msg.hWinSrc pMsg-hWin; // 源窗口是本控件 Msg.Data.v MY_NOTIFICATION_DRAG_ENDED; // 自定义通知码 WM_SendToParent(pMsg-hWin, Msg); } } break; } case WM_MOTION: { // 只有在拖动状态下才处理移动消息 if (pData-IsDragging) { const WM_MOTION_INFO * pInfo (const WM_MOTION_INFO *)pMsg-Data.p; // 根据WM_MOTION_INFO中的移动距离(dx, dy)移动窗口 // 这里我们使用简单的移动更复杂的可以处理pInfo-Cmd WM_MoveWindow(pMsg-hWin, pInfo-dx, pInfo-dy); } break; } case WM_GET_ID: { // 响应ID查询 return (WM_RESULT)WM_GetId(pMsg-hWin); } case WM_SET_ID: { // 响应ID设置通常不需要特殊处理 break; } default: // 其他未处理的消息交给默认处理器 return WM_DefaultProc(pMsg); } return WM_OK; }5.4 父窗口中的使用与通知处理最后在父窗口例如一个对话框中创建并使用这个控件并处理其发来的通知。// 在父窗口的WM_CREATE中创建控件 WM_HWIN hMyLabel CreateDraggableLabel(50, 50, 100, 40, pMsg-hWin, Drag Me!); // 在父窗口回调函数中处理自定义通知 case WM_NOTIFY_PARENT: { int Id WM_GetId(pMsg-hWinSrc); int NCode pMsg-Data.v; if (Id GUI_ID_USER 0) { // 判断是否来自我们的自定义控件 switch (NCode) { case MY_NOTIFICATION_DRAG_ENDED: // 获取控件的新位置并做相应处理例如保存配置 GUI_RECT Rect; WM_GetWindowRectEx(pMsg-hWinSrc, Rect); printf(Label moved to (%d, %d)\n, Rect.x0, Rect.y0); break; } } break; }通过这个完整的例子你可以看到如何将消息机制、API调用、用户数据封装结合起来创建一个具有交互功能的可复用GUI组件。这正是emWin窗口管理器强大和灵活之处的体现。