技术演进中的开发沉思-35 MFC系列:消息映射与命令
个人认为windows编程里最为重要的就是通讯机制了,而这里消息映射与命令传递,正是 MFC 世界里的 “通讯协议”。当用户移动鼠标、点击菜单,甚至窗口被风吹得晃动了一下,程序都能接收到对应的 “消息”,并按部就班地做出反应。这背后的逻辑,既是技术的精妙,也藏着当年的我对 “人机交互” 最朴素的理解。
一、消息分类
在 MFC 程序的世界里,消息就像穿梭于城市各个角落的通讯信号,有着不同的种类和特性。如果把程序比作一个热闹的大商场,那消息分类就如同商场里不同类型的通讯方式。
有的消息是顾客的 “即时呼唤”,比如鼠标点击按钮、键盘敲击输入,这些就像顾客在柜台前大声询问,需要店员立刻回应,属于用户输入消息,它们要求程序在瞬间做出反应,否则就会影响用户体验。还记得早年开发一个数据录入系统时,有用户反馈按下回车键后光标没按预期跳到下一个输入框,后来发现就是对键盘消息的处理不够及时,就像店员没听清顾客的要求,让顾客多等了半天。
还有的消息是商场内部的 “运营通知”,像窗口的创建、移动、销毁,这些就像是商场的开关门时间、摊位调整通知,属于系统消息,它们有着固定的流程和节奏,按部就班处理即可。这类消息虽然不那么紧急,却关系到程序的基础运行,就像商场的基础设施维护,看似不起眼,却缺一不可。
另外还有命令消息,它更像是各部门之间的 “工作指令”,比如菜单中的 “保存”“打印” 命令,就像经理给员工下达的工作任务,需要特定的部门去执行。这些消息往往关联着具体的功能实现,是程序完成各种操作的核心驱动力。
看以下的代码示例:
// 处理键盘消息(用户输入消息)void CMyWnd::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags){// 当按下回车键时,执行相应操作if (nChar == VK_RETURN){// 处理回车键逻辑,如光标跳转NextControl();}CWnd::OnKeyDown(nChar, nRepCnt, nFlags);}// 处理窗口创建消息(系统消息)int CMyWnd::OnCreate(LPCREATESTRUCT lpCreateStruct){if (CWnd::OnCreate(lpCreateStruct) == -1)return -1;// 窗口创建时的初始化操作InitControls();return 0;}// 处理命令消息(如菜单“保存”命令)void CMyDoc::OnFileSave(){// 执行保存文件的操作SaveDocument();}
二、CCmdTarget 的作用
如果把 MFC 程序的消息处理系统比作一个大型企业的办公体系,那 CCmdTarget 就像是这个企业的前台总机。所有的消息,无论是来自外部的用户操作,还是内部的系统通知,都会先经过它的处理。
它就像一位经验丰富的前台接待员,有着敏锐的判断力。当一个消息传来时,它会先看看自己能不能处理,如果能,就直接安排处理;如果不能,就会根据消息的性质,把它转接到合适的 “部门”—— 也就是 CCmdTarget 的派生类,比如视图类、文档类等。
我早年开发一个绘图软件时,曾遇到过一个棘手的问题:点击工具栏上的 “撤销” 按钮,程序毫无反应。排查了很久才发现,原来是 CCmdTarget 的派生类之间的关联出了问题,就像前台总机把经理的重要指令转错了部门,导致指令石沉大海。后来重新梳理了 CCmdTarget 派生类的关系,问题才迎刃而解,那一刻深刻体会到 CCmdTarget 这个 “前台总机” 的关键作用。
以下是体现 CCmdTarget 作用的代码示例:
// 派生自 CCmdTarget 的类class CMyCmdTarget : public CCmdTarget{DECLARE_MESSAGE_MAP()public:afx_msg void OnMyCommand();};BEGIN_MESSAGE_MAP(CMyCmdTarget, CCmdTarget)ON_COMMAND(ID_MY_COMMAND, &CMyCmdTarget::OnMyCommand)END_MESSAGE_MAP()void CMyCmdTarget::OnMyCommand(){// 处理命令的逻辑}// 在程序中发送命令消息,由 CCmdTarget 及其派生类处理void SendMyCommand(){CMyCmdTarget cmdTarget;// 发送命令消息,CCmdTarget 会找到对应的处理函数cmdTarget.SendMessage(WM_COMMAND, ID_MY_COMMAND);}
三、消息映射网的形成(宏与结构)
消息映射网的形成,就像在程序内部搭建一张无形的通讯网络,而宏和结构就是构建这张网络的砖瓦和钢筋。
BEGIN_MESSAGE_MAP和END_MESSAGE_MAP这对宏就像是网络的总入口和总出口,划定了消息映射的范围。在它们之间,ON_COMMAND ON_WM_LBUTTONDOWN等宏则像是一个个通讯节点,把特定的消息和对应的处理函数连接起来。
这些宏在编译时会被转化为特定的结构,这些结构就像网络中的交换机,记录着消息的来源和去向。当程序运行时,消息就会沿着这些由宏和结构构建的路径传递,最终到达对应的处理函数。
记得第一次成功搭建起消息映射网时,看着程序能够准确响应各种操作,那种感觉就像看着自己亲手编织的渔网成功捕到了鱼。那些看似枯燥的宏和结构,在那一刻仿佛有了生命,它们相互配合,让程序变得灵活而有序。
以下是消息映射网形成的代码示例:
class CMyWnd : public CWnd{DECLARE_MESSAGE_MAP()public:afx_msg void OnLButtonDown(UINT nFlags, CPoint point);afx_msg void OnFileOpen();};// 消息映射宏构建消息映射网BEGIN_MESSAGE_MAP(CMyWnd, CWnd)ON_WM_LBUTTONDOWN() // 映射鼠标左键按下消息ON_COMMAND(ID_FILE_OPEN, &CMyWnd::OnFileOpen) // 映射“打开文件”命令消息END_MESSAGE_MAP()// 消息处理函数void CMyWnd::OnLButtonDown(UINT nFlags, CPoint point){// 处理鼠标左键按下逻辑CWnd::OnLButtonDown(nFlags, point);}void CMyWnd::OnFileOpen(){// 处理打开文件逻辑}
四、消息传递路线
消息传递路线就像城市里的快递配送路线,一条消息从产生到被处理,往往要经过多个 “站点” 的传递。
比如用户在视图上点击鼠标,这个消息首先会到达视图类,视图类会先判断自己是否需要处理这个消息。如果不需要,就会把消息传递给文档类,文档类再根据情况决定是自己处理还是传递给应用程序类。就像一份快递,从用户手中发出,先到小区快递点,再到区域配送中心,最后到达收件人手中。
不同的消息有着不同的传递规则,有的消息只能在特定的类之间传递,有的消息则可以在多个类之间流转。这种层级分明又灵活多变的传递路线,保证了消息能够被最适合的 “处理者” 接收和处理。
在开发一个多文档应用时,我曾为了追踪一条消息的传递路线,在多个类的消息处理函数中加入日志输出,看着日志里记录的消息传递过程,就像看着快递的物流信息,清晰地知道消息在哪个 “站点” 被处理,哪个 “站点” 被转发,这种清晰的路线让程序的调试变得简单了许多。
以下是展示消息传递路线的代码示例:
// 视图类class CMyView : public CView{DECLARE_MESSAGE_MAP()public:afx_msg void OnMouseClick();};BEGIN_MESSAGE_MAP(CMyView, CView)ON_COMMAND(ID_MOUSE_CLICK, &CMyView::OnMouseClick)END_MESSAGE_MAP()void CMyView::OnMouseClick(){// 视图类先处理,如果处理不了则传递给文档类if (!HandleInView()){GetDocument()->PostMessage(WM_COMMAND, ID_MOUSE_CLICK);}}// 文档类class CMyDoc : public CDocument{DECLARE_MESSAGE_MAP()public:afx_msg LRESULT OnMouseClick(WPARAM wParam, LPARAM lParam);};BEGIN_MESSAGE_MAP(CMyDoc, CDocument)ON_MESSAGE(ID_MOUSE_CLICK, &CMyDoc::OnMouseClick)END_MESSAGE_MAP()LRESULT CMyDoc::OnMouseClick(WPARAM wParam, LPARAM lParam){// 文档类处理消息HandleInDoc();return 0;}
四、UI 对象变化
在 Scribble 程序的 Step2 中,UI 对象的变化就像是给这个绘图软件的 “通讯系统” 做了一次升级,让它变得更加智能和友好。
当我们在画板上画画时,菜单中的 “撤销”“重做” 命令会根据是否有可撤销或重做的操作,实时改变自己的状态 —— 可用或灰色不可用。工具栏上的绘图工具按钮,在被选中时会呈现按下状态,清晰地告诉用户当前的绘图模式。
这些变化的背后,正是消息映射与命令传递在发挥作用。程序会不断地发送消息查询 UI 对象的状态,UI 对象则通过消息映射把自己的状态反馈给程序,程序再根据反馈调整 UI 对象的显示。
这就像一个智能的交通系统,交通信号灯会根据路口的车流量实时调整红绿灯时长,让交通更加顺畅。Scribble Step2 中 UI 对象的实时变化,让用户能够更直观地了解程序的状态,操作起来也更加得心应手。
以下是一段与 Scribble Step2 中 UI 对象状态变化相关的 VC++ 代码示例:
BEGIN_MESSAGE_MAP(CScribbleView, CView)// 其他消息映射ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CScribbleView::OnUpdateEditUndo)ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CScribbleView::OnUpdateEditRedo)END_MESSAGE_MAP()void CScribbleView::OnUpdateEditUndo(CCmdUI* pCmdUI){// 根据是否有可撤销操作更新"撤销"命令状态pCmdUI->Enable(m_pDoc->CanUndo());}void CScribbleView::OnUpdateEditRedo(CCmdUI* pCmdUI){// 根据是否有可重做操作更新"重做"命令状态pCmdUI->Enable(m_pDoc->CanRedo());}
这段代码中,ON_UPDATE_COMMAND_UI宏就像一个状态查询器,它会定期发送消息查询 “撤销”“重做” 命令的状态。OnUpdateEditUndo和OnUpdateEditRedo函数则根据文档类提供的信息,决定这些命令是否可用,从而实现了 UI 对象状态的实时变化。
最后小结
消息映射与命令传递是 MFC 程序实现人机交互的核心机制,如同一个精密的通讯系统,让程序能够准确接收、传递和处理各种消息。
从消息分类来看,用户输入消息、系统消息和命令消息各司其职,分别对应着不同的场景需求;CCmdTarget 作为消息处理的 “前台总机”,负责消息的筛选与转接,确保消息能找到合适的处理者;消息映射网则通过宏与结构构建起消息传递的路径,让消息能沿着预设路线顺畅流转;而消息传递路线就像快递配送网络,保证消息在各个 “站点” 间有序传递;
我想理解这一机制,能体会到早期程序员在构建人机交互系统时的智慧与巧思,由Windows 消息机制中事件驱动、消息队列、循环处理、分层分发的核心思想,在后续诸多技术框架中均有体现。如:桌面开发的 Qt、.NET 的 WinForms/WPF,到 Web 前端的 JavaScript DOM 事件系统、Node.js 异步事件模型,再到移动开发的 Android 事件处理机制、iOS 的 UIKit 框架,以及跨平台框架 Flutter、React Native,都延续了这一思想,其穿越桌面、Web、移动开发时代,都成为了现在技术人机交互的底层逻辑。未完待续.........