第一點:類別型錄網的搭建:數組
類別型錄網搭建的目的是爲了實現所謂的"執行期類型識別",也就是在程序運行的時候識別出某個對象是不是某個類的實例(基類也能夠)。這裏還不是很明白爲何須要實現"執行期類型識別",這種技巧具體被應用在哪裏。架構
例如在MFC中CView繼承於CWnd,那麼能夠進行這樣的判斷:框架
CView view;函數
bool result = view.IsKindOf(CWnd); // result == truethis
如上,經過調用IsKindOf函數,能夠判斷出view對象是一個CWnd類的實例。spa
MFC經過創建一張型錄網來實現這樣的功能。也就是把類登記到一個表中,傳入特定的參數以後在這張表上進行查找比對,從而實現"執行期類型識別"。指針
具體說來比對過程以下:code
爲了在不一樣的對象和類之間互相比對,確定類裏得有一個特殊的標識才行,MFC經過爲每一個類添加一個CRuntimeClass類型的靜態成員來做爲這個標識。對象
注意這裏是靜態成員,其緣由不言自明,若是是普通成員的話,不一樣的對象之間成員都不同,沒法實現比對。blog
每一個類都有了特殊標識以後,僅僅能進行一對一的比對,也就是說只能進行CView.IsKindOf(CView)這樣的操做,沒法判斷一個CView對象是否也是一個CWnd對象。
MFC實現這種功能的方法相似於鏈表的實現:鏈表中有一個指針專門指向它下一個成員的位置,遍歷時依靠這個指針來不斷指向下一個。
CRuntimeClass類包含一個指針叫m_pBaseClass,對於CView,它有一個CRuntimeClass成員(就是以前說的特殊標識),只要使得這個成員的m_pBaseClass指針指向CWnd的CRuntimeClass成員,那麼就創建起了相似鏈表的結構。
當判斷CView.IsKindOf(CWnd)時,首先判斷CView的CRuntimeClass成員和CWnd的CRuntimeClass成員是否是一致,發現不一致以後,在CView的CRuntimeClass成員中根據m_pBaseClass來獲得CView的父類CWnd的CRuntimeClass成員,以後再進行比對,發現是一致的,所以能夠判斷CView.IsKindOf(CWnd)爲真。
下面介紹MFC中對上述機制的具體實現方法:
1.爲每一個類添加特定標識CRuntimeClass成員:
使用DECLARE_DYNAMIC宏:
class CView : public CWnd
{
DECLARE_DYNAMIC(CView)
如上,使用了DECLARE_DYNAMIC宏以後,CView類中多了一個CRuntimeClass類型的靜態成員 classCView(名爲classXXXX,也就是在類名以前加一個class),也就是以前所說的具備比較功能的"特殊標識"
2.創建類別型錄表:
也就是初始化classCView,使它的m_pBaseClass指針指向父類
IMPLEMENT_DYNCREATE(CView, CWnd)
如上,靜態成員的初始化須要在實現文件中進行,在實現文件中使用了IMPLEMENT_DYNAMIC宏以後,classCView的m_pBaseClass指針指向了CWnd的classCWnd成員
3.實現類型識別IsKindOf:
this->IsKindOf(RUNTIME_CLASS(CWnd))
如上,IsKindOf函數的參數有點特別,是一個RUNTIME_CLASS宏,這個宏的功能其實很是簡單,其實就是一個函數調用:RUNTIME_CLASS(CWnd)等價於CWnd::GetThisClass(),這個函數的返回值就是CWnd的CRuntimeClass成員,也就是CWnd的"特殊標識",把這個特殊標識傳遞給IsKindOf函數以後,事情就好辦許多,逐個提取CView及其父類的CRuntimeClass成員與這個標識進行比對就能夠達到判斷的目的了。由於是靜態變量,因此只存有一份拷貝,能夠直接把指針做爲比較時的參照。
第二點:消息映射表的搭建:
搭建消息映射表的目的是爲了找到一個消息對應的消息處理函數。對於一個CMyView窗口來講,它的某個消息處理函數有可能並非存在於CMyView類中,而是存在於它的父類甚至是別的類裏面(例如數據操做應該放在CDoc類裏面處理比較合適),MFC爲了找到正確的消息處理函數,遂給每一個類都創建一個表來存儲這個類所擁有的消息處理函數,並經過指針鏈接起來,這樣就能夠經過遍歷查找來找到正確的那個消息處理函數。
以前類別型錄網的搭建是以CRuntimeClass做爲一個類的特別標識,而這裏須要標識的則是消息和它對應的消息處理函數。也就是說,每一個類裏都存儲一張表,表裏包括了這個類能夠處理的消息和對應的處理函數,這樣對於每一個類,獲得一條消息以後,將這條消息和表裏的條目進行比對,若是比對成功,調用對應的處理函數就能夠了。
除此以外,還須要一個指針來指向這個類的父類
下面介紹MFC對上面機制的具體實現方法:
1.爲每一個類添加消息條目和指針成員:
AFX_MSGMAP_ENTRY[] 數組用來存儲消息和對應的消息響應函數指針
AFX_MSGMAP 結構,其中包含一個AFX_MSGMAP類型的指針指向基類,以及一個AFX_MSGMAP_ENTRY指針,指向以前的數組。
這樣一個類就須要兩個靜態成員就好了,一個是AFX_MSGMAP類型,一個是AFX_MSGMAP_ENTRY數組。
MFC中使用DECLARE_MESSAGE_MAP宏來實現爲一個類添加這兩個成員的功能。
2.創建消息映射表:
也就是爲AFX_MSGMAP_ENTRY[]數組添加成員,而且把指針指向基類的AFX_MSGMAP靜態成員:
BEGIN_MESSAGE_MAP(theClass, baseClass)
ON_COMMAND(MSGID,msgpfn)
END_MESSAGE_MAP()
另外,MFC還爲每一個類添加了一個虛函數GetMessageMap用於獲得這個類的AFX_MSGMAP靜態成員指針,由此指針便可進行遍歷。
第三點:命令繞行
命令繞行的目的是找到一個消息正確的消息處理函數。
對於WM_LBUTTONDOWN這樣的消息來講,消息處理函數都在本窗口類(或者父類)裏面定義,使用GetMessageMap獲得消息映射表指針以後遍歷映射表就能找到對應的消息處理函數。
但對於WM_COMMAND消息來講,消息處理函數不必定是在本類裏面,CFrameWnd窗口接收到的WM_COMMAND消息,其消息處理函數有可能在CView裏面。之因此會這樣應該是與MFC Frame\View\Doc框架有關,具體緣由之後進一步來理解,這裏主要講解一下繞行的實現機制。
繞行的實現機制其實很是簡單,就是一個if語句的判斷,例如對於CFrameWnd,先看看CView裏有沒有這個消息的處理函數,若是沒有,再遍歷本身的映射表看看有沒有,若是仍是沒有,就看看CWinApp裏有沒有,再沒有的話,就交給默認函數處理。
下面先給出消息繞行時的路徑:
MFC消息必然是屬於某個窗口的(MSG結構裏還有個HWND字段呢),也就是說在MFC框架中,窗口的產生者只能是CWnd的派生類(CView和CFrameWnd等)。
而這些窗口所使用的窗口過程函數其實都是同一個全局函數AfxWndProc,也就是說消息產生以後都會被放到AfxWndProc中進行處理。
省去中間的調用步驟,AfxWndProc在接收到不一樣窗口的消息以後會調用CWnd->WindowProc()函數:
1.若是是WM_XXXX函數,直接使用GetMessageMap獲得消息映射表指針,遍歷查找消息處理函數。
2.若是是WM_COMMAND函數,則調用CWnd::OnCommand(),對於不一樣的窗口對象,因爲多態的緣由,調用的也不會是同一個OnCommand()函數,例若有CFrameWnd::OnCommand()等等。這個函數其實算不上重點,真正起做用的是CWnd::OnCmdMsg()函數。
OnCmdMsg()函數是CCmdTarget類裏的函數,其中的關鍵代碼就是遍歷消息映射表找到消息處理函數。在CCmdTarget的子類中有幾個類重寫了這個函數,這幾個類分別是CFrameWnd,CView,CDoc。
假如如今是CFrameWnd窗口接收到了WM_COMMAND消息,以下是CFrameWnd::OnCmdMsg()的主要代碼:
BOOL
CFrameWnd::OnCmdMsg(
UINT
nID,
int
nCode,
void
* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo)
{
CPushRoutingFrame push(
this
);
// 調用CView的OnCmdMsg函數
CView* pView = GetActiveView();
if
(pView != NULL && pView->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return
TRUE;
// 其實是調用CCmdTarget的OnCmdMsg函數,也就是遍歷自身消息映射表
if
(CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return
TRUE;
// 調用CWinApp的OnCmdMsg函數,實際上也是遍歷了CWinApp自身的消息映射表
CWinApp* pApp = AfxGetApp();
if
(pApp != NULL && pApp->OnCmdMsg(nID, nCode, pExtra, pHandlerInfo))
return
TRUE;
return
FALSE;
}
|
如上,在CFrameWnd的OnCmdMsg函數裏,分別調用了CView,CWnd(並無重載,因此實際上調用的是CCmdTarget的OnCmdMsg函數),CMyApp類的OnCmdMsg函數。也就是分別在CView、CWnd(其實就是CCmdTarget)、CWinApp裏尋找消息處理函數。一旦找到消息處理函數以後,就調用之,而後OnCmdMsg函數返回。
如上,即解釋了來自CFrameWnd窗口的WM_COMMAND消息是如何繞行的,其實也就是分別調用CView、CWnd(CCmdTarget)、CWinApp的OnCmdMsg函數而已。
同理,CView和CWinApp以及CDocument類都以相似的方式來對OnCmdMsg函數來進行調用,從而實現命令繞行機制。
以面向對象的思想理解MFC
最開始的MFC框架只是用兩個類來對本來的SDK流程進行封裝,MFC使用兩個類來抽象這個流程。一個是CWinApp,封裝了建立窗口(經過實例化一個Frame對象來實現),消息循環的主流程;一個是CMainFrame,封裝了窗口註冊,建立,及消息處理等內容。
CWinApp(也許說CWinThread更合適一點)類封裝了傳統Win32程序的主流程。通常來講一個Win32程序裏會有註冊窗口類,建立窗口,進行消息循環這幾個步驟。這幾個步驟在CWinApp類裏都存在着,其中註冊窗口類並建立窗口的過程被封裝到InitInstance成員函數裏面,消息循環被封裝到了Run成員函數裏面。
CMainFrame(也許說CWnd更合適一點)類封裝了Win32程序中有關窗口的那部分東西,具體說來就是窗口類的註冊,窗口的建立,以及窗口消息的處理。其中窗口註冊被封裝在PreCreateWindow函數裏面,窗口的建立則被封裝在Create函數裏面(更準確地說是封裝在CWnd::CreateEx函數裏),還有一個窗口過程函數在哪裏這個暫時尚未搞清楚。
後來之因此有了CView和CDoc,是由於原來的CFrame負擔了過多的責任。這裏把數據的管理交給了CDoc類來負責,把數據的顯示交給了CView類來負責,明確了各自的責任。
這裏CView類仍然是一個單獨的窗口,從功能上來講應該和CFrame處於同等地位的,仍然有本身的窗口過程函數。只是從職責上來說CView只負責數據的顯示,而CFrame做爲CView外部的一個框架提供別的一些功能。
由上可知,程序的功能其實是被封裝到了Frame/View/Doc三個類裏,實際上是這三個類來合做完成程序的某個功能,正由於這樣,對於一個COMMEND消息,就能夠交給這三個類裏的某一個類來處理,無論這個消息是來源於Frame仍是View。好比在菜單上單擊一個"更新"的選項。原本這個消息是由Frame接收到的,可是由View來處理會更簡單,由於View類自己持有更新時所須要的一些信息。
從這點上能夠理解,Frame/View/Doc三個類是相互關聯的一個總體。
接着上面的闡述多說兩句:
MFC之因此創造了消息機制是爲了實現其Frame/View/Doc三位一體的架構。
Frame/View/Doc架構的意義在於將處理消息的職責分配到合理的類中去處理,例如在菜單上點擊一個"保存"選項,處理這個消息就應該交給Doc類來實現,而若是點擊"更新"選項,則將這個消息交給View類來處理更方便一些。
MFC的消息機制就負責將消息交給合適的類去處理。下面解釋消息機制的實現思路:
1.比對思路和消息表:爲每個類創建一個消息表,這個消息表裏包括了這個類可以處理的消息有哪些,消息和其處理函數也一一對應。這樣的話就能夠遍歷這張消息表並進行比對來知道這個類能夠處理那些消息,並可以一個消息的處理函數。
2.遍歷思路:要讓Frame裏產生的消息在Doc類裏進行處理,其思路也是簡單的遍歷:先看看View裏有沒有能處理的,要是沒有,再看看本身類裏能不能處理,要是再不行,再看看Doc類裏有沒有對應的處理函數。
3.Frame/View/Doc三位一體:這個我也沒記清楚,貌似在建立一個Frame的時候都會順帶建立出其對應的View和Doc(單文檔的狀況,多文檔貌似要建立多個)。因此上面能夠由View找到其對應的Doc。