【剖析MFC六大關(guān)鍵技術(shù)之初始化過程】
我并不認為MFC減輕了程序員們的負擔,MFC出現(xiàn)的目的雖然似乎是為了讓程序員不用懂得太多就可以進行視窗編程,但本人在MFC里徘徊了很久很久(因為那時沒有書本詳細介紹MFC的原理),毫無收獲??赡芘笥褌儠f,怎么一定要了解MFC的具體呢,“黑箱”作業(yè)不行嗎?這不是微軟的初衷嗎?
不行?。?!如果這樣,我寧愿永遠不選擇MFC!在學(xué)電腦之前,本人學(xué)習(xí)的東西大都與藝術(shù)不無關(guān)系,小學(xué)時參加過全國書畫比賽獲銀獎。兒時的愛好就是在一張紙上隨心所欲地畫畫!MFC“黑箱”就象一幅碩大的抽象畫(抽象到你不能理解),它用鉛筆勾畫好線條,然后請你填顏色。
我們怎么能忍受“黑箱”作業(yè)?我們選擇C++,就是因為它夠自由,夠藝術(shù),我們可以在此放飛幻想。所以,我們要攻克MFC。
偉大孫老師在剖析MFC的時候雖然盡心盡力,但可能由于篇幅所限,說得并不大清楚(我相信許多學(xué)員都有這方面的感受)。在此,我突發(fā)奇想,想與大家一同分享一下著名的MFC六大關(guān)鍵技術(shù)。
從什么地方開始講起好呢?我覺得回到最初摸索MFC的時候,從基本談起最好。
因為我知道,一個走過來程序員,總是忘記了當初自己是怎么走過來的,忘記了一個學(xué)員最想知道的是什么。一個小小的問題(一兩句話就可以解釋的),足學(xué)以令手無寸鐵的學(xué)員頭大半個月,所以,我努力回憶當初是怎么讓自己豁然開朗的。
轉(zhuǎn)入正題,MFC的六大關(guān)鍵技術(shù)包括:
·MFC程序的初始化過程
·運行時類型識別(RTTI)
·動態(tài)創(chuàng)建
·永久保存
·消息映射
·消息傳遞
MFC程序的初始化過程
1、設(shè)計一個簡單完整MFC程序,產(chǎn)生一個窗口。當然這不能讓AppWizard自動為我們生成。我們可以在Win32 Application工程下面那樣寫:
#include <afxwin.h>
class MyApp : public CWinApp
{
public:
BOOL InitInstance() //②程序入點
{
CFrameWnd *Frame=new CFrameWnd();//構(gòu)造框架
m_pMainWnd=Frame; //將m_pMainWnd設(shè)定為Frame;
Frame->Create(NULL,"最簡單的窗口");//建立框架
Frame->ShowWindow(SW_SHOW); //顯示框架
return true; //返回
}
};
MyApp theApp; //①建立應(yīng)用程序。
設(shè)定鏈接MFC庫,運行,即可看見一個窗口。
從上面,大家可以看到建立一個MFC窗口很容易,只用兩步:一是從CWinApp派生一個應(yīng)用程序類(這里是MyApp),,然后建立應(yīng)用程序?qū)ο螅╰heApp),就可以產(chǎn)生一個自己需要的窗口(即需要什么樣就在InitInstance()里創(chuàng)建就行了)。
整個程序,就改寫一個InitInstance()函數(shù),創(chuàng)建那么一個對象(theApp),就是一個完整的窗口程序。這就是“黑箱”作業(yè)的魅力?。。?!
在我們正想為微軟鼓掌的時候,我們突然覺得心里空蕩蕩的,我們想知道微軟幫我們做了什么事情,而我們想編自己的程序時又需要做什么事情,那怕在上面幾行的程序里面,我們還有不清楚的地方,比如,干嘛有一個m_pMainWnd指針變量,它從哪里來,又要到哪里去呢?想一想在DOS下編程是多么美妙的一件事呵,我們需要什么變量,就聲明什么變量,需要什么樣的函數(shù),就編寫什么樣的函數(shù),或者引用函數(shù)庫……但是現(xiàn)在我們怎么辦?。?!
我們可以逆向思維一下,MFC要達到這種效果,它是怎么做的呢?首先我們要弄明白,VC不是一種語言,它就象我們學(xué)c語言的時候的一個類似記事本的編輯器(請原諒我的不貼切的比喻),所以,在VC里面我們用的是C++語言編程,C++才是根本(初學(xué)者總是以為VC是一門什么新的什么語言,一門比C++先進很多的復(fù)雜語言,汗)。說了那么多,我想用一句簡單的話概括“MFC‘黑箱’就是幫助我們插入了‘C++代碼’的東西”。
既然MFC黑箱幫我們插入了代碼,那么大家想想它會幫我們插入什么樣的代碼呢?他會幫我們插入求解一元二次方程的代碼嗎?當然不會,所以它插入的實際上是每次編寫窗口程序必須的,通用的代碼。
再往下想,什么才是通用的呢?我們每次視窗編程都要寫WinMain()函數(shù),都要有注冊窗口,產(chǎn)生窗口,消息循環(huán),回調(diào)函數(shù)……即然每次都要的東西,就讓它們從我們眼前消失,讓MFC幫忙寫入!
要知道MFC初始化過程,大家當然可以跟蹤執(zhí)行程序。孫老師的第三課跟蹤了很長一段時間,我相信大家都有點暈頭轉(zhuǎn)向。本人覺得那怕你理解了MFC代碼,也很容易讓人找不著北,我們完全不懂的時候,在成千上萬行程序的迷宮中如何能找到出口?
我們要換一種方法,不如就來重新編寫個MFC庫吧,嘩!大家不要笑,小心你的大牙,我不是瘋子(雖然瘋子也說自己不瘋)。我們要寫的就是最簡單的MFC類庫,就是把MFC宏觀上的,理論上的東西寫出來。我們要用最簡化的代碼,簡化到剛好能運行。
既然,我們這一節(jié)寫的是MFC程序的初始化過程,上面我們還有了一個可執(zhí)行的MFC程序。程序中只是用了兩個MFC類,一個是CWinApp,另一個是CFrameWnd。當然,還有很多同樣重要MFC類如視圖類,文檔類等等。但在上面的程序可以不用到,所以暫時省去了它(總之是為了簡單)。
好,現(xiàn)在開始寫MFC類庫吧……唉,面前又有一個大難題,就是讓大家背一下MFC層次結(jié)構(gòu)圖。天,那張魚網(wǎng)怎么記得住,但既然我們要理解他,總得知道它是從那里派生出來的吧。
考慮到大家都很辛苦,那我們看一下上面兩個類的父子關(guān)系(箭頭代表派生):
CObject->CCmdTarget->CWinThread->CWinApp->自己的重寫了InitInstance()的應(yīng)用程序類。
CObject(同上)->CCmdTarget(同上)->CWnd->CFrameWnd
看到層次關(guān)系圖之后,終于可以開始寫MFC類庫了。按照上面層次結(jié)構(gòu),我們可以寫以下六個類(為了直觀,省去了構(gòu)造函數(shù)和析構(gòu)函數(shù))。
/////////////////////////////////////////////////////////
class CObiect{};//MFC類的基類。
class CCmdTarget : public CObject{};
------------------------------------------------
class CWinThread : public CCmdTarget{};
class CWinApp : public CWinThread{};
------------------------------------------------
class CWnd : public CCmdTarget{};
class CFrameWnd : public CWnd{};
/////////////////////////////////////////////////////////
大家再想一下,在上面的類里面,應(yīng)該有什么?大家馬上會想到,CWinApp類或者它的基類CCmdTarget里面應(yīng)該有一個虛函數(shù)virtual BOOL InitInstance(),是的,因為那里是程序的入口點,初始化程序的地方,那自然少不了的。可能有些朋友會說,反正InitInstance()在派生類中一定要重載,我不在CCmdTarget或CWinApp類里定義,留待CWinApp的派生類去增加這個函數(shù)可不可以。扯到這個問題可能有點越說越遠,但我想信C++的朋友對虛函數(shù)應(yīng)該是沒有太多的問題的??偟膩碚f,作為程序員如果清楚知道基類的某個函數(shù)要被派生類用到,那定義為虛函數(shù)要方便很多。也有很多朋友問,C++為什么不自動把基類的所有函數(shù)定義為虛函數(shù)呢,這樣可以省了很多麻煩,這樣所有函數(shù)都遵照派生類有定義的函數(shù)就調(diào)用派生類的,沒定義的就調(diào)用基類的,不用寫virtual的麻煩多好!其實,很多面向?qū)ο蟮恼Z言都這樣做了。但定義一個虛函數(shù)要生成一個虛函數(shù)表,要占用系統(tǒng)空間,虛函數(shù)越多,表就越大,有時得不償失!這里哆嗦幾句,是因為往后要說明的消息映射中大家更加會體驗到這一點,好了,就此打往。
上面我們自己解決了一個問題,就是在CCmdTarge寫一個virtual BOOL InitInstance()。
大家再下想,我們還要我們MFC“隱藏”更多的東西:WinMain()函數(shù),設(shè)計窗口類,窗口注冊,消息循環(huán),回調(diào)函數(shù)……我們馬上想到封裝想封裝他們。大家似乎隱約地感覺到封裝WinMain()不容易, 覺得WinMain()是一個特殊的函數(shù),許多時候它代表了一個程序的起始和終結(jié)。所以在以前寫程序的時候,我們寫程序習(xí)慣從WinMain()的左大括寫起,到右大括弧返回、結(jié)束程序。
我們換一個角度去想,有什么東西可以拿到WinMain()外面去做,許多初學(xué)者們,總覺得WinMain()函數(shù)天大的函數(shù),什么函數(shù)都好象要在它里面才能真正運行。其實這樣了解很片面,甚至錯誤。我們可以寫一個這樣的C++程序:
////////////////////////////////////////////////////
#include <iostream.h>
class test{
public:
test(){cout<<"請改變你對main()函數(shù)的看法!"<<endl;}
};
test test1;
/**************************/
void main(){}
////////////////////////////////////////////////////
在上面的程序里,入口的main()函數(shù)表面上什么也不做,但程序執(zhí)行了(注:實際入口函數(shù)做了一些我們可以不了解的事情),并輸出了一句話(注:全局對象比main()首先運行)。現(xiàn)在大家可以知道我們的WinMain()函數(shù)可以什么都不做,程序依然可以運行,但沒有這個入口函數(shù)程序會報錯。
那么WinMain()函數(shù)會放哪個類上面呢,請看下面程序:
#include <afxwin.h>
class MyApp : public CWinApp
{
public:
BOOL InitInstance() //②程序入點
{
AfxMessageBox("程序依然可以運行!");
return true;
}
};
MyApp theApp; //①建立應(yīng)用程序。
大家可以看到,我并沒有構(gòu)造框架,而程序卻可以運行了——彈出一個對話框(如果沒有WinMain()函數(shù)程序會報錯)。上面我這樣寫還是為了直觀起見,其實我們只要寫兩行程序:
#include <afxwin.h>
CWinApp theApp;
//整個程序只構(gòu)造一個CWinApp類對象,任可事情,程序就可以運行!
所以說,只要我們構(gòu)造了CWinApp對象,就可以執(zhí)行WinMain()函數(shù)。我們馬上相信WinMain()函數(shù)是在CWinApp類或它的基類中,而不是在其他類中。其實這種看法是錯誤的,我們知道編寫C++程序的時候,不可能讓你在一個類中包含入口函數(shù),WinMain()是由系統(tǒng)調(diào)用,跟我們的平時程序自身調(diào)用的函數(shù)有著本質(zhì)的區(qū)別。我們可以暫時簡單想象成,當CWinApp對象構(gòu)造完的時候,WinMain()跟著執(zhí)行。
現(xiàn)在大家明白了,大部分的“通用代碼(我們想封裝隱藏的東西)”都可以放到CWinApp類中,那么它又是怎樣運行起來的呢?為什么構(gòu)造了CWinApp類對象就“自動”執(zhí)行那么多東西。
大家再仔細想一下,CWinApp類對象構(gòu)造之后,它會“自動”執(zhí)行自己的構(gòu)造函數(shù)。那么我們可以把想要“自動”執(zhí)行的代碼放到CWinApp類的構(gòu)造函數(shù)中。
那么CWinApp類可能打算這樣設(shè)計(先不計較正確與否):
class CWinApp : public CWinThead{
public:
virtual BOOL InitInstance(); //解釋過的程序的入點
CWinApp ::CWinApp(){ //構(gòu)造函數(shù)
////////////////////////
WinMain(); //這個是大家一眼看出的錯誤
Create(); //設(shè)計、創(chuàng)建、更新顯示窗口
Run(); //消息循環(huán)
//////////////////////
}
};
寫完后,大家又馬上感覺到似乎不對,WinMain()函數(shù)在這里好象真的一點用處都沒有,并且能這樣被調(diào)用嗎(請允許我把手按在圣經(jīng)上聲明一下:WinMain()不是普通的函數(shù),它要肩負著初始化應(yīng)用程序,包括全局變量的初始化,是由系統(tǒng)而不是程序本身調(diào)用的,WinMain()返回之后,程序就結(jié)束了,進程撤消)。再看Create()函數(shù),它能確定設(shè)計什么樣的窗口,創(chuàng)建什么樣的窗口嗎?如果能在CWinApp的構(gòu)造函數(shù)里確定的話,我們以后設(shè)計MFC程序時窗口就一個樣,變得寫程序變有必要。再看Run()函數(shù),它能在WinMain()函數(shù)外面運行嗎?
回過頭來,我們可以讓W(xué)inMain()函數(shù)一條語句都不包含嗎?不可以,我們看一下WinMain() 函數(shù)的四個參數(shù):
WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
其中第一個參數(shù)指向一個實例句柄,我們在設(shè)計WNDCLASS的時候一定要指定實例句柄。我們窗口編程,肯定要設(shè)計窗口類。所以,WinMain()再簡單也要這樣寫:
int WinMain(HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{ hInstance=hinst }
既然實例句柄要等到程序開始執(zhí)行才能知道,那么我們用于創(chuàng)建窗口的Create()函數(shù)也要在WinMain()內(nèi)部才能執(zhí)行[因為如果等到WinMain()執(zhí)行完畢后,程序結(jié)束,進程撤消,當然Create()也不可能創(chuàng)建窗口]
那么Run()(消息循環(huán))放在那里執(zhí)行好呢?眾所周知,消息循環(huán)就是相同的那么幾句代碼,但我們也不要企圖把它放在WinMain()函數(shù)之外執(zhí)行。
所以我們在WinMain()函數(shù)里面,我們程序要象以下這樣寫
WinMain(……)
{
……窗口類對象執(zhí)行創(chuàng)建窗口函數(shù)……
……程序類對象執(zhí)行消息循環(huán)函數(shù)……
}
對于WinMain()的問題,得總結(jié)一下,我們封裝的時候是不可以把它封裝到CWinApp類里面,但由于WinMain()的不變性(或者說有規(guī)律可循),MFC完全有能力在我們構(gòu)造CWinApp類對象的時候,幫我們完成那幾行代碼。
轉(zhuǎn)了一個大圈,我們仿佛又回到了SDK編程的開始。但現(xiàn)在我們現(xiàn)在能清楚地知道,表面上MFC與SDK編程截然不同,但實質(zhì)上MFC只是用類的形式封裝了SDK函數(shù),封裝之后,我們在WinMain()函數(shù)中只需要幾行代碼,就可以完成一個窗口程序。我們也由此知道了應(yīng)如何去封裝應(yīng)用程序類(CWinApp)和主框架窗口類(CFrameWnd)。下面把上開始設(shè)計這兩個類。
為了簡單起見,我們忽略這兩個類的基類和派生類的編寫,可能大家會認為這是一種很不負責任的做法,但本人覺得這既可減輕負擔,又免了大家在各類之間穿來穿去,更好理解一些(我們在關(guān)鍵的地方作注明)。還有,我把全部代碼寫在同一個文件中,讓大家看起來不用那么吃力,但這是最不提倡的寫代碼方法,大家不要學(xué)哦!
#include <windows.h>
HINSTANCE hInstance;
class CFrameWnd
{
HWND hwnd;
public:
CFrameWnd(); //也可以在這里調(diào)用Create()
virtual ~CFrameWnd();
int Create(); //類就留意這一個函數(shù)就行了!
BOOL ShowWnd();
};
class CWinApp1
{
public:
CFrameWnd* m_pMainWnd;//在真正的MFC里面
//它是CWnd指針,但這里由于不寫CWnd類
//只要把它寫成CFrameWnd指針
CWinApp1* m_pCurrentWinApp;//指向應(yīng)用程序?qū)ο蟊旧?div style="height:15px;">
virtual BOOL InitInstance();//MFC原本是必須重載的函數(shù),最重要的函數(shù)?。。。?div style="height:15px;">