当前位置: 首页 > news >正文

技术演进中的开发沉思-30 MFC系列:五大机制

MFC,记得我刚毕业时在 CRT 显示器前敲下第一行 MFC 代码时,那时什么都不懂,没有框架的概念。只觉得眼前的 CObject 像位沉默且复杂的大家族, 就像老北京胡同里的大家族,每个门牌号都藏着自己的故事。但现在看看,MFC 那些看似复杂的机制,其实都是为了让程序员能快速梳能够了解它熟悉它。MFC在我眼里最重要的就是:RTTI(运行时类型识别)、Dynamic Creation(动态创建)、Persistence(永久保存机制)、Message Mapping(消息映射)、Command Routing(命令传递)。这几个部分构件成了MFC最为重要的内容,让它能够成为一把开发的利刃。

一、类层次:程序世界的家族图谱

MFC 的类层次就像一棵枝繁叶茂的老槐树。最顶端的 CObject 是所有类的老祖宗,它定下了家族的基本规矩:每个子孙都得会自我介绍(RTTI)、能自己生孩子(动态创建)、还得懂得把重要物件收进箱子(永久保存)。就像胡同里的长辈总会教晚辈 "出门要报家门,回家要锁好门"。

往下看,CDocument 和 CView 像一对默契的夫妻:文档负责管家里的 "存折"(数据),视图负责把 "家底" 展示给外人看。而 CWnd 家族更像个热闹的大家庭,按钮、编辑框、窗口都是它的孩子,每个孩子都继承了 "与人打交道" 的本事(消息处理),又各有各的脾气 —— 就像胡同里的张大爷爱下棋,李大妈爱聊天。


// 简化的类层次关系示意class CObject {}; // 老祖宗class CCmdTarget : public CObject {}; // 能处理命令的长辈class CWnd : public CCmdTarget {}; // 窗口家族家长class CFrameWnd : public CWnd {}; // 框架窗口class CEdit : public CWnd {}; // 编辑框晚辈

二、初始化

MFC 程序的启动过程,像极了剧院里一场演出的筹备。WinMain 函数就像幕后导演,先把舞台搭好(注册窗口类),再请出主角(CWinApp 对象)。当你双击 exe 文件时,就像拉开了大幕:

程序先鞠躬问好( AfxWinInit 初始化),然后主角登场(theApp 全局对象构造),接着导演喊 "开始"(Run 函数),主窗口这个 "舞台" 才缓缓升起。整个过程环环相扣,就像包饺子时先和面、再擀皮、最后包馅,少一步都不成。


// 程序启动的核心流程CMyApp theApp; // 全局应用对象,先于WinMain构造int WINAPI WinMain(...) {AfxWinInit(...); // 初始化MFC运行环境return theApp.Run(); // 进入消息循环}

三、五大机制

1. RTTI:对象的身份证

在没有身份证的年代,人们靠熟人辨认身份。MFC 的 RTTI 就像给每个对象发了张带芯片的身份证,用 IsKindOf 函数一刷,就知道它是不是某个家族的成员。我曾在调试时靠它揪出一个伪装成按钮的静态文本框,就像居委会大妈一眼识破混进小区的陌生人。


// 运行时类型判断if (pWnd->IsKindOf(RUNTIME_CLASS(CEdit))) {// 确认是编辑框对象((CEdit*)pWnd)->SetWindowText("我是编辑框");}

2. 动态创建

动态创建机制让程序能根据类名 "打印" 出对象,就像点餐时说 "来份宫保鸡丁",厨房就会按配方做出相应的菜。MFC 靠 DECLARE_DYNCREATE 和 IMPLEMENT_DYNCREATE 这对 "符咒",让每个可创建的类都藏着自己的 "菜谱"。当年做插件系统时,这招帮我们实现了 "按需加载",就像旅行时只带必要的行李。

3. 永久保存

Persistence 机制让对象能把自己的状态写进文件,就像把夏天的西瓜放进冰箱,冬天还能尝到清凉。Serialize 函数就是那个负责打包的保鲜膜,把数据一层层裹好。我至今记得第一次用它恢复误删的绘图数据时,感觉像在废墟里挖出了藏宝盒。


// 序列化示例void CMyData::Serialize(CArchive& ar) {if (ar.IsStoring()) {// 保存数据,像把东西装进箱子ar << m_nValue << m_strText;} else {// 读取数据,从箱子里取东西ar >> m_nValue >> m_strText;}}

4. 消息映射

如果说 Windows 系统是座巨型写字楼,那每个窗口都是一间办公室,而用户的每一次操作 —— 点击鼠标、敲击键盘、拖动窗口 —— 都是一封亟待投递的信件。消息映射机制,就是 MFC 为这座写字楼打造的智能邮政系统,比普通邮局多了几分 "未卜先知" 的智慧。​

记得 2003 年做工业监控软件时,车间的操作台有 16 个按钮,每个按钮按下都要触发不同的设备动作。最初我像个新手邮差,在代码里写满 if-else 逐个判断消息来源,就像捧着一堆信件挨家挨户敲门。直到用上消息映射,才明白什么叫 "精准投递"—— 每个按钮的点击消息都像贴了电子标签,会自动飞向对应的处理函数,效率比手工分拣提升了何止十倍。​

这个系统的核心是三张 "邮政清单":消息映射表(message map)、消息哈希表(hash table)和消息处理函数指针数组。当鼠标在窗口上点击时,Windows 内核会生成一封特殊的 "信"(MSG 结构体),信封上写着接收窗口的 HWND(就像办公室门牌号)、消息类型(WM_LBUTTONDOWN 相当于 "紧急快递")和附加信息(坐标值如同包裹里的物品清单)。​

MFC 收到这封信后,先查消息映射表。这个表是用 BEGIN_MESSAGE_MAP 和 END_MESSAGE_MAP 宏自动生成的,看起来像本厚厚的通讯录:​


BEGIN_MESSAGE_MAP(CMyWnd, CWnd)​ON_WM_LBUTTONDOWN() // 鼠标左键按下​ON_WM_KEYDOWN() // 键盘按键​ON_BN_CLICKED(IDC_OK, &CMyWnd::OnOK) // OK按钮点击​END_MESSAGE_MAP()​​

这些宏会在编译时变成类似这样的结构:​

static const AFX_MSGMAP_ENTRY _messageEntries[] = {​{ WM_LBUTTONDOWN, 0, 0, 0, AfxSig_vwp, (AFX_PMSG)&CMyWnd::OnLButtonDown },​{ WM_KEYDOWN, 0, 0, 0, AfxSig_vw, (AFX_PMSG)&CMyWnd::OnKeyDown },​{ WM_COMMAND, IDC_OK, IDC_OK, 0, AfxSig_v, (AFX_PMSG)&CMyWnd::OnOK },​{0, 0, 0, 0, AfxSig_end, NULL } // 表结束标记​};​

​就像邮政系统的分拣机,MFC 会用消息 ID 做哈希运算,快速定位到对应的处理函数。如果当前窗口处理不了这封信(比如子窗口收到本应由父窗口处理的命令),消息会沿着类层次向上传递,就像前台收信员处理不了的文件会递给部门经理,这就是所谓的 "消息冒泡" 机制。​

最妙的是 ON_COMMAND 这类宏,能把菜单、工具栏按钮和快捷键的命令消息统一处理。当年做文本编辑器时,我给 "复制" 功能同时绑定了菜单选项、工具栏按钮和 Ctrl+C 快捷键,消息映射像个贴心的秘书,自动把这三种操作都引向同一个 Copy 函数,省去了大量重复代码。​

但这个系统也有 "脾气"。有次调试打印功能,点击菜单后毫无反应,查了三天才发现是把 ON_COMMAND (ID_PRINT, &OnPrint) 写成了 ON_WM_COMMAND (ID_PRINT, &OnPrint)—— 就像把 "航空邮件" 的标签贴成了 "平邮",信件自然被送进了错误的分拣通道。那时没有现在的调试工具,只能靠在消息循环里加断点,看着消息一个个流过,像在监控录像里找丢失的包裹。​

如今想来,消息映射最伟大的地方,是把 Windows 复杂的消息机制包装成了程序员能理解的 "人类语言"。它就像架在机器指令和人类思维之间的翻译机,让我们不用背诵枯燥的消息常量,也能和操作系统顺畅对话。这种 "隐藏复杂性" 的智慧,正是所有优秀框架的共同特质。

5. 命令传递

命令传递机制,就像一家运转有序的公司里的审批流程,每个环节都有明确的分工和流转规则,确保每一个指令都能找到最合适的处理者。​

举个例子:如果开发了一个图书管理系统,其中有个 "借阅统计" 的功能按钮。按常理,这个按钮在工具栏上,点击后该由谁来处理呢?当时我犯了难,是让工具栏自己处理,还是交给显示图书列表的视图,或是负责数据管理的文档?后来才明白,命令传递机制早就为我们设计好了清晰的路径。​

就像员工(工具栏按钮)提交了一份审批单(命令消息),首先会交给直属部门经理(视图)。视图会看自己是否有权限和能力处理,如果它处理不了,就会把审批单交给分管副总(框架窗口)。框架窗口要是也处理不了,就会上报给总经理(文档),最后还可能提交给公司最高层(应用程序对象)。​

在 MFC 中,这个流程是通过一系列函数协作完成的。当命令消息产生后,首先会调用视图的 OnCmdMsg 函数,视图会检查自己的消息映射表,如果有对应的处理函数,就像部门经理能直接审批,事情就解决了。如果没有,它会调用 GetParent 函数找到框架窗口,把命令传递过去,就像部门经理签上 "转上级处理" 后递交给副总。​

框架窗口收到后,同样会先查看自己的消息映射表,要是处理不了,会通过 GetActiveDocument 找到文档对象,继续传递命令,这就像副总再转交给总经理。文档如果也无法处理,最终会传到应用程序对象那里。​


// 命令传递的大致流程示意​BOOL CView::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) {​// 视图先尝试处理命令​if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) {​return TRUE;​}​// 处理不了则传递给框架窗口​CFrameWnd* pFrame = GetParentFrame();​if (pFrame != NULL && pFrame->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) {​return TRUE;​}​// 再传递给文档​CDocument* pDoc = GetDocument();​if (pDoc != NULL && pDoc->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) {​return TRUE;​}​return FALSE;​}​

​就之前提到的 "借阅统计" 功能,视图负责显示统计结果,文档负责从数据库读取借阅数据。当点击按钮时,命令先到视图,视图知道自己没有数据处理能力,就把命令传给了文档。文档处理完数据后,再通知视图更新显示,整个过程行云流水,就像一场配合默契的接力赛。​

但这个流程也有需要注意的地方。如果在框架窗口和视图中都定义了同一个命令的处理函数,你胡发现,结果发现总是视图先处理。这是因为命令传递是有优先级的,就像审批流程中,低级别的管理者如果能处理,就不会麻烦上级。这就要求我们在设计时,要明确每个命令最适合的处理者,避免出现混乱。​

命令传递机制的巧妙之处在于,它让程序的各个模块既能各司其职,又能高效协作。就像一家公司,每个部门有自己的职责,但当遇到跨部门的问题时,有明确的流程让问题得到妥善处理。这种机制不仅让代码结构更清晰,也大大提高了开发效率,让程序员能更专注于业务逻辑的实现,而不用过多操心命令的传递路径。

最后小结:

如今的程序员可能都不知道 MFC,就像我的孩子看不懂 BB 机一样。但那些隐藏在代码背后的设计思想 —— 如何让复杂系统变得有序,如何让机器理解人类的意图 —— 永远不会过时。MFC 就像一座桥,一头连着底层的 Windows API,一头连着程序员的创意。而我们这些老程序员,不过是在桥上往返穿梭的赶路人,把经验刻在栏杆上,供后来者参考。

技术会迭代,但对简洁与美的追求,对问题本质的探索,永远是程序员的初心。未完待续.....

http://www.dtcms.com/a/271796.html

相关文章:

  • 删除k8s安装残留
  • Spring Boot:将应用部署到Kubernetes的完整指南
  • ACL协议:核心概念与配置要点解析
  • Docker 环境下 MySQL 主从复制集群、MGR 搭建及 Nginx 反向代理配置
  • SSRF10 各种限制绕过之30x跳转绕过协议限制
  • ip地址可以精确到什么级别?如何获取/更改ip地址
  • 配置双网卡Linux主机作为路由器(连接NAT网络和仅主机模式网络)
  • 在 Mac 上使用 Git 拉取项目:完整指南
  • 【算法笔记】6.LeetCode-Hot100-链表专项
  • selenium中find_element()用法进行元素定位
  • 在mac m1基于llama.cpp运行deepseek
  • Spring Boot 企业级动态权限全栈深度解决方案,设计思路,代码分析
  • C#基础:Winform桌面开发中窗体之间的数据传递
  • 【WEB】Polar靶场 Day8 详细笔记
  • 力扣 hot100 Day40
  • fastMCP基础(一)
  • imx6ull-裸机学习实验16——I2C 实验
  • 解锁localtime:使用技巧与避坑指南
  • shell 字符串常用操作
  • 网安系列【16】之Weblogic和jboss漏洞
  • 深入剖析 ADL:C++ 中的依赖查找机制及其编译错误案例分析
  • 短剧分销系统开发指南:从0到1构建高效变现平台
  • 基于双向cuk斩波均衡电路的串联锂离子均衡系统设计
  • 文心一言4.5开源部署指南及文学领域测评
  • frp内网穿透下创建FTP(解决FTP“服务器回应不可路由的地址。使用服务器地址替代”错误)
  • 【macos用镜像站体验】Claude Code入门使用教程和常用命令
  • JS实现页面实时时间显示/倒计时
  • SMTPman,smtp的端口号是多少全面解析配置
  • 【数据结构】时间复杂度和空间复杂度
  • 杰赛S65_中星微ZX296716免拆刷机教程解决网络错误和时钟问题