
1. 项目概述从消息驱动到交互细节在嵌入式GUI开发里消息机制就像是整个系统的“神经系统”。它不是简单的函数调用而是一套事件驱动的通信协议。想象一下你的手指触摸屏幕这个物理动作如何变成屏幕上按钮的高亮和弹起或者一个窗口被另一个窗口覆盖后如何知道自己需要重绘这些看似自动发生的“魔法”背后都是emWin窗口管理器WM通过精密的消息传递在默默调度。我刚开始接触emWin时觉得消息处理很繁琐不如直接写死逻辑来得快。但踩过几次坑后才发现正是这套机制让界面逻辑变得清晰、可维护。比如一个复杂的设置页面有几十个控件如果每个控件的点击、触摸、焦点切换都靠全局变量和标志位来联动代码很快就会变成一团乱麻。而消息机制把“发生了什么事”消息和“谁来处理这件事”窗口回调函数优雅地解耦了。ToolTip工具提示功能就是这个“神经系统”上一个非常典型的“条件反射”。它不是一个独立的、时刻显示的窗口而是由一系列消息触发和控制的动态UI元素。用户悬停Hover- WM检测到PID指针输入设备静止 - 触发定时器 - 定时器到期发送消息 - 创建并显示ToolTip窗口 - 用户移动或点击 - 发送消息 - 销毁ToolTip窗口。整个过程行云流水完全由消息驱动不需要你在主循环里不断去查询“鼠标是不是停在那了停了多久”。这就是消息机制的价值它让你从轮询的苦海中解脱出来以声明式的方式描述“当XX发生时请做YY”剩下的交给WM去调度。2. 核心原理WM消息机制深度拆解要玩转emWin的消息和ToolTip不能只停留在调用API的层面必须深入理解其消息机制的设计哲学和运行原理。这就像开车只知道踩油门和刹车也能开但懂了发动机和变速箱的原理你才能开得又快又稳出了问题也知道从哪里排查。2.1 消息系统的架构与流转emWin的消息系统是一个典型的“生产者-消费者”模型但它的生产者不止一个。1. 消息的生产者系统核心这是最主要的生产者。例如当WM检测到需要重绘一个窗口区域时它会生成一个WM_PAINT消息。当定时器到期时生成WM_TIMER消息。输入设备驱动触摸屏或鼠标的驱动层通常通过GUI_PID_StoreState()函数将原始坐标和按压状态提交给GUI。WM作为消费者会根据这些原始数据结合当前的窗口层次结构生成WM_TOUCH、WM_PID_STATE_CHANGED等消息并精准地派发到目标窗口。应用程序自身你可以通过WM_SendMessage()或WM_SendMessageNoPara()函数手动向任何窗口发送消息包括自定义消息。这是实现窗口间通信、触发特定业务逻辑的关键。2. 消息的派发与处理消息的派发是WM的核心职责。它内部维护着一个消息队列。当有消息需要处理时WM会确定目标窗口对于输入事件WM会调用WM_Screen2hWin()等函数根据坐标找出最顶层的、非禁用的、可见的窗口。封装消息结构将消息ID、源窗口句柄、目标窗口句柄以及相关的数据如坐标、键值、通知码填充到一个WM_MESSAGE结构体中。调用回调函数WM会直接调用目标窗口在创建时注册的回调函数Callback Routine并将WM_MESSAGE结构体的指针作为参数传入。默认处理如果窗口的回调函数没有处理某个消息即在switch-case中没有对应的case消息会传递给WM_DefaultProc()进行默认处理。很多基础消息如WM_PAINT调用窗口的绘制回调和WM_DELETE执行清理都在这里得到基本响应。3.WM_MESSAGE结构体消息的载体这是理解一切消息处理的基础。手册里给出了定义但我想结合我的经验用更直白的方式解释每个字段的“潜台词”typedef struct { int MsgId; // “发生了什么”——消息的唯一身份证。 WM_HWIN hWin; // “给谁的”——消息的目的地窗口句柄。 WM_HWIN hWinSrc; // “谁引起的”——消息的源头窗口句柄。对于WM_NOTIFY_PARENT这就是那个“搞事情”的子控件。 union { void * p; // “详细资料在这里”——通常是一个结构体指针承载复杂数据。 int v; // “一个整数就够了”——用于传递ID、状态值等简单信息。 } Data; } WM_MESSAGE;实操心得在处理消息时我养成了一个习惯对于Data.p第一时间去查阅手册或头文件确认它指向的具体是哪个结构体如WM_KEY_INFO,GUI_PID_STATE并做好类型转换。盲目使用p指针是内存访问错误的常见根源。对于Data.v要清楚它在不同消息下的具体含义比如在WM_SET_FOCUS中1表示获得焦点0表示失去焦点。2.2 回调函数窗口的“大脑”每个窗口都必须有一个回调函数。它不是一个被主动调用的函数而是一个被WM“叫到”时才会执行的“事件处理器”。其标准范式如下static void _cbWindow(WM_MESSAGE * pMsg) { switch (pMsg-MsgId) { case WM_PAINT: // 重绘窗口内容 _PaintWindow(pMsg-hWin); break; case WM_TOUCH: // 处理触摸事件 _HandleTouch(pMsg); break; case WM_NOTIFY_PARENT: // 处理子窗口发来的通知 _HandleNotification(pMsg); break; // ... 处理其他消息 default: // 交给默认处理器处理你不关心的消息 WM_DefaultProc(pMsg); } }注意事项WM_DefaultProc()非常重要除非你完全清楚后果否则不要在default分支里什么都不做就直接break。很多基础功能如窗口重绘、焦点切换都依赖默认处理器。漏掉它可能导致窗口无法刷新、无法删除等诡异问题。2.3 消息的分类与使用场景手册里列出了很多消息我们可以从“谁来发”和“用来干什么”的角度重新归类这样更容易理解1. 系统生命周期消息WM_CREATE窗口的“出生证明”。在这里进行资源初始化分配内存、创建子窗口、加载数据是最佳时机。此时窗口的句柄pMsg-hWin已经有效但窗口还未显示。WM_DELETE窗口的“临终遗嘱”。在这里必须释放所有在CREATE阶段或运行时申请的资源内存、定时器、设备上下文等。这是防止内存泄漏的最后关口。2. 绘制与外观消息WM_PAINT最重要的消息之一。WM在认为窗口的某部分“无效”Invalid时会发送此消息要求重绘。Data.p指向一个GUI_RECT表示需要重绘的矩形区域。高级优化技巧对于复杂图形界面可以根据这个无效区域进行局部重绘只刷新脏区能极大提升绘制效率。WM_SIZE/WM_MOVE窗口大小或位置改变后触发。通常在这里重新布局子控件或调整内部数据结构。3. 输入设备消息这是交互的核心顺序和逻辑需要仔细理解。WM_PID_STATE_CHANGED按压状态变化的第一个通知。Data.p-State从0变1表示按下从1变0表示释放。它早于WM_TOUCH发送。WM_TOUCH主要的触摸/鼠标事件消息。携带了坐标和详细的按压状态对于鼠标还能区分左右中键。绝大多数点击、拖拽逻辑在这里处理。WM_MOUSEOVER/WM_MOUSEOVER_END需要启用GUI_SUPPORT_MOUSE。用于实现更精细的鼠标悬停效果比如按钮的Hover状态。4. 焦点与通知消息WM_SET_FOCUS通知窗口它获得或失去了输入焦点。可以在这里改变控件外观如编辑框显示光标。WM_NOTIFY_PARENT子控件向父窗口“打小报告”的专用通道。Data.v里是通知码如WM_NOTIFICATION_CLICKED。这是实现“当按钮被点击后父窗口做出响应”的标准方式实现了控件与业务逻辑的解耦。5. 用户自定义消息当系统消息不够用时可以使用WM_USER为起点定义自己的消息。这是实现模块间异步通信的强大工具。#define MSG_DATA_READY (WM_USER 0) #define MSG_UPDATE_VIEW (WM_USER 1) // 在后台任务中 WM_MESSAGE Msg; Msg.MsgId MSG_DATA_READY; Msg.Data.p (void*)sensorData; WM_SendMessage(hStatusWindow, Msg);3. ToolTip功能实现全解析理解了消息机制再看ToolTip它就不再是一个黑盒功能而是一系列消息和API组合实现的经典案例。它的本质是一个由WM管理、根据PID状态和定时器自动显示/隐藏的、无边框的子窗口。3.1 ToolTip的工作原理与生命周期手册里描述了基本流程我想结合源码和调试经验更细致地还原其内部状态机启用与追踪当你为某个父窗口创建ToolTip对象后WM就开始秘密追踪所有属于该父窗口下的“工具窗口”即你通过WM_TOOLTIP_AddTool添加的窗口上的PID活动。首次悬停检测当PID在某个工具窗口上静止不动超过PERIOD_FIRST时间WM内部的一个定时器到期。此时WM会发送一个内部消息触发ToolTip窗口的创建。窗口创建与显示ToolTip窗口被创建其内容是你预设的文本。WM会根据当前PID位置通常是在工具窗口的旁边或下方计算一个合适的位置来显示它并确保它位于所有窗口之上。持续显示阶段ToolTip显示后如果PID继续保持静止它会一直显示直到又过了PERIOD_SHOW时间才自动隐藏。如果PID在PERIOD_SHOW内移动了则立即隐藏。快速再次触发如果PID移出工具窗口但未移出父窗口区域然后又快速移回另一个工具窗口则只需静止PERIOD_NEXT这个时间通常很短即可再次触发ToolTip无需等待漫长的PERIOD_FIRST。这提升了用户体验。终止条件任何点击事件或PID移出父窗口区域都会立即重置ToolTip状态下次触发需要重新等待PERIOD_FIRST。3.2 两种创建方式详解与选型emWin提供了两种关联ToolTip与工具窗口的方式适用于不同场景。3.2.1 为对话框项创建基于ID这是最常用、最简洁的方式适用于使用DIALOG资源表创建的界面。因为对话框中的每个控件按钮、文本、滑块等在创建时都被赋予了一个唯一的ID。#include DIALOG.h #include WM.h #define ID_BUTTON_OK (GUI_ID_USER 0) #define ID_SLIDER_VOL (GUI_ID_USER 1) // 1. 定义对话框资源 static const GUI_WIDGET_CREATE_INFO _aDialogCreate[] { { FRAMEWIN_CreateIndirect, Settings, 0, 10, 10, 300, 200, 0, 0 }, { BUTTON_CreateIndirect, OK, ID_BUTTON_OK, 200, 150, 80, 30 }, { SLIDER_CreateIndirect, NULL, ID_SLIDER_VOL, 20, 50, 200, 30 }, }; // 2. 定义ToolTip信息数组ID - 提示文本 static const TOOLTIP_INFO _aTooltipInfo[] { { ID_BUTTON_OK, Confirm and save settings }, { ID_SLIDER_VOL, Adjust volume level (0-100) }, }; static void _ShowSettingsDialog(void) { WM_HWIN hDlg; WM_TOOLTIP_HANDLE hToolTip; // 创建对话框 hDlg GUI_CreateDialogBox(_aDialogCreate, GUI_COUNTOF(_aDialogCreate), NULL, WM_HBKWIN, 0, 0); // 3. 一键创建ToolTipWM会自动通过ID找到对应控件。 hToolTip WM_TOOLTIP_Create(hDlg, _aTooltipInfo, GUI_COUNTOF(_aTooltipInfo)); // ... 主循环 }为什么这种方式更优解耦ToolTip配置与控件创建逻辑分离维护清晰。自动关联WM在后台通过WM_GetDialogItem()等函数根据ID自动建立映射你无需手动获取控件句柄。适用于动态控件即使控件是后来通过WM_CreateWindowAsChild动态添加到对话框的只要它有ID并且父窗口是同一个你依然可以在ToolTip信息数组中配置。3.2.2 为普通窗口创建基于句柄当你直接使用WM_CreateWindow创建自定义窗口或者控件没有ID时就需要使用基于句柄的方式。static WM_HWIN hCustomGraph; // 假设这是一个自定义的绘图窗口 void MainTask(void) { WM_HWIN hMainWin, hToolTip; WM_TOOLTIP_HANDLE hToolTip; GUI_Init(); // 创建主窗口和自定义图形窗口 hMainWin WM_CreateWindow(...); hCustomGraph _CreateGraphWindow(hMainWin, ...); // 这个函数返回窗口句柄 // 1. 先创建一个空的ToolTip对象 hToolTip WM_TOOLTIP_Create(hMainWin, NULL, 0); // 2. 通过句柄手动添加工具 if (hToolTip hCustomGraph) { WM_TOOLTIP_AddTool(hToolTip, hCustomGraph, Real-time data waveform chart); } // 甚至可以动态添加 WM_HWIN hAnotherTool _CreateAnotherWindow(hMainWin, ...); WM_TOOLTIP_AddTool(hToolTip, hAnotherTool, Dynamic added tooltip); }注意事项句柄管理你必须自己妥善保存需要添加ToolTip的窗口句柄hCustomGraph。一旦窗口被删除其句柄失效再操作会导致错误。添加时机WM_TOOLTIP_AddTool可以在ToolTip创建后的任何时间调用这为动态UI提供了灵活性。3.3 高级配置与自定义默认的ToolTip可能样式简单。emWin提供了API让你深度定制// 1. 设置外观 WM_TOOLTIP_SetDefaultFont(GUI_Font16_ASCII); // 设置字体 WM_TOOLTIP_SetDefaultColor(GUI_WHITE, GUI_DARKBLUE); // 设置文本色和背景色 // 2. 设置行为时间单位毫秒 WM_TOOLTIP_SetDefaultPeriod(800, 3000, 200); // 参数解释 // PERIOD_FIRST: 800ms - 首次悬停触发延迟 // PERIOD_SHOW: 3000ms - 显示后保持时间 // PERIOD_NEXT: 200ms - 后续快速触发延迟实操心得时间参数的“手感”调优PERIOD_FIRST不宜过短如500ms否则用户鼠标轻微移动就会误触发显得界面很“躁”也不宜过长如1500ms否则用户会觉得反应迟钝。800-1200ms是一个比较舒适的区间。PERIOD_SHOW根据提示文本的长度调整。一句话提示3000-5000ms足够阅读如果文本较长可以适当延长到8000ms。PERIOD_NEXT这个时间要短体现“快速响应”。100-300ms比较合适。4. 消息处理实战从理论到代码光说不练假把式。下面我们通过两个综合性的实战案例将消息处理和ToolTip应用串起来。4.1 案例一实现一个带状态反馈的自定义按钮我们要创建一个按钮不仅有ToolTip还能在按下、释放、获得焦点、失去焦点时改变颜色。#define ID_MY_BUTTON (GUI_ID_USER 100) static void _cbMyButton(WM_MESSAGE * pMsg) { static GUI_COLOR aColorState[4] {GUI_GRAY, GUI_BLUE, GUI_RED, GUI_GREEN}; // 正常按下焦点禁用 static int Pressed 0; static int Focused 0; switch (pMsg-MsgId) { case WM_PAINT: { GUI_COLOR BkColor; const char* pText MyBtn; // 根据状态决定颜色 if (!WM_IsEnabled(pMsg-hWin)) { BkColor aColorState[3]; // 禁用状态 } else if (Pressed) { BkColor aColorState[1]; // 按下状态 } else if (Focused) { BkColor aColorState[2]; // 焦点状态 } else { BkColor aColorState[0]; // 正常状态 } GUI_SetBkColor(BkColor); GUI_Clear(); GUI_SetColor(GUI_WHITE); GUI_SetTextMode(GUI_TM_NORMAL); GUI_DispStringHCenterAt(pText, WM_GetWindowSizeX(pMsg-hWin)/2, WM_GetWindowSizeY(pMsg-hWin)/2 - GUI_GetFontSizeY()/2); break; } case WM_TOUCH: { const GUI_PID_STATE * pState (const GUI_PID_STATE *)pMsg-Data.p; if (pState) { int IsPressed (pState-Pressed ! 0); if (IsPressed ! Pressed) { // 状态发生变化 Pressed IsPressed; WM_InvalidateWindow(pMsg-hWin); // 触发重绘更新颜色 } // 重要即使我们处理了TOUCH也要通知父窗口比如对话框按钮被点击了 if (!IsPressed Pressed) { // 从按下到释放即一次点击完成 WM_NotifyParent(pMsg-hWin, WM_NOTIFICATION_CLICKED); } } break; } case WM_SET_FOCUS: { Focused (pMsg-Data.v ! 0); // Data.v: 1获得焦点0失去焦点 WM_InvalidateWindow(pMsg-hWin); // 重绘以更新焦点状态 break; } case WM_GET_ID: { // 必须处理此消息返回按钮的IDToolTip等功能依赖它 pMsg-Data.v ID_MY_BUTTON; break; } default: WM_DefaultProc(pMsg); } } // 在对话框中创建这个自定义按钮并为其添加ToolTip static const GUI_WIDGET_CREATE_INFO _aCreate[] { { WINDOW_CreateIndirect, Win, 0, 0, 0, 320, 240 }, { BUTTON_CreateIndirect, StdBtn, ID_BUTTON_0, 50, 50, 80, 30 }, { NULL, NULL, 0, 0,0,0,0,0,0,0 }, // 自定义控件需要用这种特殊形式 }; // 注意自定义控件的创建需要更复杂的回调设置此处为示意。实际需用WM_CreateWindowAsChild。4.2 案例二利用自定义消息更新复杂界面假设我们有一个数据采集线程采集到数据后需要更新UI上的波形图和数值标签。直接在线程中调用GUI函数是不安全的非线程安全。最佳实践是发送自定义消息。// 1. 定义自定义消息 #define MSG_UPDATE_SENSOR_DATA (WM_USER 0) #define MSG_ALERT_USER (WM_USER 1) // 2. 主窗口回调函数 static void _cbMainWindow(WM_MESSAGE * pMsg) { static int sSensorValue 0; static char acBuffer[32]; switch (pMsg-MsgId) { case MSG_UPDATE_SENSOR_DATA: { SENSOR_DATA_T *pData (SENSOR_DATA_T *)pMsg-Data.p; // 假设数据放在结构体里 if (pData) { sSensorValue pData-value; // 更新数值标签假设hValueLabel是之前保存的句柄 sprintf(acBuffer, Value: %d, sSensorValue); TEXT_SetText(hValueLabel, acBuffer); // 触发波形图重绘发送WM_PAINT消息或设置无效区域 WM_InvalidateWindow(hWaveformGraph); } break; } case MSG_ALERT_USER: { const char* pAlertText (const char*)pMsg-Data.p; // 弹出警告框或更新状态栏 _ShowAlert(pAlertText); break; } // ... 处理其他系统消息 default: WM_DefaultProc(pMsg); } } // 3. 在数据采集线程或中断回调、定时器回调中 void DataAcquisition_Task(void) { SENSOR_DATA_T newData; while(1) { // ... 采集数据到 newData if (g_hMainWindow ! 0) { // 确保主窗口已创建 WM_MESSAGE Msg; Msg.MsgId MSG_UPDATE_SENSOR_DATA; Msg.hWin g_hMainWindow; Msg.Data.p (void*)newData; // 发送消息这是线程安全的通信方式。 WM_SendMessage(g_hMainWindow, Msg); } OS_Delay(100); // 假设每100ms采集一次 } }5. 深度优化与避坑指南掌握了基础我们再来探讨一些高级话题和常见陷阱。这些内容往往在手册里一笔带过却是项目稳定性的关键。5.1 性能优化消息处理与绘制1. 无效区域与局部重绘WM_PAINT的Data.p指向一个GUI_RECT无效区域。盲目重绘整个窗口是性能杀手。case WM_PAINT: { const GUI_RECT * pRect (const GUI_RECT *)pMsg-Data.p; // 只绘制无效区域内的内容 if (GUI_RectsIntersect(MyContentRect, pRect)) { _DrawPartialContent(pMsg-hWin, pRect); } break; }2. 启用内存设备Memory Device对于频繁更新、动画复杂的窗口启用内存设备可以彻底消除闪烁。WM_EnableMemdev(hMyWindow); // 为该窗口启用 // 或者在GUIConf.h中全局启用 WM支持的内存设备原理所有绘制操作先在内存中的位图上完成然后一次性拷贝到屏幕避免中间状态的显示。3. 谨慎使用WM_InvalidateWindow()这个函数会标记整个窗口为无效导致全窗口重绘。尽量使用WM_InvalidateRect()指定需要更新的最小矩形区域。5.2 内存与资源管理1. ToolTip的内存泄漏WM_TOOLTIP_Create创建的对象必须用WM_TOOLTIP_Delete删除。常见的错误是在对话框关闭时只删除了对话框窗口忘了删除关联的ToolTip对象。最佳实践在父窗口的WM_DELETE消息中删除其创建的所有ToolTip。2. 定时器管理通过WM_CreateTimer创建的定时器会在窗口收到WM_TIMER消息。务必在窗口的WM_DELETE消息中调用WM_DeleteTimer删除所有关联的定时器否则会导致定时器回调访问已删除的窗口引发崩溃。3. 用户数据User Data每个窗口都可以通过WM_SetUserData和WM_GetUserData关联一个自定义的指针。这是存储窗口实例私有数据的推荐方式比使用全局变量更安全、更清晰。例如你可以将一个结构体指针存储在这里里面包含该窗口的所有状态变量。5.3 常见问题排查FAQQ1: ToolTip为什么不显示检查1父窗口是否正确ToolTip是关联到父窗口的确保WM_TOOLTIP_Create传入的句柄是目标控件直接或间接的父窗口。检查2PID支持是否开启确保在GUIConf.h中GUI_SUPPORT_TOUCH或GUI_SUPPORT_MOUSE已定义为1并且底层驱动正确调用了GUI_PID_StoreState。检查3时间参数是否极端PERIOD_FIRST是否设得太大比如10000检查4窗口是否可见且启用被WM_HideWindow隐藏或被WM_DisableWindow禁用的窗口不会触发ToolTip。Q2: 我的自定义窗口收不到WM_TOUCH消息检查1窗口样式。创建窗口时确保没有使用WM_CF_MEMDEV_ONLY等可能影响消息接收的标志。检查2窗口层级和可见性。确认你的窗口在触摸点位置是最顶层的、可见的、已启用的窗口。可以用WM_Screen2hWin()函数调试触摸点的窗口句柄。检查3回调函数是否正确传递。确保在switch-case的最后有default: WM_DefaultProc(pMsg);。Q3:WM_NOTIFY_PARENT消息没反应检查1通知码是否正确子控件发送的通知码必须是预定义的如WM_NOTIFICATION_CLICKED。拼写错误或数值错误会导致父窗口无法识别。检查2父窗口回调是否处理在父窗口的WM_NOTIFY_PARENTcase中需要解析pMsg-Data.v来获取通知码并根据pMsg-hWinSrc判断是哪个子控件发出的。Q4: 使用内存设备后局部更新无效这是正常现象。内存设备的工作原理是全缓冲。当你调用WM_InvalidateRect只标记局部无效时WM在下次绘制时会先将内存设备中该窗口的整个位图拷贝到屏幕然后只重绘无效区域到内存设备。所以视觉上你还是看到了全窗口更新。要真正优化需要在WM_PAINT中根据无效区域进行绘制内容的裁剪而不是依赖WM的局部更新机制。这通常需要更复杂的绘制逻辑。6. 配置选项与移植要点最后我们看看那些影响WM和ToolTip行为的编译时配置。这些宏定义在GUIConf.h或WM_Conf.h中需要在项目初期就规划好。6.1 关键配置宏WM_SUPPORT_NOTIFY_VIS_CHANGED(默认 0)设为1后当窗口的可见性如被遮挡、显示隐藏发生变化时会收到WM_NOTIFY_VIS_CHANGED消息。仅在需要极精细控制绘制如叠加硬件图层视频时开启因为会增加系统开销。WM_SUPPORT_TRANSPARENCY(默认 1)支持透明窗口。如果你的项目完全用不到透明效果可以设为0来节省一部分ROM和RAM。GUI_SUPPORT_MOUSE/GUI_SUPPORT_TOUCH(在GUIConf.h)必须根据你的硬件至少开启一个PID消息机制和ToolTip才有效。WM_MAX_WINDOWS系统支持的最大窗口数量。务必根据项目实际窗口数设置设得太小会创建窗口失败设太大会浪费内存。6.2 移植与调试建议从简单开始先实现一个只有按钮和ToolTip的简单界面确保消息循环和PID输入正常。善用模拟器SEGGER的emWin模拟器是强大的调试工具。你可以单步跟踪消息发送、观察窗口句柄变化、检查无效区域这比在目标板上调试高效得多。自定义消息调试可以在自定义消息处理中加入日志输出跟踪复杂的业务逻辑流。关注栈空间消息回调函数、特别是WM_PAINT内的局部变量和函数调用会消耗栈空间。在资源紧张的MCU上需要合理分配任务栈大小避免溢出。消息机制是emWin GUI的基石初学时会觉得概念繁多但一旦掌握设计复杂交互界面就会变得得心应手。我的经验是多写几个例子故意制造一些错误比如不调用WM_DefaultProc观察现象理解会深刻得多。把每个窗口想象成一个有独立生命和感知能力的对象消息就是它们之间的语言而你的代码就是在教它们如何听、如何说、如何做出反应。