MFC程序框架剖析
即便是基于MFC的應用程序,建立窗口類也是會遵循如下的過程:
設計窗口類->注冊窗口類->生成窗口->顯示窗口->更新窗口->消息循環->消息路由到窗口過程函數處理。下面就剖析一下在MFC中是如何完成上述過程的。
(1)每個應用程序都有且僅有一個應用類的全局變量theApp,全局變量先于WinMain函數進行處理。
(2)WinMain函數體在APPMODUL.CPP文件中,定義如下:
extern "C" int WINAPI
_tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
// call shared/exported WinMain
return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
}
其中#define _tWinMain WinMain
(3)AfxWinMain函數體在WINMAIN.CPP文件中,里面有如下兩句話:
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
其實這里得到的這兩個指針都是指向全局的對象theApp的;
接下來有函數調用pThread->InitInstance(),根據多態性,會調用CXXXApp類中的InitInstance()函數。該函數很重要,在對該函數的調用中就會完成:設計窗口類->注冊窗口類->生成窗口->顯示窗口->更新窗口。
接下來,該函數中會繼續調用pThread->Run(),這就完成了:消息循環->消息路由到窗口過程函數處理。
(4)進入CXXXApp::InitInstance()函數體中,對于單文檔應用程序,調用ProcessShellCommand(cmdInfo),通過調用該函數就會完成:設計窗口類->注冊窗口類->生成窗口。
再接下來就會調用m_pMainWnd->ShowWindow(SW_SHOW);m_pMainWnd->UpdateWindow();這就完成了:顯示窗口->更新窗口。
(5)在函數CWinApp::ProcessShellCommand(CCommandLineInfo& rCmdInfo)中會進入到如下的case分支:case CCommandLineInfo::FileNew:
if (!AfxGetApp()->OnCmdMsg(ID_FILE_NEW, 0, NULL, NULL))
(6)進入函數CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,
AFX_CMDHANDLERINFO* pHandlerInfo),調用_AfxDispatchCmdMsg(this, nID, nCode,
lpEntry->pfn, pExtra, lpEntry->nSig, pHandlerInfo);
(7)進入函數AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode,
AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo),調用
case AfxSig_vv:
// normal command or control notification
ASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKED
ASSERT(pExtra == NULL);
(pTarget->*mmf.pfn_COMMAND)();
(8)進入CWinApp::OnFileNew(),調用m_pDocManager->OnFileNew();這個函數很特殊,它本身是個消息響應函數,當我們點擊ID為ID_FILE_NEW的菜單時,會產生一個命令消息,由于命令消息可以被CCmdTarget類及其派生類來捕獲,而CWinApp是從CCmdTarget派生出來的,因此可以捕獲這個消息。當應用程序創建完成并成功顯示后,當我們點擊文件菜單下的新建菜單項時,就會首先進入這個函數,然后再依次執行下去,最后就會執行到pDocument->OnNewDocument()中,往往我們會對這個函數不解,不知道它為什么會響應ID_FILE_NEW的命令消息,至此真相大白了。順便說一句,為什么程序在剛啟動的時候,我們并沒有點擊菜單項,為什么會自動的產生這個消息呢?這是因為在CXXXXApp::InitInstance()函數中有“CCommandLineInfo cmdInfo;”這個類的構造函數是這樣的:CCommandLineInfo::CCommandLineInfo()
{
m_bShowSplash = TRUE;
m_bRunEmbedded = FALSE;
m_bRunAutomated = FALSE;
m_nShellCommand = FileNew;
},因此就會在第(5)步驟的時候進入到“case CCommandLineInfo::FileNew:”這個分支中,就相當于產生了這樣一個FileNew的消息。同理對于ID為ID_FILE_OPEN(在CWinApp::OnFileOpen()中響應)、ID_FILE_SAVE(在CDocument::OnFileSave()中響應)等等在MFC向導為我們生成的單文檔類中找不到消息響應的入口時,其實都是在基類CWinApp或者CDocument類中進行了響應。對于CXXXXDoc::Serialize(CArchive& ar)函數也是通過ID_FILE_SAVE和ID_FILE_OPEN產生命令消息后就行響應從而才調用該函數的。
(9)進入CDocManager::OnFileNew(),CDocManager類有一個成員變量是CPtrList m_templateList;該變量保存了一個文檔模版鏈表指針,在CDocManager::OnFileNew()函數體中會調用CDocTemplate* pTemplate = (CDocTemplate*)m_templateList.GetHead();得到鏈表中的頭,也就是第一個文檔模版,后面就會用得到的這個指針去調用pTemplate->OpenDocumentFile(NULL);緊接著就會有一個判斷,用來確定該鏈表中是否只有一項,如果鏈表中保存了多個文檔模版,則會彈出一個對話框,來讓我們選擇到底是使用哪一套文檔模版來構建應用程序,相信大家也都見到過這種情況吧。對了,還有一點要說明的是:pTemplate是一個CDocTemplate的指針,但接下來程序為什么會進入到CSingleDocTemplate::OpenDocumentFile的函數體內呢,這是因為CDocTemplate類中的OpenDocumentFile函數被定義為純虛函數,而CSingleDocTemplate類又是從CDocTemplate類派生出來的,并且實現了該函數,因此就會進入到子類的函數體中了。
(10)進入CDocument* CSingleDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName,
BOOL bMakeVisible),先調用CreateNewDocument()創建文檔類,再調用pFrame = CreateNewFrame(pDocument, NULL);創建框架類和視圖類,從這里也可以看出MFC體系結構中文檔、框架、視圖“三位一體”的模式,在這一個函數中同時創建三個類;再會調用pDocument->OnNewDocument();因此就會進入到子類的文檔類中的pDocument->OnNewDocument()中了。
(11)進入CFrameWnd* CDocTemplate::CreateNewFrame(CDocument* pDoc, CFrameWnd* pOther),調用if (!pFrame->LoadFrame(m_nIDResource,
WS_OVERLAPPEDWINDOW | FWS_ADDTOTITLE, // default frame styles
NULL, &context))
(12)進入BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext),調用VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));
(13)進入BOOL AFXAPI AfxEndDeferRegisterClass(LONG fToRegister),該函數內部就完成了:設計窗口類->注冊窗口類。MFC通過給我們提供好一些已經訂制好的窗口類,我們不需要自己再設計窗口類,只需要到那些訂制好的窗口類“倉庫”中尋找一種適合我們需要的窗口類就可以了,然后通過AfxRegisterClass函數注冊窗口類。還需要說明的是,再后續的跟蹤過程中,我們會發現還會進入到AfxEndDeferRegisterClass函數中進行設計和注冊窗口類,這主要是因為單文檔應用程序比較特殊,它提前通過這樣的一種途徑進行了窗口類的設計和注冊步驟,其實是應該在BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs)函數的調用中完成窗口類的設計和注冊的,這一點我們要清楚,也就是說設計和注冊窗口類的正宗發源地應該是PreCreateWindow(CREATESTRUCT& cs)。此外,我們還會注意到在該函數體的前部分有一語句為“wndcls.lpfnWndProc = DefWindowProc;”因此所有窗口類的窗口過程函數都是DefWindowProc,這一點在后面的跟蹤中可以看到,每次生成窗口之后都會調用幾次DefWindowProc函數。也就是說MFC都是讓我們采用默認的窗口過程函數,這并不是說我們因此就不能使用自己的窗口過程函數實現個性化的消息處理了,MFC采用了一種基于消息映射的機制完成了消息個性化處理。
(14)回到BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)中,調用LPCTSTR lpszClass = GetIconWndClass(dwDefaultStyle, nIDResource);
(15)進入LPCTSTR CFrameWnd::GetIconWndClass(DWORD dwDefaultStyle, UINT nIDResource),調用PreCreateWindow(cs);
(16)進入BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs),調用CFrameWnd::PreCreateWindow(cs)
(17)進入BOOL CFrameWnd::PreCreateWindow(CREATESTRUCT& cs),調用VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));又一次設計和注冊窗口類
(18)回到BOOL CFrameWnd::LoadFrame(UINT nIDResource, DWORD dwDefaultStyle,
CWnd* pParentWnd, CCreateContext* pContext)中,調用if (!Create(lpszClass, lpszTitle, dwDefaultStyle, rectDefault, pParentWnd, MAKEINTRESOURCE(nIDResource), 0L, pContext))
(19)進入BOOL CFrameWnd::Create(LPCTSTR lpszClassName,
LPCTSTR lpszWindowName,
DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd,
LPCTSTR lpszMenuName,
DWORD dwExStyle,
CCreateContext* pContext),調用if (!CreateEx(dwExStyle, lpszClassName, lpszWindowName, dwStyle,
rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top,
pParentWnd->GetSafeHwnd(), hMenu, (LPVOID)pContext))
(20)BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam),調用if (!PreCreateWindow(cs))
,接下來調用HWND hWnd = ::CreateWindowEx(cs.dwExStyle, cs.lpszClass,
cs.lpszName, cs.style, cs.x, cs.y, cs.cx, cs.cy,
cs.hwndParent, cs.hMenu, cs.hInstance, cs.lpCreateParams);好了,終于讓我們找到生成窗口的地方了——函數::CreateWindowEx!
(21)進入int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct),調用if (CFrameWnd::OnCreate(lpCreateStruct) == -1)
(22)進入int CFrameWnd::OnCreate(LPCREATESTRUCT lpcs),調用return OnCreateHelper(lpcs, pContext);
(23)進入int CFrameWnd::OnCreateHelper(LPCREATESTRUCT lpcs, CCreateContext* pContext),調用if (CWnd::OnCreate(lpcs) == -1)
(24)進入_AFXWIN_INLINE int CWnd::OnCreate(LPCREATESTRUCT),調用return (int)Default();
(25)進入LRESULT CWnd::Default(),調用return DefWindowProc(pThreadState->m_lastSentMsg.message, pThreadState->m_lastSentMsg.wParam, pThreadState->m_lastSentMsg.lParam);
(26)進入LRESULT CWnd::DefWindowProc(UINT nMsg, WPARAM wParam, LPARAM lParam),調用return ::CallWindowProc(m_pfnSuper, m_hWnd, nMsg, wParam, lParam);
(27)回到int CFrameWnd::OnCreateHelper(LPCREATESTRUCT lpcs, CCreateContext* pContext),調用if (!OnCreateClient(lpcs, pContext))
(28)進入BOOL CFrameWnd::OnCreateClient(LPCREATESTRUCT, CCreateContext* pContext),調用if (CreateView(pContext, AFX_IDW_PANE_FIRST) == NULL)
(29)進入CWnd* CFrameWnd::CreateView(CCreateContext* pContext, UINT nID),調用if (!pView->Create(NULL, NULL, AFX_WS_DEFAULT_VIEW, CRect(0,0,0,0), this, nID, pContext))
(30)進入BOOL CWnd::Create(LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
const RECT& rect,
CWnd* pParentWnd, UINT nID,
CCreateContext* pContext),調用return CreateEx(0, lpszClassName, lpszWindowName,
dwStyle | WS_CHILD,
rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
pParentWnd->GetSafeHwnd(), (HMENU)nID, (LPVOID)pContext);
(31)進入BOOL CWnd::CreateEx(DWORD dwExStyle, LPCTSTR lpszClassName,
LPCTSTR lpszWindowName, DWORD dwStyle,
int x, int y, int nWidth, int nHeight,
HWND hWndParent, HMENU nIDorHMenu, LPVOID lpParam),重復生成框架類CMainFrame的過程來生成CXXXView,因為它也是一個窗口類,因此也需要進行那一系列過程才能最終顯示更新出來。
調用的順序是這個樣子的:PreCreateWindow(cs)->BOOL CXXXView::PreCreateWindow(CREATESTRUCT& cs)->CView::PreCreateWindow(cs)->VERIFY(AfxDeferRegisterClass(AFX_WNDFRAMEORVIEW_REG));->::CreateWindowEx(...)->CWnd::DefWindowProc->::CallWindowProc(...)->...->CXXXView::OnCreate->CView::OnCreate->CWnd::OnCreate->...
寫到這里,基本上就清楚了,中間的省略號表示的部分大多數都是在與窗口過程函數有關的,因為在生成窗口的時候需要響應一些消息,因此需要調用一些窗口過程函數,每次在調用::CreateWindowEx(...)函數后都會調用一些窗口過程函數,然后再去調用該窗口類對應的OnCreate函數,其實在調用OnCreate函數之前調用CreateWindowEx只是生成了一個窗口,至于這個窗口里面要放置些什么東西,以及該如何裝飾該窗口,則就需要由OnCreate來完成了,往往我們都會在OnCreate函數的后面(這樣做是為了不影響窗口本身應該布置的格局)添加一些代碼,創建我們自己的東西,比如我們通常會在CMainFrame類的OnCreate函數后面放置一些Create代碼,來創建我們自己的可停靠的工具欄或者按鈕之類的東西,當然我們也可以在CXXXView類的OnCreate函數的后面添加一些代碼,來創建我們需要的東西,比如按鈕之類的東西。在完成了從設計、注冊到生成窗口的過程之后,往往還需要顯示更新,有些時候,我們不必要每次都顯示的調用CWnd的ShowWindow和UpdateWindow兩個函數,我們可以在創建的時候,給窗口風格中添加WS_VISIBLE即可,因此有些時候會跟蹤不到ShowWindow和UpdateWindow兩個函數這兩個函數,因為窗口在創建的時候就可見了。
總的來說,先初始化應用類,然后注冊生成框架類,然后再注冊生成視圖類,然后注冊生成視圖類OnCreate函數后面用戶添加的、用Create來準備創建的窗口,然后再注冊生成框架類的OnCreate函數后面需要生成的m_wndToolBar、m_wndStatusBar以及我們自己添加的要創建的窗口類,最后在回到應用類的初始化的函數體中,調用框架類的顯示和更新函數,然后再進入由框架類定義的窗口的消息循環中。
消息循環的過程是這個樣子的:
(1)調用int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)函數中的pThread->Run()
(2)進入int CWinApp::Run(),調用return CWinThread::Run();
(3)進入int CWinThread::Run(),調用if (!PumpMessage())
(4)進入BOOL CWinThread::PumpMessage(),調用if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))
(5)回到BOOL CWinThread::PumpMessage(),調用::TranslateMessage(&m_msgCur);::DispatchMessage(&m_msgCur);
(6)回到int CWinThread::Run(),調用while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
(7)再重復(4)-(6)的步驟
下面給出int CWinThread::Run()中消息循環的部分代碼:
do
{
// pump message, but quit on WM_QUIT
if (!PumpMessage())
return ExitInstance();
// reset "no idle" state after pumping "normal" message
if (IsIdleMessage(&m_msgCur))
{
bIdle = TRUE;
lIdleCount = 0;
}
} while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
這段代碼其實本質上與我們基于Win32 SDK手寫的代碼:
//消息循環
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
//簡單的說,函數TranslateMessage就是把WM_KEYDOWN和WM_KEYUP翻譯成WM_CHAR消息,沒有該函數就不能產生WM_CHAR消息。
TranslateMessage(&msg);
::DispatchMessage(&msg);
}
是一致的。
posted on 2008-02-23 19:15 so true 閱讀(2502) 評論(3) 編輯 收藏 所屬分類: C&C++