MFC六大核心機制概述html
咱們選擇了C++,主要是由於它夠藝術、夠自由,使用它咱們能夠實現各類想法,而MFC將多種可靈活使用的功能封裝起來,咱們豈能忍受這種「黑盒」操做?因而研究分析MFC的核心機制成爲必然。ios
首先,列出要講的MFC六大核心機制:程序員
一、MFC程序的初始化。
二、運行時類型識別(RTTI)。
三、動態建立。
四、永久保存。
五、消息映射。
六、消息傳遞。編程
本文講第一部分,MFC程序的初始化過程。小程序
簡單的MFC窗口程序windows
設計一個簡單完整MFC程序,產生一個窗口。固然這不能讓AppWizard自動爲咱們生成。咱們能夠在Win32 Application工程下面那樣寫:數組
C++代碼數據結構
1 #include <afxwin.h> 2 class MyApp : public CWinApp 3 { 4 public: 5 BOOL InitInstance() //②程序入點 6 { 7 CFrameWnd *Frame=new CFrameWnd();//構造框架 8 m_pMainWnd=Frame; //將m_pMainWnd設定爲Frame; 9 Frame->Create(NULL,"最簡單的窗口");//創建框架 10 Frame->ShowWindow(SW_SHOW); //顯示框架 11 return true; //返回 12 } 13 }; 14 MyApp theApp; //①創建應用程序。
設定連接MFC庫,運行,便可看見一個窗口。框架
從上面,你們能夠看到創建一個MFC窗口很容易,只用兩步:一是從CWinApp派生一個應用程序類(這裏是MyApp),而後創建應用程序對象(theApp),就能夠產生一個本身須要的窗口(即須要什麼樣就在InitInstance()裏建立就好了)。編輯器
整個程序,就改寫一個InitInstance()函數,建立那麼一個對象(theApp),就是一個完整的窗口程序。這就是「黑盒」操做的魔力!
在咱們正想爲微軟鼓掌的時候,咱們忽然以爲內心空蕩蕩的,咱們想知道微軟幫咱們作了什麼事情,而咱們想編本身的程序時又須要作什麼事情,哪怕在上面幾行的程序裏面,咱們還有不清楚的地方,好比,幹嗎有一個m_pMainWnd指針變量,它從哪裏來,又要到哪裏去呢?想想在DOS下編程是多麼美妙的一件事呵,咱們須要什麼變量,就聲明什麼變量,須要什麼樣的函數,就編寫什麼樣的函數,或者引用函數庫……可是如今咱們怎麼辦?
咱們能夠逆向思惟一下,MFC要達到這種效果,它是怎麼作的呢?首先咱們要弄明白,VC++不是一種語言,它就象咱們學c語言的時候的一個相似記事本的編輯器(請原諒個人不貼切的比喻),因此,在VC裏面咱們用的是C++語言編程,C++纔是根本(初學者老是覺得VC是一門什麼新的什麼語言,一門比C++先進不少的複雜語言,汗)。說了那麼多,我想用一句簡單的話歸納「MFC黑箱’,就是爲咱們的程序加入一些固化的‘C++代碼’的東西」。
既然MFC黑箱幫咱們加入了代碼,那麼你們想一想它會幫咱們加入什麼樣的代碼呢?他會幫咱們加入求解一元二次方程的代碼嗎?固然不會,因此它加入的其實是每次編寫窗口程序必須的,通用的代碼。
再往下想,什麼纔是通用的呢?咱們每次視窗編程都要寫WinMain()函數,都要有註冊窗口,產生窗口,消息循環,回調函數……即然每次都要的東西,就讓它們從咱們眼前消失,讓MFC幫忙寫入!
手動模擬MFC程序的初始化
要知道MFC初始化過程,你們固然能夠跟蹤執行程序。但這種跟蹤很麻煩,我相信你們都會跟蹤的暈頭轉向。本人以爲哪怕你理解了MFC代碼,也很容易讓人找不着北,咱們徹底不懂的時候,在成千上萬行程序的迷宮中如何能找到出口?
咱們要換一種方法,不如就來從新編寫個MFC庫吧,譁!你們不要笑,當心你的大牙,我不是瘋子(雖然瘋子也說本身不瘋)。咱們要寫的就是最簡單的MFC類庫,就是把MFC宏觀上的,理論上的東西寫出來。咱們要用最簡化的代碼,簡化到恰好能運行。
1、須要「重寫」的MFC庫
既然,咱們這一節寫的是MFC程序的初始化過程,上面咱們還有了一個可執行的MFC程序。程序中只是用了兩個MFC類,一個是CWinApp,另外一個是CFrameWnd。固然,還有不少一樣重要MFC類如視圖類,文檔類等等。但在上面的程序能夠不用到,因此暫時省去了它(總之是爲了簡單)。
好,如今開始寫MFC類庫吧……唉,面前又有一個大難題,就是讓你們背一下MFC層次結構圖。天,那張魚網怎麼記得住,但既然咱們要理解他,總得知道它是從那裏派生出來的吧。
考慮到你們都很辛苦,那咱們看一下上面兩個類的父子關係(箭頭表明派生):
CObject->CCmdTarget->CWinThread->CWinApp->本身的重寫了InitInstance()的應用程序類。
CObject(同上)->CCmdTarget(同上)->CWnd->CFrameWnd
看到層次關係圖以後,終於能夠開始寫MFC類庫了。按照上面層次結構,咱們能夠寫如下六個類(爲了直觀,省去了構造函數和析構函數)。
C++代碼
15 /////////////////////////////////////////////////////////
16 class CObiect{};//MFC類的基類。
17 class CCmdTarget : public CObject{};
18 ------------------------------------------------
19 class CWinThread : public CCmdTarget{};
20 class CWinApp : public CWinThread{};
21 ------------------------------------------------
22 class CWnd : public CCmdTarget{};
23 class CFrameWnd : public CWnd{};
24 /////////////////////////////////////////////////////////
你們再想一下,在上面的類裏面,應該有什麼?你們立刻會想到,CWinApp類或者它的基類CCmdTarget裏面應該有一個虛函數virtual BOOL InitInstance(),是的,由於那裏是程序的入口點,初始化程序的地方,那天然少不了的。可能有些朋友會說,反正InitInstance()在派生類中必定要重載,我不在CCmdTarget或CWinApp類裏定義,留待CWinApp的派生類去增長這個函數可不能夠。扯到這個問題可能有點越說越遠,但我想信C++的朋友對虛函數應該是沒有太多的問題的。總的來講,做爲程序員若是清楚知道基類的某個函數要被派生類用到,那定義爲虛函數要方便不少。
也有不少朋友問,C++爲何不自動把基類的全部函數定義爲虛函數呢,這樣能夠省了不少麻煩,這樣全部函數都遵守派生類有定義的函數就調用派生類的,沒定義的就調用基類的,不用寫virtual的麻煩多好!其實,不少面向對象的語言都這樣作了。但定義一個虛函數要生成一個虛函數表,要佔用系統空間,虛函數越多,表就越大,有時得不償失!這裏哆嗦幾句,是由於日後要說明的消息映射中你們更加會體驗到這一點,好了,就此打往。
上面咱們本身解決了一個問題,就是在CCmdTarge寫一個virtual BOOL InitInstance()。
2、WinMain()函數和CWinApp類
你們再往下想,咱們還要咱們MFC「隱藏」更多的東西:WinMain()函數,設計窗口類,窗口註冊,消息循環,回調函數……咱們立刻想到封裝想封裝他們。你們彷佛隱約地感受到封裝WinMain()不容易,以爲WinMain()是一個特殊的函數,許多時候它表明了一個程序的起始和終結。因此在之前寫程序的時候,咱們寫程序習慣從WinMain()的左大括寫起,到右大括弧返回、結束程序。
咱們換一個角度去想,有什麼東西能夠拿到WinMain()外面去作,許多初學者們,總以爲WinMain()函數是天大的函數,什麼函數都好象要在它裏面才能真正運行。其實這樣瞭解很片面,甚至錯誤。咱們能夠寫一個這樣的C++程序:
C++代碼
25 ////////////////////////////////////////////////////
26 #include <iostream.h>
27 class test{
28 public:
29 test(){cout<<"請改變你對main()函數的見解!"<<endl;}
30 };
31 test test1;
32 /**************************/
33 void main(){}
34 ////////////////////////////////////////////////////
在上面的程序裏,入口的main()函數表面上什麼也不作,但程序執行了(注:實際入口函數作了一些咱們能夠不瞭解的事情),並輸出了一句話(注:全局對象比main()首先運行)。如今你們能夠知道咱們的WinMain()函數能夠什麼都不作,程序依然能夠運行,但沒有這個入口函數程序會報錯。
那麼WinMain()函數會放哪一個類上面呢,請看下面程序:
C++代碼
35 #include <afxwin.h>
36 class MyApp : public CWinApp
37 {
38 public:
39 BOOL InitInstance() //②程序入點
40 {
41 AfxMessageBox("程序依然能夠運行!");
42 return true;
43 }
44 };
45
46 MyApp theApp; //①創建應用程序。
你們能夠看到,我並無構造框架,而程序卻能夠運行了——彈出一個對話框(若是沒有WinMain()函數程序會報錯)。上面我這樣寫仍是爲了直觀起見,其實咱們只要寫兩行程序:
#include <afxwin.h>
CWinApp theApp; //整個程序只構造一個CWinApp類對象,程序就能夠運行!
因此說,只要咱們構造了CWinApp對象,就能夠執行WinMain()函數。咱們立刻相信WinMain()函數是在CWinApp類或它的基類中,而不是在其餘類中。其實這種見解是錯誤的,咱們知道編寫C++程序的時候,不可能讓你在一個類中包含入口函數,WinMain()是由系統調用,跟咱們的平時程序自身調用的函數有着本質的區別。咱們能夠暫時簡單想象成,當CWinApp對象構造完的時候,WinMain()跟着執行。
如今你們明白了,大部分的「通用代碼(咱們想封裝隱藏的東西)」均可以放到CWinApp類中,那麼它又是怎樣運行起來的呢?爲何構造了CWinApp類對象就「自動」執行那麼多東西。
你們再仔細想一下,CWinApp類對象構造以後,它會「自動」執行本身的構造函數。那麼咱們能夠把想要「自動」執行的代碼放到CWinApp類的構造函數中。
那麼CWinApp類可能打算這樣設計(先不計較正確與否):
C++代碼
47 class CWinApp : public CWinThead{
48 public:
49 virtual BOOL InitInstance(); //解釋過的程序的入點
50 CWinApp ::CWinApp(){ //構造函數
51 ////////////////////////
52 WinMain(); //這個是你們一眼看出的錯誤
53 Create(); //設計、建立、更新顯示窗口
54 Run(); //消息循環
55 //////////////////////
56 }
57 };
寫完後,你們又立刻感受到彷佛不對,WinMain()函數在這裏好象真的一點用處都沒有,而且能這樣被調用嗎(請容許我把手按在聖經上聲明一下:WinMain()不是普通的函數,它要肩負着初始化應用程序,包括全局變量的初始化,是由系統而不是程序自己調用的,WinMain()返回以後,程序就結束了,進程撤消)。再看Create()函數,它能肯定設計什麼樣的窗口,建立什麼樣的窗口嗎?若是能在CWinApp的構造函數裏肯定的話,咱們之後設計MFC程序時窗口就一個樣,這樣彷佛不太合理。
回過頭來,咱們可讓WinMain()函數一條語句都不包含嗎?不能夠,咱們看一下WinMain() 函數的四個參數:
WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
其中第一個參數指向一個實例句柄,咱們在設計WNDCLASS的時候必定要指定實例句柄。咱們窗口編程,確定要設計窗口類。因此,WinMain()再簡單也要這樣寫:
int WinMain(HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{ hInstance=hinst }
既然實例句柄要等到程序開始執行才能知道,那麼咱們用於建立窗口的Create()函數也要在WinMain()內部才能執行(由於若是等到WinMain()執行完畢後,程序結束,進程撤消,固然Create()也不可能建立窗口)。
再看Run()(消息循環)函數,它能在WinMain()函數外面運行嗎?衆所周知,消息循環就是相同的那麼幾句代碼,但咱們也不要企圖把它放在WinMain()函數以外執行。
因此咱們的WinMain()函數能夠像下面這樣寫:
WinMain(……)
{
……窗口類對象執行建立窗口函數……
……程序類對象執行消息循環函數……
}
對於WinMain()的問題,得總結一下,咱們封裝的時候是不能夠把它封裝到CWinApp類裏面,但因爲WinMain()的不變性(或者說有規律可循),MFC徹底有能力在咱們構造CWinApp類對象的時候,幫咱們完成那幾行代碼。
轉了一個大圈,咱們彷彿又回到了SDK編程的開始。但如今咱們如今能清楚地知道,表面上MFC與SDK編程大相徑庭,但實質上MFC只是用類的形式封裝了SDK函數,封裝以後,咱們在WinMain()函數中只須要幾行代碼,就能夠完成一個窗口程序。咱們也由此知道了應如何去封裝應用程序類(CWinApp)和主框架窗口類(CFrameWnd)。下面把上開始設計這兩個類。
3、MFC庫的「重寫」
爲了簡單起見,咱們忽略這兩個類的基類和派生類的編寫,可能你們會認爲這是一種很不負責任的作法,但本人以爲這既可減輕負擔,又免了你們在各種之間穿來穿去,更好理解一些(咱們在關鍵的地方做註明)。還有,我把所有代碼寫在同一個文件中,讓你們看起來不用那麼吃力,但這是最不提倡的寫代碼方法,你們不要學哦!
C++代碼
58 #include <windows.h>
59 HINSTANCE hInstance;
60
61 class CFrameWnd
62 {
63 HWND hwnd;
64 public:
65 CFrameWnd(); //也能夠在這裏調用Create()
66 virtual ~CFrameWnd();
67 int Create(); //類就留意這一個函數就好了!
68 BOOL ShowWnd();
69 };
70 class CWinApp1
71 {
72 public:
73 CFrameWnd* m_pMainWnd;//在真正的MFC裏面
74 //它是CWnd指針,但這裏因爲不寫CWnd類
75 //只要把它寫成CFrameWnd指針
76 CWinApp1* m_pCurrentWinApp;//指向應用程序對象自己
77 CWinApp1();
78 virtual ~CWinApp1();
79 virtual BOOL InitInstance();//MFC本來是必須重載的函數,最重要的函數!!!!
80 virtual BOOL Run();//消息循環
81 };
82 CFrameWnd::CFrameWnd(){}
83 CFrameWnd::~CFrameWnd(){}
84
85 int CFrameWnd::Create() //封裝建立窗口代碼
86 {
87 WNDCLASS wndcls;
88 wndcls.style=0;
89 wndcls.cbClsExtra=0;
90 wndcls.cbWndExtra=0;
91 wndcls.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);
92 wndcls.hCursor=LoadCursor(NULL,IDC_CROSS);
93 wndcls.hIcon=LoadIcon(NULL,IDC_ARROW);
94 wndcls.hInstance=hInstance;
95 wndcls.lpfnWndProc=DefWindowProc;//默認窗口過程函數。
96 //你們能夠想象成MFC通用的窗口過程。
97 wndcls.lpszClassName="窗口類名";
98 wndcls.lpszMenuName=NULL;
99 RegisterClass(&wndcls);
100
101 hwnd=CreateWindow("窗口類名","窗口實例標題名",WS_OVERLAPPEDWINDOW,0,0,600,400,NULL,NULL,hInstance,NULL);
102 return 0;
103 }
104
105 BOOL CFrameWnd::ShowWnd()//顯示更新窗口
106 {
107 ShowWindow(hwnd,SW_SHOWNORMAL);
108 UpdateWindow(hwnd);
109 return 0;
110 }
111
112 /////////////
113 CWinApp1::CWinApp1()
114 {
115 m_pCurrentWinApp=this;
116 }
117 CWinApp1::~CWinApp1(){}
118 //如下爲InitInstance()函數,MFC中要爲CWinApp的派生類改寫,
119 //這裏爲了方便理解,把它放在CWinApp類裏面完成!
120 //你只要記住真正的MFC在派生類改寫此函數就好了。
121 BOOL CWinApp1::InitInstance()
122 {
123 m_pMainWnd=new CFrameWnd;
124 m_pMainWnd->Create();
125 m_pMainWnd->ShowWnd();
126 return 0;
127 }
128
129 BOOL CWinApp1::Run()//////////////////////封裝消息循環
130 {
131 MSG msg;
132 while(GetMessage(&msg,NULL,0,0))
133 {
134 TranslateMessage(&msg);
135 DispatchMessage(&msg);
136 }
137 return 0;
138 } //////////////////////////////////////////////////////封裝消息循環
139
140 CWinApp1 theApp; //應用程序對象(全局)
141
142 int WINAPI WinMain( HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
143 {
144 hInstance=hinst;
145 CWinApp1* pApp=theApp.m_pCurrentWinApp;
146 //真正的MFC要寫一個全局函數AfxGetApp,以獲取CWinApp指針。
147 pApp->InitInstance();
148 pApp->Run();
149 return 0;
150 }
代碼那麼長,實際上只是寫了三個函數,一是CFrameWnd類的Create(),第二個是CWinApp類的InitInstance()和Run()。在此特別要說明的是InitInstance(),真正的MFC中,那是咱們跟據本身構造窗口的須要,本身改寫這個函數。
你們能夠看到,封裝了上面兩個類之後,在入口函數WinMain中就寫幾行代碼,就能夠產生一個窗口程序。在MFC中,由於WinMain函數就是固定的那麼幾行代碼,因此MFC絕對能夠幫咱們自動完成(MFC的特長就是幫咱們完成有規律的代碼),也所以咱們建立MFC應用程序的時候,看不到WinMain函數。
MFC六大核心機制之二:運行時類型識別(RTTI)
typeid運算子
運行時類型識別(RTTI)便是程序執行過程當中知道某個對象屬於某個類,咱們平時用C++編程接觸的RTTI通常是編譯器的RTTI,便是在新版本的VC++編譯器裏面選用「使能RTTI」,而後載入typeinfo.h文件,就能夠使用一個叫typeid()的運算子,它的地位與在C++編程中的sizeof()運算子相似的地方(包含一個頭文件,而後就有一個熟悉好用的函數)。typdid()關鍵的地方是能夠接受兩個類型的參數:一個是類名稱,一個是對象指針。因此咱們判別一個對象是否屬於某個類就能夠象下面那樣:
C++代碼
1 if (typeid (ClassName)== typeid(*ObjectName)){
2 ((ClassName*)ObjectName)->Fun();
3 }
像上面所說的那樣,一個typeid()運算子就能夠輕鬆地識別一個對象是否屬於某一個類,但MFC並非用typeid()的運算子來進行動態類型識別,而是用一大堆使人費解的宏。不少學員在這裏很疑惑,好象MFC在大部分地方都是故做神祕。使們你們編程時很迷惘,只知道在這裏加入一組宏,又在那兒加入一個映射,而不知道咱們爲何要加入這些東東。
其實,早期的MFC並無typeid()運算子,因此只能沿用一個老辦法。咱們甚至能夠想象一下,若是MFC早期就有template(模板)的概念,可能更容易解決RTTI問題。
因此,咱們要回到「古老」的年代,想象一下,要完成RTTI要作些什麼事情。就好像咱們在一個新型(新型到咱們還不認識)電器公司裏面,咱們要識別哪一個是電飯鍋,哪一個是電磁爐等等,咱們要查看登記的各電器一系列的信息,咱們才能夠比較、鑑別,那個東西是什麼!
CRuntimeClass鏈表的設計
要登記一系列的消息並非一件簡單的事情,你們可能首先想到用數組登記對象。但若是用數組,咱們要定義多大的數組纔好呢,大了浪費空間,小了更加不行。因此咱們要用另外一種數據結構——鏈表。由於鏈表理論上可大可小,能夠無限擴展。
鏈表是一種經常使用的數據結構,簡單地說,它是在一個對象裏面保存了指向下一個同類型對象的指針。咱們大致能夠這樣設計咱們的類:
C++代碼
4 struct CRuntimeClass
5 {
6 ……類的名稱等一切信息……
7 CRuntimeClass * m_pNextClass;//指向鏈表中下一CRuntimeClass對象的指針
8 };
鏈表還應該有一個表頭和一個表尾,這樣咱們在查鏈表中各對象元素的信息的時候才知道從哪裏查起,到哪兒結束。咱們還要註明自己是由哪能個類派生。因此咱們的鏈表類要這樣設計:
C++代碼
9 struct CRuntimeClass
10 {
11 ……類的名稱等一切信息……
12 CRuntimeClass * m_pBaseClass;//指向所屬的基類。
13 CRuntimeClass * m_pNextClass;//定義表尾的時候只要定義此指針爲空就能夠 了。
14 static CRuntimeClass* pFirstClass;//這裏表頭指針屬於靜態變量,由於咱們知道static變量在內存中只初始化一次,就能夠爲各對象所用!保證了各對象只有一個表頭。
15 };
有了CRuntimeClass結構後,咱們就能夠定義鏈表了:
C++代碼
16 static CRuntimeClass classCObject={NULL,NULL}; //這裏定義了一個CRuntimeClass對象,由於classCObject無基類,因此m_pBaseClass爲NULL。由於目前只有一個元素(即目前沒有下一元素),因此m_pNextClass爲NULL(表尾)。
至於pFirstClass(表頭),你們可能有點想不通,它到什麼地方去了。由於咱們這裏並不想把classCObject做爲鏈表表頭,咱們還要在前面插入不少的CRuntimeClass對象,而且由於pFirstClass爲static指針,便是說它不是屬於某個對象,因此咱們在用它以前要先初始化:CRuntimeClass* CRuntimeClass::pFirstClass=NULL;。
如今咱們能夠在前面插入一個CRuntimeClass對象,插入以前我得重要申明一下:若是單純爲了運行時類型識別,咱們未必用到m_pNextClass指針(更可能是在運行時建立時用),咱們關心的是類自己和它的基類。這樣,查找一個對象是否屬於一個類時,主要關心的是類自己及它的基類。
C++代碼
17 CRuntimeClass classCCmdTarget={ &classCObject, NULL};
18 CRuntimeClass classCWnd={ &classCCmdTarget ,NULL };
19 CRuntimeClass classCView={ &classCWnd , NULL };
好了,上面只是僅僅爲一個指針m_pBaseClass賦值(MFC中真正CRuntimeClass有多個成員變量和方法),就鏈接成了鏈表。假設咱們如今已所有構造完成本身須要的CRuntimeClass對象,那麼,這時候應該定義表頭。即要用pFirstClass指針指向咱們最後構造的CRuntimeClass對象——classCView。
C++代碼
20 CRuntimeClass::pFirstClass=&classCView;
如今鏈表有了,表頭表尾都完善了,問題又出現了,咱們應該怎樣訪問每個CRuntimeClass對象?要判斷一個對象屬於某類,咱們要從表頭開始,一直向表尾查找到表尾,而後才能比較得出結果嗎。確定不是這樣!
類中構造CRuntimeClass對象
你們能夠這樣想一下,咱們構造這個鏈表的目的,就是構造完以後,可以按主觀地拿一個CRuntimeClass對象和鏈表中的元素做比較,看看其中一個對象是否屬於你指定的類。這樣,咱們須要有一個函數,一個能返回自身類型名的函數GetRuntimeClass()。
上面簡單地說了一下鏈表的過程,但單純有這個鏈表是沒有任何意義。回到MFC中來,咱們要實現的是在每一個須要有RTTI能力的類中構造一個CRuntimeClass對象,比較一個類是否屬於某個CRuntimeClass對象的時候,實際上只是比較CRuntimeClass對象。
如何在各個類之中插入CRuntimeClass對象,而且指定CRuntimeClass對象的內容及CRuntimeClass對象的連接,這裏起碼有十行的代碼才能完成。在每一個須要有RTTI能力的類設計中都要重複那十多行代碼是一件乏味的事情,也容易出錯,因此MFC用了兩個宏代替這些工做,即DECLARE_DYNAMIC(類名)和IMPLEMENT_DYNAMIC(類名,基類名)。從這兩個宏咱們能夠看出在MFC名類中的CRuntimeClass對象構造鏈接只有類名及基類名的不一樣!
到此,可能會有朋友問:爲何要用兩個宏,用一個宏不能夠代換CRuntimeClass對象構造鏈接嗎?我的認爲確定能夠,由於宏只是文字代換的遊戲而已。但咱們在編程之中,頭文件與源文件是分開的,咱們要在頭文件頭聲明變量及方法,在源文件裏實具體實現。便是說咱們要在頭文件中聲明:
C++代碼
21 public:
22 static CRuntimeClass classXXX //XXX爲類名
23 virtual CRuntime* GetRuntimeClass() const;
而後在源文件裏實現:
C++代碼
24 CRuntimeClass* XXX::classXXX={……};
25 CRuntime* GetRuntimeClass() const;
26 { return &XXX:: classXXX;}//這裏不能直接返回&classXXX,由於static變量是類擁有而不是對象擁有。
咱們一眼能夠看出MFC中的DECLARE_DYNAMIC(類名)宏應該這樣定義:
C++代碼
27 #define DECLARE_DYNAMIC(class_name) public: static CRuntimeClass class##class_name; virtual CRuntimeClass* GetRuntimeClass() const;
其中##爲鏈接符,可讓咱們傳入的類名前面加上class,不然跟原類同名,你們會知道產生什麼後果。
有了上面的DECLARE_DYNAMIC(類名)宏以後,咱們在頭文件裏寫上一句
DECLARE_DYNAMIC(XXX)
宏展開後就有了咱們想要的:
public:
static CRuntimeClass classXXX //XXX爲類名
virtual CRuntime* GetRuntimeClass() const;
對於IMPLEMENT_DYNAMIC(類名,基類名),看來也不值得在這裏代換文字了,你們知道它是知道回事,宏展開後爲咱們作了什麼,再深究真是一點意義都沒有!
有了此鏈表以後,就像有了一張存放各種型的網,咱們能夠垂手可得地RTTI。
IsKindOf函數
CObject有一個函數BOOL IsKindOf(const CRuntimeClass* pClass) const;,被它如下全部派生類繼承。
此函數實現以下:
C++代碼
28 BOOL CObject::IsKindOf(const CRuntimeClass* pClass) const
29 {
30 CRuntimeClass* pClassThis=GetRuntimeClass();//得到本身的CRuntimeClass對象指針。
31 while(pClassThis!=NULL)
32 {
33 if(pClassThis==pClass) return TRUE;
34 pClassThis=pClassThis->m_pBaseClass;//這句最關鍵,指向本身基類,再回頭比較,一直到盡頭m_pBaseClass爲NULL結束。
35 }
36 return FALSE;
37 }
說到這裏,運行時類型識別(RTTI)算是完成了。
MFC六大核心機制之三:動態建立
不少地方都使用了動態建立技術。動態建立就是在程序運行時建立指定類的對象。例如MFC的單文檔程序中,文檔模板類的對象就動態建立了框架窗口對象、文檔對象和視圖對象。動態建立技術對於但願瞭解MFC底層運行機制的朋友來講,很是有必要弄清楚。
不須要手動實例化對象的疑惑
MFC編程入門時,通常人都會有這樣的疑惑:MFC中幾個主要的類不須要咱們設計也就罷了,爲何連實例化對象都不用咱們來作?咱們認爲本該是:須要框架的時候,親手寫上CFrameWnd myFrame;須要視的時候,親自打上CView myView;……。
但MFC不給咱們這個機會,導致咱們錯覺窗口沒有實例化就彈出來了!但大夥想了一下,可能會一拍腦門,認爲簡單不過:MFC自動幫咱們完成CView myView之流的代碼不就好了麼!其實否則,寫MFC程序的時候,咱們幾乎要對每一個大類進行派生改寫。換句話說,MFC並不知道咱們打算怎樣去改寫這些類,固然也不打算所有爲咱們「靜態」建立這些類了。即便靜態了建立這些類也沒有用,由於咱們歷來也不會直接利用這些類的實例幹什麼事情。咱們只知道,想作什麼事情就往各大類裏塞,無論什麼變量、方法照塞,塞完以後,咱們彷佛並未實例化對象,程序就能夠運行!
CRuntimeClass鏈表
要作到把本身的類交給MFC,MFC就用同同樣的方法,把不一樣的類一一準確建立,咱們要作些什麼事情呢?一樣地,咱們要創建鏈表,記錄各種的關鍵信息,在動態建立的時候找出這些信息,就象上一節RTTI那樣!咱們能夠設計一個類:
C++代碼
1 struct CRuntimeClass{
2 LPCSTR m_lpszClassName; //類名指針
3 CObject* (PASCAL *m_pfnCreateObject)(); //建立對象的函數的指針
4 CRuntimeClass* m_pBaseClass; //講RTTI時介紹過
5 CRuntimeClass* m_pNextClass; //指向鏈表的下一個元素(許多朋友說上一節講RTTI時並無用到這個指針,我本來覺得這樣更好理解一些,由於沒有這個指針,這個鏈表是沒法連起來,而m_pBaseClass僅僅是向基類走,在MFC的樹型層次結構中m_pBaseClass是不能遍歷的)
6 CObject* CreateObject(); //建立對象
7 static CRuntimeClass* PASCAL Load(); //遍歷整個類型鏈表,返回符合動態建立的對象。
8 static CRuntimeClass* pFirstClass; //類型鏈表的頭指針
9 };
一會兒往結構裏面塞了那麼多的東西,你們能夠以爲有點頭暈。至於CObject* (PASCAL *m_pfnCreateObject)();,這定義函數指針的方法,你們可能有點陌生。函數指針在C++書籍裏通常被定爲選學章節,但MFC仍是常常用到此類的函數,好比咱們所熟悉的回調函數。簡單地說m_pfnCreateObject便是保存了一個函數的地址,它將會建立一個對象。便是說,之後,m_pfnCreateObject指向不一樣的函數,咱們就會建立不一樣類型的對象。
有函數指針,咱們要實現一個與原定義參數及返回值都相同一個函數,在MFC中定義爲:
static CObject* PASCAL CreateObject(){return new XXX};//XXX爲類名。類名不一樣,咱們就建立不一樣的對象。
由此,咱們能夠以下構造CRuntimeClass到鏈表(僞代碼):
CRuntimeClass classXXX={
類名,
……,
XXX::CreateObject(), //m_pfnCreateObject指向的函數
RUNTIME_CLASS(基類名), // RUNTIME_CLASS宏能夠返回CRuntimeClass對象指針。
NULL //m_pNextClass暫時爲空,最後會咱們再設法讓它指向舊鏈表表頭。
};
這樣,咱們用函數指針m_pfnCreateObject(指向CreateObject函數),就隨時可new新對象了。而且你們留意到,咱們在設計CRuntimeClass類對時候,只有類名(和基類名)的不一樣(咱們用XXX代替的地方),其它的地方同樣,這正是咱們想要的,由於咱們動態建立也象RTTI那樣用到兩個宏,只要傳入類名和基類做宏參數,就能夠知足條件。
便是說,咱們類說明中使用DECLARE_DYNCREATE(CLASSNMAE)宏和在類的實現文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏來爲咱們加入鏈表,至於這兩個宏怎麼爲咱們創建一個鏈表,咱們本身能夠玩玩文字代換的遊戲,在此不一一累贅。但要說明的一點就是:動態建立宏xxx_DYNCREATE包含了RTTI宏,便是說, xxx_DYNCREATE是xxx_DYNAMIC的「加強版」。
到此,咱們有必要了解一下上節課沒有明講的m_pNextClass指針。由於MFC層次結構是樹狀的,並非直線的。若是咱們只有一個m_pBaseClass指針,它只會沿着基類上去,會漏掉其它分支。在動態建立時,必須要檢查整個鏈表,看有多少個要動態建立的對象,便是說要從表頭(pFirstClass)開始一直遍歷到表尾(m_pNextClass=NULL),不能漏掉一個CRuntimeClass對象。
因此每當有一個新的鏈表元素要加入鏈表的時候,咱們要作的就是使新的鏈表元素成爲表頭,而且m_pNextClass指向原來鏈表的表頭,即像下面那樣(固然,這些不須要咱們操心,是RTTI宏幫助咱們完成的):
C++代碼
10 pNewClass->m_pNextClass=CRuntimeClass::pFirstClass;//新元素的m_pNextClass指針指向想加入的鏈表的表頭。
11 CRuntimeClass::pFirstClass=pNewClass;//鏈表的頭指針指向剛插入的新元素。
好了,有了上面的鏈表,咱們就能夠分析動態建立了。
動態建立的步驟
有了一個包含類名,函數指針,動態建立函數的鏈表,咱們就能夠知道應該按什麼步驟去動態建立了:
一、得到一要動態建立的類的類名(假設爲A)。
二、將A跟鏈表裏面每一個元素的m_lpszClassName指向的類名做比較。
三、若找到跟A相同的類名就返回A所屬的CRuntimeClass元素的指針。
四、判斷m_pfnCreateObject是否有指向建立函數,有則建立對象,並返回該對象。
代碼演示以下(如下兩個函數都是CRuntimeClass類函數):
C++代碼
12 ///////////////如下爲根據類名從表頭向表尾查找所屬的CRuntimeClass對象////////////
13
14 CRuntimeClass* PASCAL CRuntimeClass::Load()
15 {
16 char szClassXXX[64];
17 CRuntimeClass* pClass;
18 cin>>szClassXXX; //假定這是咱們但願動態建立的類名
19 for(pClass=pFirstClass;pClass!=NULL;pClass=pClass->m_pNextClass)
20 {
21 if(strcmp(szClassXXX,pClass->m_lpszClassName)==0)
22 return pClass;
23 }
24 return NULL;
25 }
26
27 ///////////根據CRuntimeClass建立對象///////////
28 CObject* CRuntimeClass::CreateObject()
29 {
30 if(m_pfnCreateObject==NULL) return NULL;
31 CObject *pObject;
32 pObject=(* m_pfnCreateObject)(); //函數指針調用
33 return pObject;
34 }
有了上面兩個函數,咱們在程序執行的時候調用,就能夠動態建立對象了。
簡單實現動態建立
咱們還能夠更簡單地實現動態建立,你們注意到,就是在咱們的程序類裏面有一個RUNTIME_CLASS(class_name)宏,這個宏在MFC裏定義爲:
RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))
做用就是獲得類的RunTime信息,即返回class_name所屬CRuntimeClass的對象。在咱們的應用程序類(CMyWinApp)的InitInstance()函數下面的CSingleDocTemplate函數中,有:
RUNTIME_CLASS(CMyDoc),
RUNTIME_CLASS(CMainFrame), // main SDI frame window
RUNTIME_CLASS(CMyView)
構造文檔模板的時候就用這個宏獲得文檔、框架和視的RunTime信息。有了RunTime信息,咱們只要一條語句就能夠動態建立了,如:
classMyView->CreateObject(); //對象直接調用用CRuntimeClass自己的CreateObject()
總結
最後再總結和明確下動態建立的具體步驟:
一、定義一個不帶參數的構造函數(默認構造函數);由於咱們是用CreateObject()動態建立,它只有一條語句就是return new XXX,不帶任何參數。因此咱們要有一個無參構造函數。
二、類說明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;和在類的實現文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;這個宏完成構造CRuntimeClass對象,並加入到鏈表中。
三、使用時先經過宏RUNTIME_CLASS獲得類的RunTime信息,而後使用CRuntimeClass的成員函數CreateObject建立一個該類的實例。
四、CObject* pObject = pRuntimeClass->CreateObject();//完成動態建立。
MFC六大核心機制之四:永久保存(串行化)
永久保存(串行化)是MFC的重要內容,能夠用一句簡明直白的話來形容其重要性:弄懂它之後,你就愈來愈像個程序員了!
若是咱們的程序不須要永久保存,那幾乎能夠確定是一個小玩兒。那怕咱們的記事本、畫圖等小程序,也須要保存纔有真正的意義。
對於MFC的不少地方我不甚滿意,總以爲它喜歡拿一組低能而神祕的宏來故弄玄虛,但對於它的連續存儲(serialize)機制,倒是我十分鐘愛的地方。在此,可以讓你們感覺到面向對象的幸福。
MFC的連續存儲(serialize)機制俗稱串行化。「在你的程序中儘管有着各類各樣的數據,serialize機制會象流水同樣按順序存儲到單一的文件中,而又能按順序地取出,變成各類不一樣的對象數據。」不知我在說上面這一句話的時候,你們有什麼反應,可能不少朋友直覺是一件很簡單的事情,只是說了一個「爽」字就沒有下文了。
串行化原理的討論
要實現象流水同樣存儲實際上是一個很大的難題。試想,在咱們的程序裏有各式各樣的對象數據。如畫圖程序中,裏面設計了點類,矩形類,圓形類等等,它們的繪圖方式及對數據的處理各不相同,用它們實現了成百上千的對象以後,如何存儲起來?不想由可,一想頭都大了:咱們要在程序中設計函數store(),在咱們單擊「文件/保存」時能把各對象往裏存儲。那麼這個store()函數要神通廣大,它能清楚地知道咱們設計的是什麼樣的類,產生什麼樣的對象。你們可能並不以爲這是一件很困難的事情,程序有能力知道咱們的類的樣子,對象也不過是一塊初始化了存儲區域罷了。就把一大堆對象「轉換」成磁盤文件就好了。
即便上面的存儲能成立,但當咱們單擊「文件/打開」時,程序固然不能預測用戶想打開哪一個文件,而且當打開文件的時候,要根據你那一大堆垃圾數據new出數百個對象,還原爲你原來存儲時的樣子,你又該怎麼作呢?
試想,要是咱們有一個能容納各類不一樣對象的容器,這樣,用戶用咱們的應用程序打開一個磁盤文件時,就能夠把文件的內容讀進咱們程序的容器中。把磁盤文件讀進內存,而後識別它「是什麼對象」是一件很難的事情。首先,保存過程不像電影的膠片,把景物直接映射進去,而後,看一下膠片就知道那是什麼內容。可能有朋友說它象錄像磁帶,拿着錄像帶咱們看不出裏面變化的磁場信號,但通過錄像機就能把它還原出來。
其實不是這樣的,好比保存一個矩形,程序並非把矩形自己按點陣存儲到磁盤中,由於咱們繪製矩形的整個過程只不過是調用一個GDI函數罷了。它保存只是座標值、線寬和某些標記等。程序面對「00 FF」這樣的東西,固然不知道它是一個圓或是一個字符!
拿剛纔錄像帶的例子,咱們之因此能最後放映出來,前提咱們知道這對象是「錄像帶」,即肯定了它是什麼類對象。若是咱們事先只知道它「裏面保存有東西,但不知道它是什麼類型的東西」,這就致使咱們沒法把它讀出來。拿錄像帶到錄音機去放,對錄音機來講,那徹底是垃圾數據。便是說,要了解永久保存,要對動態建立有深入的認識。
如今你們能夠知道困難的根源了吧。咱們在寫程序的時候,會不斷創造新的類,構造新的對象。這些對象,固然是舊的類對象(如MyDocument)從未見過的。那麼,咱們如何才能使文檔對象能夠保存本身新對象呢,又能動態建立本身新的類對象呢?
許多朋友在這個時候想起了CObject這個類,也想到了虛函數的概念。因而覺得本身「大體瞭解」串行化的概念。他們設想:「咱們設計的MyClass(咱們想用於串行化的對象)所有從CObject類派生,CObject類對象固然是MyDocument能認識的。」這樣就實現了一個目的:原本MyDocument不能識別咱們建立的MyClass對象,但它能識別CObject類對象。因爲MyClass從CObject類派生,構造的新類對象「是一個CObject」,因此MyDocument能把咱們的新對象看成CObiect對象讀出。或者根據書本上所說的:打開或保存文件的時候,MyDocument會調用Serialize(),MyDocument的Serialize()函會呼叫咱們建立類的Serialize函數[便是在MyDocument Serialize()中調用:m_pObject->Serialize(),注意:在此m_pObject是CObject類指針,它能夠指向咱們設計的類對象]。最終結果是MyDocument的讀出和保存變成了咱們建立的類對象的讀出和保存,這種認識是不明朗的。
有意思還有,在網上我遇到幾位自覺得懂了Serialize的朋友,竟然不約而同的犯了一個很低級得讓人難以想象的錯誤。他們說:Serialize太簡單了!Serialize()是一個虛函數,虛函數的做用就是「優先派生類的操做」。因此MyDocument不實現Serialize()函數,留給咱們本身的MyClass對象去調用Serialize()……真是啼笑皆非,咱們建立的類MyClass並非由MyDocument類派生,Serialize()函數爲虛在MyDocument和MyClass之間沒有任何意義。MyClass產生的MyObject對象僅僅是MyDocument的一個成員變量罷了。
話說回來,因爲MyClass從CObject派生,因此CObject類型指針能指向MyClass對象,而且可以讓MyClass對象執行某些函數(特指重載的CObject虛函數),但前提必須在MyClass對象實例化了,即在內存中佔領了一塊存儲區域以後。不過,咱們的問題偏偏就是在應用程序隨便打開一個文件,面對的是它不認識的MyClass類,固然實例化不了對象。
幸虧咱們在上一節課中懂得了動態建立。即想要從CObject派生的MyClass成爲能夠動態建立的對象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏就能夠了(注意:最終能夠Serialize的對象僅僅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL宏,這是由於DECLARE_SERIAL/IMPLEMENT_SERIAL包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏)。
整理思路,深刻理解串行化
從解決上面的問題中,咱們能夠分步理解了:
一、Serialize的目的:讓MyDocument對象在執行打開/保存操做時,能讀出(構造)和保存它不認的MyClass類對象。
二、MyDocument對象在執行打開/保存操做時會調用它自己的Serialize()函數。但不要期望它會自動保存和讀出咱們的MyClass類對象。這個問題很容易解決,以下便可:
C++代碼
1 MyDocument:: Serialize(){
2 // 在此函數調用MyClass類的Serialize()就好了!即
3 MyObject. Serialize();
4 }
三、咱們但願MyClass對象爲能夠動態建立的對象,因此要求在MyClass類中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。
但目前的Serialize機制還很抽象。咱們僅僅知道了表面上的東西,實際又是如何的呢?下面做一個簡單深入的詳解。
先看一下咱們文檔類的Serialize():
C++代碼
5 void CMyDoc::Serialize(CArchive& ar)
6 {
7 if (ar.IsStoring())
8 {
9 // TODO: add storing code here
10 }
11 else
12 {
13 // TODO: add loading code here
14 }
15 }
目前這個子數什麼也沒作(沒有數據的讀出和寫入),CMyDoc類正等待着咱們去改寫這個函數。如今假設CMyDoc有一個MFC可識別的成員變量m_MyVar,那麼函數就可改寫成以下形式:
C++代碼
16 void CMyDoc::Serialize(CArchive& ar)
17 {
18 if (ar.IsStoring()) //讀寫判斷
19 {
20 ar<<m_MyVar; //寫
21 }
22 else
23 {
24 ar>>m_MyVar; //讀
25 }
26 }
許多網友問:本身寫的類(即MFC未包含的類)爲何不行?咱們在CMyDoc裏包含自寫類的頭文件MyClass.h,這樣CMyDoc就認識MyDoc類對象了。這是通常常識性的錯誤,MyDoc類認識MyClass類對象與否並無用,關鍵是CArchive類,即對象ar不認識MyClass(固然你夢想重寫CArchive類當別論)。「>>」、「<<」都是CArchive重載的操做符。上面ar>>m_MyVar說白便是在執行一個以ar和m_MyVar 爲參數的函數,相似於function(ar,m_MyVar)罷了。咱們固然不能傳遞一個它不認識的參數類型,也所以不會執行function(ar,m_MyObject)了。
【注:這裏咱們能夠用指針。讓MyClass從Cobject派生,一切又起了質的變化,假設咱們定義了:MyClass *pMyClass = new MyClass;由於MyClass從CObject派生,根據虛函數原理,pMyClass也是一個CObject*,即pMyClass指針是CArchive類可認識的。因此執行上述function(ar, pMyClass),即ar << pMyClass是沒有太多的問題(在保證了MyClass對象能夠動態建立的前提下)。】
回過頭來,若是想讓MyClass類對象能Serialize,就得讓MyClass從CObject派生,Serialize()函數在CObject裏爲虛,MyClass從CObject派生以後就能夠根據本身的要求去改寫它,像上面改寫CMyDoc::Serialize()方法同樣。這樣MyClass就獲得了屬於MyClass本身特有的Serialize()函數。
如今,程序就能夠這樣寫:
C++代碼
27 ……
28
29 #include 「MyClass.h」
30
31 ……
32
33 void CMyDoc::Serialize(CArchive& ar)
34 {
35 //在此調用MyClass重寫過的Serialize()
36 m_MyObject.Serialize(ar); // m_MyObject爲MyClass實例
37 }
至此,串行化工做就算完成了,簡單直觀的講:從CObject派生本身的類,重寫Serialize()。在此過程當中,我刻意安排:在沒有用到DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也沒有用到CArray等模板類的前提下就完成了串行化的工做。我看過某些書,老是一開始就講DECLARE_SERIAL/IMPLEMENT_SERIAL宏或立刻用CArray模板,讓讀者以爲串行化就是這兩個東西,致使許多朋友所以找不着北。
你們看到了,沒有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等數據結構模板也依然能夠完成串行化工做。
CArchive
最後再補充講解一下有些抽象的CArchive。咱們先看如下程序(注:如下程序包含動態建立等,請包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)
C++代碼
38 void MyClass::Serialize(CArchive& ar)
39 {
40 if (ar.IsStoring()) //讀寫判斷
41 {
42 ar<< m_pMyVar; //問題:ar 如何把m_pMyVar所指的對象變量保存到磁盤?
43 }
44 else
45 {
46 pMyClass = new MyClass; //準備存儲空間
47 ar>> m_pMyVar;
48 }
49 }
爲回答上面的問題,即「ar<<XXX」的問題,這裏給出一段模擬CArchive的代碼。
「ar<<XXX」是執行CArchive對運算符「<<」的重載動做。ar和XXX都是該重載函數中的一參數而已。函數大體以下:
C++代碼
50 CArchive& operator<<( CArchive& ar, const CObject* pOb)
51 {
52 …………
53 //如下爲CRuntimeClass鏈表中找到、識別pOb資料。
54 CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
55 //保存pClassRef即類信息(略)
56
57 ((CObject*)pOb)->Serialize();//保存MyClass數據
58 …………
59 }
從上面能夠看出,由於Serialize()爲虛函數,即「ar<<XXX」的結果是執行了XXX所指向對象自己的Serialize()。對於「ar>>XXX」,雖然不是「ar<<XXX」逆過程,你們可能根據動態建立和虛函數的原理料想到它。
至此,永久保存算是寫完了。在此過程當中,我一直努力用最少的代碼,詳盡的解釋來講明問題。之前我爲本課題寫過一個版本,並在幾個論壇上發表過,但不知怎麼在網上遺失(可能被刪除)。因此這篇文章是我重寫的版本。記得第一個版本中,我是對DECLARE_SERIAL/IMPLEMENT_SERIAL和可串行化的數組及鏈表對象說了許多。這個版本中我對DECLARE_SERIAL/IMPLEMENT_SERIAL其中奧祕幾乎一句不提,目的是讓你們能找到中心,有更簡潔的永久保存的概念,我以爲這種感受很好!
MFC六大核心機制之5、六:消息映射和命令傳遞
做爲C++程序員,咱們老是但願本身程序的全部代碼都是本身寫出來的,若是使用了其餘的一些庫,也老是想方設法想弄清楚其中的類和函數的原理,不然就會感受不踏實。因此,咱們對於在進行MFC視窗程序設計時常常要用到的消息機制也不知足於會使用,而是但願能理解箇中道理。本文就爲你們剖析MFC消息映射和命令傳遞的原理。
理解MFC消息機制的必要性
說到消息,在MFC中,「最熟悉的神祕」能夠說是消息映射了,那是咱們剛開始接觸MFC時就要面對的東西。有過SDK編程經驗的朋友轉到MFC編程的時候,一會兒以爲什麼都變了樣。特別是窗口消息及對消息的處理跟之前相比,更是風馬牛不相及的。如文檔不是窗口,是怎樣響應命令消息的呢?
初次用MFC編程,咱們只會用MFC ClassWizard爲咱們作大量的東西,最主要的是添加消息響應。記憶中,若是是自已添加消息響應,咱們應何等的當心翼翼,對BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個魔盒子,把咱們的咒語放入恰當的地方,就會發生神奇的力量,放錯了,本身的程序就連「命」都沒有。
聽說,知道得太多未必是好事。我也曾經打算不去理解這神祕的區域,以爲編程的時候知道本身想作什麼就好了。MFC外表上給咱們提供了東西,直觀地說,不但給了我個一個程序的外殼,更給咱們許多方便。微軟的出發點多是但願達到「傻瓜編程」的結果,試想,誰不會用ClassWizard?你們知道,Windows是基於消息的,有了ClassWizard,你又會添加類,又會添加消息,那麼你所學的東西彷佛學到頭了。因而許多程序員認爲「咱們沒有必要走SDK的老路,直接用MFC編程,新的東西一般是簡單、直觀、易學……」。
到你真正想用MFC編程的時候,你會發覺光會ClassWizard的你是多麼的愚蠢。MFC不是一個普通的類庫,普通的類庫咱們徹底能夠不理解裏面的細節,只要知道這些類庫能幹什麼,接口參數如何就萬事大吉。如string類,操做順序是定義一個string對象,而後修改屬性,調用方法。但對於MFC,並非在你的程序中寫上一句「#include MFC.h」,而後就使用MFC類庫的。
MFC是一塊包着糖衣的牛骨頭。你很輕鬆地寫出一個單文檔窗口,在窗口中間打印一句「I love MFC!」,而後,惡夢開始了……想逃避,打算永遠不去理解MFC內幕?門都沒有!在MFC這個黑暗神祕的洞中,即便你打算摸着石頭前行,也註定找不到出口。對着MFC這塊牛骨頭,微軟溫和、民主地告訴你「你固然能夠選擇不啃掉它,咳咳……但你必然會所以而餓死!」
MFC消息機制與SDK的不一樣
消息映射與命令傳遞體現了MFC與SDK的不一樣。在SDK編程中,沒有消息映射的概念,它有明確的回調函數,經過一個switch語句去判斷收到了何種消息,而後對這個消息進行處理。因此,在SDK編程中,會發送消息和在回調函數中處理消息就差很少能夠寫SDK程序了。
在MFC中,看上去發送消息和處理消息比SDK更簡單、直接,但惋惜不直觀。舉個簡單的例子,若是咱們想自定義一個消息,SDK是很是簡單直觀的,用一條語句:SendMessage(hwnd,message/*一個大於或等於WM_USER的數字*/,wparam,lparam),以後就能夠在回調函數中處理了。但MFC就不一樣了,由於你一般不直接去改寫窗口的回調函數,因此只能亦步亦趨對照原來的MFC代碼,把消息放到恰當的地方。這確實是同樣很痛苦的勞動。
要了解MFC消息映射原理並非一件輕鬆的事情。咱們能夠逆向思惟,想象一下消息映射爲咱們作了什麼工做。MFC在自動化給咱們提供了很大的方便,好比,全部的MFC窗口都使用同一窗口過程,即全部的MFC窗口都有一個默認的窗口過程。不像在SDK編程中,要爲每一個窗口類寫一個窗口過程。
MFC消息映射原理
對於消息映射,最直截了當地猜測是:消息映射就是用一個數據結構把「消息」與「響應消息函數名」串聯起來。這樣,當窗口感知消息發生時,就對結構查找,找到相應的消息響應函數執行。其實這個想法也不能簡單地實現:咱們每一個不一樣的MFC窗口類,對同一種消息,有不一樣的響應方式。便是說,對同一種消息,不一樣的MFC窗口會有不一樣的消息響應函數。
這時,你們又想了一個可行的方法。咱們設計窗口基類(CWnd)時,咱們讓它對每種不一樣的消息都來一個消息響應,並把這個消息響應函數定義爲虛函數。這樣,從CWnd派生的窗口類對全部消息都有了一個空響應,咱們要響應一個特定的消息就重載這個消息響應函數就能夠了。但這樣作的結果,一個幾乎什麼也不作的CWnd類要有幾百個「多餘」的函數,哪怕這些消息響應函數都爲純虛函數,每一個CWnd對象也要揹負着一個巨大的虛擬表,這也是得不償失的。
許多朋友在學習消息映射時苦無突破,其緣由是一開始就認爲MFC的消息映射的目的是爲了替代SDK窗口過程的編寫——這原本沒有理解錯。但他們還有多一層的理解,認爲既然是替代「舊」的東西,那麼MFC消息映身應該是更高層次的抽象、更簡單、更容易認識。但結果是,若是咱們不經過ClassWizard工具,手動添加消息是至關迷茫的一件事。
因此,咱們在學習MFC消息映射時,首先要弄清楚:消息映射的目的,不是爲是更加快捷地向窗口過程添加代碼,而是一種機制的改變。若是不想改變窗口過程函數,那麼應該在哪裏進行消息響應呢?許多朋友只知其一;不知其二地認爲:咱們能夠用HOOK技術,搶在消息隊列前把消息抓取,把消息響應提到窗口過程的外面。再者,不一樣的窗口,會有不一樣的感興趣的消息,因此每一個MFC窗口都應該有一個表把感興趣的消息和相應消息響應函數連繫起來。而後得出——消息映射機制執行步驟是:當消息發生,咱們用HOOK技術把原本要發送到窗口過程的消息抓獲,而後對照一下MFC窗口的消息映射表,若是是表裏面有的消息,就執行其對應的函數。
固然,用HOOK技術,咱們理論上能夠在不改變窗口過程函數的狀況下,能夠完成消息響應。MFC確實是這樣作的,但實際操做起來可能跟你的想象差異很大。
如今咱們來編寫消息映射表,咱們先定義一個結構,這個結構至少有兩個項:一是消息ID,二是響應該消息的函數。以下:
C++代碼
1 struct AFX_MSGMAP_ENTRY
2 {
3 UINT nMessage; //感興趣的消息
4 AFX_PMSG pfn; //響應以上消息的函數指針
5 }
固然,只有兩個成員的結構鏈接起來的消息映射表是不成熟的。Windows消息分爲標準消息、控件消息和命令消息,每類型的消息都是包含數百不一樣ID、不一樣意義、不一樣參數的消息。咱們要準確地判別發生了何種消息,必須再增長几個成員。還有,對於AFX_PMSG pfn,實際上等於做如下聲明:
void (CCmdTarget::*pfn)(); // 提示:AFX_PMSG爲類型標識,具體聲明是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
pfn是一個不帶參數和返回值的CCmdTarget類型函數指針,只能指向CCmdTarget類中不帶參數和返回值的成員函數,這樣pfn更爲通用,但咱們響應消息的函數許多須要傳入參數的。爲了解決這個矛盾,咱們還要增長一個表示參數類型的成員。固然,還有其它……
最後,MFC咱們消息映射表成員結構以下定義:
C++代碼
6 struct AFX_MSGMAP_ENTRY
7 {
8 UINT nMessage; //Windows 消息ID
9 UINT nCode; // 控制消息的通知碼
10 UINT nID; //命令消息ID範圍的起始值
11 UINT nLastID; //命令消息ID範圍的終點
12 UINT nSig; // 消息的動做標識
13 AFX_PMSG pfn;
14 };
有了以上消息映射表成員結構,咱們就能夠定義一個AFX_MSGMAP_ENTRY類型的數組,用來容納消息映射項。定義以下:
AFX_MSGMAP_ENTRY _messageEntries[];
但這樣還不夠,每一個AFX_MSGMAP_ENTRY數組,只能保存着當前類感興趣的消息,而這僅僅是咱們想處理的消息中的一部分。對於一個MFC程序,通常有多個窗口類,裏面都應該有一個AFX_MSGMAP_ENTRY數組。
咱們知道,MFC還有一個消息傳遞機制,能夠把本身不處理的消息傳送給別的類進行處理。爲了能查找各下MFC對象的消息映射表,咱們還要增長一個結構,把全部的AFX_MSGMAP_ENTRY數組串聯起來。因而,咱們定義了一個新結構體:
C++代碼
15 struct AFX_MSGMAP
16 {
17 const AFX_MSGMAP* pBaseMap; //指向別的類的AFX_MSGMAP對象
18 const AFX_MSGMAP_ENTRY* lpEntries; //指向自身的消息表
19 };
以後,在每一個打算響應消息的類中聲明這樣一個變量:AFX_MSGMAP messageMap,讓其中的pBaseMap指向基類或另外一個類的messageMap,那麼將獲得一個AFX_MSGMAP元素的單向鏈表。這樣,全部的消息映射信息造成了一張消息網。
固然,僅有消息映射表還不夠,它只能把各個MFC對象的消息、參數與相應的消息響應函數連成一張網。爲了方便查找,MFC在上面的類中插入了兩個函數(其中theClass表明當前類):
一個是_GetBaseMessageMap(),用來獲得基類消息映射的函數。函數原型以下:
C++代碼
20 const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() /
21 { return &baseClass::messageMap; } /
另外一個是GetMessageMap() ,用來獲得自身消息映射的函數。函數原型以下:
C++代碼
22 const AFX_MSGMAP* theClass::GetMessageMap() const /
23 { return &theClass::messageMap; } /
有了消息映射表以後,咱們得討論到問題的關鍵,那就是消息發生之後,其對應的響應函數如何被調用。你們知道,全部的MFC窗口,都有一個一樣的窗口過程——AfxWndProc(…)。在這裏順便要提一下的是,看過MFC源代碼的朋友都得,從AfxWndProc函數進去,會遇到一大堆曲折與迷團,由於對於這個龐大的消息映射機制,MFC要作的事情不少,如優化消息,加強兼容性等,這一大量的工做,有些甚至用匯編語言來完成,對此,咱們很難深究它。因此咱們要省略大量代碼,理性地分析它。
對已定型的AfxWndProc來講,對全部消息,最多隻能提供一種默認的處理方式。這固然不是咱們想要的。咱們想經過AfxWndProc最終執行消息映射網中對應的函數。那麼,這個執行路線是怎麼樣的呢?
從AfxWndProc下去,最終會調用到一個函數OnWndMsg。請看代碼:
C++代碼
24 LRESULT CALLBACK AfxWndProc(HWND hWnd,UINT nMsg,WPARAM wParam, LPARAM lParam)
25 {
26 ……
27 CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //把對句柄的操做轉換成對CWnd對象。
28 Return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
29 }
把對句柄的操做轉換成對CWnd對象是很重要的一件事,由於AfxWndProc只是一個全局函數,固然不知怎麼樣去處理各類windows窗口消息,因此它聰明地把處理權交給windows窗口所關聯的MFC窗口對象。
如今,你們幾乎能夠想象獲得AfxCallWndProc要作的事情,不錯,它當中有一句:
pWnd->WindowProc(nMsg,wParam,lParam);
到此,MFC窗口過程函數變成了本身的一個成員函數。WindowProc是一個虛函數,咱們甚至能夠經過改寫這個函數去響應不一樣的消息,固然,這是題外話。
WindowProc會調用到CWnd對象的另外一個成員函數OnWndMsg,下面看看大概的函數原型是怎麼樣的:
C++代碼
30 BOOL CWnd::OnWndMsg(UINT message,WPARAM wParam,LPARAM lParam,LRESULT* pResult)
31 {
32 if(message==WM_COMMAND)
33 {
34 OnCommand(wParam,lParam);
35 ……
36 }
37 if(message==WM_NOTIFY)
38 {
39 OnCommand(wParam,lParam,&lResult);
40 ……
41 }
42 const AFX_MSGMAP* pMessageMap; pMessageMap=GetMessageMap();
43 const AFX_MSGMAP_ENTRY* lpEntry;
44 /*如下代碼做用爲:用AfxFindMessageEntry函數從消息入口pMessageMap處查找指定消息,若是找到,返回指定消息映射表成員的指針給lpEntry。而後執行該結構成員的pfn所指向的函數*/
45 if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
46 {
47 lpEntry->pfn();/*注意:真正MFC代碼中沒有用這一條語句。上面提到,不一樣的消息參數表明不一樣的意義和不一樣的消息響應函數有不一樣類型的返回值。而pfn是一個不帶參數的函數指針,因此真正的MFC代碼中,要根據對象lpEntry的消息的動做標識nSig給消息處理函數傳遞參數類型。這個過程包含很複雜的宏代換,你們在此知道:找到匹配消息,執行相應函數就行!*/
48 }
49 }
MFC命令傳遞
在上面的代碼中,你們看到了OnWndMsg能根據傳進來的消息參數,查找到匹配的消息和執行相應的消息響應。但這還不夠,咱們日常響應菜單命令消息的時候,本來屬於框架窗口(CFrameWnd)的WM_COMMAND消息,卻能夠放到視對象或文檔對象中去響應。其原理以下:
咱們看上面函數OnWndMsg原型中看到如下代碼:
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
即對於命令消息,其實是交給OnCommand函數處理。而OnCommand是一個虛函數,即WM_COMMAND消息發生時,最終是發生該消息所對應的MFC對象去執行OnCommand。好比點框架窗口菜單,即向CFrameWnd發送一個WM_COMMAND,將會致使CFrameWnd::OnCommand(wParam,lParam)的執行。且看該函數原型:
C++代碼
50 BOOL CFrameWnd::OnCommand(WPARAM wParam,LPARAM lParam)
51 {
52 ……
53 return CWnd:: OnCommand(wParam,lParam);
54 }
能夠看出,它最後把該消息交給CWnd:: OnCommand處理。再看:
C++代碼
55 BOOL CWnd::OnCommand(WPARAM wParam,LPARAM lParam)
56 {
57 ……
58 return OnCmdMsg(nID,nCode,NULL,NULL);
59 }
這裏包含了一個C++多態性很經典的問題。在這裏,雖然是執行CWnd類的函數,但因爲這個函數在CFrameWnd:: OnCmdMsg裏執行,即當前指針是CFrameWnd類指針,再有OnCmdMsg是一個虛函數,因此若是CFrameWnd改寫了OnCommand,程序會執行CFrameWnd::OnCmdMsg(…)。
對CFrameWnd::OnCmdMsg(…)函數的原理扼要分析以下:
C++代碼
60 BOOL CFrameWnd:: OnCmdMsg(…)
61 {
62 CView pView = GetActiveView();//獲得活動視指針。
63 if(pView-> OnCmdMsg(…))
64 return TRUE; //若是CView類對象或其派生類對象已經處理該消息,則返回。
65 ……//不然,同理向下執行,交給文檔、框架、及應用程序執行自身的OnCmdMsg。
66 }
到此,CFrameWnd:: OnCmdMsg完成了把WM_COMMAND消息傳遞到視對象、文檔對象及應用程序對象實現消息響應。
寫了這麼多,咱們已經清楚了MFC消息映射與命令傳遞的大體過程。
MFC消息映射宏
如今,咱們來看MFC「神祕代碼」,會發覺好看多了。
先看DECLARE_MESSAGE_MAP()宏,它在MFC中定義以下:
C++代碼
67 #define DECLARE_MESSAGE_MAP() /
68 private: /
69 static const AFX_MSGMAP_ENTRY _messageEntries[]; /
70 protected: /
71 static AFX_DATA const AFX_MSGMAP messageMap; /
72 virtual const AFX_MSGMAP* GetMessageMap() const; /
能夠看出DECLARE_MESSAGE_MAP()定義了咱們熟悉的兩個結構和一個函數,顯而易見,這個宏爲每一個須要實現消息映射的類提供了相關變量和函數。
如今集中精力來看一下BEGIN_MESSAGE_MAP,END_MESSAGE_MAP和ON_COMMAND三個宏,它們在MFC中定義以下(其中ON_COMMAND與另外兩個宏並無定義在同一個文件中,把它放到一塊兒是爲了好看):
C++代碼
73 #define BEGIN_MESSAGE_MAP(theClass, baseClass) /
74 const AFX_MSGMAP* theClass::GetMessageMap() const /
75 { return &theClass::messageMap; } /
76 AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = /
77 { &baseClass::messageMap, &theClass::_messageEntries[0] }; /
78 AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = /
79 { /
80
81 #define ON_COMMAND(id, memberFxn) /
82 { WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&memberFxn },
83
84 #define END_MESSAGE_MAP() /
85 {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } /
86 }; /
一會兒看三個宏以爲有點複雜,但這僅僅是複雜,公式性的文字代換並非很難。且看下面例子,假設咱們框架中有一菜單項爲「Test」,即定義了以下宏:
C++代碼
87 BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
88 ON_COMMAND(ID_TEST, OnTest)
89 END_MESSAGE_MAP()
那麼宏展開以後獲得以下代碼:
C++代碼
90 const AFX_MSGMAP* CMainFrame::GetMessageMap() const
91
92 { return &CMainFrame::messageMap; }
93
94 ///如下填入消息表映射信息
95
96 const AFX_MSGMAP CMainFrame::messageMap =
97
98 { &CFrameWnd::messageMap, &CMainFrame::_messageEntries[0] };
99
100 //下面填入保存着當前類感興趣的消息,可填入多個AFX_MSGMAP_ENTRY對象
101
102 const AFX_MSGMAP_ENTRY CMainFrame::_messageEntries[] =
103
104 {
105
106 { WM_COMMAND, CN_COMMAND, (WORD)ID_TEST, (WORD)ID_TEST, AfxSig_vv, (AFX_PMSG)&OnTest }, // 加入的ID_TEST消息參數
107
108 {0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } //本類的消息映射的結束項
109
110 };
你們知道,要完成ID_TEST消息映射,還要定義和實現OnTest函數。即在頭文件中寫afx_msg void OnTest()並在源文件中實現它。根據以上所學的東西,咱們知道了當ID爲ID_TEST的命令消息發生,最終會執行到咱們寫的OnTest函數。
至此,MFC六大關鍵技術寫完了。其中寫得最難的是消息映射與命令傳遞,除了技術複雜以外,最難的是有許多避不開的代碼。爲了你們看得輕鬆一點,我把那繁雜的宏放在文章最後,但願能給你閱讀帶來方便。