一、引言
要想熟练掌握Windows应用程序的开发,首先需要理解Windows平台下程序运行的内部机制,然而在.NET平台下,创建一个Windows桌面程序,只需要简单地选择Windows窗体应用程序就可以了,微软帮我们做了非常好的封装,以至于对于很多.NET开发人员至今也不清楚Windows 平台下程序运行的内部机制,所以本专题将深入剖析下Windows 程序的内部运行机制。
二、Windows平台下几个基础概念
有朋友会问,理解了程序运行的内部机制有什么用,因为在我们实际开发中用得微软提供的模板来进行编程?对于这个疑问,我的回答是——理解了Windows平台下程序的运行内部机制可以使我们更有自信地写代码,因为我们知道模板后台帮我们封装的内容,并且理解这点也是打好了基础,基础打好了,学习新的知识也就快了。
2.1 窗口与句柄
窗口是Windows应用程序中非常重要的一个元素,一个Windows应用程序至少要有一个窗口,称为主窗口,窗口是我们看到的一块矩形区域,它是与用户进行交互的接口,利用窗口可以接受用户的输入以及对用户输入的响应。例如我们看到的QQ登陆界面就是一个窗口。窗口又可分为客户区和非客户区,如下图所示,其中,客户区通常用来显示控件或文字,标题栏、系统菜单,菜单栏、最小化框和最大化框、可调边框都称为窗口的非客户区,它们主要由Windows系统进行管理,我们创建的应用程序主要负责客户区的外观显示和操作,窗口也可以有一个父窗口,并且,对话框和消息框都是属于窗口。
在Windows应用程序中,句柄是用来唯一标识窗口的,我们想对某个窗体进行操作时,必须首先获得该窗口的句柄,句柄还包括图标句柄(如上图中Form1前小图标),光标句柄(即移到窗体时显示的光标),和画刷句柄(上图中客户区中颜色就是通过指定窗口类的背景画刷句柄进行设置的,关于窗口类的结构会在下面介绍)。对于句柄的理解,大家简单理解为用来标识窗体,它的类型为struct,这点可以通过在VS中通过F12查看HWND的定义。
2.2 消息与消息队列
Windows 操作系统是基于事件驱动的一种操作系统,所以在Windows平台下所有应用程序也是基于事件驱动机制,即是基于消息的。例如,当用户在窗口中按下鼠标左键时,操作系统会知晓这一事件,于是将事件封装成一个消息,传递到应用程序的消息队列中,,然后应用程序从消息队列中取出消息并进行响应。在这个处理过程中,操作系统会调用应用程序中专门负责消息处理的函数,该函数称为窗口过程。
1. 消息
消息是由MSG结构体表示的,MSG的结构体定义如下(也可以参考MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms644958(v=vs.85).aspx):
// MSG
typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG, *LPMSG;
该结构体中各参数的含义如下:
- hwnd——表示消息所属的窗口,我们通常开发的窗口应用程序中,一个消息都是与某个窗口相关联的。
- message——指定消息的标识符,在Windows中,消息是由一个数值来表示的,不同消息对应于不同的数值,但是由于数值不便于记忆,所以Windows将消息对应的数值定义为宏,为了使开发人员明白定义的宏为一个消息时,把消息宏都定义以WM(Windows Message的缩写)为前缀。例如 WM_CHAR表示字符消息,WM_LBUTTONDOWN表示鼠标左键按下消息。要想知道每个宏对应的数组,可以在VS中用F12 来查看宏对应的数值。
- wParam和lParam——用于指定消息的附加信息。关于每个消息的附件信息可以参考MSDN中相关信息的说明文档,这里列出WM_CHAR消息的说明文档:http://msdn.microsoft.com/en-us/library/windows/desktop/ms646276(v=vs.85).aspx
- time——表示消息被传送到消息队列的时间
- pt——表示当消息被传送时光标在屏幕中的位置
2. 消息队列
每一个Windows应用程序开始执行后,系统都会为该程序创建一个消息队列(从而得出消息队列是由系统创建的)来存放该程序创建过程中的窗口消息。当用户在窗口中发送一个消息时,系统会将该消息推送到消息队列中,而应用程序的过程函数则通过一个消息循环不断地从消息队列中取出消息,并进行响应。这种消息机制,就是Windows程序运行的机制。
在Windows程序中,消息又可分为“进队消息”和“不进队消息”。进队的消息由系统放入到应用程序的消息队列中,然后由应用程序取出并发送给窗口过程处理。不进队的消息由系统直接调用窗口过程进行处理。
三、动手实现第一个Windows桌面程序
下面,让我们手动要完成一个完整的Win32桌面程序,该程序实现的功能就是简单地创建一个窗体,并在窗体中响应键盘及鼠标消息,实现该程序的步骤可分为:
- WinMain函数的定义
- 创建一个窗口
- 进行消息循环,从消息队列中获得一个消息
- 编写窗口过程函数,用于处理消息。
3.1 WinMain函数的定义
当Windows启动一个桌面程序时,它调用的就是该程序的WinMain函数,该函数是Windows桌面程序的入口函数,与控制台中的main函数的作用相同。WinMain函数的声明如下所示(也可以参考MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms633559(v=vs.85).aspx):
int WINAPI WinMain( _In_ HINSTANCE hInstance, _In_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow );
上面我列出的定义与MSDN略有不同,MSDN中定义使用的是CALLBACK,而我上面列出的是WINAPI,其实两者都是一样的,可以在VS中通过F12查看宏的定义可以发现:
#define CALLBACK __stdcall #define WINAPI __stdcall
它们都是代表_stdcall,_stdcall是一种函数调用方式,__stdcall 调用约定用来调用 Win32 API 函数,更多介绍可以参考MSDN:http://msdn.microsoft.com/zh-cn/library/zxk0tw93(v=vs.120).aspx。
WinMain函数的4个参数是由系统调用WinMain函数时,传递给应用程序应用程序的,它们具体的含义为:
- hInstance——表示该程序当前运行实例的句柄,当程序在Windows平台下运行时,该值唯一标识着运行中的实例,这里需要朱注意:一个应用程序可以运行多个实例,每运行一个实例,系统都会为该实例分配一个句柄值,并通过hInstance参数传递给WinMain函数(从这句话可以得出,WinMain函数并不是程序调用的第一个函数,你在VS的调用堆栈中可以发现,在WinMain函数之前还有:WinMainCRTStartup()和_tmainCRTStartup()函数)。这里的句柄应该与窗口句柄区分开来,hInstance参数代表的是应用程序实例的句柄,而一个应用程序可以有多个窗口,每个窗口都对应一个句柄;
- hPrevInstance——表示当前实例的前一个实例的句柄。在Win32环境下,它的值总是为NULL;
- lpCmdLine——表示一个以空终止的字符串,指定传递给应用程序的命令行参数,例如:你双击一个Word文件,此时此时将该文件的路径作为命令行参数传递给Word应用程序,安装的Word应用程序得到该文件的路径后,就在窗口中打开文件的内容。我们可以在VS中通过属性——>调试——>命令参数来编辑想输入的命令参数;
- nCmdShow——指定窗口应该如何显示,如最大化、最小化等。如.NET中Form的WindowState属性。
3.2 创建一个窗口
创建一个完整的窗口,需要经过下面4个步骤:
- 设计一个窗口类
- 注册窗口类
- 创建窗口类
- 显示及更新窗口。
上面四个步骤,仔细想想你也知道的,我们想创建一个窗口,首先应该设计下它长什么样子吧(第一步),设计完成之后,总要让系统知道已经设计完了窗口了吧,所以我们要通过注册窗口类的方式来通知系统(第二步),成功注册之后,系统已经知道存在这样一个窗口了,接下来就应该创建窗口类的一个实例了(第三步),最后就是把创建完的窗口显示和再加修饰下(第四步)。这四步完全来源我们生活,例如,上司找你做一个东西出来,你首先要在脑海中构想它的样子(设计,第一步),设计完之后,要让老板知道你设计完成了就应该告知老板你设计完了(第二步),老板知道之后,老板觉得可以就命令工厂把模型做出来(第三步),最后就是拿给客户看(第四步)。下面我们按照这4步来完成一个窗口的创建。
3.2.1 设计一个窗口类
Windows已经为我们定义好了一个窗口类,它的具体定义如下,你也可以自我查看MSDN:http://msdn.microsoft.com/en-us/library/windows/desktop/ms633576(v=vs.85).aspx。
typedef struct tagWNDCLASS { UINT style; // 窗口的样式,如CS_HREDRAW表示当窗口水平向上的宽度发生变化时,将重绘整个窗口 WNDPROC lpfnWndProc; // 一个函数指针,指向窗口过程函数,用于对事件进行响应,该函数是回调函数,可以对照委托回调进行理解 int cbClsExtra; // 类的附加内存,用于存储类的附加信息,一般把该参数设置为0 int cbWndExtra; // 窗口的附加内存,一般也设置为0 HINSTANCE hInstance; // 包含窗口的应用程序实例句柄 HICON hIcon; // 窗口类的图标句柄,如果设置为NULL,那么系统会提供一个默认的图标 HCURSOR hCursor; // 窗口类的光标 HBRUSH hbrBackground;// 窗口类的背景画刷句柄, LPCTSTR lpszMenuName; // 指定菜单资源的名字 LPCTSTR lpszClassName; // 指定窗口类的名字 } WNDCLASS, *PWNDCLASS;
在程序中,我们创建一个窗口类对象,然后为该对象指定其属性来完成窗口类的设计。
3.2.2 注册窗口类
设计完窗口类之后,我们需要使用RegisterClass(CONST WNDCLASS *lpWndClass)函数来完成窗口类的注册,注册成功之后,我们才可以创建该类型的窗口。
3.2.3 创建窗口
注册窗口类之后,即已经告知系统,我们已经存在这样的一个窗口类,下面可以使用CreateWindow()函数来创建该类型的一个窗口,CreateWindow函数的定义如下:
HWND WINAPI CreateWindow( _In_opt_ LPCTSTR lpClassName,// 注册窗口类的名字 _In_opt_ LPCTSTR lpWindowName, // 窗口名,如果窗口样式指定了标题栏,那么窗口名将显示在标题栏上 _In_ DWORD dwStyle, // 指定创建窗口的样式 _In_ int x,// 窗口左上角x坐标 _In_ int y, // 窗口左上角y坐标 _In_ int nWidth,// 窗口宽度 _In_ int nHeight,// 窗口高度 _In_opt_ HWND hWndParent,// 窗口的父窗口句柄 _In_opt_ HMENU hMenu,// 窗口菜单句柄 _In_opt_ HINSTANCE hInstance,// 包含窗口的应用程序实例 _In_opt_ LPVOID lpParam // 作为WM_CREATE消息的附加参数lParam传入给窗口,WM_CREATE有两个参数:wParam和lParam参数,更多内容参考MSDN:http://msdn.microsoft.com/en-us/library/ms632619(v=vs.85).aspx );
3.2.4 显示及更新窗口
创建窗口后,最好一步需要做的就是将窗口展示给用户看,我们可以通过ShowWindow()和UpdateWindow()函数来完成,ShowWindow()函数来设置窗口的特殊状态,调用完ShowWindow()之后,接下来调用UpdateWindow()函数来刷新窗口。即把设置好的窗口绘制在桌面上。调用UpdateWindow()函数之后将发送一个WM_PAINT消息给窗口过程函数来进行处理,从而来刷新窗口,注意,该WM_PAINT消息是没有放到前面介绍的消息队列中,属于不入队的消息。
3.3 进行消息循环,从消息队列中获得一个消息
接下面,我们需要实现一个消息循环函数,来完成不断从消息队列中取出消息,并交给窗口过程函数进行处理。我们可以通过Windows API 中GetMessage()函数来完成这个过程,下面是该函数的原型:
BOOL WINAPI GetMessage(
_Out_ LPMSG lpMsg, // 输出参数,指向消息的结构体指针
_In_opt_ HWND hWnd, // 指定接收从哪个窗口的消息
_In_ UINT wMsgFilterMin,// 获取消息的最小值,通常设置为0
_In_ UINT wMsgFilterMax// 获取消息的最大值。如果wMsgFilterMin和wMsgFilterMax都设置为0时,表示接收所有消息
);
GetMessage()函数除了接收WM_QUIT消息(接收WM_QUIT消息返回0)外,接收其他函数都返回非零值,如果出现错误则返回-1。如参数hWnd为无效句柄时(即传递NULL),此时发送错误返回为-1.
3.4 编写窗口过程函数,用于处理消息
前面都涉及到对消息的处理,下面就完成最后一步——窗口过程函数的实现,该函数为回调函数,窗口过程函数的声明如下,(也可以参考MSDNhttp://msdn.microsoft.com/en-us/library/windows/desktop/ms633573(v=vs.85).aspx):
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,// 处理消息的窗口句柄
_In_ UINT uMsg,// 消息代码
_In_ WPARAM wParam,
_In_ LPARAM lParam // wParam和lParam是消息的附加参数
);
3.5 完整的实现代码
有了上面的实现思路之后,那么实现该程序将再简单不过了,同时,大家可以根据下面代码来对比理解下上面介绍的理论,具体实现代码如下(这里需要指明一点,如果不小心把回调函数的实现的名字输入错误时,将出现如下图所示的错误):
// 手动实现一个Windows 程序 #include <Windows.h> #include <stdio.h> // 定义窗口过程函数,这里可以设置为你想要的名字 LRESULT CALLBACK WinProc( HWND hwnd, // 窗口句柄 UINT uMsg,// 消息代码 WPARAM wParam, // 第一个消息参数 LPARAM lParam // 第二个消息参数 ); int WINAPI WinMain( HINSTANCE hInstance, // 当前运行实例的句柄 HINSTANCE hPrevInstance, // 当前实例的前一个实例句柄 LPSTR lpCmdLine,// 指定传递给应用程序的命令行参数 int nCmdShow // 指定程序的窗口如何显示,例如最大化、最小化等 ) { // 1. 设计一个窗口类 WNDCLASS wndclass; wndclass.cbClsExtra=0; wndclass.cbWndExtra=0; wndclass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH);// 指定背景画刷 wndclass.hCursor =LoadCursor(NULL,IDC_CROSS);// 指定窗口类的光标句柄 wndclass.hIcon=LoadIcon(NULL,IDI_ERROR); wndclass.hInstance=hInstance; wndclass.lpfnWndProc=WinProc; wndclass.lpszClassName=L"learninghard2013"; // 设置窗口类的名称 wndclass.lpszMenuName=NULL;// 设计窗口类创建的窗口没有默认的菜单 wndclass.style=CS_HREDRAW|CS_VREDRAW;// 设置窗口样式为宽度和高度变化时,将重新绘制整个窗口 // 2. 注册窗口类 RegisterClass(&wndclass); // 3. 创建窗口,定义一个变量来保存成功创建窗口后返回的句柄 HWND hwnd; hwnd =CreateWindow(L"learninghard2013",L"手动实现窗口应用程序",WS_OVERLAPPEDWINDOW,100,100,600,400,NULL,NULL,hInstance,NULL); // 4. 显示和刷新窗口 ShowWindow(hwnd,SW_SHOWNORMAL); UpdateWindow(hwnd); // 定义消息结构体,开始消息循环 MSG msg; BOOL breturn; // GetMessage接受到WM_QUIT消息时返回为0,即为假 while((breturn=GetMessage(&msg,hwnd,0,0))!=0) { if(breturn==-1) { // 出错时退出 return -1; } else { // 接受到消息不为WM_QUIT消息的情况 // 将虚拟键消息转化为字符消息,字符消息被传递到调用线程的消息队列中,当下一次调用GetMessage函数被取出 TranslateMessage(&msg); // 分发一个消息到窗口过程,由窗口过程函数对消息进行处理 DispatchMessage(&msg); } } return msg.wParam; } // 实现窗口过程函数 LRESULT CALLBACK WinProc(HWND hwnd, UINT uMsg, WPARAM wParam,LPARAM lParam) { switch(uMsg) { case WM_CHAR: WCHAR szChar[20]; swprintf(szChar,L"字符代码是 %d",wParam); MessageBox(hwnd,szChar,L"字符",0); break; case WM_LBUTTONDOWN: MessageBox(hwnd,L"鼠标点击",L"消息",0); swprintf(szChar,L"消息附加信息 %d",wParam); MessageBox(hwnd,szChar,L"消息",0); HDC hdc; hdc =GetDC(hwnd); TextOut(hdc,0,50,L"LearningHard实现",wcslen(L"LearningHard实现")); ReleaseDC(hwnd,hdc); break; case WM_PAINT: HDC hDC; PAINTSTRUCT ps; hDC =BeginPaint(hwnd,&ps); // BeiginPaint只能在WM_PAINT消息时调用 TextOut(hDC,0,0,L"http://www.cnblogs.com/zhili/",wcslen(L"http://www.cnblogs.com/zhili/")); EndPaint(hwnd,&ps); break; case WM_CLOSE: if(IDYES==MessageBox(hwnd,L"是否真的结束",L"消息窗口",MB_YESNO)) { DestroyWindow(hwnd); } break; case WM_DESTROY: PostQuitMessage(0); break; default: // 调用默认的窗口过程 return DefWindowProc(hwnd,uMsg,wParam,lParam); } return 0; }
输入上面的代码在VS中,按下Ctrl+F5按钮运行程序,你将看到下面的窗口(你可以测试在窗口点击的效果和键盘按下效果):
四、小结
本专题介绍了Windows程序运行的内部机制,了解了本专题的内容,相信对.NET中WinForm应用程序背后实现的原理将不再陌生了。这里再总结下纯手动创建Windows 桌面程序的步骤:
- 查看MSDN查找WinMain的声明并编写应用程序中的WinMain函数
- 设计窗口类,查看WNDCLASS
- 注册窗口类,设计RegisterClass()函数
- 创建窗口,设计CreateWindow()函数
- 显示并更新窗口,设计ShowWindow()和UpdateWindow()函数
- 实现消息循环,从消息队列中取出消息交给窗口过程函数处理,设计GetMessage()函数
- 实现窗口过程函数,查看WindowProc函数。
本专题所有源代码下载:Windows程序运行的内部机制源码