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.pswp.cn/web/88437.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/88437.shtml
英文地址,请注明出处:http://en.pswp.cn/web/88437.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习-06(Optimization-自动调整学习率)

临界点其实不一定是在训练神经网络过程中遇到的最大阻碍。随着对参数的不断更新&#xff0c;Loss值会不断下降&#xff0c;直至几乎没有变化&#xff0c;不再下降。当参数更新到临界点时&#xff0c;意味着gradient非常小&#xff0c;所以要认定参数是否到达临界点应当确认grad…

Uniapp中的uni.scss

uni.scss为uni-app新建项目自带工程文件&#xff0c;使用的预处理器为sass/scss&#xff0c;由此可见&#xff0c;uni-app官方推荐的是scss。 uni.scss特点 无需引入&#xff0c;uni-app在编译时&#xff0c;会自动引入此文件在此中定义的scss变量&#xff0c;可以全局使用&…

PreparedStatement 实现分页查询详解

PreparedStatement 实现分页查询详解 在 JDBC 中使用 PreparedStatement 实现分页查询是高效安全的方式&#xff0c;可以避免 SQL 注入并提升性能。下面我将详细说明实现步骤和原理。 &#x1f4d0; 分页查询核心参数参数名说明计算公式pageNum当前页码&#xff08;从1开始&…

ClamAV 和 FreshClam:Linux 服务器上的开源杀毒解决方案

ClamAV 和 FreshClam:Linux 服务器上的开源杀毒解决方案 1. 概述 ClamAV 是一款开源的防病毒引擎,专为 Linux 服务器设计,用于检测恶意软件、病毒、木马和其他安全威胁。它广泛应用于邮件服务器、文件存储系统和 Web 服务器,提供高效的病毒扫描功能。 主要特点: 免费开…

PySpark中python环境打包和JAR包依赖

在 PySpark 中打包 Python 环境并调度到集群是处理依赖一致性的关键步骤。以下是完整的解决方案&#xff0c;包含环境打包、分发和配置方法&#xff1a; 一、环境打包方法 使用 Conda 打包环境 # 创建 Conda 环境 conda create -n pyspark_env python3.8 conda activate pyspar…

和鲸社区深度学习基础训练营2025年关卡2(1)纯numpy

拟分3种实现方法&#xff1a;1.纯numpy2.sklearn中的MLPClassifier3.pytorch题目&#xff1a; 在 MNIST 数据集上训练 MLP 模型并比较不同的激活函数和优化算法任务描述&#xff1a;使用 MNIST 数据集中的前 20,000 个样本训练一个多层感知机 (MLP) 模型。你需要比较三种不同的…

Sequential Thinking:AI深度思考的新范式及其与CoT、ReAct的对比分析

引言&#xff1a;AI深度思考的演进与Sequential Thinking的崛起在人工智能技术快速发展的今天&#xff0c;AI模型的思考能力正经历着从简单应答到深度推理的革命性转变。这一演进过程不仅反映了技术本身的进步&#xff0c;更体现了人类对机器智能认知边界的持续探索。早期的大语…

云原生详解:构建现代化应用的未来

引言 在数字化转型的浪潮中,"云原生"已成为技术领域最热门的话题之一。从初创公司到全球500强企业,都在积极探索云原生技术以提升业务敏捷性和创新能力。本文将全面解析云原生的概念、核心技术、优势以及实践路径,帮助您深入理解这一改变IT格局的技术范式。 什么…

SSE事件流简单示例

文章目录1、推送-SseEmitter2、接收-EventSourceListenerSSE&#xff08;Server-Sent Events&#xff0c;服务器推送事件&#xff09;是一种基于HTTP的服务器向客户端实时推送数据的技术标准。1、推送-SseEmitter SseEmitter用于实现服务器向客户端单向、长连接的实时数据推送…

Elasticsearch RESTful API入门:基础搜索与查询DSL

Elasticsearch RESTful API入门&#xff1a;基础搜索与查询DSL 本文为Elasticsearch初学者详细解析RESTful API的核心操作与查询DSL语法&#xff0c;包含大量实战示例及最佳实践。 一、Elasticsearch与RESTful API简介 Elasticsearch&#xff08;ES&#xff09;作为分布式搜索…

(六)复习(OutBox Message)

文章目录 项目地址一、OutBox Message1.1 OutBox表配置1. OutBoxMessage类2. OutboxMessage表配置3. 给每个模块生成outboxmessage表1.2 发布OutBox Message1. 修改Intercepotor2. 配置Quartz3. 创建Quatz方法发布领域事件4. 创建Quatz定时任务5. 注册Quatz服务和配置6. 流程梳…

STM32-ADC内部温度

在通道16无引脚&#xff08;测量温度不准确&#xff09;跟ADC代码差不多&#xff1b;不需要使能引脚时钟&#xff1b;将内部温度测量打开/*** brief 启用或禁用温度传感器和内部参考电压功能* param NewState: 新的功能状态&#xff0c;取值为ENABLE或DISABLE* retval 无* no…

「Linux命令基础」文本模式系统关闭与重启

关机重启基本命令 直接拔掉计算机电源可能损坏内部元件;Linux系统通过命令关闭计算机则是安全流程,让所有程序有机会保存数据、释放资源。 关机命令:shutdown Linux系统提供了多种用于关闭或重启系统的命令,其中 shutdown 是最常用的一种,它可以安全地通知用户系统即将…

射频信号(大宽高比)时频图目标检测anchors配置

一、大宽高比目标YOLO检测参数设置 这是yolov7的一个label的txt文件&#xff1a; 1 0.500 0.201 1.000 0.091 2 0.500 0.402 1.000 0.150 3 0.500 0.604 1.000 0.093 0 0.500 0.804 1.000 0.217 对应的样本&#xff1a; 长宽比分别是&#xff1a;1/0.09110.98, 1/0.1506.67…

OpenStack 鉴权服务介绍.md

引言 OpenStack是一个开源的云计算管理平台&#xff0c;其中的Keystone组件承担了身份认证和授权的关键任务。Keystone的主要功能包括管理用户及其权限、维护OpenStack Services的Endpoint&#xff0c;以及实现认证&#xff08;Authentication&#xff09;和鉴权&#xff08;Au…

Linux_3:进程间通信

IPC1.什么是IPC&#xff1f;Inter Process Communication2.进程间通信常用的几种方式1&#xff0c;管道通信&#xff1a;有名管道&#xff0c;无名管道2&#xff0c;信号- 系统开销小3&#xff0c;消息队列-内核的链表4&#xff0c;信号量-计数器5&#xff0c;共享内存6&#x…

【Springboot】Bean解释

在 Spring Boot 中&#xff0c;Bean 就像是你餐厅里的一名员工。比如&#xff0c;你有一名服务员&#xff08;Service&#xff09;、一名厨师&#xff08;Chef&#xff09;和一名收银员&#xff08;Cashier&#xff09;。这些员工都是餐厅正常运转所必需的&#xff0c;他们各自…

axios的post请求,数据为什么要用qs处理?什么时候不用?

为什么使用 qs 处理 POST 数据axios 的 POST 请求默认将 JavaScript 对象序列化为 JSON 格式&#xff08;Content-Type: application/json&#xff09;。但某些后端接口&#xff08;尤其是传统表单提交&#xff09;要求数据以 application/x-www-form-urlencoded 格式传输&…

【unitrix】 4.21 类型级二进制数基本结构体(types.rs)

一、源码 这段代码定义了一个类型级数值系统的 Rust 实现&#xff0c;主要用于在编译时表示和操作各种数值类型。 use crate::sealed::Sealed; use crate::number::{NonZero, TypedInt, Unsigned, Primitive}; // // 特殊浮点值枚举 ///// 特殊浮点值&#xff08;NaN/∞&#x…

UI前端与数字孪生结合实践案例:智慧零售的库存管理优化系统

hello宝子们...我们是艾斯视觉擅长ui设计和前端数字孪生、大数据、三维建模、三维动画10年经验!希望我的分享能帮助到您!如需帮助可以评论关注私信我们一起探讨!致敬感谢感恩!一、引言&#xff1a;数字孪生重构零售库存的 “人 - 货 - 场” 协同在零售行业利润率持续承压的背景…