原文地址:http://maqianli210.blog.sohu.com/75497589.htmlhtml
这篇文章原本只是想介绍一会儿类化和超类化这两个比较“生僻”的名词。为了叙述的完整性而讨论了Windows的窗口和消息,也简要讨论了进程和线程。子类化(Subclassing)和超类化(Superclassing)是伴随Windows窗口机制而产生的两个复用代码的方法。不要把“子类化、超类化”与面向对象语言中的派生类、基类混淆起来。“子类化、超类化”中的“类”是指Windows的窗口类。程序员
但愿读者在阅读本节前先看看"谈谈Windows程序中的字符编码"开头的第0节和附录0。第0节介绍了Windows系统的几个重要模块。附录0概述了Windows的启动过程,从上电到启动Explorer.exe。本节介绍的是运行程序时发生的事情。编程
当咱们经过Explorer.exe运行一个程序时,Explorer.exe会调用CreateProcess函数请求系统为这个程序建立进程。固然,其它程序也能够调用CreateProcess函数建立进程。windows
系统在为进程分配内部资源,创建独立的地址空间后,会为进程建立一个主线程。咱们能够把进程看做单位,把线程看做员工。进程拥有资源,但真正在CPU上运行和调度的是线程。系统以挂起状态建立主线程,即主线程建立好,不会当即运行,而是等待系统调度。系统向Win32子系统的管理员csrss.exe登记新建立的进程和线程。登记结束后,系统通知挂起的主线程能够运行,新程序才开始运行。数据结构
这时,在建立进程中CreateProcess函数返回;在被建立进程中,主线程在完成最后的初始化后进入程序的入口函数(Entry-point)。建立进程与被建立进程在各自的地址空间独立运行。这时,即便咱们结束建立进程,也不会影响被建立进程。框架
可执行文件(PE文件)的文件头结构包含入口函数的地址。入口函数通常是Windows在运行时库中提供的,咱们在编译时能够根据程序类型设定。在VC中编译、运行程序的小知识点讨论了Entry-point,读者能够参考。函数
入口函数前的过程能够被看做程序的装载过程。在装载时,系统已经作过全局和静态变量(在编译时能够肯定地址)的初始化,有初值的全局变量拥有了它们的初值,没有初值的变量被设为0,咱们能够在入口函数处设置断点确认这一点。ui
进入入口函数后,程序继续运行环境的创建,例如调用全部全局对象的构造函数。在一切就绪后,程序调用咱们提供的主函数。主函数名是入口函数决定的,例如main或WinMain。若是咱们没有提供入口函数要求的主函数,编译时就会产生连接错误。this
咱们一般把存储介质(例如硬盘)上的可执行文件称做程序。程序被装载、运行后就成为进程。系统会为每一个进程建立一个主线程,主线程经过入口函数进入咱们提供的主函数。咱们能够在程序中建立其它线程。编码
线程能够建立一个或多个窗口,也能够不建立窗口。系统会为有窗口的线程创建消息队列。有消息队列的线程就能够接收消息,例如咱们能够用PostThreadMessage函数向线程发送消息。
没有窗口的线程只要调用了PeekMessage或GetMessage,系统也会为它建立消息队列。
每一个运行的程序就是一个进程。每一个进程有一个或多个线程。有的线程没有窗口,有的线程有一个或多个窗口。
咱们能够向线程发送消息,但大多数消息都是发给窗口的。发给窗口的消息一样放在线程的消息队列中。咱们能够把线程的消息队列看做信箱,把窗口看做收信人。咱们在向指定窗口发送消息时,系统会找到该窗口所属的线程,而后把消息放到该线程的消息队列中。
线程消息队列是系统内部的数据结构,咱们在程序中看不到这个结构。但咱们能够经过Windows的API向消息队列发送、投递消息;从消息队列接收消息;转换和分派接收到的消息。
Windows的程序员大概都看过这么一个最小的Windows程序:
// 例程1
#include "windows.h"
static const char m_szName[] = "窗口";
////////////////////////////////////////////////////////////////////////////////////////////////////
// 主窗口回调函数 若是直接用 DefWindowProc, 关闭窗口时不会结束消息循环
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0); // 关闭窗口时发送WM_QUIT消息结束消息循环
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
// 主函数
int __stdcall WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow)
{
WNDCLASS wc;
memset(&wc, 0, sizeof(WNDCLASS));
wc.style = CS_VREDRAW|CS_HREDRAW;
wc.lpfnWndProc = (WNDPROC)WindowProc;
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW);
wc.lpszClassName = m_szName;
RegisterClass(&wc); // 登记窗口类
HWND hWnd;
hWnd = CreateWindow(m_szName,m_szName,WS_OVERLAPPEDWINDOW,100,100,320,240,
NULL,NULL,hInstance,NULL); // 建立窗口
ShowWindow(hWnd, nCmdShow); // 显示窗口
MSG sMsg;
while (int ret=GetMessage(&sMsg, NULL, 0, 0)) { // 消息循环
if (ret != -1) {
TranslateMessage(&sMsg);
DispatchMessage(&sMsg);
}
}
return 0;
}
这个程序虽然只显示一个窗口,但常常被用来讲明Windows程序的基本结构。在MFC框架内部咱们一样能够找到相似的程序结构。这个程序包含如下基本概念:
下面分别介绍。
建立窗口时要提供窗口类的名字。窗口类至关于窗口的模板,咱们能够基于同一个窗口类建立多个窗口。咱们可使用Windows预先登记好的窗口类。但在更多的状况下,咱们要登记本身的窗口类。在登记窗口类时,咱们要登记名称、风格、图标、光标、菜单等项,其中最重要的就是窗口过程的地址。
窗口过程是一个函数。窗口收到的全部消息都会被送到这个函数处理。那么,发到线程消息队列的消息是怎么被送到窗口的呢?
熟悉嵌入式多任务程序的程序员,都知道任务(至关于Windows的线程)的结构基本上都是:
while (1) {
等待信号;
处理信号;
}
任务收到信号就处理,不然就挂起,让其它任务运行。这就是消息驱动程序的基本结构。Windows程序一般也是这样:
while (int ret=GetMessage(&sMsg, NULL, 0, 0)) { // 消息循环
if (ret != -1) {
TranslateMessage(&sMsg);
DispatchMessage(&sMsg);
}
}
GetMessage从消息队列接收消息;TranslateMessage根据按键产生WM_CHAR消息,放入消息队列;DispatchMessage根据消息中的窗口句柄将消息分发到窗口,即调用窗口过程函数处理消息。
建立窗口的函数会返回一个窗口句柄。窗口句柄在系统范围内(不是进程范围)标识一个惟一的窗口实例。经过向窗口发送消息,咱们能够实现进程内和进程间的通讯。
咱们能够用SendMessage或PostMessage向窗口发送或投递消息。SendMessage必须等到目标窗口处理过消息才会返回。我试过:若是向一个没有消息循环的窗口SendMessage,SendMessage函数永远不会返回。PostMessage在把消息放入线程的消息队列后当即返回。
其实只有投递的消息才是经过DispatchMessage分派到窗口过程的。经过SendMessage发送的消息,在线程GetMessage时,就已经被分派到窗口过程了,不通过DispatchMessage。
你们是否是以为“例程1”没什么意思,让咱们用它来作个小游戏:让“例程1”和一个控制台程序作一次亲密接触。咱们首先将“例程1”的窗口过程修改成:
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static DWORD tid = 0;
switch (uMsg) {
case WM_DESTROY:
PostQuitMessage(0); // 关闭窗口时发送WM_QUIT消息结束消息循环
break;
case WM_USER:
tid = wParam; // 保存控制台程序的线程ID
SetWindowText(hWnd, "收到");
break;
case WM_CHAR:
if (tid) {
switch(wParam) {
case '1':
PostThreadMessage(tid, WM_USER+1, 0, 0); // 向控制台程序发送消息1
break;
case '2':
PostThreadMessage(tid, WM_USER+2, 0, 0); // 向控制台程序发送消息2
break;
} }
break;
default:
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
return 0;
}
而后,咱们建立一个控制台程序,代码以下:
#include "windows.h"
#include "stdio.h"
static HWND m_hWnd = 0;
void process_msg(UINT msg, WPARAM wp, LPARAM lp)
{
char buf[100];
static int i = 1;
if (!m_hWnd) {
return;
}
switch (msg) {
case WM_USER+1:
SendMessage(m_hWnd, WM_GETTEXT, sizeof(buf), (LPARAM)buf);
printf("你如今叫:%s\n\n", buf); // 读取、显示对方的名字
break;
case WM_USER+2:
sprintf(buf, "我是窗口%d", i++);
SendMessage(m_hWnd, WM_SETTEXT, sizeof(buf), (LPARAM)buf); // 修改对方名字
printf("给你更名\n\n");
break;
}
}
int main()
{
MSG sMsg;
printf("Start with thread id %d\n", GetCurrentThreadId());
m_hWnd = FindWindow(NULL,"窗口");
if (m_hWnd) {
printf("找到窗口%x\n\n", m_hWnd);
SendMessage(m_hWnd, WM_USER, GetCurrentThreadId(), 0);
}
else {
printf("没有找到窗口\n\n");
}
while (int ret=GetMessage(&sMsg, NULL, 0, 0)) { // 消息循环
if (ret != -1) {
process_msg(sMsg.message, sMsg.wParam, sMsg.lParam);
}
}
return 0;
}
你们能看懂这游戏怎么玩吗?首先运行“例程1”wnd,而后运行控制台程序msg。msg会找到wnd的窗口,并将本身的主线程ID发给wnd。wnd收到msg的消息后,会显示收到。这时,wnd和msg已经创建了通讯的渠道:wnd能够向msg的主线程发消息,msg能够向wnd的窗口发消息。
咱们若是在wnd窗口按下键'1',wnd会向msg发送消息1,msg收到后会经过WM_GETTEXT消息得到wnd的窗口名称并显示。咱们若是在wnd窗口按下键'2',wnd会向msg发送消息2,msg收到后会经过WM_SETTEXT消息修改wnd的窗口名称。
这个小例子演示了控制台程序的消息循环,向线程发消息,以及进程间的消息通讯。
不一样的进程拥有独立的地址空间,若是咱们在消息参数中包含一个进程A的地址,而后发送到进程B。进程B若是在本身的地址空间里操做这个地址,就会发生错误。那么,为何上例中的WM_GETTEXT和WM_SETEXT能够正常工做?
这是由于WM_GETTEXT和WM_SETEXT都是Windows本身定义的消息,Windows知道参数的含义,并做了特殊的处理,即在进程B的空间分配一块内存做为中转,并在进程A和进程B的缓冲区之间复制数据。例如:在1.5.1节的例子中,若是咱们设置断点观察,就会发现msg发送的WM_SETTEXT消息中的lParam不等于wnd接收到的WM_SETTEXT消息中的lParam。
若是咱们在本身定义的消息中传递内存地址,系统不会作任何特殊处理,因此必然发生错误。
Windows提供了WM_COPYDATA消息用来向窗口传递数据,Windows一样会为这个消息做特殊处理。
在进程间发送这些须要额外分配内存的消息时,咱们应该用SendMessage,而不是PostMessage。由于SendMessage会等待接收方处理完后再返回,这样系统才有机会额外释放分配的内存。在这种场合使用PostMessage,系统会忽略要求投递的消息,读者能够在msg程序中试验一下。
窗口类是窗口的模板,窗口是窗口类的实例。窗口类和每一个窗口实例都有本身的内部数据结构。Windows虽然没有公开这些数据结构,但提供了读写这些数据的API。
例如:用GetClassLong和SetClassLong函数能够读写窗口类的数据;用GetWindowLong和SetWindowLong能够读写指定窗口实例的数据。使用这些接口,能够在运行时读取或修改窗口类或窗口实例的窗口过程地址。这些接口是子类化的实现基础。
子类化的目的是在不修改现有代码的前提下,扩展示有窗口的功能。它的思路很简单,就是将窗口过程地址修改成一个新函数地址,新的窗口过程函数处理本身感兴趣的消息,将其它消息传递给原窗口过程。经过子类化,咱们不须要现有窗口的源代码,就能够定制窗口功能。
子类化能够分为实例子类化和全局子类化。实例子类化就是修改窗口实例的窗口过程地址,全局子类化就是修改窗口类的窗口过程地址。实例子类化只影响被修改的窗口。全局子类化会影响在修改以后,按照该窗口类建立的全部窗口。显然,全局子类化不会影响修改前已经建立的窗口。
子类化方法虽然是二十年前的概念,却很好地实践了面向对象技术的开闭原则(OCP:The Open-Closed Principle):对扩展开放,对修改关闭。
超类化的概念更简单,就是读取现有窗口类的数据,保存窗口过程函数地址。对窗口类数据做必要的修改,设置新窗口过程,再换一个名称后登记一个新窗口类。新窗口类的窗口过程函数仍是仅处理本身感兴趣的消息,而将其它消息传递给原窗口过程函数处理。使用GetClassInfo函数能够读取现有窗口类的数据。
MFC将子类化方法应用得淋漓尽致,是一个不错的例子。候捷先生的《深刻浅出MFC》已经将MFC的主要框架分析得很透彻了,本节只是看看MFC的消息循环,简单分析MFC对子类化的应用。
随便创建一个MFC单文档程序,在视图类中添加WM_RBUTTONDOWN的处理函数,并在该处理函数中设置断点。运行,断下后,查看调用堆栈:
CHelloView::OnRButtonDown(unsigned int, CPoint)
CWnd::OnWndMsg(unsigned int, unsigned int, long, long *)
CWnd::WindowProc(unsigned int, unsigned int, long)
AfxCallWndProc(CWnd *, HWND__ *, unsigned int, unsigned int, long)
AfxWndProc(HWND__ *, unsigned int, unsigned int, long)
AfxWndProcBase(HWND__ *, unsigned int, unsigned int, long)
USER32! 7e418734()
USER32! 7e418816()
USER32! 7e4189cd()
USER32! 7e4196c7()
CWinThread::PumpMessage()
CWinThread::Run()
CWinApp::Run()
AfxWinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMain(HINSTANCE__ *, HINSTANCE__ *, char *, int)
WinMainCRTStartup()
KERNEL32! 7c816fd7()
WinMainCRTStartup是这个程序的入口函数。候捷先生已经详细介绍过AfxWinMain。咱们就看看CWinThread::PumpMessage中的消息循环:
BOOL CWinThread::PumpMessage()
{
if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) {
return FALSE;
}
if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur))
{
::TranslateMessage(&m_msgCur);
::DispatchMessage(&m_msgCur);
}
return TRUE;
}
这就是MFC程序主线程中的消息循环,它把发送到线程消息队列的消息分派到线程的窗口。
CWnd::CreateEx在建立窗口前调用SetWindowsHookEx函数安装了一个钩子函数_AfxCbtFilterHook。窗口刚建立好,钩子函数_AfxCbtFilterHook就被调用。_AfxCbtFilterHook调用SetWindowLong将窗口过程替换为AfxWndProcBase,并将SetWindowLong返回的原窗口地址保存到成员变量oldWndProc。上节调用堆栈中的AfxWndProcBase就是由此而来。
可见,经过CWnd::CreateEx建立的全部窗口都会被子类化,即它们的窗口过程都会被替换为AfxWndProcBase。MFC为何要这样作?
让咱们再看看调用堆栈中的CWnd::WindowProc函数:
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
{
LRESULT lResult = 0;
if (!OnWndMsg(message, wParam, lParam, &lResult))
lResult = DefWindowProc(message, wParam, lParam);
return lResult;
}
按照侯捷先生的介绍,CWnd::OnWndMsg就是“MFC消息泵”的入口,消息经过这个入口流入MFC消息映射中的消息处理函数。消息泵只会处理咱们定制过的消息,咱们没有添加过处理的消息会原封不动地流过"消息泵",进入DefWindowProc函数。在DefWindowProc函数中,消息会传给子类化时保存的原窗口地址oldWndProc。
CWnd::CreateEx里的钩子会子类化全部窗口吗?其实不尽然。的确,MFC全部窗口相关的类都是从CWnd派生的,这些类的实例在建立窗口时都会调用CWnd::CreateEx,都会被子类化。可是,经过对话框模板建立的窗口是经过CreateDlgIndirect建立的,不通过CWnd::CreateEx函数。
但这点其实也不是问题,由于若是咱们想经过MFC定制一个控件的消息映射,就必须先子类化这个控件,MFC仍是有机会将窗口过程替换成本身的AfxWndProcBase。下一节将介绍对话框控件的子类化。
我写了一个很简单的对话框程序,用来演示子类化和超类化。这个对话框程序有两个编辑框,我将编辑框的右键菜单换成了一个消息框。两个编辑框的定制分别采用了子类化和超类化技术:
首先从CEdit派生出CMyEdit1,定制WM_RBUTTONDOWN的处理。不少文章都建议咱们在对话框的OnInitDialog中用SubclassDlgItem实现子类化:
m_edit1.SubclassDlgItem(IDC_EDIT1, this);
这样作固然能够。其实若是咱们已经为IDC_EDIT1添加过CMyEdit1对象:
void CSubclassingDlg::DoDataExchange(CDataExchange* pDX)
{
CDialog::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CSubclassingDlg)
DDX_Control(pDX, IDC_EDIT1, m_edit1);
//}}AFX_DATA_MAP
}
DDX_Control会自动帮咱们完成子类化,没有必要手工调用SubclassDlgItem。你们能够经过在PreSubclassWindow中设置断点看看。
经过DDX_Control或者SubclassDlgItem子类化控件的效果是同样的,MFC都是把窗口过程替换成AfxWndProcBase。用户添加过处理函数的消息经过MFC消息泵流入用户的处理函数。
PreSubclassWindow是一个很好的定制控件的位置。若是咱们经过重载CWnd::PreCreateWindow定制控件,而用户在对话框中使用控件。因为对话框中的控件窗口是经过CreateDlgIndirect建立,不通过CWnd::CreateEx函数,PreCreateWindow函数不会被调用。
其实,用户要在对话框中使用定制控件,必须用DDX或者SubclassDlgItem函数子类化控件,这时PreSubclassWindow必定会被调用。
若是用户直接建立定制控件窗口,CWnd::CreateEx函数就必定会被调用,控件窗口必定会被子类化以安装MFC消息泵。因此在MFC中,PreSubclassWindow是建立窗口的必经之路。
我不多看到超类化的例子(除了罗云彬的Win32汇编),在大多数应用中,子类化技术已经足够了。但我仍是写了一个例子:CMyEdit2从CEdit派生。CMyEdit2::RegisterMe获取窗口类Edit的信息,保存原窗口过程,设置新窗口过程MyWndProc和新名称MyEdit,登记一个新窗口类。新窗口过程MyWndProc定制本身须要处理的消息,将其它消息送回原窗口过程。
我在对话框的OnInitDialog中先调用CMyEdit2::RegisterMe登记新窗口类,而后建立窗口。这样建立窗口必须通过CWnd::CreateEx,因此MFC仍是会把窗口过程换成AfxWndProcBase。没有被MFC消息映射拦截的消息才会流入MyWndProc。
这篇文章介绍了一些Windows和MFC的基础知识。写这篇文章的目的不是介绍什么编程技巧,而是让咱们更了解程序运行时发生的事情。