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,一頭連著程序員的創意。而我們這些老程序員,不過是在橋上往返穿梭的趕路人,把經驗刻在欄桿上,供后來者參考。
技術會迭代,但對簡潔與美的追求,對問題本質的探索,永遠是程序員的初心。未完待續.....