追根究底,剖析MFC六大關鍵技術(一)

追根究底,剖析MFC六大關鍵技術(一)

追根究底,剖析MFC六大關鍵技術(第一部分)

題外話:
我並不認爲MFC減輕了程序員們的負擔,MFC出現的目的雖然似乎是爲了讓程序員不用懂得太多就可以進行視窗編程,但本人在MFC裏徘徊了很久很久(因爲那時沒有書本詳細介紹MFC的原理),毫無收穫。可能朋友們會說,怎麼一定要了解MFC的具體呢,“黑箱”作業不行嗎?這不是微軟的初衷嗎?
不行!!!如果這樣,我寧願永遠不選擇MFC!在學電腦之前,本人學習的東西大都與藝術不無關係,小學時參加過全國書畫比賽獲銀獎。兒時的愛好就是在一張紙上隨心所欲地畫畫!MFC“黑箱”就象一幅碩大的抽象畫(抽象到你不能理解),它用鉛筆勾畫好線條,然後請你填顏色。
我們怎麼能忍受“黑箱”作業?我們選擇C++,就是因爲它夠自由,夠藝術,我們可以在此放飛幻想。所以,我們要攻克MFC。
偉大孫老師在剖析MFC的時候雖然盡心盡力,但可能由於篇幅所限,說得並不大清楚(我相信許多學員都有這方面的感受)。在此,我突發奇想,想與大家一同分享一下著名的MFC六大關鍵技術。
從什麼地方開始講起好呢?我覺得回到最初摸索MFC的時候,從基本談起最好。
因爲我知道,一個走過來程序員,總是忘記了當初自己是怎麼走過來的,忘記了一個學員最想知道的是什麼。一個小小的問題(一兩句話就可以解釋的),足學以令手無寸鐵的學員頭大半個月,所以,我努力回憶當初是怎麼讓自己豁然開朗的。
轉入正題:
MFC的六大關鍵技術包括:
MFC程序的初始化過程。
運行時類型識別(RTTI)。
動態創建。
永久保存。
消息映射。
消息傳遞。
MFC程序的初始化過程
1、設計一個簡單完整MFC程序,產生一個窗口。當然這不能讓AppWizard自動爲我們生成。我們可以在Win32 Application工程下面那樣寫:
#i nclude <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程序的初始化過程,上面我們還有了一個可執行的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()。
大家再下想,我們還要我們MFC“隱藏”更多的東西:WinMain()函數,設計窗口類,窗口註冊,消息循環,回調函數……我們馬上想到封裝想封裝他們。大家似乎隱約地感覺到封裝WinMain()不容易, 覺得WinMain()是一個特殊的函數,許多時候它代表了一個程序的起始和終結。所以在以前寫程序的時候,我們寫程序習慣從WinMain()的左大括寫起,到右大括弧返回、結束程序。
我們換一個角度去想,有什麼東西可以拿到WinMain()外面去做,許多初學者們,總覺得WinMain()函數天大的函數,什麼函數都好象要在它裏面才能真正運行。其實這樣瞭解很片面,甚至錯誤。我們可以寫一個這樣的C++程序:
////////////////////////////////////////////////////
#i nclude <iostream.h>
class test{
public:
 test(){cout<<"請改變你對main()函數的看法!"<<endl;}
};
test test1;
/**************************/
void main(){}
////////////////////////////////////////////////////
在上面的程序裏,入口的main()函數表面上什麼也不做,但程序執行了(注:實際入口函數做了一些我們可以不瞭解的事情),並輸出了一句話(注:全局對象比main()首先運行)。現在大家可以知道我們的WinMain()函數可以什麼都不做,程序依然可以運行,但沒有這個入口函數程序會報錯。
那麼WinMain()函數會放哪個類上面呢,請看下面程序:
#i nclude <afxwin.h>
class MyApp : public CWinApp
{
public:
 BOOL InitInstance()  //②程序入點
 {
  AfxMessageBox("程序依然可以運行!");
  return true;
 }
};

MyApp theApp;  //①建立應用程序。

大家可以看到,我並沒有構造框架,而程序卻可以運行了——彈出一個對話框(如果沒有WinMain()函數程序會報錯)。上面我這樣寫還是爲了直觀起見,其實我們只要寫兩行程序:
#i nclude <afxwin.h>
CWinApp theApp; 
//整個程序只構造一個CWinApp類對象,任可事情,程序就可以運行!
所以說,只要我們構造了CWinApp對象,就可以執行WinMain()函數。我們馬上相信WinMain()函數是在CWinApp類或它的基類中,而不是在其他類中。其實這種看法是錯誤的,我們知道編寫C++程序的時候,不可能讓你在一個類中包含入口函數,WinMain()是由系統調用,跟我們的平時程序自身調用的函數有着本質的區別。我們可以暫時簡單想象成,當CWinApp對象構造完的時候,WinMain()跟着執行。
現在大家明白了,大部分的“通用代碼(我們想封裝隱藏的東西)”都可以放到CWinApp類中,那麼它又是怎樣運行起來的呢?爲什麼構造了CWinApp類對象就“自動”執行那麼多東西。
大家再仔細想一下,CWinApp類對象構造之後,它會“自動”執行自己的構造函數。那麼我們可以把想要“自動”執行的代碼放到CWinApp類的構造函數中。
那麼CWinApp類可能打算這樣設計(先不計較正確與否):
class CWinApp : public CWinThead{
public:
virtual BOOL InitInstance(); //解釋過的程序的入點
  CWinApp ::CWinApp(){   //構造函數
   ////////////////////////
   WinMain();   //這個是大家一眼看出的錯誤
   Create();    //設計、創建、更新顯示窗口
   Run();     //消息循環
   //////////////////////
}
};
寫完後,大家又馬上感覺到似乎不對,WinMain()函數在這裏好象真的一點用處都沒有,並且能這樣被調用嗎(請允許我把手按在聖經上聲明一下:WinMain()不是普通的函數,它要肩負着初始化應用程序,包括全局變量的初始化,是由系統而不是程序本身調用的,WinMain()返回之後,程序就結束了,進程撤消)。再看Create()函數,它能確定設計什麼樣的窗口,創建什麼樣的窗口嗎?如果能在CWinApp的構造函數裏確定的話,我們以後設計MFC程序時窗口就一個樣,變得寫程序變有必要。再看Run()函數,它能在WinMain()函數外面運行嗎?
回過頭來,我們可以讓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()的問題,得總結一下,我們封裝的時候是不可以把它封裝到CWinApp類裏面,但由於WinMain()的不變性(或者說有規律可循),MFC完全有能力在我們構造CWinApp類對象的時候,幫我們完成那幾行代碼。
轉了一個大圈,我們彷彿又回到了SDK編程的開始。但現在我們現在能清楚地知道,表面上MFC與SDK編程截然不同,但實質上MFC只是用類的形式封裝了SDK函數,封裝之後,我們在WinMain()函數中只需要幾行代碼,就可以完成一個窗口程序。我們也由此知道了應如何去封裝應用程序類(CWinApp)和主框架窗口類(CFrameWnd)。下面把上開始設計這兩個類。
爲了簡單起見,我們忽略這兩個類的基類和派生類的編寫,可能大家會認爲這是一種很不負責任的做法,但本人覺得這既可減輕負擔,又免了大家在各類之間穿來穿去,更好理解一些(我們在關鍵的地方作註明)。還有,我把全部代碼寫在同一個文件中,讓大家看起來不用那麼喫力,但這是最不提倡的寫代碼方法,大家不要學哦!

#i nclude <windows.h>
HINSTANCE hInstance;

class CFrameWnd 
{
 HWND hwnd;
public:
 CFrameWnd();   //也可以在這裏調用Create()
 virtual ~CFrameWnd();
 int Create();    //類就留意這一個函數就行了!
 BOOL ShowWnd();
};
class CWinApp1 
{
public:
 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();
 return 0;
}

BOOL CWinApp1::Run()//////////////////////封裝消息循環
{
 MSG msg;
 while(GetMessage(&msg,NULL,0,0))
 {
  TranslateMessage(&msg);
  DispatchMessage(&msg);
 }
 return 0;
} //////////////////////////////////////////////////////封裝消息循環

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();
 return 0;
}

代碼那麼長,實際上只是寫了三個函數,一是CFrameWnd類的Create(),第二個是CWinApp類的InitInstance()和Run()。在此特別要說明的是InitInstance(),真正的MFC中,那是我們跟據自己構造窗口的需要,自己改寫這個函數。
大家可以看到,封裝了上面兩個類以後,在入口函數WinMain中就寫幾行代碼,就可以產生一個窗口程序。在MFC中,因爲WinMain函數就是固定的那麼幾行代碼,所以MFC絕對可以幫我們自動完成(MFC的特長就是幫我們完成有規律的代碼),所以我們創造MFC應用程序的時候,看不到WinMain函數。
寫到這裏,MFC六大關鍵技術之一:MFC程序的初始化過程(模擬),就差不多寫完了。回頭看一下,居然寫了八千多字,原本以爲寫完六大關鍵技術也不用寫那麼多字,現在還覺得慶幸的是不把文檔、視類牽連進去,否則更不知寫到何時。
還有五大關鍵技術沒有寫,我還應該寫下去嗎?上面寫了八千多字,都是我一個字一個字地敲進去,每個例子都是自己生硬地想出來。用了十多個小時,換來的可能更多是論壇中朋友們的漫罵,譏諷!
但我覺得還是值得的,我一向認爲VC沒有敵人,只有朋友,放眼周圍,發覺學VC的朋友越來越少,也沒有發現多少招收VC程序員的地方。記得讀大學的時候,我遇到一位搞美術的師兄,本來同行如敵國(我曾經搞過美術)。師兄美術功底很好,但他從來沒有在學校獲過美術一等獎,原因評獎的不懂得欣賞他的作品。我的出現,他深刻地體會到了:多一個朋友,會少一分孤獨!有時覺得學習VC的朋友是英雄(但我不是英雄,因爲我學VC多年來無甚突破),是值得尊敬的人物,大家交流一下,糾正一下自己的錯誤,真是一種福份……

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章