网站建设与管理必修,国外有名的网站,wordpress增加下载量显示,网站制作多久介绍Windows的窗口、消息、子类化和超类化 这篇文章本来只是想介绍一下子类化和超类化这两个比较“生僻”的名词。为了叙述的完整性而讨论了Windows的窗口和消息#xff0c;也简要讨论了进程和线程。子类化#xff08;Subclassing#xff09;和超类化#xff08;Superclass…介绍Windows的窗口、消息、子类化和超类化 这篇文章本来只是想介绍一下子类化和超类化这两个比较“生僻”的名词。为了叙述的完整性而讨论了Windows的窗口和消息也简要讨论了进程和线程。子类化Subclassing和超类化Superclassing是伴随Windows窗口机制而产生的两个复用代码的方法。不要把“子类化、超类化”与面向对象语言中的派生类、基类混淆起来。“子类化、超类化”中的“类”是指Windows的窗口类。 0 运行程序 希望读者在阅读本节前先看看谈谈Windows程序中的字符编码开头的第0节和附录0。第0节介绍了Windows系统的几个重要模块。附录0概述了Windows的启动过程从上电到启动Explorer.exe。本节介绍的是运行程序时发生的事情。 0.1 程序的启动 当我们通过Explorer.exe运行一个程序时Explorer.exe会调用CreateProcess函数请求系统为这个程序创建进程。当然其它程序也可以调用CreateProcess函数创建进程。 系统在为进程分配内部资源建立独立的地址空间后会为进程创建一个主线程。我们可以把进程看作单位把线程看作员工。进程拥有资源但真正在CPU上运行和调度的是线程。系统以挂起状态创建主线程即主线程创建好不会立即运行而是等待系统调度。系统向Win32子系统的管理员csrss.exe登记新创建的进程和线程。登记结束后系统通知挂起的主线程可以运行新程序才开始运行。 这时在创建进程中CreateProcess函数返回在被创建进程中主线程在完成最后的初始化后进入程序的入口函数Entry-point。创建进程与被创建进程在各自的地址空间独立运行。这时即使我们结束创建进程也不会影响被创建进程。 0.2 程序的执行 可执行文件PE文件的文件头结构包含入口函数的地址。入口函数一般是Windows在运行时库中提供的我们在编译时可以根据程序类型设定。在VC中编译、运行程序的小知识点讨论了Entry-point读者可以参考。 入口函数前的过程可以被看作程序的装载过程。在装载时系统已经做过全局和静态变量在编译时可以确定地址的初始化有初值的全局变量拥有了它们的初值没有初值的变量被设为0我们可以在入口函数处设置断点确认这一点。 进入入口函数后程序继续运行环境的建立例如调用所有全局对象的构造函数。在一切就绪后程序调用我们提供的主函数。主函数名是入口函数决定的例如main或WinMain。如果我们没有提供入口函数要求的主函数编译时就会产生链接错误。 0.3 进程和线程 我们通常把存储介质例如硬盘上的可执行文件称作程序。程序被装载、运行后就成为进程。系统会为每个进程创建一个主线程主线程通过入口函数进入我们提供的主函数。我们可以在程序中创建其它线程。 线程可以创建一个或多个窗口也可以不创建窗口。系统会为有窗口的线程建立消息队列。有消息队列的线程就可以接收消息例如我们可以用PostThreadMessage函数向线程发送消息。 没有窗口的线程只要调用了PeekMessage或GetMessage系统也会为它创建消息队列。 1 窗口和消息 1.1 线程的消息队列 每个运行的程序就是一个进程。每个进程有一个或多个线程。有的线程没有窗口有的线程有一个或多个窗口。 我们可以向线程发送消息但大多数消息都是发给窗口的。发给窗口的消息同样放在线程的消息队列中。我们可以把线程的消息队列看作信箱把窗口看作收信人。我们在向指定窗口发送消息时系统会找到该窗口所属的线程然后把消息放到该线程的消息队列中。 线程消息队列是系统内部的数据结构我们在程序中看不到这个结构。但我们可以通过Windows的API向消息队列发送、投递消息从消息队列接收消息转换和分派接收到的消息。 1.2 最小的Windows程序 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 retGetMessage(sMsg, NULL, 0, 0)) { // 消息循环 if (ret ! -1) { TranslateMessage(sMsg); DispatchMessage(sMsg); } } return 0;
} 这个程序虽然只显示一个窗口但经常被用来说明Windows程序的基本结构。在MFC框架内部我们同样可以找到类似的程序结构。这个程序包含以下基本概念 窗口类、窗口和窗口过程消息循环下面分别介绍。 1.3 窗口类、窗口和窗口过程 创建窗口时要提供窗口类的名字。窗口类相当于窗口的模板我们可以基于同一个窗口类创建多个窗口。我们可以使用Windows预先登记好的窗口类。但在更多的情况下我们要登记自己的窗口类。在登记窗口类时我们要登记名称、风格、图标、光标、菜单等项其中最重要的就是窗口过程的地址。 窗口过程是一个函数。窗口收到的所有消息都会被送到这个函数处理。那么发到线程消息队列的消息是怎么被送到窗口的呢 1.4 消息循环 熟悉嵌入式多任务程序的程序员都知道任务相当于Windows的线程的结构基本上都是 while (1) { 等待信号; 处理信号; } 任务收到信号就处理否则就挂起让其它任务运行。这就是消息驱动程序的基本结构。Windows程序通常也是这样 while (int retGetMessage(sMsg, NULL, 0, 0)) { // 消息循环 if (ret ! -1) { TranslateMessage(sMsg); DispatchMessage(sMsg); } } GetMessage从消息队列接收消息TranslateMessage根据按键产生WM_CHAR消息放入消息队列DispatchMessage根据消息中的窗口句柄将消息分发到窗口即调用窗口过程函数处理消息。 1.5 通过消息通信 创建窗口的函数会返回一个窗口句柄。窗口句柄在系统范围内不是进程范围标识一个唯一的窗口实例。通过向窗口发送消息我们可以实现进程内和进程间的通信。 我们可以用SendMessage或PostMessage向窗口发送或投递消息。SendMessage必须等到目标窗口处理过消息才会返回。我试过如果向一个没有消息循环的窗口SendMessageSendMessage函数永远不会返回。PostMessage在把消息放入线程的消息队列后立即返回。 其实只有投递的消息才是通过DispatchMessage分派到窗口过程的。通过SendMessage发送的消息在线程GetMessage时就已经被分派到窗口过程了不经过DispatchMessage。 1.5.1 窗口程序与控制台程序的通信实例 大家是不是觉得“例程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_USER1, 0, 0); // 向控制台程序发送消息1 break; case 2: PostThreadMessage(tid, WM_USER2, 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_USER1: SendMessage(m_hWnd, WM_GETTEXT, sizeof(buf), (LPARAM)buf); printf(你现在叫:%s\n\n, buf); // 读取、显示对方的名字 break; case WM_USER2: 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 retGetMessage(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窗口按下键1wnd会向msg发送消息1msg收到后会通过WM_GETTEXT消息获得wnd的窗口名称并显示。我们如果在wnd窗口按下键2wnd会向msg发送消息2msg收到后会通过WM_SETTEXT消息修改wnd的窗口名称。 这个小例子演示了控制台程序的消息循环向线程发消息以及进程间的消息通信。 1.5.2 地址空间的问题 不同的进程拥有独立的地址空间如果我们在消息参数中包含一个进程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程序中试验一下。 2 子类化和超类化 窗口类是窗口的模板窗口是窗口类的实例。窗口类和每个窗口实例都有自己的内部数据结构。Windows虽然没有公开这些数据结构但提供了读写这些数据的API。 例如用GetClassLong和SetClassLong函数可以读写窗口类的数据用GetWindowLong和SetWindowLong可以读写指定窗口实例的数据。使用这些接口可以在运行时读取或修改窗口类或窗口实例的窗口过程地址。这些接口是子类化的实现基础。 2.1 子类化 子类化的目的是在不修改现有代码的前提下扩展现有窗口的功能。它的思路很简单就是将窗口过程地址修改为一个新函数地址新的窗口过程函数处理自己感兴趣的消息将其它消息传递给原窗口过程。通过子类化我们不需要现有窗口的源代码就可以定制窗口功能。 子类化可以分为实例子类化和全局子类化。实例子类化就是修改窗口实例的窗口过程地址全局子类化就是修改窗口类的窗口过程地址。实例子类化只影响被修改的窗口。全局子类化会影响在修改之后按照该窗口类创建的所有窗口。显然全局子类化不会影响修改前已经创建的窗口。 子类化方法虽然是二十年前的概念却很好地实践了面向对象技术的开闭原则OCPThe Open-Closed Principle对扩展开放对修改关闭。 2.2 超类化 超类化的概念更简单就是读取现有窗口类的数据保存窗口过程函数地址。对窗口类数据作必要的修改设置新窗口过程再换一个名称后登记一个新窗口类。新窗口类的窗口过程函数还是仅处理自己感兴趣的消息而将其它消息传递给原窗口过程函数处理。使用GetClassInfo函数可以读取现有窗口类的数据。 3 MFC中的消息循环和子类化 MFC将子类化方法应用得淋漓尽致是一个不错的例子。候捷先生的《深入浅出MFC》已经将MFC的主要框架分析得很透彻了本节只是看看MFC的消息循环简单分析MFC对子类化的应用。 3.1 消息循环 随便建立一个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程序主线程中的消息循环它把发送到线程消息队列的消息分派到线程的窗口。 3.2 子类化 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。下一节将介绍对话框控件的子类化。 4 子类化和超类化的例子 我写了一个很简单的对话框程序用来演示子类化和超类化。这个对话框程序有两个编辑框我将编辑框的右键菜单换成了一个消息框。两个编辑框的定制分别采用了子类化和超类化技术 4.1 子类化的例子 首先从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消息泵流入用户的处理函数。 4.2 必经之路PreSubclassWindow PreSubclassWindow是一个很好的定制控件的位置。如果我们通过重载CWnd::PreCreateWindow定制控件而用户在对话框中使用控件。由于对话框中的控件窗口是通过CreateDlgIndirect创建不经过CWnd::CreateEx函数PreCreateWindow函数不会被调用。 其实用户要在对话框中使用定制控件必须用DDX或者SubclassDlgItem函数子类化控件这时PreSubclassWindow一定会被调用。 如果用户直接创建定制控件窗口CWnd::CreateEx函数就一定会被调用控件窗口一定会被子类化以安装MFC消息泵。所以在MFC中PreSubclassWindow是创建窗口的必经之路。 4.3 超类化的例子 我很少看到超类化的例子除了罗云彬的Win32汇编在大多数应用中子类化技术已经足够了。但我还是写了一个例子CMyEdit2从CEdit派生。CMyEdit2::RegisterMe获取窗口类Edit的信息保存原窗口过程设置新窗口过程MyWndProc和新名称MyEdit登记一个新窗口类。新窗口过程MyWndProc定制自己需要处理的消息将其它消息送回原窗口过程。 我在对话框的OnInitDialog中先调用CMyEdit2::RegisterMe登记新窗口类然后创建窗口。这样创建窗口必须经过CWnd::CreateEx所以MFC还是会把窗口过程换成AfxWndProcBase。没有被MFC消息映射拦截的消息才会流入MyWndProc。 5 结束语 这篇文章介绍了一些Windows和MFC的基础知识。写这篇文章的目的不是介绍什么编程技巧而是让我们更了解程序运行时发生的事情。惟有深入其中方能跳出其外不受羁绊。