在这里,我想谈谈自己学习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()处理,在某些情况下,要仔细处理,以免漏掉消息。