天天看點

MFC六大核心機制之一:MFC程式的初始化

很多做軟體開發的人都有一種對事情刨根問底的精神,例如我們一直在用的MFC,很友善,不用學太多原理性的知識就可以做出各種視窗程式,但喜歡鑽研的朋友肯定想知道,到底微軟幫我們做了些什麼,讓我們在它的架構下可以簡單的寫程式。本文開始就跟大家分享一位同行前輩寫的MFC核心機制分析(稍作整理),語言樸實易懂,在讀完此深入淺析的剖析系列後,相信留給大家的是對MFC運作機制的深入了解。

       MFC六大核心機制概述

       我們選擇了C++,主要是因為它夠藝術、夠自由,使用它我們可以實作各種想法,而MFC将多種可靈活使用的功能封裝起來,我們豈能忍受這種“黑盒”操作?于是研究分析MFC的核心機制成為必然。

       首先,列出要講的MFC六大核心機制:

       1、MFC程式的初始化。

       2、運作時類型識别(RTTI)。

       3、動态建立。

       4、永久儲存。

       5、消息映射。

       6、消息傳遞。

       本文講第一部分,MFC程式的初始化過程。

       簡單的MFC視窗程式

       設計一個簡單完整MFC程式,産生一個視窗。當然這不能讓AppWizard自動為我們生成。我們可以在Win32 Application工程下面那樣寫:

C++代碼

#include <afxwin.h>   

class MyApp : public CWinApp   

{   

public:   

BOOL InitInstance()  //②程式入點   

  CFrameWnd *Frame=new CFrameWnd();//構造架構   

  m_pMainWnd=Frame; //将m_pMainWnd設定為Frame;   

  Frame->Create(NULL,"最簡單的視窗");//建立架構   

  Frame->ShowWindow(SW_SHOW);  //顯示架構   

  return true;         //傳回   

 }   

};   

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類庫了。按照上面層次結構,我們可以寫以下六個類(為了直覺,省去了構造函數和析構函數)。

/////////////////////////////////////////////////////////   

class CObiect{};//MFC類的基類。   

class CCmdTarget : public CObject{};   

------------------------------------------------   

class CWinThread : public CCmdTarget{};   

class CWinApp : public CWinThread{};   

class CWnd : public CCmdTarget{};   

class CFrameWnd : public CWnd{};   

/////////////////////////////////////////////////////////  

       大家再想一下,在上面的類裡面,應該有什麼?大家馬上會想到,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++程式:

////////////////////////////////////////////////////   

#include <iostream.h>   

class test{   

 test(){cout<<"請改變你對main()函數的看法!"<<endl;}   

test test1;   

/**************************/  

void main(){}   

////////////////////////////////////////////////////  

       在上面的程式裡,入口的main()函數表面上什麼也不做,但程式執行了(注:實際入口函數做了一些我們可以不了解的事情),并輸出了一句話(注:全局對象比main()首先運作)。現在大家可以知道我們的WinMain()函數可以什麼都不做,程式依然可以運作,但沒有這個入口函數程式會報錯。

       那麼WinMain()函數會放哪個類上面呢,請看下面程式:

 BOOL InitInstance()  //②程式入點   

 {   

  AfxMessageBox("程式依然可以運作!");   

  return true;   

 }   

       大家可以看到,我并沒有構造架構,而程式卻可以運作了——彈出一個對話框(如果沒有WinMain()函數程式會報錯)。上面我這樣寫還是為了直覺起見,其實我們隻要寫兩行程式:

       #include <afxwin.h>

       CWinApp theApp;     //整個程式隻構造一個CWinApp類對象,程式就可以運作!

       是以說,隻要我們構造了CWinApp對象,就可以執行WinMain()函數。我們馬上相信WinMain()函數是在CWinApp類或它的基類中,而不是在其他類中。其實這種看法是錯誤的,我們知道編寫C++程式的時候,不可能讓你在一個類中包含入口函數,WinMain()是由系統調用,跟我們的平時程式自身調用的函數有着本質的差別。我們可以暫時簡單想象成,當CWinApp對象構造完的時候,WinMain()跟着執行。

       現在大家明白了,大部分的“通用代碼(我們想封裝隐藏的東西)”都可以放到CWinApp類中,那麼它又是怎樣運作起來的呢?為什麼構造了CWinApp類對象就“自動”執行那麼多東西。

       大家再仔細想一下,CWinApp類對象構造之後,它會“自動”執行自己的構造函數。那麼我們可以把想要“自動”執行的代碼放到CWinApp類的構造函數中。

       那麼CWinApp類可能打算這樣設計(先不計較正确與否):

class CWinApp : public CWinThead{   

virtual BOOL InitInstance(); //解釋過的程式的入點   

  CWinApp ::CWinApp(){   //構造函數   

   ////////////////////////   

   WinMain();   //這個是大家一眼看出的錯誤   

   Create();    //設計、建立、更新顯示視窗   

   Run();     //消息循環   

   //////////////////////   

}   

};  

       寫完後,大家又馬上感覺到似乎不對,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庫的“重寫”

       為了簡單起見,我們忽略這兩個類的基類和派生類的編寫,可能大家會認為這是一種很不負責任的做法,但本人覺得這既可減輕負擔,又免了大家在各類之間穿來穿去,更好了解一些(我們在關鍵的地方作注明)。還有,我把全部代碼寫在同一個檔案中,讓大家看起來不用那麼吃力,但這是最不提倡的寫代碼方法,大家不要學哦!

#include <windows.h>   

HINSTANCE hInstance;   

class CFrameWnd     

 HWND hwnd;   

 CFrameWnd();   //也可以在這裡調用Create()   

 virtual ~CFrameWnd();   

 int Create();    //類就留意這一個函數就行了!   

 BOOL ShowWnd();   

class CWinApp1     

 CFrameWnd* m_pMainWnd;//在真正的MFC裡面   

//它是CWnd指針,但這裡由于不寫CWnd類   

//隻要把它寫成CFrameWnd指針   

 CWinApp1* m_pCurrentWinApp;//指向應用程式對象本身   

 CWinApp1();   

 virtual ~CWinApp1();   

 virtual BOOL InitInstance();//MFC原本是必須重載的函數,最重要的函數!!!!   

 virtual BOOL Run();//消息循環   

CFrameWnd::CFrameWnd(){}   

CFrameWnd::~CFrameWnd(){}   

int CFrameWnd::Create()   //封裝建立視窗代碼   

 WNDCLASS wndcls;   

 wndcls.style=0;   

 wndcls.cbClsExtra=0;   

 wndcls.cbWndExtra=0;   

 wndcls.hbrBackground=(HBRUSH)GetStockObject(WHITE_BRUSH);   

 wndcls.hCursor=LoadCursor(NULL,IDC_CROSS);   

 wndcls.hIcon=LoadIcon(NULL,IDC_ARROW);   

 wndcls.hInstance=hInstance;   

 wndcls.lpfnWndProc=DefWindowProc;//預設視窗過程函數。   

//大家可以想象成MFC通用的視窗過程。   

 wndcls.lpszClassName="視窗類名";   

 wndcls.lpszMenuName=NULL;   

 RegisterClass(&wndcls);   

 hwnd=CreateWindow("視窗類名","視窗執行個體标題名",WS_OVERLAPPEDWINDOW,0,0,600,400,NULL,NULL,hInstance,NULL);   

  return 0;   

BOOL CFrameWnd::ShowWnd()//顯示更新視窗   

 ShowWindow(hwnd,SW_SHOWNORMAL);   

 UpdateWindow(hwnd);   

 return 0;   

/////////////   

CWinApp1::CWinApp1()   

 m_pCurrentWinApp=this;   

CWinApp1::~CWinApp1(){}   

//以下為InitInstance()函數,MFC中要為CWinApp的派生類改寫,   

//這裡為了友善了解,把它放在CWinApp類裡面完成!   

//你隻要記住真正的MFC在派生類改寫此函數就行了。   

BOOL CWinApp1::InitInstance()   

 m_pMainWnd=new CFrameWnd;   

 m_pMainWnd->Create();   

 m_pMainWnd->ShowWnd();   

BOOL CWinApp1::Run()//////////////////////封裝消息循環   

 MSG msg;   

 while(GetMessage(&msg,NULL,0,0))   

  TranslateMessage(&msg);   

  DispatchMessage(&msg);   

} //////////////////////////////////////////////////////封裝消息循環   

CWinApp1 theApp;   //應用程式對象(全局)   

int WINAPI WinMain( HINSTANCE hinst, HINSTANCE hPrevInstance,   LPSTR lpCmdLine,  int nCmdShow)   

 hInstance=hinst;   

 CWinApp1* pApp=theApp.m_pCurrentWinApp;   

//真正的MFC要寫一個全局函數AfxGetApp,以擷取CWinApp指針。   

 pApp->InitInstance();   

 pApp->Run();   

}  

       代碼那麼長,實際上隻是寫了三個函數,一是CFrameWnd類的Create(),第二個是CWinApp類的InitInstance()和Run()。在此特别要說明的是InitInstance(),真正的MFC中,那是我們跟據自己構造視窗的需要,自己改寫這個函數。

       大家可以看到,封裝了上面兩個類以後,在入口函數WinMain中就寫幾行代碼,就可以産生一個視窗程式。在MFC中,因為WinMain函數就是固定的那麼幾行代碼,是以MFC絕對可以幫我們自動完成(MFC的特長就是幫我們完成有規律的代碼),也是以我們建立MFC應用程式的時候,看不到WinMain函數。

繼續閱讀