本文以孙鑫老师VC++教程中的程序为基础,详细讲解了Windows程序内部运行机制,相信可以帮助大家更好的理解Windows程序运行原理及相应的VC++程序设计。具体内容如下:
创建一个Win32应用程序步骤:
1、编写WinMain函数;
2、创建窗口(步骤如下):
a、设计(一个)窗口类(WNDCLASS)
b、注册(该)窗口类。
c、创建窗口。
d、显示并更新窗口。
3、编写消息循环。
4、编写窗口过程函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
|
//WinMain.cpp #include <windows.h> #include <stdio.h> LRESULT CALLBACK WinAzeProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ); int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance HINSTANCE hPrevInstance, // handle to previous instance LPSTR lpCmdLine, // command line int nCmdShow // show state ) { //设计一个窗口类 WNDCLASS wndcls; wndcls.cbClsExtra = 0; wndcls.cbWndExtra = 0; wndcls.hbrBackground = ( HBRUSH )GetStockObject(BLACK_BRUSH); wndcls.hCursor = LoadCursor(NULL, IDC_CROSS); wndcls.hIcon = LoadIcon(NULL, IDI_ERROR); wndcls.hInstance = hInstance; //应用程序实例句柄由WinMain函数传进来 wndcls.lpfnWndProc = WinAzeProc; wndcls.lpszClassName = "aze_003" ; wndcls.lpszMenuName = NULL; wndcls.style = CS_HREDRAW | CS_VREDRAW; RegisterClass(&wndcls); //注册窗口类 //创建窗口,定义一个变量用来保存成功创建后返回的句柄 HWND hwnd; hwnd = CreateWindow( "aze_003" , "first Application" , WS_OVERLAPPEDWINDOW, 0, 0, 600, 500, NULL, NULL,hInstance, NULL); ShowWindow(hwnd, SW_SHOWNORMAL); //显示窗口 UpdateWindow(hwnd); //刷新窗口 //定义消息结构体,开始消息循环 MSG msg; while ( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage(&msg); DispatchMessage(&msg); } return msg.wParam; } //编写窗口过程函数 LRESULT CALLBACK WinAzeProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { switch (uMsg) { case WM_CHAR: char szChar[20]; sprintf (szChar, "char code is %d" , wParam); MessageBox(hwnd, szChar, "char" , 0); break ; case WM_LBUTTONDOWN: MessageBox(hwnd, "mouse clicked" , "message" , 0); HDC hdc; hdc = GetDC(hwnd); //不能在响应WM_PAINT消息时调用 TextOut( hdc, 0, 50, "程序员之家!" , strlen ( "程序员之家!" ) ); ReleaseDC(hwnd, hdc); break ; case WM_PAINT: HDC hDC; PAINTSTRUCT ps; hDC = BeginPaint(hwnd, &ps); //BeginPaint只能在响应WM_PAINT消息是调用 TextOut(hDC, 0, 0, "http://www.sunxin.org" , strlen ( "http://www.sunxin.org" )); EndPaint(hwnd, &ps); break ; case WM_CLOSE: if ( IDYES == MessageBox(hwnd, "是否真的退出?" , "message" , MB_YESNO) ) { DestroyWindow(hwnd); } break ; case WM_DESTROY: PostQuitMessage(0); break ; default : return DefWindowProc(hwnd, uMsg, wParam, lParam); } return 0; } |
程序运行后显示界面如下:
窗口分为客户区(是窗口的一部分)与非客户区。
标题栏、菜单栏、系统菜单、最小(大)化框、可调边框统称为窗口的非客户区,由Windows系统管理;应用程序主要管理客户区的外观及操作(显示文字、绘制图形)。
对话框、消息框也是一种窗口;对话框上还包括许多子窗口:按钮、单选按钮、复选框、组狂、文本编辑框等。
2、窗口与句柄:
在Windows应用程序中,窗口是通过窗口句柄(HWND)来标识的;要对某个窗口进行操作,就必须要得到这个窗口的句柄。
句柄是Windows程序中一个重要的概念(图标句柄(HICON)、光标句柄(HCURSOR)、画刷句柄(HBRUSH))。
3、消息与消息队列:
Windows程序设计模式是一种事件驱动方式的程序设计模式,主要是基于消息的。(当系统感知到一事件时(如点击鼠标),系统会将这个事件包装成一个消息,投递到应用程序的消息队列中,然后应用程序从消息队列中取出消息并进行响应。在这个处理过程中,操作系统也会给应用程序“发送消息”。“发送消息”:实际指:操作系统调用程序中一个负责处理消息的窗口过程函数)
(1)消息:Windows中,消息由MSG结构体表示,如下:
1
2
3
4
5
6
7
8
9
10
|
//The MSG structure contains message information from a thread's message queue. typedef struct tagMSG { HWND hwnd; //消息所属的窗口,消息都是与窗口相关联的 UINT message; //the message identifier WPARAM wParam; //指定消息的附加消息 LPARAM lParam; //指定消息的附加消息 DWORD time ; //消息投递到队列中的时间 POINT pt; //鼠标的当前位置 } MSG, *PMSG; |
Windows中,消息是由一个个数值表示的;Windows将消息对应的数值定义为WM_XXX宏(WM:Window Message)的形式,XXX对应某种消息的英文拼写的大写形式。如:WM_LBUTTONDOWN:鼠标左键按下消息、WM_KEYDOWN:键盘按下消息、WM_CHAR:字符消息···
(2)消息队列:每一个Windows应用程序开始执行后,系统都会为改程序创建一个消息队列,这个消息队列用来存放改程序创建的窗口的消息。
(3)进队消息 与 不进队消息:
进队的消息将由系统放入到应用程序的消息队列中,然后由应用程序取出并发送;
不进队消息在系统调用窗口过程时,直接发送给窗口;
两者最终都是有系统调用窗口过程函数对消息进行处理。
4、WinMain函数
(一)MSDN上的WinMain函数定义如下(备有详尽的注释):
1
2
3
4
5
6
7
8
|
//The WinMain function is called by the system as the initial entry point for a Windows-based application. int WINAPI WinMain( HINSTANCE hInstance, // handle to current instance当前窗口句柄 HINSTANCE hPrevInstance, // handle to previous instance前一个打开的窗口句柄 LPSTR lpCmdLine, // command line 指定传递给应用程序的*命令行*参数 int nCmdShow // show state 指定窗口应该如何显示,如:最大(小)化、隐藏等 ); |
(二)窗口类的结构体的定义:
(1)本文程序中对应代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
|
typedef struct _WNDCLASS { UINT style; //指定*这一类型*窗口的样式,如:CS_HREDRAW、CS_VREDRAW、CS_NOCLOSE、CS_DBLCLKS WNDPROC lpfnWndProc; //函数指针,指向窗口过程函数(窗口过程函数是一回调函数) int cbClsExtra; //一般为0 int cbWndExtra; //同上 HINSTANCE hInstance; //指定包含窗口过程的程序的实例句柄 HICON hIcon; //指定窗口类的图标句柄 HCURSOR hCursor; //指定窗口类的光标句柄 HBRUSH hbrBackground; //指定窗口类的背景画刷句柄;当窗口发生重绘值,系统使用这里指定的画刷来查处窗口的背景 LPCTSTR lpszMenuName; //指定菜单资源的名字 **菜单并不是一个窗口** LPCTSTR lpszClassName; //指定窗口类的名字 } WNDCLASS, *PWNDCLASS; |
回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时有另一方调用的,用于该事件或条件进行响应。
回调函数的实现机制是:
①定义一个回调函数。
②提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者。
③当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。
针对Windows的消息处理机制,窗口过程函数被调用的过程如下:
①在设计窗口类的时候,将窗口过程函数的地址赋值给lpfnWndProc成员变量;
②调用RegisterClass(&wndclass)注册窗口类,那么系统就有了我们所编写的窗口过程函数的地址。
③当应用程序接收到某一窗口的消息时,调用DispatchMessage(&msg)将对消息回传给系统。系统则利用先前注册窗口类时得到的函数指针,调用窗口过程函数对消息进行处理。
提示:一个Windows程序可以包含多个窗口过程函数,一个窗口过程总是与某一个特定的窗口类相关联(通过WNDCLASS结构体中的lpfnWndProc成员变量指定),基于该窗口类创建的窗口使用同一个窗口过程。
lpfnWndProc成员变量的类型是WNDPROC,定义如下:
1
|
typedef LRESULT (CALLBACK* WNDPROC)( HWND , UINT , WPARAM , LPARAM ); //LRESULT=long, CALLBACK=_stdcall WNDPROC是函数指针类型。 |
注意:WNDPROC被定义为指向窗口过程函数的指针类型,窗口过程函数的格式必须与WNDPROC相同。
在VC++中,资源是通过标识符(ID)来标识的,同一个ID可以标识多个不同的资源(资源的ID本质上是一个整数)。如:菜单资源:IDM_XXX(M表示Menu)、图标资源:IDI_XXX(I表示图标)、按钮资源:IDB_XXX(B表示Button)
可以调用GetStockObject(int fnObject) 来得到系统的标准画刷。声明如下:
1
2
3
4
5
|
//The GetStockObject function retrieves a handle to one of the stock pens, brushes, fonts, or palettes. HGDIOBJ GetStockObject( int fnObject // stock object type ); |
GetStockObject函数:返回多种资源对象的句柄,如:画刷、画笔、字体、调色板等;
函数返回时,需进行类型转换。如:
1
|
wndcls.hbrBackground = ( HBRUSH )GetStockObject(BLACK_BRUSH); |
(2)注册窗口类:设计窗口类(WNDCLASS)后,需要调用RegisterClass函数对其进行注册,注册成功后,才可以创建该类型的窗口。声明如下:
1
2
3
4
|
ATOM RegisterClass( CONST WNDCLASS *lpWndClass // class data, 窗口类对象的指针 // Pointer to a WNDCLASS structure. You must fill the structure with the appropriate class attributes before passing it to the function. ); |
(3)创建窗口:CreateWindow函数声明如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
HWND CreateWindow( LPCTSTR lpClassName, // registered class name 即:窗口类WNDCLASS的lpszClassName成员指定的名称(必须先注册) LPCTSTR lpWindowName, // window name 指定窗口的名字 DWORD dwStyle, // window style 指定创建窗口的样式 如:WS_OVERLAPPEDWINDOW int x, // horizontal position of window int y, // vertical position of window int nWidth, // window width int nHeight, // window height HWND hWndParent, // handle to parent or owner window 指定被创建窗口的父窗口句柄 HMENU hMenu, // menu handle or child identifier HINSTANCE hInstance, // handle to application instance LPVOID lpParam // window-creation data 作为WM_CREATE消息的附加参数lParam传入的数据指针(一般为:NULL) ); |
如果窗口创建成功,CreateWindow函数将返回系统为该窗口分配的句柄;否则,返回NULL。
·注意:在创建窗口之前应先定义一个窗口句柄变量来接收创建窗口之后的句柄值。
显示及更新窗口:
(4)显示窗口:ShowWindow声明如下:
1
2
3
4
|
BOOL ShowWindow( HWND hWnd, // handle to window 该参数为成功创建窗口后返回的那个窗口句柄 int nCmdShow // show state 如:SW_HIDE、SW_SHOW、SW_SHOWNORMAL、SW_SHOWMINIMIZED、SW_SHOWMAXIMIZED·· ); |
(5)更新(刷新)窗口:UpdateWindow函数声明原型如下:
1
2
3
|
BOOL UpdateWindow( HWND hWnd // handle to window 创建成功后的窗口句柄 ); |
UpdateWindow函数通过发送一个WM_PAINT消息来刷新窗口,UpdateWindow将WM_PAINT消息直接发送给了窗口过程函数进行处理,而没有放到消息队列里面。
(三)、消息循环
窗口 创建、显示、更新后;需要编写一个消息循环,不断的从消息队列中取出消息,并进行响应。
GetMessage()函数:从消息队列中取出消息
1
2
3
4
5
6
|
BOOL GetMessage( LPMSG lpMsg, // message information 指向一个消息(MSG)结构体,GetMessage从线程的消息队列中取出的消息信息将保存在该结构体对象中 HWND hWnd, // handle to window 指定接收属于哪一个窗口的消息;NULL:用于接收属于调用线程的所有窗口的窗口消息 UINT wMsgFilterMin, // first message 指定获取打的消息的最小值 UINT wMsgFilterMax // last message 如果wMsgFilterMin=0和wMsgFilterMax=0,则接收所有消息 ); |
GetMessage函数接收到除WM_QUIT外的消息均返回非零值。
1
2
3
4
5
6
7
8
|
//消息循环代码,一般形式 MSG msg; while ( GetMessage(&msg, NULL, 0, 0) ) { TranslateMessage(&msg); //TranslateMessage函数将虚拟键消息*转换*为字符消息,被投递到调用线程的消息队列中,当下一次调用GetMessage函数时被取出 DispatchMessage(&msg); //DispatchMessage函数分派一个消息到窗口过程,有窗口过程函数对消息进行处理 //DispatchMessage实际上是将消息会传给操作系统,有操作系统调用窗口过程函数对消息进行处理(响应) } |
Windows应用程序的消息处理机制如下图所示:
Windows应用程序的消息处理过程:
(1)操作系统就收到应用程序的窗口消息,将消息投递到该应用程序的消息队列中。
(2)应用程序在消息循环汇总调用GetMessage函数从消息队列中取出一条一条的消息。取出消息后,应用程序可以对消息进行一些预处理,如:放弃对某些消息的响应,或者调用TranslateMessage产生新的消息。
(3)应用程序调用DisPatchMessage,将消息回传给操作系统。消息是由MSG结构体对象来表示的,其中就包含了接收消息的窗口的句柄。故:DisPatchMessage函数总能进行正确的传递。
(4)操作利用WNDCLASS结构体的lpfnWndProc成员保存的窗口过程函数的指针调用窗口过程,对消息进行处理(即“系统给应用程序发送了消息”)。
补充:
(1)从消息队列中获取消息还可以调用PeekMessage函数,函数原型如下:
1
2
3
4
5
6
7
|
BOOL PeekMessage( LPMSG lpMsg, // message information HWND hWnd, // handle to window UINT wMsgFilterMin, // first message UINT wMsgFilterMax, // last message UINT wRemoveMsg // removal options ); |
前四个参数与GetMessage函数的参数作用相同;
最后一个参数指定消息获取的方式;如果设为PM_NOREMOVE, 那么消息将不会从消息队列中被移除;如果设为PM_REMOVE, 那么消息将从消息队列中被移除(与GetMessage函数的行为一致)
(2)发送消息可以使用SendMessage和PostMessage函数。
SendMessage将消息直接发送给窗口,并调用该窗口的窗口过程进行处理;在窗口过程对消息处理完毕后,该函数才返回(SendMessage发送的消息为不进队消息)。
PostMessage函数将消息放入与创建窗口的线程相关联的消息队列后立即返回。
PostThreadMessage函数,用于向线程发送消息。
对于线程消息,MSG结构体中的hwnd成员为NULL。
(四)、编写窗口过程函数:用于处理发送给窗口的消息
1
2
3
4
5
6
|
LRESULT CALLBACK WindowProc( //窗口过程函数的名字可以随便取,如:WinAzeProc,但函数声明与定义要一致; HWND hwnd, // handle to window UINT uMsg, // message identifier 消息代码 WPARAM wParam, // first message parameter 消息代码的两个附加值 LPARAM lParam // second message parameter ); |
提示:系统通过窗口过程函数的地址(指针)来调用窗口过程函数,而不是名字。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
//编写窗口过程函数 LRESULT CALLBACK WinAzeProc( HWND hwnd, // handle to window UINT uMsg, // message identifier WPARAM wParam, // first message parameter LPARAM lParam // second message parameter ) { switch (uMsg) { case WM_CHAR: //通过调用TranslateMessage函数转换得到 char szChar[20]; sprintf (szChar, "char code is %d" , wParam); MessageBox(hwnd, szChar, "char" , 0); break ; case WM_LBUTTONDOWN: MessageBox(hwnd, "mouse clicked!" , "message" , 0); HDC hdc; hdc = GetDC(hwnd); //用hdc保存GetDC函数返回的与特定窗口相关联的DC的句柄。 //GetDC()不能在响应WM_PAINT消息时调用 TextOut( hdc, 0, 50, "程序员之家!" , strlen ( "程序员之家!" ) ); //TextOut利用得到的DC句柄在指定的位置(0,50)出输出一行文字 ReleaseDC(hwnd, hdc); //释放hdc break ; case WM_PAINT: //当窗口客服区的一部分或者全部变为“无效”时,系统会发送WM_PAINT消息,通知应用程序重新绘制窗口 //窗口刚创建时,客户区是无效状态,当调用UpdateWindow函数时,会发送WM_PAINT消息给窗口过程,对窗口进行刷新 //当窗口从无到有、改变尺寸、最小化在恢复、被其他窗口遮盖后在显示时,窗口的客户区都将变为无效,此时系统会给应用程序发送WM_PAINT消息,通知应用程序重新绘制 //提示:窗口大小发生变化时,是否发生重绘,取决于WNDCLASS结构体中style成员是否设置了CS_HREDRAW和CS_VREDRAW标志 HDC hDC; PAINTSTRUCT ps; //ps用于接收绘制的信息 hDC = BeginPaint(hwnd, &ps); //BeginPaint只能在响应WM_PAINT消息是调用 TextOut(hDC, 0, 0, "http://www.sunxin.org" , strlen ( "http://www.sunxin.org" )); EndPaint(hwnd, &ps); break ; case WM_CLOSE: if ( IDYES == MessageBox(hwnd, "是否真的退出?" , "message" , MB_YESNO) ) { DestroyWindow(hwnd); } break ; case WM_DESTROY: PostQuitMessage(0); break ; default : return DefWindowProc(hwnd, uMsg, wParam, lParam); //DefWindowProc调用默认的窗口过程,对应用程序没有处理的其他消息提供默认处理。 //对于大多数的消息,应用程序可以直接调用DefWindowProc函数进行处理。 //在编写窗口过程时,应将DefWindowProc函数的调用放到default语句中,并将该函数的返回值作为窗口过程函数的返回值。 } return 0; } |
提示:要在窗口中输出文字或者显示图形,需要用到设备描述表(Device ConText)。
设备描述表(简称DC):
DC是一个包含设备(物理输出设备,如显示器、设备驱动器)信息的结构体,在Windows平台下,所有的图形操作都是利用DC来完成的。
第30、31行代码:在调用BeginPaint时,如果客户区的背景还没有被擦除,那么BeginPaint会发送WM_ERASEBKGND消息给窗口,系统就会使用WNDCLASS结构体的hbrBackGround成员指定的画刷来擦除背景。如果我们想要让某个图形时钟在窗口中显示,就应该将图形的绘制操作放到响应WM_PAINT消息的代码中,如TextOut()的位置。
第34-48行代码:DestroyWindow函数在销毁窗口后会向窗口过程发送WM_DESTROY消息。注意:此时窗口虽然销毁了,但应用程序并没有退出。故:如果自己要控制程序是否退出,应该在WM_CLOSE消息的响应代码中完成。
对WM_CLOSE消息的响应并不是必须的,如果应用程序没有对该消息进行响应,系统将把这条消息传给DefWindowProc函数,而DefWindowProc函数则条用DestroyWindow函数来响应 这条WM_CLOSE消息。
第40-42行代码:DestroyWindow函数在销毁窗口后,会给窗口过程发送WM_DESTROY消息, 然后在该消息的响应代码中调用PostQuitMessage函数。PostQuitMessage函数项应用程序的消息队列中投递一条WM_QUIT消息并返回。GetMessage函数只有在收到WM_QUIT消息时才返回0,此时消息循环才结束,程序退成。
想让程序正常退出,我们必须响应WM_DESTROY消息,并在消息响应代码中调用PostQuitMessage,向应用程序的消息队列中投递WM_QUIT消息。传递给PostQuitMessage函数的参数值将作为WM_QUIT消息的wParam参数,这个值通常用做WinMain函数的返回值。