MFC六大关键技术
2012-09-06 14:58 1272人閱讀 評(píng)論(0) 收藏 舉報(bào) mfccommandnullclasspascal編程 MFC六大關(guān)鍵技術(shù)之初始化過(guò)程 我并不認(rèn)為MFC減輕了程序員們的負(fù)擔(dān),MFC出現(xiàn)的目的雖然似乎是為了讓程序員不用懂得太多就可以進(jìn)行視窗編程,但本人在MFC里徘徊了很久很久(因?yàn)槟菚r(shí)沒(méi)有書(shū)本詳細(xì)介紹MFC的原理),毫無(wú)收獲。可能朋友們會(huì)說(shuō),怎么一定要了解MFC的具體呢,“黑箱”作業(yè)不行嗎?這不是微軟的初衷嗎?
不行!!!如果這樣,我寧愿永遠(yuǎn)不選擇MFC!在學(xué)電腦之前,本人學(xué)習(xí)的東西大都與藝術(shù)不無(wú)關(guān)系,小學(xué)時(shí)參加過(guò)全國(guó)書(shū)畫(huà)比賽獲銀獎(jiǎng)。兒時(shí)的愛(ài)好就是在一張紙上隨心所欲地畫(huà)畫(huà)!MFC“黑箱”就象一幅碩大的抽象畫(huà)(抽象到你不能理解),它用鉛筆勾畫(huà)好線條,然后請(qǐng)你填顏色。
我們?cè)趺茨苋淌?/span>“黑箱”作業(yè)?我們選擇C++,就是因?yàn)樗鼔蜃杂?#xff0c;夠藝術(shù),我們可以在此放飛幻想。所以,我們要攻克MFC。
偉大孫老師在剖析MFC的時(shí)候雖然盡心盡力,但可能由于篇幅所限,說(shuō)得并不大清楚(我相信許多學(xué)員都有這方面的感受)。在此,我突發(fā)奇想,想與大家一同分享一下著名的MFC六大關(guān)鍵技術(shù)。
從什么地方開(kāi)始講起好呢?我覺(jué)得回到最初摸索MFC的時(shí)候,從基本談起最好。
因?yàn)槲抑?#xff0c;一個(gè)走過(guò)來(lái)程序員,總是忘記了當(dāng)初自己是怎么走過(guò)來(lái)的,忘記了一個(gè)學(xué)員最想知道的是什么。一個(gè)小小的問(wèn)題(一兩句話就可以解釋的),足學(xué)以令手無(wú)寸鐵的學(xué)員頭大半個(gè)月,所以,我努力回憶當(dāng)初是怎么讓自己豁然開(kāi)朗的。
轉(zhuǎn)入正題,MFC的六大關(guān)鍵技術(shù)包括:
·MFC程序的初始化過(guò)程
·運(yùn)行時(shí)類型識(shí)別(RTTI)
·動(dòng)態(tài)創(chuàng)建
·永久保存
·消息映射
·消息傳遞
MFC程序的初始化過(guò)程
1、設(shè)計(jì)一個(gè)簡(jiǎn)單完整MFC程序,產(chǎn)生一個(gè)窗口。當(dāng)然這不能讓AppWizard自動(dòng)為我們生成。我們可以在Win32 Application工程下面那樣寫:
| #include <afxwin.h> class MyApp : public CWinApp { public: BOOL InitInstance() //②程序入點(diǎn) { CFrameWnd *Frame=new CFrameWnd();//構(gòu)造框架 m_pMainWnd=Frame; //將m_pMainWnd設(shè)定為Frame; Frame->Create(NULL,"最簡(jiǎn)單的窗口");//建立框架 Frame->ShowWindow(SW_SHOW); //顯示框架 return true; //返回 } }; MyApp theApp; //①建立應(yīng)用程序。 |
設(shè)定鏈接MFC庫(kù),運(yùn)行,即可看見(jiàn)一個(gè)窗口。
從上面,大家可以看到建立一個(gè)MFC窗口很容易,只用兩步:一是從CWinApp派生一個(gè)應(yīng)用程序類(這里是MyApp),,然后建立應(yīng)用程序?qū)ο?#xff08;theApp),就可以產(chǎn)生一個(gè)自己需要的窗口(即需要什么樣就在InitInstance()里創(chuàng)建就行了)。
整個(gè)程序,就改寫一個(gè)InitInstance()函數(shù),創(chuàng)建那么一個(gè)對(duì)象(theApp),就是一個(gè)完整的窗口程序。這就是“黑箱”作業(yè)的魅力!!!!
在我們正想為微軟鼓掌的時(shí)候,我們突然覺(jué)得心里空蕩蕩的,我們想知道微軟幫我們做了什么事情,而我們想編自己的程序時(shí)又需要做什么事情,那怕在上面幾行的程序里面,我們還有不清楚的地方,比如,干嘛有一個(gè)m_pMainWnd指針變量,它從哪里來(lái),又要到哪里去呢?想一想在DOS下編程是多么美妙的一件事呵,我們需要什么變量,就聲明什么變量,需要什么樣的函數(shù),就編寫什么樣的函數(shù),或者引用函數(shù)庫(kù)……但是現(xiàn)在我們?cè)趺崔k!!!
我們可以逆向思維一下,MFC要達(dá)到這種效果,它是怎么做的呢?首先我們要弄明白,VC不是一種語(yǔ)言,它就象我們學(xué)c語(yǔ)言的時(shí)候的一個(gè)類似記事本的編輯器(請(qǐng)?jiān)徫业牟毁N切的比喻),所以,在VC里面我們用的是C++語(yǔ)言編程,C++才是根本(初學(xué)者總是以為VC是一門什么新的什么語(yǔ)言,一門比C++先進(jìn)很多的復(fù)雜語(yǔ)言,汗)。說(shuō)了那么多,我想用一句簡(jiǎn)單的話概括“MFC‘黑箱’就是幫助我們插入了‘C++代碼’的東西”。
既然MFC黑箱幫我們插入了代碼,那么大家想想它會(huì)幫我們插入什么樣的代碼呢?他會(huì)幫我們插入求解一元二次方程的代碼嗎?當(dāng)然不會(huì),所以它插入的實(shí)際上是每次編寫窗口程序必須的,通用的代碼。
再往下想,什么才是通用的呢?我們每次視窗編程都要寫WinMain()函數(shù),都要有注冊(cè)窗口,產(chǎn)生窗口,消息循環(huán),回調(diào)函數(shù)……即然每次都要的東西,就讓它們從我們眼前消失,讓MFC幫忙寫入!
要知道MFC初始化過(guò)程,大家當(dāng)然可以跟蹤執(zhí)行程序。孫老師的第三課跟蹤了很長(zhǎng)一段時(shí)間,我相信大家都有點(diǎn)暈頭轉(zhuǎn)向。本人覺(jué)得那怕你理解了MFC代碼,也很容易讓人找不著北,我們完全不懂的時(shí)候,在成千上萬(wàn)行程序的迷宮中如何能找到出口?
我們要換一種方法,不如就來(lái)重新編寫個(gè)MFC庫(kù)吧,嘩!大家不要笑,小心你的大牙,我不是瘋子(雖然瘋子也說(shuō)自己不瘋)。我們要寫的就是最簡(jiǎn)單的MFC類庫(kù),就是把MFC宏觀上的,理論上的東西寫出來(lái)。我們要用最簡(jiǎn)化的代碼,簡(jiǎn)化到剛好能運(yùn)行。
既然,我們這一節(jié)寫的是MFC程序的初始化過(guò)程,上面我們還有了一個(gè)可執(zhí)行的MFC程序。程序中只是用了兩個(gè)MFC類,一個(gè)是CWinApp,另一個(gè)是CFrameWnd。當(dāng)然,還有很多同樣重要MFC類如視圖類,文檔類等等。但在上面的程序可以不用到,所以暫時(shí)省去了它(總之是為了簡(jiǎn)單)。
好,現(xiàn)在開(kāi)始寫MFC類庫(kù)吧……唉,面前又有一個(gè)大難題,就是讓大家背一下MFC層次結(jié)構(gòu)圖。天,那張魚(yú)網(wǎng)怎么記得住,但既然我們要理解他,總得知道它是從那里派生出來(lái)的吧。
考慮到大家都很辛苦,那我們看一下上面兩個(gè)類的父子關(guān)系(箭頭代表派生):
| CObject->CCmdTarget->CWinThread->CWinApp->自己的重寫了InitInstance()的應(yīng)用程序類。 CObject(同上)->CCmdTarget(同上)->CWnd->CFrameWnd |
看到層次關(guān)系圖之后,終于可以開(kāi)始寫MFC類庫(kù)了。按照上面層次結(jié)構(gòu),我們可以寫以下六個(gè)類(為了直觀,省去了構(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)該有什么?大家馬上會(huì)想到,CWinApp類或者它的基類CCmdTarget里面應(yīng)該有一個(gè)虛函數(shù)virtual BOOL InitInstance(),是的,因?yàn)槟抢锸浅绦虻娜肟邳c(diǎn),初始化程序的地方,那自然少不了的。可能有些朋友會(huì)說(shuō),反正InitInstance()在派生類中一定要重載,我不在CCmdTarget或CWinApp類里定義,留待CWinApp的派生類去增加這個(gè)函數(shù)可不可以。扯到這個(gè)問(wèn)題可能有點(diǎn)越說(shuō)越遠(yuǎn),但我想信C++的朋友對(duì)虛函數(shù)應(yīng)該是沒(méi)有太多的問(wèn)題的。總的來(lái)說(shuō),作為程序員如果清楚知道基類的某個(gè)函數(shù)要被派生類用到,那定義為虛函數(shù)要方便很多。也有很多朋友問(wèn),C++為什么不自動(dòng)把基類的所有函數(shù)定義為虛函數(shù)呢,這樣可以省了很多麻煩,這樣所有函數(shù)都遵照派生類有定義的函數(shù)就調(diào)用派生類的,沒(méi)定義的就調(diào)用基類的,不用寫virtual的麻煩多好!其實(shí),很多面向?qū)ο蟮恼Z(yǔ)言都這樣做了。但定義一個(gè)虛函數(shù)要生成一個(gè)虛函數(shù)表,要占用系統(tǒng)空間,虛函數(shù)越多,表就越大,有時(shí)得不償失!這里哆嗦幾句,是因?yàn)橥笠f(shuō)明的消息映射中大家更加會(huì)體驗(yàn)到這一點(diǎn),好了,就此打往。
上面我們自己解決了一個(gè)問(wèn)題,就是在CCmdTarge寫一個(gè)virtual BOOL InitInstance()。 大家再下想,我們還要我們MFC“隱藏”更多的東西:WinMain()函數(shù),設(shè)計(jì)窗口類,窗口注冊(cè),消息循環(huán),回調(diào)函數(shù)……我們馬上想到封裝想封裝他們。大家似乎隱約地感覺(jué)到封裝WinMain()不容易,覺(jué)得WinMain()是一個(gè)特殊的函數(shù),許多時(shí)候它代表了一個(gè)程序的起始和終結(jié)。所以在以前寫程序的時(shí)候,我們寫程序習(xí)慣從WinMain()的左大括寫起,到右大括弧返回、結(jié)束程序。
我們換一個(gè)角度去想,有什么東西可以拿到WinMain()外面去做,許多初學(xué)者們,總覺(jué)得WinMain()函數(shù)天大的函數(shù),什么函數(shù)都好象要在它里面才能真正運(yùn)行。其實(shí)這樣了解很片面,甚至錯(cuò)誤。我們可以寫一個(gè)這樣的C++程序:
| #include <iostream.h> class test{ public: test(){cout<<"請(qǐng)改變你對(duì)main()函數(shù)的看法!"<<endl;} }; test test1; /**************************/ void main(){} |
在上面的程序里,入口的main()函數(shù)表面上什么也不做,但程序執(zhí)行了(注:實(shí)際入口函數(shù)做了一些我們可以不了解的事情),并輸出了一句話(注:全局對(duì)象比main()首先運(yùn)行)。現(xiàn)在大家可以知道我們的WinMain()函數(shù)可以什么都不做,程序依然可以運(yùn)行,但沒(méi)有這個(gè)入口函數(shù)程序會(huì)報(bào)錯(cuò)。
那么WinMain()函數(shù)會(huì)放哪個(gè)類上面呢,請(qǐng)看下面程序:
| #include <afxwin.h> class MyApp : public CWinApp { public: BOOL InitInstance() //②程序入點(diǎn) { AfxMessageBox("程序依然可以運(yùn)行!"); return true; } }; MyApp theApp; //①建立應(yīng)用程序。 |
大家可以看到,我并沒(méi)有構(gòu)造框架,而程序卻可以運(yùn)行了——彈出一個(gè)對(duì)話框(如果沒(méi)有WinMain()函數(shù)程序會(huì)報(bào)錯(cuò))。上面我這樣寫還是為了直觀起見(jiàn),其實(shí)我們只要寫兩行程序:
| #include <afxwin.h> CWinApp theApp; //整個(gè)程序只構(gòu)造一個(gè)CWinApp類對(duì)象,任可事情,程序就可以運(yùn)行! |
所以說(shuō),只要我們構(gòu)造了CWinApp對(duì)象,就可以執(zhí)行WinMain()函數(shù)。我們馬上相信WinMain()函數(shù)是在CWinApp類或它的基類中,而不是在其他類中。其實(shí)這種看法是錯(cuò)誤的,我們知道編寫C++程序的時(shí)候,不可能讓你在一個(gè)類中包含入口函數(shù),WinMain()是由系統(tǒng)調(diào)用,跟我們的平時(shí)程序自身調(diào)用的函數(shù)有著本質(zhì)的區(qū)別。我們可以暫時(shí)簡(jiǎn)單想象成,當(dāng)CWinApp對(duì)象構(gòu)造完的時(shí)候,WinMain()跟著執(zhí)行。
現(xiàn)在大家明白了,大部分的“通用代碼(我們想封裝隱藏的東西)”都可以放到CWinApp類中,那么它又是怎樣運(yùn)行起來(lái)的呢?為什么構(gòu)造了CWinApp類對(duì)象就“自動(dòng)”執(zhí)行那么多東西。
大家再仔細(xì)想一下,CWinApp類對(duì)象構(gòu)造之后,它會(huì)“自動(dòng)”執(zhí)行自己的構(gòu)造函數(shù)。那么我們可以把想要“自動(dòng)”執(zhí)行的代碼放到CWinApp類的構(gòu)造函數(shù)中。
那么CWinApp類可能打算這樣設(shè)計(jì)(先不計(jì)較正確與否):
| class CWinApp : public CWinThead{ public: virtual BOOL InitInstance(); //解釋過(guò)的程序的入點(diǎn) CWinApp ::CWinApp(){ //構(gòu)造函數(shù) WinMain(); //這個(gè)是大家一眼看出的錯(cuò)誤 Create(); //設(shè)計(jì)、創(chuàng)建、更新顯示窗口 Run(); //消息循環(huán) // } }; |
寫完后,大家又馬上感覺(jué)到似乎不對(duì),WinMain()函數(shù)在這里好象真的一點(diǎn)用處都沒(méi)有,并且能這樣被調(diào)用嗎(請(qǐng)?jiān)试S我把手按在圣經(jīng)上聲明一下:WinMain()不是普通的函數(shù),它要肩負(fù)著初始化應(yīng)用程序,包括全局變量的初始化,是由系統(tǒng)而不是程序本身調(diào)用的,WinMain()返回之后,程序就結(jié)束了,進(jìn)程撤消)。再看Create()函數(shù),它能確定設(shè)計(jì)什么樣的窗口,創(chuàng)建什么樣的窗口嗎?如果能在CWinApp的構(gòu)造函數(shù)里確定的話,我們以后設(shè)計(jì)MFC程序時(shí)窗口就一個(gè)樣,變得寫程序變有必要。再看Run()函數(shù),它能在WinMain()函數(shù)外面運(yùn)行嗎?
回過(guò)頭來(lái),我們可以讓WinMain()函數(shù)一條語(yǔ)句都不包含嗎?不可以,我們看一下WinMain()函數(shù)的四個(gè)參數(shù):
| WinMain(HINSTANCE, HINSTANCE, LPSTR, int) |
其中第一個(gè)參數(shù)指向一個(gè)實(shí)例句柄,我們?cè)谠O(shè)計(jì)WNDCLASS的時(shí)候一定要指定實(shí)例句柄。我們窗口編程,肯定要設(shè)計(jì)窗口類。所以,WinMain()再簡(jiǎn)單也要這樣寫:
| int WinMain(HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { hInstance=hinst } |
既然實(shí)例句柄要等到程序開(kāi)始執(zhí)行才能知道,那么我們用于創(chuàng)建窗口的Create()函數(shù)也要在WinMain()內(nèi)部才能執(zhí)行[因?yàn)槿绻鹊?/span>WinMain()執(zhí)行完畢后,程序結(jié)束,進(jìn)程撤消,當(dāng)然Create()也不可能創(chuàng)建窗口]
那么Run()(消息循環(huán))放在那里執(zhí)行好呢?眾所周知,消息循環(huán)就是相同的那么幾句代碼,但我們也不要企圖把它放在WinMain()函數(shù)之外執(zhí)行。
所以我們?cè)?/span>WinMain()函數(shù)里面,我們程序要象以下這樣寫
| WinMain(……) { ……窗口類對(duì)象執(zhí)行創(chuàng)建窗口函數(shù)…… ……程序類對(duì)象執(zhí)行消息循環(huán)函數(shù)…… } |
對(duì)于WinMain()的問(wèn)題,得總結(jié)一下,我們封裝的時(shí)候是不可以把它封裝到CWinApp類里面,但由于WinMain()的不變性(或者說(shuō)有規(guī)律可循),MFC完全有能力在我們構(gòu)造CWinApp類對(duì)象的時(shí)候,幫我們完成那幾行代碼。 轉(zhuǎn)了一個(gè)大圈,我們仿佛又回到了SDK編程的開(kāi)始。但現(xiàn)在我們現(xiàn)在能清楚地知道,表面上MFC與SDK編程截然不同,但實(shí)質(zhì)上MFC只是用類的形式封裝了SDK函數(shù),封裝之后,我們?cè)?/span>WinMain()函數(shù)中只需要幾行代碼,就可以完成一個(gè)窗口程序。我們也由此知道了應(yīng)如何去封裝應(yīng)用程序類(CWinApp)和主框架窗口類(CFrameWnd)。下面把上開(kāi)始設(shè)計(jì)這兩個(gè)類。
為了簡(jiǎn)單起見(jiàn),我們忽略這兩個(gè)類的基類和派生類的編寫,可能大家會(huì)認(rèn)為這是一種很不負(fù)責(zé)任的做法,但本人覺(jué)得這既可減輕負(fù)擔(dān),又免了大家在各類之間穿來(lái)穿去,更好理解一些(我們?cè)陉P(guān)鍵的地方作注明)。還有,我把全部代碼寫在同一個(gè)文件中,讓大家看起來(lái)不用那么吃力,但這是最不提倡的寫代碼方法,大家不要學(xué)哦!
| #include <windows.h> HINSTANCE hInstance; class CFrameWnd { HWND hwnd; public: CFrameWnd(); //也可以在這里調(diào)用Create() virtual ~CFrameWnd(); int Create(); //類就留意這一個(gè)函數(shù)就行了! BOOL ShowWnd(); }; class CWinApp1 { public: CFrameWnd* m_pMainWnd;//在真正的MFC里面 //它是CWnd指針,但這里由于不寫CWnd類 //只要把它寫成CFrameWnd指針 CWinApp1* m_pCurrentWinApp;//指向應(yīng)用程序?qū)ο蟊旧?/span> CWinApp1(); virtual ~CWinApp1(); virtual BOOL InitInstance();//MFC原本是必須重載的函數(shù),最重要的函數(shù)!!!! virtual BOOL Run();//消息循環(huán) }; CFrameWnd::CFrameWnd(){} CFrameWnd::~CFrameWnd(){} int CFrameWnd::Create() //封裝創(chuàng)建窗口代碼 { 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;//默認(rèn)窗口過(guò)程函數(shù)。 //大家可以想象成MFC通用的窗口過(guò)程。 wndcls.lpszClassName="窗口類名"; wndcls.lpszMenuName=NULL; RegisterClass(&wndcls); hwnd=CreateWindow("窗口類名","窗口實(shí)例標(biāo)題名",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()函數(shù),MFC中要為CWinApp的派生類改寫, //這里為了方便理解,把它放在CWinApp類里面完成! //你只要記住真正的MFC在派生類改寫此函數(shù)就行了。 BOOL CWinApp1::InitInstance() { m_pMainWnd=new CFrameWnd; m_pMainWnd->Create(); m_pMainWnd->ShowWnd(); return 0; } BOOL CWinApp1::Run()//封裝消息循環(huán) { MSG msg; while(GetMessage(&msg,NULL,0,0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } //封裝消息循環(huán) CWinApp1 theApp; //應(yīng)用程序?qū)ο?#xff08;全局) int WINAPI WinMain( HINSTANCE hinst, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { hInstance=hinst; CWinApp1* pApp=theApp.m_pCurrentWinApp; //真正的MFC要寫一個(gè)全局函數(shù)AfxGetApp,以獲取CWinApp指針。 pApp->InitInstance(); pApp->Run(); return 0; } |
代碼那么長(zhǎng),實(shí)際上只是寫了三個(gè)函數(shù),一是CFrameWnd類的Create(),第二個(gè)是CWinApp類的InitInstance()和Run()。在此特別要說(shuō)明的是InitInstance(),真正的MFC中,那是我們跟據(jù)自己構(gòu)造窗口的需要,自己改寫這個(gè)函數(shù)。
大家可以看到,封裝了上面兩個(gè)類以后,在入口函數(shù)WinMain中就寫幾行代碼,就可以產(chǎn)生一個(gè)窗口程序。在MFC中,因?yàn)?/span>WinMain函數(shù)就是固定的那么幾行代碼,所以MFC絕對(duì)可以幫我們自動(dòng)完成(MFC的特長(zhǎng)就是幫我們完成有規(guī)律的代碼),所以我們創(chuàng)造MFC應(yīng)用程序的時(shí)候,看不到WinMain函數(shù)。
寫到這里,MFC六大關(guān)鍵技術(shù)之一:MFC程序的初始化過(guò)程(模擬),就差不多寫完了。回頭看一下,居然寫了八千多字,原本以為寫完六大關(guān)鍵技術(shù)也不用寫那么多字,現(xiàn)在還覺(jué)得慶幸的是不把文檔、視類牽連進(jìn)去,否則更不知寫到何時(shí)。
還有五大關(guān)鍵技術(shù)沒(méi)有寫,我還應(yīng)該寫下去嗎?上面寫了八千多字,都是我一個(gè)字一個(gè)字地敲進(jìn)去,每個(gè)例子都是自己生硬地想出來(lái)。用了十多個(gè)小時(shí),換來(lái)的可能更多是論壇中朋友們的漫罵,譏諷!
但我覺(jué)得還是值得的,我一向認(rèn)為VC沒(méi)有敵人,只有朋友,放眼周圍,發(fā)覺(jué)學(xué)VC的朋友越來(lái)越少,也沒(méi)有發(fā)現(xiàn)多少招收VC程序員的地方。記得讀大學(xué)的時(shí)候,我遇到一位搞美術(shù)的師兄,本來(lái)同行如敵國(guó)(我曾經(jīng)搞過(guò)美術(shù))。師兄美術(shù)功底很好,但他從來(lái)沒(méi)有在學(xué)校獲過(guò)美術(shù)一等獎(jiǎng),原因評(píng)獎(jiǎng)的不懂得欣賞他的作品。我的出現(xiàn),他深刻地體會(huì)到了:多一個(gè)朋友,會(huì)少一分孤獨(dú)!有時(shí)覺(jué)得學(xué)習(xí)VC的朋友是英雄(但我不是英雄,因?yàn)槲覍W(xué)VC多年來(lái)無(wú)甚突破),是值得尊敬的人物,大家交流一下,糾正一下自己的錯(cuò)誤,真是一種福份…… MFC六大關(guān)鍵技術(shù)之運(yùn)行時(shí)類型識(shí)別(RTTI) 運(yùn)行時(shí)類型識(shí)別(RTTI)即是程序執(zhí)行過(guò)程中知道某個(gè)對(duì)象屬于某個(gè)類,我們平時(shí)用C++編程接觸的RTTI一般是編譯器的RTTI,即是在新版本的VC++編譯器里面選用“使能RTTI”,然后載入typeinfo.h文件,就可以使用一個(gè)叫typeid()的運(yùn)算子,它的地位與在C++編程中的sizeof()運(yùn)算子類似的地方(包含一個(gè)頭文件,然后就有一個(gè)熟悉好用的函數(shù))。typdid()關(guān)鍵的地方是可以接受兩個(gè)類型的參數(shù):一個(gè)是類名稱,一個(gè)是對(duì)象指針。所以我們判別一個(gè)對(duì)象是否屬于某個(gè)類就可以象下面那樣:
if (typeid (ClassName)== typeid(*ObjectName)){
((ClassName*)ObjectName)->Fun();
}
象上面所說(shuō)的那樣,一個(gè)typeid()運(yùn)算子就可以輕松地識(shí)別一個(gè)對(duì)象是否屬于某一個(gè)類,但MFC并不是用typeid()的運(yùn)算子來(lái)進(jìn)行動(dòng)態(tài)類型識(shí)別,而是用一大堆令人費(fèi)解的宏。很多學(xué)員在這里很疑惑,好象MFC在大部分地方都是故作神秘。使們大家編程時(shí)很迷惘,只知道在這里加入一組宏,又在那兒加入一個(gè)映射,而不知道我們?yōu)槭裁匆尤脒@些東東。
其實(shí),早期的MFC并沒(méi)有typeid()運(yùn)算子,所以只能沿用一個(gè)老辦法。我們甚至可以想象一下,如果MFC早期就有template(模板)的概念,可能更容易解決RTTI問(wèn)題。
所以,我們要回到“古老”的年代,想象一下,要完成RTTI要做些什么事情。就好像我們?cè)谝粋€(gè)新型(新型到我們還不認(rèn)識(shí))電器公司里面,我們要識(shí)別哪個(gè)是電飯鍋,哪個(gè)是電磁爐等等,我們要查看登記的各電器一系列的信息,我們才可以比較、鑒別,那個(gè)東西是什么!
要登記一系列的消息并不是一件簡(jiǎn)單的事情,大家可能首先想到用數(shù)組登記對(duì)象。但如果用數(shù)組,我們要定義多大的數(shù)組才好呢,大了浪費(fèi)空間,小了更加不行。所以我們要用另一種數(shù)據(jù)結(jié)構(gòu)——鏈表。因?yàn)殒湵砝碚撋峡纱罂尚?#xff0c;可以無(wú)限擴(kuò)展。
鏈表是一種常用的數(shù)據(jù)結(jié)構(gòu),簡(jiǎn)單地說(shuō),它是在一個(gè)對(duì)象里面保存了指向下一個(gè)同類型對(duì)象的指針。我們大體可以這樣設(shè)計(jì)我們的類:
struct CRuntimeClass
{
……類的名稱等一切信息……
CRuntimeClass * m_pNextClass;//指向鏈表中下一CRuntimeClass對(duì)象的指針
};
鏈表還應(yīng)該有一個(gè)表頭和一個(gè)表尾,這樣我們?cè)诓殒湵碇懈鲗?duì)象元素的信息的時(shí)候才知道從哪里查起,到哪兒結(jié)束。我們還要注明本身是由哪能個(gè)類派生。所以我們的鏈表類要這樣設(shè)計(jì):
struct CRuntimeClass
{
……類的名稱等一切信息……
CRuntimeClass * m_pBaseClass;//指向所屬的基類。
CRuntimeClass * m_pNextClass;//定義表尾的時(shí)候只要定義此指針為空就可以 了。
static CRuntimeClass* pFirstClass;//這里表頭指針屬于靜態(tài)變量,因?yàn)槲覀冎纒tatic變量在內(nèi)存中只初始化一次,就可以為各對(duì)象所用!保證了各對(duì)象只有一個(gè)表頭。
};
有了CRuntimeClass結(jié)構(gòu)后,我們就可以定義鏈表了:
static CRuntimeClass classCObject={NULL,NULL};//這里定義了一個(gè)CRuntimeClass對(duì)象,因?yàn)閏lassCObject無(wú)基類,所以m_pBaseClass為NULL。因?yàn)槟壳爸挥幸粋€(gè)元素(即目前沒(méi)有下一元素),所以m_pNextClass為NULL(表尾)。
至于pFirstClass(表頭),大家可能有點(diǎn)想不通,它到什么地方去了。因?yàn)槲覀冞@里并不想把classCObject作為鏈表表頭,我們還要在前面插入很多的CRuntimeClass對(duì)象,并且因?yàn)閜FirstClass為static指針,即是說(shuō)它不是屬于某個(gè)對(duì)象,所以我們?cè)谟盟耙瘸跏蓟?#xff1a;CRuntimeClass* CRuntimeClass::pFirstClass=NULL;
現(xiàn)在我們可以在前面插入一個(gè)CRuntimeClass對(duì)象,插入之前我得重要申明一下:如果單純?yōu)榱诉\(yùn)行時(shí)類型識(shí)別,我們未必用到m_pNextClass指針(更多是在運(yùn)行時(shí)創(chuàng)建時(shí)用),我們關(guān)心的是類本身和它的基類。這樣,查找一個(gè)對(duì)象是否屬于一個(gè)類時(shí),主要關(guān)心的是類本身及它的基類,
CRuntimeClass classCCmdTarget={ &classCObject, NULL};
CRuntimeClass classCWnd={ &classCCmdTarget ,NULL };
CRuntimeClass classCView={ &classCWnd , NULL };
好了,上面只是僅僅為一個(gè)指針m_pBaseClass賦值(MFC中真正CRuntimeClass有多個(gè)成員變量和方法),就連接成了鏈表。假設(shè)我們現(xiàn)在已全部構(gòu)造完成自己需要的CRuntimeClass對(duì)象,那么,這時(shí)候應(yīng)該定義表頭。即要用pFirstClass指針指向我們最后構(gòu)造的CRuntimeClass對(duì)象——classCView。
CRuntimeClass::pFirstClass=&classCView;
現(xiàn)在鏈表有了,表頭表尾都完善了,問(wèn)題又出現(xiàn)了,我們應(yīng)該怎樣訪問(wèn)每一個(gè)CRuntimeClass對(duì)象?要判斷一個(gè)對(duì)象屬于某類,我們要從表頭開(kāi)始,一直向表尾查找到表尾,然后才能比較得出結(jié)果嗎。肯定不是這樣!
大家可以這樣想一下,我們構(gòu)造這個(gè)鏈表的目的,就是構(gòu)造完之后,能夠按主觀地拿一個(gè)CRuntimeClass對(duì)象和鏈表中的元素作比較,看看其中一個(gè)對(duì)象中否屬于你指定的類。這樣,我們需要有一個(gè)函數(shù),一個(gè)能返回自身類型名的函數(shù)GetRuntimeClass()。
上面簡(jiǎn)單地說(shuō)一下鏈表的過(guò)程,但單純有這個(gè)鏈表是沒(méi)有任何意義。回到MFC中來(lái),我們要實(shí)現(xiàn)的是在每個(gè)需要有RTTI能力的類中構(gòu)造一個(gè)CRuntimeClass對(duì)象,比較一個(gè)類是否屬于某個(gè)對(duì)象的時(shí)候,實(shí)際上只是比較CRuntimeClass對(duì)象。
如何在各個(gè)類之中插入CRuntimeClass對(duì)象,并且指定CRuntimeClass對(duì)象的內(nèi)容及CRuntimeClass對(duì)象的鏈接,這里起碼有十行的代碼才能完成。在每個(gè)需要有RTTI能力的類設(shè)計(jì)中都要重復(fù)那十多行代碼是一件乏味的事情,也容易出錯(cuò),所以MFC用了兩個(gè)宏代替這些工作,即DECLARE_DYNAMIC(類名)和IMPLEMENT_DYNAMIC(類名,基類名)。從這兩個(gè)宏我們可以看出在MFC名類中的CRuntimeClass對(duì)象構(gòu)造連接只有類名及基類名的不同!
到此,可能會(huì)有朋友問(wèn):為什么要用兩個(gè)宏,用一個(gè)宏不可以代換CRuntimeClass對(duì)象構(gòu)造連接嗎?個(gè)人認(rèn)為肯定可以,因?yàn)楹曛皇俏淖执鷵Q的游戲而已。但我們?cè)诰幊讨?#xff0c;頭文件與源文件是分開(kāi)的,我們要在頭文件頭聲明變量及方法,在源文件里實(shí)具體實(shí)現(xiàn)。即是說(shuō)我們要在頭文件中聲明:
public:
static CRuntimeClass classXXX //XXX為類名
virtual CRuntime* GetRuntimeClass() const;
然后在源文件里實(shí)現(xiàn):
CRuntimeClass* XXX::classXXX={……};
CRuntime* GetRuntimeClass() const;
{ return &XXX:: classXXX;}//這里不能直接返回&classXXX,因?yàn)閟tatic變量是類擁有而不是對(duì)象擁有。
我們一眼可以看出MFC中的DECLARE_DYNAMIC(類名)宏應(yīng)該這樣定義:
#define DECLARE_DYNAMIC(class_name) public: static CRuntimeClass class##class_name; virtual CRuntimeClass* GetRuntimeClass() const;
其中##為連接符,可以讓我們傳入的類名前面加上class,否則跟原類同名,大家會(huì)知道產(chǎn)生什么后果。
有了上面的DECLARE_DYNAMIC(類名)宏之后,我們?cè)陬^文件里寫上一句
DECLARE_DYNAMIC(XXX)
宏展開(kāi)后就有了我們想要的:
public:
static CRuntimeClass classXXX //XXX為類名
virtual CRuntime* GetRuntimeClass() const;
對(duì)于IMPLEMENT_DYNAMIC(類名,基類名),看來(lái)也不值得在這里代換文字了,大家知道它是知道回事,宏展開(kāi)后為我們做了什么,再深究真是一點(diǎn)意義都沒(méi)有!
有了此鏈表之后,就像有了一張存放各類型的網(wǎng),我們可以輕而易舉地RTTI。CObject有一個(gè)函數(shù)BOOL IsKindOf(const CRuntimeClass* pClass) const;,被它以下所有派生員繼承。
此函數(shù)實(shí)現(xiàn)如下:
BOOL CObject::IsKindOf(const CRuntimeClass* pClass) const
{
CRuntimeClass* pClassThis=GetRuntimeClass();//獲得自己的CRuntimeClass對(duì)象指針。
while(pClassThis!=NULL)
{
if(pClassThis==pClass) return TRUE;
pClassThis=pClassThis->m_pBaseClass;//這句最關(guān)鍵,指向自己基類,再回頭比較,一直到盡頭m_pBaseClass為NULL結(jié)束。
}
return FALSE;
}
說(shuō)到這里,運(yùn)行時(shí)類型識(shí)別(RTTI)算是完成了。寫這篇文章的時(shí)候,我一直重感冒。我曾一度在想,究竟寫這東西是為了什么。因?yàn)槿绻野堰@些時(shí)間用在別的地方(甚至幫別人打打字),應(yīng)該有數(shù)百元的報(bào)酬。
是什么讓“嗜財(cái)如命”的我繼續(xù)寫下去?我想,無(wú)非是想交幾個(gè)計(jì)算機(jī)的朋友而已。計(jì)算機(jī)是大家公認(rèn)高科技的東西,但學(xué)習(xí)它的朋友大多只能默默無(wú)聞,外界的朋友也不知道怎么去認(rèn)識(shí)你。程序員更不是“潮流”的東西,更加得不到別人的認(rèn)可。
有一件個(gè)人認(rèn)為很典型的事情,有一天,我跟一個(gè)朋友到一個(gè)單位里面。里面有一個(gè)女打字員。朋友看著她熟練的指法,心悅誠(chéng)服地說(shuō):“她的電腦水平比你的又高了一個(gè)很高的層次!”,那個(gè)女的打字高手亦自豪地說(shuō):“我靠電腦為生,電腦水平肯定比你(指筆者)的好一點(diǎn)!換著是你,如果以電腦為生,我也不敢說(shuō)好過(guò)你!”。雖然我想聲明我是計(jì)算機(jī)專業(yè)的,但我知道沒(méi)有理解,所以我只得客氣地點(diǎn)頭。
選擇電腦“潮流”的東西實(shí)際是選擇了平凡,而選擇做程序員就是選擇了孤獨(dú)!幸好我不是一個(gè)專門的程序員,但即使如此,我愿意做你們的朋友,因?yàn)槲覑?ài)你們! MFC六大關(guān)鍵技術(shù)之動(dòng)態(tài)創(chuàng)建 動(dòng)態(tài)創(chuàng)建就是運(yùn)行時(shí)創(chuàng)建指定類的對(duì)象,在MFC中大量使用。如框架窗口對(duì)象、視對(duì)象,還有文檔對(duì)象都需要由文檔模板類對(duì)象來(lái)動(dòng)態(tài)的創(chuàng)建。我覺(jué)得這是每個(gè)MFC的學(xué)習(xí)者很希望理解的問(wèn)題。 初次接觸MFC的時(shí)候,很容易有這樣的迷惘。MFC的幾大類不用我們設(shè)計(jì)也就罷了,但最疑惑的是不用我們實(shí)例化對(duì)象。本來(lái)最直觀的理解就是,我們需要框架的時(shí)候,親手寫上CFrameWnd myFrame;需要視的時(shí)候,親自打上CView myView;…… 但MFC不給我們這個(gè)機(jī)會(huì),致使我們錯(cuò)覺(jué)窗口沒(méi)有實(shí)例化就彈出來(lái)了!就象畫(huà)了張電視機(jī)的電路圖就可以看電視一樣令人難以置信。但大伙想了一下,可能會(huì)一拍腦門,認(rèn)為簡(jiǎn)單不過(guò):MFC自動(dòng)幫我們完成CView myView之流的代碼不就行了么!!!其實(shí)不然,寫MFC程序的時(shí)候,我們幾乎要對(duì)每個(gè)大類進(jìn)行派生改寫。換句話說(shuō),MFC并不知道我們打算怎樣去改寫這些類,當(dāng)然也不打算全部為我們“靜態(tài)”創(chuàng)建這些類了。即使靜態(tài)了創(chuàng)建這些類也沒(méi)有用,因?yàn)槲覀儚膩?lái)也不會(huì)直接利用這些類的實(shí)例干什么事情。我們只知道,想做什么事情就往各大類里塞,不管什么變量、方法照塞,塞完之后,我們似乎并未實(shí)例化對(duì)象,程序就可以運(yùn)行! 要做到把自己的類交給MFC,MFC就用同一樣的方法,把不同的類一一準(zhǔn)確創(chuàng)建,我們要做些什么事情呢?同樣地,我們要建立鏈表,記錄各類的關(guān)鍵信息,在動(dòng)態(tài)創(chuàng)建的時(shí)候找出這些信息,就象上一節(jié)RTTI那樣!我們可以設(shè)計(jì)一個(gè)類: struct CRuntimeClass{ ???? LPCSTR m_lpszClassName;??????????????? //類名指針 ???? CObject* (PASCAL *m_pfnCreateObject)();?? //創(chuàng)建對(duì)象的函數(shù)的指針 ????? CRuntimeClass* m_pBaseClass;???????????????????????? //講RTTI時(shí)介紹過(guò) ???? CRuntimeClass* m_pNextClass;?????????? ?//指向鏈表的下一個(gè)元素(許多朋友說(shuō)上一節(jié)講RTTI時(shí)并沒(méi)有用到這個(gè)指針,我原本以為這樣更好理解一些,因?yàn)闆](méi)有這個(gè)指針,這個(gè)鏈表是無(wú)法連起來(lái),而m_pBaseClass僅僅是向基類走,在MFC的樹(shù)型層次結(jié)構(gòu)中m_pBaseClass是不能遍歷的) ??????? CObject* CreateObject();???????????????? //創(chuàng)建對(duì)象 ???? static CRuntimeClass* PASCAL Load();??? //遍歷整個(gè)類型鏈表,返回符合動(dòng)態(tài)創(chuàng)建的對(duì)象。 ?static CRuntimeClass* pFirstClass;??????? //類型鏈表的頭指針 }; 一下子往結(jié)構(gòu)里面塞了那么多的東西,大家可以覺(jué)得有點(diǎn)頭暈。至于CObject* (PASCAL *m_pfnCreateObject)();,這定義函數(shù)指針的方法,大家可能有點(diǎn)陌生。函數(shù)指針在C++書(shū)籍里一般被定為選學(xué)章節(jié),但MFC還是經(jīng)常用到此類的函數(shù),比如我們所熟悉的回調(diào)函數(shù)。簡(jiǎn)單地說(shuō)m_pfnCreateObject即是保存了一個(gè)函數(shù)的地址,它將會(huì)創(chuàng)建一個(gè)對(duì)象。即是說(shuō),以后,m_pfnCreateObject指向不同的函數(shù),我們就會(huì)創(chuàng)建不同類型的對(duì)象。 有函數(shù)指針,我們要實(shí)現(xiàn)一個(gè)與原定義參數(shù)及返回值都相同一個(gè)函數(shù),在MFC中定義為: static CObject* PASCAL CreateObject(){return new XXX};//XXX為類名。類名不同,我們就創(chuàng)建不同的對(duì)象。 由此,我們可以如下構(gòu)造 CRuntimeClass到鏈表: CRuntimeClass classXXX={ 類名, ……, XXX::CreateObject(),?? //m_pfnCreateObject指向的函數(shù) RUNTIME_CLASS(基類名)?// RUNTIME_CLASS宏可以返回CRuntimeClass對(duì)象指針。 NULL????? //m_pNextClass暫時(shí)為空,最后會(huì)我們?cè)僭O(shè)法讓它指向舊鏈表表頭。 }; 這樣,我們用函數(shù)指針m_pfnCreateObject(指向CreateObject函數(shù)),就隨時(shí)可new新對(duì)象了。并且大家留意到,我們?cè)?/span>設(shè)計(jì)CRuntimeClass類對(duì)時(shí)候,只有類名(和基類名)的不同(我們用XXX代替的地方),其它的地方一樣,這正是我們想要的,因?yàn)槲覀儎?dòng)態(tài)創(chuàng)建也象RTTI那樣用到兩個(gè)宏,只要傳入類名和基類作宏參數(shù),就可以滿足條件。 即是說(shuō),我們類說(shuō)明中使用DECLARE_DYNCREATE(CLASSNMAE)宏和在類的實(shí)現(xiàn)文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏來(lái)為我們加入鏈表,至于這兩個(gè)宏怎么為我們建立一個(gè)鏈表,我們自己可以玩玩文字代換的游戲,在此不一一累贅。但要說(shuō)明的一點(diǎn)就是:動(dòng)態(tài)創(chuàng)建宏xxx_DYNCREATE包含了RTTI宏,即是說(shuō), xxx_DYNCREATE是xxx_DYNAMIC的“增強(qiáng)版”。 到此,我們有必要了解一下上節(jié)課沒(méi)有明講的m_pNextClass指針。因?yàn)?/span>MFC層次結(jié)構(gòu)是樹(shù)狀的,并不是直線的。如果我們只有一個(gè)m_pBaseClass指針,它只會(huì)沿著基類上去,會(huì)漏掉其它分支。在動(dòng)態(tài)創(chuàng)建時(shí),必需要檢查整個(gè)鏈表,看有多少個(gè)要?jiǎng)討B(tài)創(chuàng)建的對(duì)象,即是說(shuō)要從表頭(pFirstClass)開(kāi)始一直遍歷到表尾(m_pNextClass=NULL),不能漏掉一個(gè)CRuntimeClass對(duì)象。 所以每當(dāng)有一個(gè)新的鏈表元素要加入鏈表的時(shí)候,我們要做的就是使新的鏈表元素成為表頭,并且m_pNextClass指向原來(lái)鏈表的表頭,即像下面那樣(當(dāng)然,這些不需要我們操心,是RTTI宏幫助我們完成的): pNewClass->m_pNextClass=CRuntimeClass::pFirstClass;//新元素的m_pNextClass指針指向想加入的鏈表的表頭。 CRuntimeClass::pFirstClass=pNewClass;//鏈表的頭指針指向剛插入的新元素。 好了,有了上面的鏈表,我們就可以分析動(dòng)態(tài)創(chuàng)建了。 有一了張有類名,函數(shù)指針,動(dòng)態(tài)創(chuàng)建函數(shù)的鏈表,我們就可以知道應(yīng)該按什么步驟去動(dòng)態(tài)創(chuàng)建了:1、獲得一要?jiǎng)討B(tài)創(chuàng)建的類的類名(假設(shè)為A)。2、將A跟鏈表里面每個(gè)元素的m_lpszClassName指向的類名作比較。3、若找到跟A相同的類名就返回A所屬的CRuntimeClass元素的指針。4、判斷m_pfnCreateObject是否有指向創(chuàng)建函數(shù),有則創(chuàng)建對(duì)象,并返回該對(duì)象。代碼演示如下(以下兩個(gè)函數(shù)都是CRuntimeClass類函數(shù)): ///以下為根據(jù)類名從表頭向表尾查找所屬的CRuntimeClass對(duì)象 CRuntimeClass* PASCAL CRuntimeClass::Load() { char szClassXXX[64]; CRuntimeClass* pClass; cin>>szClassXXX;????? //假定這是我們希望動(dòng)態(tài)創(chuàng)建的類名 for(pClass=pFirstClass;pClass!=NULL;pClass=pClass->m_pNextClass) { ??????? if(strcmp(szClassXXX,pClass->m_lpszClassName)==0) ??????? return pClass; } ??????? return NULL } ///根據(jù)CRuntimeClass創(chuàng)建對(duì)象/// CObject* CRuntimeClass::CreateObject() { ??????? if(m_pfnCreateObject==NULL) return NULL; ??????? CObject *pObject; pObject=(* m_pfnCreateObject)();??????? //函數(shù)指針調(diào)用 ??????? return pObject;?????????????????????????????????? } 有了上面兩個(gè)函數(shù),我們?cè)诔绦驁?zhí)行的時(shí)候調(diào)用,就可以動(dòng)態(tài)創(chuàng)建對(duì)象了。 我們還可以更簡(jiǎn)單地實(shí)現(xiàn)動(dòng)態(tài)創(chuàng)建,大家注意到,就是在我們的程序類里面有一個(gè)RUNTIME_CLASS(class_name)宏,這個(gè)宏在MFC里定義為: RUNTIME_CLASS(class_name)?((CRuntimeClass*)(&class_name::class##class_name)) 作用就是得到類的RunTime信息,即返回class_name所屬CRuntimeClass的對(duì)象。在我們的應(yīng)用程序員類(CMyWinApp)的InitInstance()函數(shù)下面的CSingleDocTemplate函數(shù)中,有: RUNTIME_CLASS(CMyDoc), ??????? RUNTIME_CLASS(CMainFrame),?????? // main SDI frame window ??????? RUNTIME_CLASS(CMyView) 構(gòu)造文檔模板的時(shí)候就用這個(gè)宏得到文檔、框架和視的RunTime信息。有了RunTime信息,我們只要一條語(yǔ)句就可以動(dòng)態(tài)創(chuàng)建了,如: classMyView->CreateObject();????? //對(duì)象直接調(diào)用用CRuntimeClass本身的CreateObject() 現(xiàn)在,細(xì)心的朋友已經(jīng)能清楚動(dòng)態(tài)創(chuàng)建需要的步驟: 1、定義一個(gè)不帶參數(shù)的構(gòu)造函數(shù)(默認(rèn)構(gòu)造函數(shù));因?yàn)槲覀兪怯?/span>CreateObject()動(dòng)態(tài)創(chuàng)建,它只有一條語(yǔ)句就是return new XXX,不帶任何參數(shù)。所以我們要有一個(gè)無(wú)參構(gòu)造函數(shù)。 2、類說(shuō)明中使用DECLARE_DYNCREATE(CLASSNMAE)宏;和在類的實(shí)現(xiàn)文件中使用IMPLEMENT_DYNCREATE(CLASSNAME,BASECLASS)宏;這個(gè)宏完成構(gòu)造CRuntimeClass對(duì)象,并加入到鏈表中。 3、使用時(shí)先通過(guò)宏RUNTIME_CLASS得到類的RunTime信息,然后使用CRuntimeClass的成員函數(shù)CreateObject創(chuàng)建一個(gè)該類的實(shí)例。 4、CObject* pObject = pRuntimeClass->CreateObject();//完成動(dòng)態(tài)創(chuàng)建。 MFC六大關(guān)鍵技術(shù)之永久保存(串行化) 先用一句話來(lái)說(shuō)明永久保存的重要:弄懂它以后,你就越來(lái)越像個(gè)程序員了!
如果我們的程序不需要永久保存,那幾乎可以肯定是一個(gè)小玩兒。那怕我們的記事本、畫(huà)圖等小程序,也需要保存才有真正的意義。
對(duì)于MFC的很多地方我不甚滿意,總覺(jué)得它喜歡拿一組低能而神秘的宏來(lái)故弄玄虛,但對(duì)于它的連續(xù)存儲(chǔ)(serialize)機(jī)制,卻是我十分鐘愛(ài)的地方。在此,可讓大家感受到面向?qū)ο蟮男腋!?/span>
MFC的連續(xù)存儲(chǔ)(serialize)機(jī)制俗稱串行化。“在你的程序中盡管有著各種各樣的數(shù)據(jù),serialize機(jī)制會(huì)象流水一樣按順序存儲(chǔ)到單一的文件中,而又能按順序地取出,變成各種不同的對(duì)象數(shù)據(jù)。”不知我在說(shuō)上面這一句話的時(shí)候,大家有什么反應(yīng),可能很多朋友直覺(jué)是一件很簡(jiǎn)單的事情,只是說(shuō)了一個(gè)“爽”字就沒(méi)有下文了。
要實(shí)現(xiàn)象流水一樣存儲(chǔ)其實(shí)是一個(gè)很大的難題。試想,在我們的程序里有各式各樣的對(duì)象數(shù)據(jù)。如畫(huà)圖程序中,里面設(shè)計(jì)了點(diǎn)類,矩形類,圓形類等等,它們的繪圖方式及對(duì)數(shù)據(jù)的處理各不相同,用它們實(shí)現(xiàn)了成百上千的對(duì)象之后,如何存儲(chǔ)起來(lái)?不想由可,一想頭都大了:我們要在程序中設(shè)計(jì)函數(shù)store(),在我們單擊“文件/保存”時(shí)能把各對(duì)象往里存儲(chǔ)。那么這個(gè)store()函數(shù)要神通廣大,它能清楚地知道我們?cè)O(shè)計(jì)的是什么樣的類,產(chǎn)生什么樣的對(duì)象。大家可能并不覺(jué)得這是一件很困難的事情,程序有能力知道我們的類的樣子,對(duì)象也不過(guò)是一塊初始化了存儲(chǔ)區(qū)域罷了。就把一大堆對(duì)象“轉(zhuǎn)換”成磁盤文件就行了。
即使上面的存儲(chǔ)能成立,但當(dāng)我們單擊“文件/打開(kāi)”時(shí),程序當(dāng)然不能預(yù)測(cè)用戶想打開(kāi)哪個(gè)文件,并且當(dāng)打開(kāi)文件的時(shí)候,要根據(jù)你那一大堆垃圾數(shù)據(jù)new出數(shù)百個(gè)對(duì)象,還原為你原來(lái)存儲(chǔ)時(shí)的樣子,你又該怎么做呢?
試想,要是我們有一個(gè)能容納各種不同對(duì)象的容器,這樣,用戶用我們的應(yīng)用程序打開(kāi)一個(gè)磁盤文件時(shí),就可以把文件的內(nèi)容讀進(jìn)我們程序的容器中。把磁盤文件讀進(jìn)內(nèi)存,然后識(shí)別它“是什么對(duì)象”是一件很難的事情。首先,保存過(guò)程不像電影的膠片,把景物直接映射進(jìn)去,然后,看一下膠片就知道那是什么內(nèi)容。可能有朋友說(shuō)它象錄像磁帶,拿著錄像帶我們看不出里面變化的磁場(chǎng)信號(hào),但經(jīng)過(guò)錄像機(jī)就能把它還原出來(lái)。
其實(shí)不是這樣的,比如保存一個(gè)矩形,程序并不是把矩形本身按點(diǎn)陣存儲(chǔ)到磁盤中,因?yàn)槲覀兝L制矩形的整個(gè)過(guò)程只不過(guò)是調(diào)用一個(gè)GDI函數(shù)罷了。它保存只是坐標(biāo)值、線寬和某些標(biāo)記等。程序面對(duì)“00 FF”這樣的東西,當(dāng)然不知道它是一個(gè)圓或是一個(gè)字符!
拿剛才錄像帶的例子,我們之所以能最后放映出來(lái),前提我們知道這對(duì)象是“錄像帶”,即確定了它是什么類對(duì)象。如果我們事先只知道它“里面保存有東西,但不知道它是什么類型的東西”,這就導(dǎo)致我們無(wú)法把它讀出來(lái)。拿錄像帶到錄音機(jī)去放,對(duì)錄音機(jī)來(lái)說(shuō),那完全是垃圾數(shù)據(jù)。即是說(shuō),要了解永久保存,要對(duì)動(dòng)態(tài)創(chuàng)建有深刻的認(rèn)識(shí)。
現(xiàn)在大家可以知道困難的根源了吧。我們?cè)趯懗绦虻臅r(shí)候,會(huì)不斷創(chuàng)造新的類,構(gòu)造新的對(duì)象。這些對(duì)象,當(dāng)然是舊的類對(duì)象(如MyDocument)從未見(jiàn)過(guò)的。那么,我們?nèi)绾尾拍苁刮臋n對(duì)象可以保存自己新對(duì)象呢,又能動(dòng)態(tài)創(chuàng)建自己新的類對(duì)象呢?
許多朋友在這個(gè)時(shí)候想起了CObject這個(gè)類,也想到了虛函數(shù)的概念。于是以為自己“大致了解”串行化的概念。他們?cè)O(shè)想:“我們?cè)O(shè)計(jì)的MyClass(我們想用于串行化的對(duì)象)全部從CObject類派生,CObject類對(duì)象當(dāng)然是MyDocument能認(rèn)識(shí)的。”這樣就實(shí)現(xiàn)了一個(gè)目的:本來(lái)MyDocument不能識(shí)別我們創(chuàng)建的MyClass對(duì)象,但它能識(shí)別CObject類對(duì)象。由于MyClass從CObject類派生,我產(chǎn)的新類對(duì)象“是一個(gè)CObject”,所以MyDocument能把我們的新對(duì)象當(dāng)作CObiect對(duì)象讀出。或者根據(jù)書(shū)本上所說(shuō)的:打開(kāi)或保存文件的時(shí)候,MyDocument會(huì)調(diào)用Serialize(),MyDocument的Serialize()函會(huì)呼叫我們創(chuàng)建類的Serialize函數(shù)[即是在MyDocument Serialize()中調(diào)用:m_pObject->Serialize(),注意:在此m_pObject是CObject類指針,它可以指向我們?cè)O(shè)計(jì)的類對(duì)象]。最終結(jié)果是MyDocument的讀出和保存變成了我們創(chuàng)建的類對(duì)象的讀出和保存,這種認(rèn)識(shí)是不明朗的。
有意思還有,在網(wǎng)上我遇到幾位自以為懂了Serialize的朋友,居然不約而同的犯了一個(gè)很低級(jí)得讓人不可思議的錯(cuò)誤。他們說(shuō):Serialize太簡(jiǎn)單了!Serialize()是一個(gè)虛函數(shù),虛函數(shù)的作用就是“優(yōu)先派生類的操作”。所以MyDocument不實(shí)現(xiàn)Serialize()函數(shù),留給我們自己的MyClass對(duì)象去調(diào)用Serialize()……真是哭笑不得,我們創(chuàng)建的類MyClass并不是由MyDocument類派生,Serialize()函數(shù)為虛在MyDocument和MyClass之間沒(méi)有任何意義。MyClass產(chǎn)生的MyObject對(duì)象僅僅是MyDocument的一個(gè)成員變量罷了。
話說(shuō)回來(lái),由于MyClass從CObject派生,所以CObject類型指針能指向MyClass對(duì)象,并且能夠讓MyClass對(duì)象執(zhí)行某些函數(shù)(特指重載的CObject虛函數(shù)),但前提必須在MyClass對(duì)象實(shí)例化了,即在內(nèi)存中占領(lǐng)了一塊存儲(chǔ)區(qū)域之后。不過(guò),我們的問(wèn)題恰恰就是在應(yīng)用程序隨便打開(kāi)一個(gè)文件,面對(duì)的是它不認(rèn)識(shí)的MyClass類,當(dāng)然實(shí)例化不了對(duì)象。
幸好我們?cè)谏弦还?jié)課中懂得了動(dòng)態(tài)創(chuàng)建。即想要從CObject派生的MyClass成為可以動(dòng)態(tài)創(chuàng)建的對(duì)象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏就可以了(注意:最終可以Serialize的對(duì)象僅僅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL宏,這是因?yàn)?/span>DECLARE_SERIAL/IMPLEMENT_SERIAL包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏)。
從解決上面的問(wèn)題中,我們可以分步理解了:
1、 Serialize的目的:讓MyDocument對(duì)象在執(zhí)行打開(kāi)/保存操作時(shí),能讀出(構(gòu)造)和保存它不認(rèn)的MyClass類對(duì)象。
2、 MyDocument對(duì)象在執(zhí)行打開(kāi)/保存操作時(shí)會(huì)調(diào)用它本身的Serialize()函數(shù)。但不要指望它會(huì)自動(dòng)保存和讀出我們的MyClass類對(duì)象。這個(gè)問(wèn)題很容易解決,就直接在MyDocument:: Serialize(){
// 在此函數(shù)調(diào)用MyClass類的Serialize()就行了!即
MyObject. Serialize();
}
3、我們希望MyClass對(duì)象為可以動(dòng)態(tài)創(chuàng)建的對(duì)象,所以要求在MyClass類中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。
但目前的Serialize機(jī)制還很抽象。我們僅僅知道了表面上的東西,實(shí)際又是如何的呢?下面作一個(gè)簡(jiǎn)單深刻的詳解。
先看一下我們文檔類的Serialize()
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
目前這個(gè)子數(shù)什么也沒(méi)做(沒(méi)有數(shù)據(jù)的讀出和寫入),CMyDoc類正等待著我們?nèi)ジ膶戇@個(gè)函數(shù)。現(xiàn)在假設(shè)CMyDoc有一個(gè)MFC可識(shí)別的成員變量m_MyVar,那么函數(shù)就可改寫成如下形式:
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring()) //讀寫判斷
{
ar<<m_MyVar; //寫
}
else
{
ar>>m_MyVar; //讀
}
}
許多網(wǎng)友問(wèn):自己寫的類(即MFC未包含的類)為什么不行?我們?cè)?/span>CMyDoc里包含自寫類的頭文件MyClass.h,這樣CMyDoc就認(rèn)識(shí)MyDoc類對(duì)象了。這是一般常識(shí)性的錯(cuò)誤,MyDoc類認(rèn)識(shí)MyClass類對(duì)象與否并沒(méi)有用,關(guān)鍵是CArchive類,即對(duì)象ar不認(rèn)識(shí)MyClass(當(dāng)然你夢(mèng)想重寫CArchive類當(dāng)別論)。“>>”、“<<”都是CArchive重載的操作符。上面ar>>m_MyVar說(shuō)白即是在執(zhí)行一個(gè)以ar和m_MyVar為參數(shù)的函數(shù),類似于function(ar,m_MyVar)罷了。我們當(dāng)然不能傳遞一個(gè)它不認(rèn)識(shí)的參數(shù)類型,也因此不會(huì)執(zhí)行function(ar,m_MyObject)了。
[注:這里我們可以用指針。讓MyClass從Cobject派生,一切又起了質(zhì)的變化,假設(shè)我們定義了:MyClass *pMyClass = new MyClass;因?yàn)?/span>MyClass從CObject派生,根據(jù)虛函數(shù)原理,pMyClass也是一個(gè)CObject*,即pMyClass指針是CArchive類可認(rèn)識(shí)的。所以執(zhí)行上述function(ar, pMyClass),即ar << pMyClass是沒(méi)有太多的問(wèn)題(在保證了MyClass對(duì)象可以動(dòng)態(tài)創(chuàng)建的前提下)。]
回過(guò)頭來(lái),如果想讓MyClass類對(duì)象能Serialize,就得讓MyClass從CObject派生,Serialize()函數(shù)在CObject里為虛,MyClass從CObject派生之后就可以根據(jù)自己的要求去改寫它,象上面改寫CMyDoc::Serialize()方法一樣。這樣MyClass就得到了屬于MyClass自己特有的Serialize()函數(shù)。
現(xiàn)在,程序就可以這樣寫:
……
#include “MyClass.h”
……
void CMyDoc::Serialize(CArchive& ar)
{
//在此調(diào)用MyClass重寫過(guò)的Serialize()
m_MyObject. Serialize(ar); // m_MyObject為MyClass實(shí)例
}
至此,串行化工作就算完成了,一即簡(jiǎn)單直觀:從CObject派生自己的類,重寫Serialize()。在此過(guò)程中,我刻意安排:在沒(méi)有用到DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也沒(méi)有用到CArray等模板類的前提下就完成了串行化的工作。我看過(guò)某些書(shū),總是一開(kāi)始就講DECLARE_SERIAL/IMPLEMENT_SERIAL宏或馬上用CArray模板,讓讀者覺(jué)得串行化就是這兩個(gè)東西,導(dǎo)致許多朋友因此找不著北。
大家看到了,沒(méi)有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等數(shù)據(jù)結(jié)構(gòu)模板也依然可以完成串行化工作。
現(xiàn)在可以騰出時(shí)間講一下大家覺(jué)得十分抽象的CArchive。我們先看以下程序(注:以下程序包含動(dòng)態(tài)創(chuàng)建等,請(qǐng)包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)
void MyClass::Serialize(CArchive& ar)
{
if (ar.IsStoring()) //讀寫判斷
{
ar<< m_pMyVar; //問(wèn)題:ar如何把m_pMyVar所指的對(duì)象變量保存到磁盤?
}
else
{
pMyClass = new MyClass; //準(zhǔn)備存儲(chǔ)空間
ar>> m_pMyVar;
}
}
要回答上面的問(wèn)題,即“ar<<XXX”的問(wèn)題。和我們得看一下模擬CArchive的代碼。
“ar<<XXX”是執(zhí)行CArchive對(duì)運(yùn)算符“<<”的重載動(dòng)作。ar和XXX都是該重載函數(shù)中的一參數(shù)而已。函數(shù)大致如下:
CArchive& operator<<( CArchive& ar, const CObject* pOb)
{
…………
//以下為CRuntimeClass鏈表中找到、識(shí)別pOb資料。
CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
//保存pClassRef即類信息(略)
((CObject*)pOb)->Serialize();//保存MyClass數(shù)據(jù)
…………
}
從上面可以看出,因?yàn)?/span>Serialize()為虛函數(shù),即“ar<<XXX”的結(jié)果是執(zhí)行了XXX所指向?qū)ο蟊旧淼?/span>Serialize()。對(duì)于“ar>>XXX”,雖然不是“ar<<XXX”逆過(guò)程,大家可能根據(jù)動(dòng)態(tài)創(chuàng)建和虛函數(shù)的原理料想到它。
至此,永久保存算是寫完了。在此過(guò)程中,我一直努力用最少的代碼,詳盡的解釋來(lái)說(shuō)明問(wèn)題。以前我為本課題寫過(guò)一個(gè)版本,并在幾個(gè)論壇上發(fā)表過(guò),但不知怎么在網(wǎng)上遺失(可能被刪除)。所以這篇文章是我重寫的版本。記得第一個(gè)版本中,我是對(duì)DECLARE_SERIAL/IMPLEMENT_SERIAL和可串行化的數(shù)組及鏈表對(duì)象說(shuō)了許多。這個(gè)版本中我對(duì)DECLARE_SERIAL/IMPLEMENT_SERIAL其中奧秘幾乎一句不提,目的是讓大家能找到中心,有更簡(jiǎn)潔的永久保存的概念,我覺(jué)得這種感覺(jué)很好! ? MFC六大關(guān)鍵技術(shù)之消息映射與命令傳遞
???????? 題外話:剛開(kāi)始學(xué)視窗程序設(shè)計(jì)的時(shí)候,我就打印了一本W(wǎng)indows消息詳解,里面列舉了各種已定義消息的意義和作用,共10多頁(yè),在編程的時(shí)候翻翻,有時(shí)覺(jué)得很受用。我發(fā)覺(jué)很多編程的朋友,雖然每天都面對(duì)消息,卻很少關(guān)注它。C++程序員有一個(gè)通病,很想寫“自己”的程序,即每一行代碼都想自己寫出來(lái)。如果用了一些庫(kù),總希望能完全理解庫(kù)里的類或函數(shù)是怎么一回事,否則就“不踏實(shí)”。對(duì)于消息,許多朋友只關(guān)心常用的幾個(gè),對(duì)其余的漠不關(guān)心。其實(shí),Windows中有很多不常用的消息卻很有用,程序員可能通過(guò)響應(yīng)這些消息實(shí)現(xiàn)更簡(jiǎn)捷的編程。
說(shuō)到消息,在MFC中,“最熟悉的神秘”可算是消息映射,那是我們剛開(kāi)始接觸MFC時(shí)就要面對(duì)的東西。有過(guò)SDK編程經(jīng)驗(yàn)的朋友轉(zhuǎn)到MFC編程的時(shí)候,一下子覺(jué)得什么都變了樣。特別是窗口消息及對(duì)消息的處理跟以前相比,更是風(fēng)馬牛不相及的。如文檔不是窗口,是怎樣響應(yīng)命令消息的呢?
初次用MFC編程,我們只會(huì)用MFC ClassWizard為我們做大量的東西,最主要的是添加消息響應(yīng)。記憶中,如果是自已添加消息響應(yīng),我們應(yīng)何等的小心翼翼,對(duì)BEGIN_MESSAGE_MAP()……END_MESSAGE_MAP()更要奉若神靈。它就是一個(gè)魔盒子,把我們的咒語(yǔ)放入恰當(dāng)?shù)牡胤?#xff0c;就會(huì)發(fā)生神奇的力量,放錯(cuò)了,自己的程序就連“命”都沒(méi)有。
據(jù)說(shuō),知道得太多未必是好事。我也曾經(jīng)打算不去理解這神秘的區(qū)域,覺(jué)得編程的時(shí)候知道自己想做什么就行了。MFC外表上給我們提供了東西,直觀地說(shuō),不但給了我個(gè)一個(gè)程序的外殼,更給我們?cè)S多方便。微軟的出發(fā)點(diǎn)可能是希望達(dá)到“傻瓜編程”的結(jié)果,試想,誰(shuí)不會(huì)用ClassWizard?大家知道,Windows是基于消息的,有了ClassWizard,你又會(huì)添加類,又會(huì)添加消息,那么你所學(xué)的東西似乎學(xué)到頭了。于是許多程序員認(rèn)為“我們沒(méi)有必要走SDK的老路,直接用MFC編程,新的東西通常是簡(jiǎn)單、直觀、易學(xué)……”
到你真正想用MFC編程的時(shí)候,你會(huì)發(fā)覺(jué)光會(huì)ClassWizard的你是多么的愚蠢。MFC不是一個(gè)普通的類庫(kù),普通的類庫(kù)我們完全可以不理解里面的細(xì)節(jié),只要知道這些類庫(kù)能干什么,接口參數(shù)如何就萬(wàn)事大吉。如string類,操作順序是定義一個(gè)string對(duì)象,然后修改屬性,調(diào)用方法。
但對(duì)于MFC,你并不是在你的程序中寫上一句“#include MFC.h”,然后就在你的程序中用MFC類庫(kù)。
MFC是一塊包著糖衣的牛骨頭。你很輕松地寫出一個(gè)單文檔窗口,在窗口中間打印一句“I love MFC!”,然后,惡夢(mèng)開(kāi)始了……想逃避,打算永遠(yuǎn)不去理解MFC內(nèi)幕?門都沒(méi)有!在MFC這個(gè)黑暗神秘的洞中,即使你打算摸著石頭前行,也注定找不到出口。對(duì)著MFC這塊牛骨頭,微軟溫和、民主地告訴你“你當(dāng)然可以選擇不啃掉它,咳咳……但你必然會(huì)因此而餓死!”
消息映射與命令傳遞體現(xiàn)了MFC與SDK的不同。在SDK編程中,沒(méi)有消息映射的概念,它有明確的回調(diào)函數(shù)中,通過(guò)一個(gè)switch語(yǔ)句去判斷收到了何種消息,然后對(duì)這個(gè)消息進(jìn)行處理。所以,在SDK編程中,會(huì)發(fā)送消息和在回調(diào)函數(shù)中處理消息就差不多可以寫SDK程序了。
在MFC中,看上去發(fā)送消息和處理消息比SDK更簡(jiǎn)單、直接,但可惜不直觀。舉個(gè)簡(jiǎn)單的例子,如果我們想自定義一個(gè)消息,SDK是非常簡(jiǎn)單直觀的,用一條語(yǔ)句:SendMessage(hwnd,message/*一個(gè)大于或等于WM_USER的數(shù)字*/,wparam,lparam),之后就可以在回調(diào)函數(shù)中處理了。但MFC就不同了,因?yàn)槟阃ǔ2恢苯尤ジ膶懘翱诘幕卣{(diào)函數(shù),所以只能亦步亦趨對(duì)照原來(lái)的MFC代碼,把消息放到恰當(dāng)?shù)牡胤健_@確實(shí)是一樣很痛苦的勞動(dòng)。
要了解MFC消息映射原理并不是一件輕松的事情。我們可以逆向思維,想象一下消息映射為我們做了什么工作。MFC在自動(dòng)化給我們提供了很大的方便,比如,所有的MFC窗口都使用同一窗口過(guò)程,即所有的MFC窗口都有一個(gè)默認(rèn)的窗口過(guò)程。不象在SDK編程中,要為每個(gè)窗口類寫一個(gè)窗口過(guò)程。
對(duì)于消息映射,最直截了當(dāng)?shù)夭孪胧?#xff1a;消息映射就是用一個(gè)數(shù)據(jù)結(jié)構(gòu)把“消息”與“響應(yīng)消息函數(shù)名”串聯(lián)起來(lái)。這樣,當(dāng)窗口感知消息發(fā)生時(shí),就對(duì)結(jié)構(gòu)查找,找到相應(yīng)的消息響應(yīng)函數(shù)執(zhí)行。其實(shí)這個(gè)想法也不能簡(jiǎn)單地實(shí)現(xiàn):我們每個(gè)不同的MFC窗口類,對(duì)同一種消息,有不同的響應(yīng)方式。即是說(shuō),對(duì)同一種消息,不同的MFC窗口會(huì)有不同的消息響應(yīng)函數(shù)。
這時(shí),大家又想了一個(gè)可行的方法。我們?cè)O(shè)計(jì)窗口基類(CWnd)時(shí),我們讓它對(duì)每種不同的消息都來(lái)一個(gè)消息響應(yīng),并把這個(gè)消息響應(yīng)函數(shù)定義為空虛函數(shù)。這樣,從CWnd派生的窗口類對(duì)所有消息都有了一個(gè)空響應(yīng),我們要響應(yīng)一個(gè)特定的消息就重載這個(gè)消息響應(yīng)函數(shù)就可以了。但這樣做的結(jié)果,一個(gè)幾乎什么也不做的CWnd類要有幾百個(gè)“多余”的函數(shù),那怕這些消息響應(yīng)函數(shù)都為純虛函數(shù),每個(gè)CWnd對(duì)象也要背負(fù)著一個(gè)巨大的虛擬表,這也是得不償失的。
許多朋友在學(xué)習(xí)消息映射時(shí)苦無(wú)突破,其原因是一開(kāi)始就認(rèn)為MFC的消息映射的目的是為了替代SDK窗口過(guò)程的編寫——這本來(lái)沒(méi)有理解錯(cuò)。但他們還有多一層的理解,認(rèn)為既然是替代“舊”的東西,那么MFC消息映身應(yīng)該是更高層次的抽象、更簡(jiǎn)單、更容易認(rèn)識(shí)。但結(jié)果是,如果我們不通過(guò)ClassWizard工具,手動(dòng)添加消息是相當(dāng)迷茫的一件事。
所以,我們?cè)趯W(xué)習(xí)MFC消息映射時(shí),首先要弄清楚:消息映射的目的,不是為是更加快捷地向窗口過(guò)程添加代碼,而是一種機(jī)制的改變。如果不想改變窗口過(guò)程函數(shù),那么應(yīng)該在哪里進(jìn)行消息響應(yīng)呢?許多朋友一知半解地認(rèn)為:我們可以用HOOK技術(shù),搶在消息隊(duì)列前把消息抓取,把消息響應(yīng)提到窗口過(guò)程的外面。再者,不同的窗口,會(huì)有不同的感興趣的消息,所以每個(gè)MFC窗口都應(yīng)該有一個(gè)表把感興趣的消息和相應(yīng)消息響應(yīng)函數(shù)連系起來(lái)。然后得出——消息映射機(jī)制執(zhí)行步驟是:當(dāng)消息發(fā)生,我們用HOOK技術(shù)把本發(fā)送到窗口過(guò)程的消息抓獲,然后對(duì)照一下MFC窗口的消息映射表,如果是表里面有的消息,就執(zhí)行其對(duì)應(yīng)的函數(shù)。
當(dāng)然,用HOOK技術(shù),我們理論上可以在不改變窗口過(guò)程函數(shù)的情況下,可以完成消息響應(yīng)。MFC確實(shí)是這樣做,但實(shí)際操作起來(lái)可能跟你的想象差別很大。
現(xiàn)在我們來(lái)編寫消息映射表,我們先定義一個(gè)結(jié)構(gòu),這個(gè)結(jié)構(gòu)至少有兩個(gè)項(xiàng):一是消息ID,二是響應(yīng)該消息的函數(shù)。如下:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //感興趣的消息
AFX_PMSG pfn; //響應(yīng)以上消息的函數(shù)指針
}
當(dāng)然,只有兩個(gè)成員的結(jié)構(gòu)連接起來(lái)的消息映射表是不成熟的。Windows消息分為標(biāo)準(zhǔn)消息、控件消息和命令消息,每類型的消息包含數(shù)百不同ID、不同意義、不同參數(shù)的消息。我們要準(zhǔn)確地判別發(fā)生了何種消息,必須再增加幾個(gè)成員。還有,對(duì)于AFX_PMSG pfn,實(shí)際上等于作以下聲明:
void (CCmdTarget::*pfn)();
(提示:AFX_PMSG為類型標(biāo)識(shí),具體聲明是:typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);)
pfn是不一不帶參數(shù)和返回值的CCmdTarget類型函數(shù)指針,只能指向CCmdTarget類中不帶參數(shù)和返回值的成員函數(shù),這樣pfn更為通用,但我們響應(yīng)消息的函數(shù)許多需要傳入?yún)?shù)的。為了解決這個(gè)矛盾,我們還要增加一個(gè)表示參數(shù)類型的成員。當(dāng)然,還有其它……
最后,MFC我們消息映射表成員結(jié)構(gòu)如下定義:
struct AFX_MSGMAP_ENTRY
{
UINT nMessage; //Windows 消息ID
UINT nCode; // 控制消息的通知碼
UINT nID; //命令消息ID范圍的起始值
UINT nLastID; //命令消息ID范圍的終點(diǎn)
UINT nSig; // 消息的動(dòng)作標(biāo)識(shí)
AFX_PMSG pfn;
};
有了以上消息映射表成員結(jié)構(gòu),我們就可以定義一個(gè)AFX_MSGMAP_ENTRY類型的數(shù)組,用來(lái)容納消息映射項(xiàng)。定義如下:
AFX_MSGMAP_ENTRY _messageEntries[];
但這樣還不夠,每個(gè)AFX_MSGMAP_ENTRY數(shù)組,只能保存著當(dāng)前類感興趣的消息,而這僅僅是我們想處理的消息中的一部分。對(duì)于一個(gè)MFC程序,一般有多個(gè)窗口類,里面都應(yīng)該有一個(gè)AFX_MSGMAP_ENTRY數(shù)組。我們知道,MFC還有一個(gè)消息傳遞機(jī)制,可以把自己不處理的消息傳送給別的類進(jìn)行處理。為了能查找各下MFC對(duì)象的消息映射表,我們還要增加一個(gè)結(jié)構(gòu),把所有的AFX_MSGMAP_ENTRY數(shù)組串聯(lián)起來(lái)。
于是,我們定義了一個(gè)新結(jié)構(gòu)體:
struct AFX_MSGMAP
{
const AFX_MSGMAP* pBaseMap; //指向別的類的AFX_MSGMAP對(duì)象
const AFX_MSGMAP_ENTRY* lpEntries; //指向自身的消息表
};
之后,在每個(gè)打算響應(yīng)消息的類中這樣聲明一個(gè)變量:AFX_MSGMAP messageMap,讓其中的pBaseMap指向基類或另一個(gè)類的messageMap,那么將得到一個(gè)AFX_MSGMAP元素的單向鏈表。這樣,所有的消息映射信息形成了一張消息網(wǎng)。
當(dāng)然,僅有消息映射表還不夠,它只能把各個(gè)MFC對(duì)象的消息、參數(shù)與相應(yīng)的消息響應(yīng)函數(shù)連成一張網(wǎng)。為了方便查找,MFC在上面的類中插入了兩個(gè)函數(shù)(其中theClass代表當(dāng)前類):
一個(gè)是_GetBaseMessageMap(),用來(lái)得到基類消息映射的函數(shù)。函數(shù)原型如下:
const AFX_MSGMAP* PASCAL theClass::_GetBaseMessageMap() /
{ return &baseClass::messageMap; } /
別一個(gè)是GetMessageMap() ,用來(lái)得到自身消息映射的函數(shù)。函數(shù)原型如下:
const AFX_MSGMAP* theClass::GetMessageMap() const /
{ return &theClass::messageMap; } /
有了消息映射表之后,我們得討論到問(wèn)題的關(guān)鍵,那就是消息發(fā)生以后,其對(duì)應(yīng)的響應(yīng)函數(shù)如何被調(diào)用。大家知道,所有的MFC窗口,都有一個(gè)同樣的窗口過(guò)程——AfxWndProc(…)。在這里順便要提一下的是,看過(guò)MFC源代碼的朋友都得,從AfxWndProc函數(shù)進(jìn)去,會(huì)遇到一大堆曲折與迷團(tuán),因?yàn)閷?duì)于這個(gè)龐大的消息映射機(jī)制,MFC要做的事情很多,如優(yōu)化消息,增強(qiáng)兼容性等,這一大量的工作,有些甚至用匯編語(yǔ)言來(lái)完成,對(duì)此,我們很難深究它。所以我們要省略大量代碼,理性地分析它。
對(duì)已定型的AfxWndProc來(lái)說(shuō),對(duì)所有消息,最多只能提供一種默認(rèn)的處理方式。這當(dāng)然不是我們想要的。我們想通過(guò)AfxWndProc最終執(zhí)行消息映射網(wǎng)中對(duì)應(yīng)的函數(shù)。那么,這個(gè)執(zhí)行路線是怎么樣的呢?
從AfxWndProc下去,最終會(huì)調(diào)用到一個(gè)函數(shù)OnWndMsg。請(qǐng)看代碼:
LRESULT CALLBACK AfxWndProc(HWND hWnd,UINT nMsg,WPARAM wParam, LPARAM lParam)
{
……
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); //把對(duì)句柄的操作轉(zhuǎn)換成對(duì)CWnd對(duì)象。
Return AfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
}
把對(duì)句柄的操作轉(zhuǎn)換成對(duì)CWnd對(duì)象是很重要的一件事,因?yàn)锳fxWndProc只是一個(gè)全局函數(shù),當(dāng)然不知怎么樣去處理各種windows窗口消息,所以它聰明地把處理權(quán)交給windows窗口所關(guān)聯(lián)的MFC窗口對(duì)象。
現(xiàn)大,大家?guī)缀蹩梢韵胂蟮玫紸fxCallWndProc要做的事情,不錯(cuò),它當(dāng)中有一句:
pWnd->WindowProc(nMsg,wParam,lParam);
到此,MFC窗口過(guò)程函數(shù)變成了自己的一個(gè)成員函數(shù)。WindowProc是一個(gè)虛函數(shù),我們甚至可以通過(guò)改寫這個(gè)函數(shù)去響應(yīng)不同的消息,當(dāng)然,這是題外話。
WindowProc會(huì)調(diào)用到CWnd對(duì)象的另一個(gè)成員函數(shù)OnWndMsg,下面看看大概的函數(shù)原形是怎么樣的:
BOOL CWnd::OnWndMsg(UINT message,WPARAM wParam,LPARAM lParam,LRESULT* pResult)
{
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
if(message==WM_NOTIFY)
{
OnCommand(wParam,lParam,&lResult);
……
}
const AFX_MSGMAP* pMessageMap; pMessageMap=GetMessageMap();
const AFX_MSGMAP_ENTRY* lpEntry;
/*以下代碼作用為:用AfxFindMessageEntry函數(shù)從消息入口pMessageMap處查找指定消息,如果找到,返回指定消息映射表成員的指針給lpEntry。然后執(zhí)行該結(jié)構(gòu)成員的pfn所指向的函數(shù)*/ if((lpEntry=AfxFindMessageEntry(pMessageMap->lpEntries,message,0,0)!=NULL)
{
lpEntry->pfn();/*注意:真正MFC代碼中沒(méi)有用這一條語(yǔ)句。上面提到,不同的消息參數(shù)代表不同的意義和不同的消息響應(yīng)函數(shù)有不同類型的返回值。而pfn是一個(gè)不帶參數(shù)的函數(shù)指針,所以真正的MFC代碼中,要根據(jù)對(duì)象lpEntry的消息的動(dòng)作標(biāo)識(shí)nSig給消息處理函數(shù)傳遞參數(shù)類型。這個(gè)過(guò)程包含很復(fù)雜的宏代換,大家在此知道:找到匹配消息,執(zhí)行相應(yīng)函數(shù)就行!*/
}
}
以上,大家看到了OnWndMsg能根據(jù)傳進(jìn)來(lái)的消息參數(shù),查找到匹配的消息和執(zhí)行相應(yīng)的消息響應(yīng)。但這還不夠,我們平常響應(yīng)菜單命令消息的時(shí)候,原本屬于框架窗口(CFrameWnd)的WM_COMMAND消息,卻可以放到視對(duì)象或文檔對(duì)象中去響應(yīng)。其原理如下:
我們看上面函數(shù)OnWndMsg原型中看到以下代碼:
if(message==WM_COMMAND)
{
OnCommand(wParam,lParam);
……
}
即對(duì)于命令消息,實(shí)際上是交給OnCommand函數(shù)處理。而OnCommand是一個(gè)虛函數(shù),即WM_COMMAND消息發(fā)生時(shí),最終是發(fā)生該消息所對(duì)應(yīng)的MFC對(duì)象去執(zhí)行OnCommand。比如點(diǎn)框架窗口菜單,即向CFrameWnd發(fā)送一個(gè)WM_COMMAND,將會(huì)導(dǎo)致CFrameWnd::OnCommand(wParam,lParam)的執(zhí)行。
且看該函數(shù)原型
BOOL CFrameWnd::OnCommand(WPARAM wParam,LPARAM lParam)
{
……
return CWnd:: OnCommand(wParam,lParam);
}
可以看出,它最后把該消息交給CWnd:: OnCommand處理。再看:
BOOL CWnd::OnCommand(WPARAM wParam,LPARAM lParam)
{
……
return OnCmdMsg(nID,nCode,NULL,NULL);
}
這里包含了一個(gè)C++多態(tài)性很經(jīng)典的問(wèn)題。在這里,雖然是執(zhí)行CWnd類的函數(shù),但由于這個(gè)函數(shù)在CFrameWnd:: OnCmdMsg里執(zhí)行,即當(dāng)前指針是CFrameWnd類指針,再有OnCmdMsg是一個(gè)虛函數(shù),所以如果CFrameWnd改寫了OnCommand,程序會(huì)執(zhí)行CFrameWnd::OnCmdMsg(…)。
對(duì)CFrameWnd::OnCmdMsg(…)函數(shù)原理扼要分析如下:
BOOL CFrameWnd:: OnCmdMsg(…)
{
CView pView = GetActiveView();//得到活動(dòng)視指針。
if(pView-> OnCmdMsg(…))
return TRUE; //如果CView類對(duì)象或其派生類對(duì)象已經(jīng)處理該消息,則返回。
……//否則,同理向下執(zhí)行,交給文檔、框架、及應(yīng)用程序執(zhí)行自身的OnCmdMsg。
}
到此,CFrameWnd:: OnCmdMsg完成了把WM_COMMAND消息傳遞到視對(duì)象、文檔對(duì)象及應(yīng)用程序?qū)ο髮?shí)現(xiàn)消息響應(yīng)。
寫了這么多,我們清楚MFC消息映射與命令傳遞的大致過(guò)程。現(xiàn)在,我們來(lái)看MFC“神秘代碼”,會(huì)發(fā)覺(jué)好看多了。
先看DECLARE_MESSAGE_MAP()宏,它在MFC中定義如下:
#define DECLARE_MESSAGE_MAP() /
private: /
static const AFX_MSGMAP_ENTRY _messageEntries[]; /
protected: /
static AFX_DATA const AFX_MSGMAP messageMap; /
virtual const AFX_MSGMAP* GetMessageMap() const; /
可以看出DECLARE_MESSAGE_MAP()定義了我們熟悉的兩個(gè)結(jié)構(gòu)和一個(gè)函數(shù),顯而易見(jiàn),這個(gè)宏為每個(gè)需要實(shí)現(xiàn)消息映射的類提供了相關(guān)變量和函數(shù)。
現(xiàn)在集中精力來(lái)看一下BEGIN_MESSAGE_MAP,END_MESSAGE_MAP和ON_COMMAND三個(gè)宏,它們?cè)贛FC中定義如下(其中ON_COMMAND與另外兩個(gè)宏并沒(méi)有定義在同一個(gè)文件中,把它放到一起是為了好看):
#define BEGIN_MESSAGE_MAP(theClass, baseClass) /
const AFX_MSGMAP* theClass::GetMessageMap() const /
{ return &theClass::messageMap; } /
AFX_COMDAT AFX_DATADEF const AFX_MSGMAP theClass::messageMap = /
{ &baseClass::messageMap, &theClass::_messageEntries[0] }; /
AFX_COMDAT const AFX_MSGMAP_ENTRY theClass::_messageEntries[] = /
{ /
#define ON_COMMAND(id, memberFxn) /
{ WM_COMMAND, CN_COMMAND, (WORD)id, (WORD)id, AfxSig_vv, (AFX_PMSG)&memberFxn },
#define END_MESSAGE_MAP() /
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } /
}; /
一下子看三個(gè)宏覺(jué)得有點(diǎn)復(fù)雜,但這僅僅是復(fù)雜,公式性的文字代換并不是很難。且看下面例子,假設(shè)我們框架中有一菜單項(xiàng)為“Test”,即定義了如下宏:
BEGIN_MESSAGE_MAP(CMainFrame, CFrameWnd)
ON_COMMAND(ID_TEST, OnTest)
END_MESSAGE_MAP()
那么宏展開(kāi)之后得到如下代碼:
const AFX_MSGMAP* CMainFrame::GetMessageMap() const
{ return &CMainFrame::messageMap; }
///以下填入消息表映射信息
const AFX_MSGMAP CMainFrame::messageMap =
{ &CFrameWnd::messageMap, &CMainFrame::_messageEntries[0] };
//下面填入保存著當(dāng)前類感興趣的消息,可填入多個(gè)AFX_MSGMAP_ENTRY對(duì)象
const AFX_MSGMAP_ENTRY CMainFrame::_messageEntries[] =
{
{ WM_COMMAND, CN_COMMAND, (WORD)ID_TEST, (WORD)ID_TEST, AfxSig_vv, (AFX_PMSG)&OnTest }, // 加入的ID_TEST消息參數(shù)
{0, 0, 0, 0, AfxSig_end, (AFX_PMSG)0 } //本類的消息映射的結(jié)束項(xiàng)
};
大家知道,要完成ID_TEST消息映射,還要定義和實(shí)現(xiàn)OnTest函數(shù)。在此即要在頭文件寫afx_msg void OnTest()并在源文件中實(shí)現(xiàn)它。根據(jù)以上所學(xué)的東西,我們知道了當(dāng)ID為ID_TEST的命令消息發(fā)生,最終會(huì)執(zhí)行到我們寫的OnTest函數(shù)。
??? 至此,MFC六大關(guān)鍵技術(shù)寫完了。其中寫得最難的是消息映射與命令傳遞,除了技術(shù)復(fù)雜之外,最難的是有許多避不開(kāi)的代碼。為了大家看得輕松一點(diǎn),我把那繁雜的宏放在文章最后,希望能給你閱讀帶來(lái)方便。
??? 其實(shí),較早前這就描述過(guò)MFC六大關(guān)鍵技術(shù),也在一些論壇上發(fā)表過(guò),但后三篇不知如何遺失了。許多朋友向我索稿,但苦于沒(méi)有備份,也不想枯燥地重寫,故推延至今,請(qǐng)大家見(jiàn)諒。
??? 還有,許多朋友說(shuō)(二)(三)寫得十分不明白,這也是我意料到的地方。我寫文章,很少回頭看一下,有時(shí)自己也不知道自己寫了什么。有許多朋友把修改后的版本給我,雖然修改了一點(diǎn)點(diǎn),甚至僅僅修改了一下錯(cuò)別字,但讀起來(lái)容易理解很多。對(duì)此,我內(nèi)心十分感激。如果有更多的時(shí)間,我會(huì)修改一下前面的文章,放到我的網(wǎng)站上去!
總結(jié)
- 上一篇: C++异常处理try throw cat
- 下一篇: 终极会话劫持工具SSClone