起因

用 emWin 的 MESSAGEBOX 时,默认的 OK 按键行为不一定满足需求——比如要在关闭前做确认、清理资源,或者像本文一样需要删除整个对话框,但又不可能去改 emWin 的库源码。emWin 提供的正式入口就是 WM_SetCallback():把窗口的消息处理函数换成自己的。

需求

  • MESSAGEBOX 里点 OK 按键后,删除整个消息框对话框
  • 除这个行为外,窗口其余行为(重绘、可移动等)保持默认

技术实现原理

emWin 是消息驱动的

emWin 窗口管理器(WM)的核心机制是回调 + 消息:每个窗口创建时都带一个回调函数,系统把按键、触摸、定时器、重绘请求统统打包成 WM_MESSAGE 结构投递给这个回调。控件(BUTTON)自己不做业务,它把"被点了"这件事包装成 WM_NOTIFY_PARENT 消息发给父窗口,消息里携带三样东西:

  • pMsg->MsgId:消息类型(这里是 WM_NOTIFY_PARENT);
  • pMsg->hWinSrc:消息来源——哪个子控件发的,用 WM_GetId() 取出控件 Id;
  • pMsg->Data.v:通知码 NCode(CLICKED / RELEASED 等)。

WM_SetCallback 与消息流向

WM_SetCallback(hWin, _cbDialogMessage) 把窗口原来的内部默认回调替换成我们的函数,之后这个窗口的所有消息都先进 _cbDialogMessage。整个链路是:

  1. BUTTON 被按下/松开 → BUTTON 内部回调给父窗口发 WM_NOTIFY_PARENT;
  2. 这条消息进入替换后的 _cbDialogMessage
  3. 只关心 GUI_ID_OK + WM_NOTIFICATION_RELEASED,命中就 WM_DeleteWindow(hWin) 删除对话框;
  4. 其他所有消息交给 WM_DefaultProc(pMsg) 走默认处理。

关键在第 4 步:不认识的消息必须交还默认过程。窗口的重绘(WM_PAINT)、移动(MOVEABLE 特性)、子窗口管理都依赖默认处理。替换回调等于接管了这个窗口的全部消息,而不只是想处理的那一条——漏掉 default 分支,轻则窗口不刷新、拖不动,重则花屏。这就是自定义回调后最常见 BUG 的原理根因。

代码实现

下段代码就是通过 MESSAGEBOX 控件的 OK 按键实现删除信息框的对话框,即消息框的背景窗口 DIALOG 删除了,留下了 TEXT & BUTTON

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 hWin = MESSAGEBOX_Create("这是一个测试", "测试标题", GUI_MESSAGEBOX_CF_MOVEABLE);
 WM_SetCallback(hWin, _cbDialogMessage);
 static void _cbDialogMessage(WM_MESSAGE *pMsg)
 {
     WM_HWIN hWin;
     int NCode;
     int Id;
     hWin = pMsg->hWin;
     switch (pMsg->MsgId)
     {
         case WM_NOTIFY_PARENT:
             Id = WM_GetId(pMsg->hWinSrc);
             NCode = pMsg->Data.v;
             switch (Id)
             {
                 case GUI_ID_OK:
                     switch (NCode)
                     {
                         case WM_NOTIFICATION_CLICKED:
                             break;
                         case WM_NOTIFICATION_RELEASED:
                             WM_DeleteWindow(hWin);
                             break;
                     }
                     break;
             }
             break;
         default:
             WM_DefaultProc(pMsg);
             break;
      }
 }

代码说明

  • MESSAGEBOX_Create(..., GUI_MESSAGEBOX_CF_MOVEABLE):创建可移动的消息框,返回背景窗口(DIALOG)句柄;
  • WM_SetCallback(hWin, _cbDialogMessage):接管这个背景窗口的消息处理;
  • WM_NOTIFY_PARENT 分支:用 WM_GetId(pMsg->hWinSrc) 取来源控件 Id,通知码在 pMsg->Data.v 里;
  • 只在 WM_NOTIFICATION_RELEASED(松开)时执行删除,CLICKED(按下)忽略——避免按下去的瞬间误触发;
  • default: WM_DefaultProc(pMsg):其余消息交还系统,保持默认行为。

踩坑记录与注意事项

  • 漏 default 分支:窗口不重绘、拖不动、花屏。替换回调是全接管,必须把不处理的消息还给 WM_DefaultProc
  • CLICKED 和 RELEASED 别混:CLICKED 是按下瞬间、RELEASED 是松开,做"删除/确认"类动作用 RELEASED 更符合操作直觉;
  • 回调里别做耗时操作:回调在 WM 消息循环的上下文里执行,长循环会卡住整个 GUI,多任务版本里还会导致喂狗超时;
  • 多个窗口可共用一个回调:回调是普通 C 函数,区分"是谁的消息"要靠 pMsg->hWin,不要在回调里写死窗口句柄;
  • 删除要删对句柄WM_DeleteWindow 会连同子窗口一起删;如果删完发现"留下了 TEXT & BUTTON"(本文观察到的现象),说明删到的只是背景窗口,需要确认传入的 hWin 是不是整个对话框。