嵌入式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就是窗口坐标。
- 桌面坐标:以物理显示屏的左上角为原点(0,0),向右为X轴正方向,向下为Y轴正方向。这是绝对的坐标参考系,
理解并正确运用这两套坐标系,是避免出现控件错位、触摸点不准等问题的关键。一个常见的技巧是,使用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_DELETE
WM_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_TIMER
WM_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
这是理解触摸处理的关键。当用户手指接触屏幕时,并非只产生一个事件,而是一个有序的事件流。
按下(Press):
WM_PID_STATE_CHANGED:State从0变为1,StatePrev为0。这个消息最先到达,纯粹告知“按压状态已改变”。WM_TOUCH:Pressed为1。紧随其后,携带具体的坐标信息。
移动(Move, 手指保持按压并滑动):
- 仅发送
WM_TOUCH消息,且Pressed保持为1。不会发送WM_PID_STATE_CHANGED,因为按压状态没有改变。
- 仅发送
释放(Release):
WM_PID_STATE_CHANGED:State从1变为0,StatePrev为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_MOUSEOVER
WM_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,则当用户释放触摸(收到Pressed=0的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 { // 释放,捕获会因AutoRelease=1而自动结束 _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的AutoRelease=1而自动释放 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窗口管理器强大和灵活之处的体现。
