在這裏,我想談談自己學習MFC的一些體會。我是從1997年纔開始在Window下編寫程序的。在這之前,我編寫過一些DOS程序,包括一個簡單的全屏幕編輯器和一個帶函數的表達式解釋器,都是一些小的程序。Window 3.1流行後,我開始在它下面編寫程序。
從編寫DOS程序到編寫Window程序,需要從編程思想上作一個比較大的調整。在DOS下編寫程序,程序的總體流程完全由應用程序自己控制;但在Window下,程序的總體流程是由操作系統控制的,這一點對在DOS下“胡作非爲”的DOS程序員而然,特別不習慣,思想上一時很難轉過彎來,總覺得操作系統所控制的應用程序流程能夠滿足我們所提出的任意要求嗎?萬一某個應用程序所需要的流程同它相抵觸,那該怎麼樣?
但後來隨着學習的深入,我覺得這種擔心是完全多餘的,就我個人而然在還沒有碰到上面的問題。
另外一個轉變就是,在Window下,程序是由事件(或消息)驅動的,程序員在程序中主要是提供事件處理程序的代碼,然後由操作系統來調用這些代碼,從程序員的角度看,就是操作系統在“回調”他或她所寫的代碼。這一點也很不習慣,因爲在DOS下,都是應用程序調用操作系統的代碼(API),現在一下反過來了,角色變化了,受不了!不過,隨作編程量的增加,這一點也慢慢淡化了。
剛開始,我是用SDK編程的,使用了半年後,我受不了了,太麻煩了,編寫一個簡單的顯示”hello, world!”的程序就得上百行代碼,再加上討厭的make文件和.def文件(那時我使用的是Borland C++ 3.1,而且也不知道有OWL這個東西)。後來聽人說,現在在Window下編寫C或C++程序用的都是MFC,MFC的功能很強大!於是,我到圖書館去借了兩本講VC的書,照着書上的內容,折騰了一個禮拜。
說實在話,那一個禮拜是把我搞得最迷糊的一個禮拜,MFC把我給嚇壞了。是的,用MFC編寫一個“hello, world!”程序只需自己編寫一行代碼,但我不知道我所編寫的那一行代碼是什麼時候執行的,我不知道MFC在背後幹了什麼。這些倒不是最主要的,更讓我難以接受的是,我覺的我所有的編程行動都在MFC的控制之下,而且控制得更“死”了,我的思想鑽進上面所提到的“死衚衕”中去了。後來我想,如果那時候我看了一些有關構件(Framework)的文章或書,我想,這個“死衚衕”對我而然,應該是不存在的。
其實,所有這些都是由於對MFC不熟悉所造成的,MFC是一個框架(Framework)式類庫,框架式類庫同一般的類庫的不同之處在於,庫中的各個類之間是有聯繫的,它們是按照框架所定義的模式去協作完成任務的。所以,要學習MFC,首先就要了解各個類之間是如何協作的以及它們的接口。
另外,我覺得,如果熟悉SDK的話,對理解MFC和使用MFC編寫程序是有很大幫助的,因此在後面的講解中,我會根據需要穿插一些SDK方面的知識,以助理解。
最後,必須具有一定的C++知識,完全不知道C++爲何物而去使用MFC,我實在難以想象其最後的結果,最好掌握C++的基本知識。
MFC應用程序的控制流程
一般的Window應用程序基本流程
WinMain()函數
任何一個應用程序都有一個入口函數,在Window下,程序的入口函數根據應用程序的類型,有兩種選擇:控制檯程序的入口函數是main(),一般的Window界面程序的入口函數是WinMain()。這裏只探討同我們下面的討論有關的WinMain()函數。下面是該函數的原型:(Visuall C++中)
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
其中:
hInstance是標識當前進程的實例,它實際上是進程所佔據的地址空間的首地址,在很多Window API中,都要將它作爲一個參數傳進去,所以,應用程序一般都會將它保存在一個全局量中。
hPreInstance是應用程序前一個實例的實例句柄。這是16位Window的殘留物,在Win32應用程序中,這個參數始終爲NULL。所以,某些從16爲移植到32位的應用程序,如果使用了hPreInstance,就應該對代碼作相應的修改。
lpCmdLine是命令行參數,這同main()中的argv[]類似。
nCmdShow用來指明應用程序的主窗口的顯示方式(最大化顯示,最小化顯示,一般化顯示)。
一個實例
下面是一個顯示”Hello, world”的程序的代碼,它體現了一般的Window應用程序的基本流程。
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
MSG msg;
file://註冊窗口類
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = (WNDPROC)WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, (LPCTSTR)IDI_HELLOWORLD);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
wcex.lpszMenuName = (LPCSTR)IDC_HELLOWORLD;
wcex.lpszClassName = szWindowClass;
wcex.hIconSm = LoadIcon(wcex.hInstance, (LPCTSTR)IDI_SMALL);
RegisterClassEx(&wcex);
file://創建一個該類型的窗口
HWND hWnd;
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd) return FALSE;
file://一nCmdShow所指定的方式顯示窗口
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
file://啓動消息循環,將消息發送給相應的窗口函數
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
file://窗口函數
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
PAINTSTRUCT ps;
HDC hdc;
char* szHello = “Hello, world!”;
switch (message)
{
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
RECT rt;
GetClientRect(hWnd, &rt);
DrawText(hdc, szHello, strlen(szHello), &rt, DT_CENTER);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
上面程序的執行過程如下:
1、註冊一個窗口類
這是爲後面的創建窗口作準備,在使用CreateWindwo()和CreateWindowEx()創建窗口時,都必須提供一個標識窗口類的字符串。創建窗口類的主要意圖是向操作系統提供窗口處理函數。
2、創建窗口
啓動消息循環,分發並處理消息。
其中的關鍵部分是消息循環:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
調用GetMessage()從線程的消息隊列中取出一條消息,將消息翻譯後,再調用
DispatchMessage()將該消息分發至相應的窗口過程。(實際上DispatchMessage()是將該消息作爲參數調用對應的窗口的窗口函數,這就是分發的實質),在後面我們會詳細討論MFC的消息環同上面的消息環的區別。
MFC的WinMain
使用MFC編程的程序員剛開始都會提出這樣一個問題:我的程序是從哪兒開始執行的?回答是:從WinMain()開始執行的。提出這樣的問題是由於在他們所編寫的MFC應用中看不到WinMain()函數。這個函數是隱藏在MFC框架中,MFC的設計者將它作得很通用(這主要得益於Window的消息驅動的編程機制,使得作一個通用的WinMain()很容易),因此在一般情況下,無需更改WinMain()的代碼,MFC的設計者也不提倡程序員修改WinMain()的代碼。在MFC中,實際實現WinMain()的代碼是AfxWinMain()函數(根據其前綴Afx就知道這是一個全局的MFC函數)。
一個Win32應用程序(或進程)是由一個或多個併發的線程組成的,其中第一個啓動的線程稱爲主線程,在Window下,一般將線程分成兩大類,界面線程和工作線程,工作線程就是一般的線程,它沒有窗口,沒有消息隊列等,界面線程擁有一個或多個窗口,擁有一個消息隊列和其他專屬於界面線程的元素。在討論AfxWinMain()之前,首先要簡略提一下MFC中的兩個重要的類,CWinThread和CWinApp,CWinThread是用來封裝界面線程的類,CWinApp是從CWinThread派生而來的。在CWinThread中,有兩個很重要的虛擬函數InitInstance()和ExitInistance(),MFC的程序員應該對這兩個函數應該很熟悉。在CWinApp中,增加了另外一個虛擬函數InitApplication(),討論AfxWinMain()的主要目的是看這些函數是如何被調用的。
AfxWinMain()的代碼如下:
int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPTSTR lpCmdLine, int nCmdShow)
{
ASSERT(hPrevInstance == NULL); file://在win32下,hPrevInstance始終爲NULL
int nReturnCode = -1;
CWinThread* pThread = AfxGetThread();
CWinApp* pApp = AfxGetApp();
// AFX internal initialization
if (!AfxWinInit(hInstance, hPrevInstance, lpCmdLine, nCmdShow))
goto InitFailure;
// App global initializations (rare)
if (pApp != NULL && !pApp->InitApplication())
goto InitFailure;
// Perform specific initializations
if (!pThread->InitInstance())
{
if (pThread->m_pMainWnd != NULL)
{
TRACE0("Warning: Destroying non-NULL m_pMainWnd\n");
pThread->m_pMainWnd->DestroyWindow();
}
nReturnCode = pThread->ExitInstance();
goto InitFailure;
}
nReturnCode = pThread->Run();
InitFailure:
AfxWinTerm();
return nReturnCode;
}
在上面的代碼中,AfxGetThread()返回的是當前界面線程對象的指針,AfxGetApp()返回的是應用程序對象的指針,如果該應用程序(或進程)只有一個界面線程在運行,那麼這兩者返回的都是一個全局的應用程序對象指針,這個全局的應用程序對象就是MFC應用框架所默認的theApp對象(每次使用AppWizard生成一個SDI或MDI應用程序時,AppWizard都會添加CYourApp theApp這條語句,AfxGetApp()返回的就是這個theApp的地址)。
CWinApp::InitApplication(), CWinThread::InitInstance(), CWinThread::ExitInstance()是如何被調用的,從上面的代碼一看就知,我不再贅述。下面我們把焦點放在CWinThread::Run()上。
MFC的控制中心――CWinThread::Run()
說CWinThread::Run()是MFC的控制中心,一點也沒有誇大。在MFC中,所有來自於消息隊列的消息的分派都是在CWinThread::Run()函數中完成的,同AfxWinMain()一樣,這個函數也是對程序員是不可見的,其道理同AfxWinMain()的一樣。
首先要提的一點是,對每條從消息隊列取出來的消息,MFC根據消息的類型,按照某個特定的模式進行分發處理,這個分發模式是MFC自己定義的。固定的消息分發流程和在這個流程中的可動態改變其行爲的虛擬函數就構成了MFC的消息分發模式。應用程序可以通過重載這些虛擬函數,來局部定製自己的的消息分發模式。正是通過這些虛擬函數,MFC爲應用程序提供了足夠的靈活性。下面討論的所有代碼都來自於MFC源代碼中的threadcore.cpp文件,它們都是CWinThread的成員。
CWinThread::Run()的結構
CWinThread::Run()的代碼如下:
int CWinThread::Run()
{
ASSERT_VALID(this);
// for tracking the idle time state
BOOL bIdle = TRUE;
LONG lIdleCount = 0;
// acquire and dispatch messages until a WM_QUIT message is received.
for (;;)
{
// phase1: check to see if we can do idle work
while (bIdle &&
!::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE))
{
// call OnIdle while in bIdle state
if (!OnIdle(lIdleCount++))
bIdle = FALSE; // assume "no idle" state
}
// phase2: pump messages while available
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));
}
ASSERT(FALSE); // not reachable
}
CWinThread::Run()的處理過程如下:
先根據空閒標誌以及消息隊列是否爲空這兩個條件判斷當前線程是否處於空閒狀態(這個“空閒”的含義同操作系統的含義不同,是MFC自己所謂的“空閒”),如果是,就調用CWinThread::OnIdle(),這也是我們比較熟悉的一個虛擬函數。
如果不是,從消息隊列中取出消息,進行處理,直到消息隊列爲空。
在這裏,我們發現,MFC不是調用GetMessage()從線程消息隊列中取消息,而是調用PeekMessage()。其原因在於,GetMessage()是一個具有同步行爲的函數,如果消息隊列中沒有消息,GetMessage()會一直阻塞,使得線程處於睡眠狀態,直到消息隊列中有一條或多條消息,操作系統纔會喚醒該線程,GetMessage()纔會返回,如果線程處於睡眠狀態了,就不會使線程具有MFC所謂的“空閒”狀態了;而PeekMessage()則是一個具有異步行爲的函數,如果消息隊列中沒有消息,它馬上返回0,不會導致線程處於睡眠狀態。
在上面的代碼中,有兩個函數值得探討,一個是空閒處理函數OnIdle(),另外一個是消息分發處理函數PumpMessage()。不要忽視CWinThread的OnIdle()函數,它作了很多有意義的事情。下面討論PumpMessage(),OnIdle()將在後面的章節裏討論。
CWinThread::Run()的核心――CWinThread::PumpMessage()
標題強調了PumpMessage()的重要性,Run()是MFC的控制中心,而PumpMessage()又是Run()的核心,所以從MFC的真正控制中心是PumpMessage()。PumpMessage()的代碼極其簡單:
BOOL CWinThread::PumpMessage()
{
ASSERT_VALID(this);
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL))
return FALSE;
// process this message
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
::TranslateMessage(&m_msgCur);
::DispatchMessage(&m_msgCur);
}
return TRUE;
}
首先,PumpMessage()調用GetMessage()從消息隊列中取一條消息,由於PumpMessage()是在消息隊列中有消息的時候才被調用的,所以GetMessage()會馬上返回,根據其返回值,判斷當前取出的消息是不是WM_QUIT消息(這個消息一般對是通過調用PostQuitMessage()放入線程消息隊列的),如果是,就返回FALSE,CWinThread::Run()該退出了,CWinThread::Run()直接調用CWinThread::ExitInstance()退出應用程序。在GetMessage()的後面是我們所熟悉的TranslateMessage()和DispatchMessage()函數。
可以看出,是否調用TranslateMessage()和DispatchMessage()是由一個名稱爲PreTranslateMessage()函數的返回值決定的,如果該函數返回TRUE,則不會把該消息分發給窗口函數處理。
就我個人觀點而言,正是有了這個PreTranslateMessage(),才使得MFC能夠靈活的控制消息的分發模式,可以說,PreTranslateMessage()就是MFC的消息分發模式。
<三>MFC的特色――PreTranslateMessage()
經過層層扒皮,終於找到了CWinThread::Run()最具特色的地方,這就是PreTranslateMessage()函數。同前面使用SDK編寫的顯示”Hello, world!”程序的消息循環不同的地方在於,MFC多了這個PreTranslateMessage(),PreTranslateMessage()最先獲得了應用程序的消息處理權!下面我們對PreTranslateMessage()進行剝皮式分析。同前面一樣,首先看看實際的PreTranslateMessage()的代碼:
BOOL CWinThread::PreTranslateMessage(MSG* pMsg)
{
ASSERT_VALID(this);
// if this is a thread-message, short-circuit this function
if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg)) return TRUE;
// walk from target to main window
CWnd* pMainWnd = AfxGetMainWnd();
if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg)) return TRUE;
// in case of modeless dialogs, last chance route through main
// window's accelerator table
if (pMainWnd != NULL)
{
CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd);
if (pWnd->GetTopLevelParent() != pMainWnd)
return pMainWnd->PreTranslateMessage(pMsg);
}
return FALSE; // no special processing
}
PreTranslateMessage()的處理過程如下:
首先判斷該消息是否是一個線程消息(消息的窗口句柄爲空的消息),如果是,交給DispatchThreadMessageEx()處理。我們暫時不管DispatchThreadMessageEx(),它不是我們討論的重點。
調用CWnd::WalkPreTranslateTree()對該消息進行處理,注意該函數的一個參數是線程主窗口的句柄,這是PreTranslateMessage()的核心代碼,在後面會對這個函數進行詳細的分析。
對於非模式對話框,這特別的、額外的處理。
下面詳細討論一下CWnd::WalkPreTranslateTree()函數,它的代碼很簡單:
BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg)
{
ASSERT(hWndStop == NULL || ::IsWindow(hWndStop));
ASSERT(pMsg != NULL);
// walk from the target window up to the hWndStop window checking
// if any window wants to translate this message
for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd))
{
CWnd* pWnd = CWnd::FromHandlePermanent(hWnd);
if (pWnd != NULL)
{
// target window is a C++ window
if (pWnd->PreTranslateMessage(pMsg))
return TRUE; // trapped by target window (eg: accelerators)
}
// got to hWndStop window without interest
if (hWnd == hWndStop)
break;
}
return FALSE; // no special processing
}
CWnd::WalkPreTranslateTree()的所使用的策略很簡單,擁有該消息的窗口最先獲得該消息的處理權,如果它不想對該消息進行處理(該窗口對象的PreTranslateMessage()函數返回FALSE),就將處理權交給它的父親窗口,如此向樹的根部遍歷,直到遇到hWndStop(在CWinThread::PreTranslateMessage()中,hWndStop表示的是線程主窗口的句柄)。記住這個消息處理權的傳遞方向,是由樹的某個一般節點或葉子節點向樹的根部傳遞!
小結:
下面對這一章作一個小結。
MFC消息控制流最具特色的地方是CWnd類的虛擬函數PreTranslateMessage(),通過重載這個函數,我們可以改變MFC的消息控制流程,甚至可以作一個全新的控制流出來,在下面的一章會對MFC的實現作詳細介紹。
只有穿過消息隊列的消息才受PreTranslateMessage()影響,採用SendMessage()或其他類似的方式向窗口直接發送的而不經過消息隊列的消息根本不會理睬PreTranslateMessage()的存在
傳給PreTranslateMessage()的消息是未經翻譯過的消息,它沒有經過TranslateMessage()處理,在某些情況下,要仔細處理,以免漏掉消息。