刚开始学Windows程序设计不久,今天看到了 “窗口与消息” 这一章节,其中有一个非常经典的Windows小程序。虽然程序很小,但是该程序包括了几乎所有Windows程序都有的一些常规而繁琐的步骤,比如:窗口类的初始化与注册、窗口的创建与显示,以及用于处理消息的窗口过程,和获取消息队列中消息的消息循环等。
程序完整代码如下:
#include
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT ("HelloWin") ;
HWND hwnd ;
MSG msg ;
WNDCLASS wndclass ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
hwnd = CreateWindow (szAppName, // window class name
TEXT ("The Hello Program"), // window caption
WS_OVERLAPPEDWINDOW, // window style
CW_USEDEFAULT, // initial x position
CW_USEDEFAULT, // initial y position
CW_USEDEFAULT, // initial x size
CW_USEDEFAULT, // initial y size
NULL, // parent window handle
NULL, // window menu handle
hInstance, // program instance handle
NULL) ; // creation parameters
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
RECT rect ;
switch (message)
{
case WM_CREATE:
PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
case WM_PAINT:
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY:
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam) ;
}
下面分段讲解:
首先是头文件为什么要加"windows.h"相信大家都已经很清楚,这里就不做解释了。
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM); //窗口过程函数声明语句
因为该窗口过程函数体定义是在WinMain后面定义的,而WinMain里面的初始化语句中引用了该函数,所以必须在WInMain前面做该函数的前置声明,以便让编译器知道有这个函数。
static TCHAR szAppName[] = TEXT ("HelloWin"); //定义一个静态TCHAR类型的以'\0'结尾的字符串
HWND hwnd; //定义一个窗口句柄变量
MSG msg; //定义一个消息结构
WNDCLASS wndclass; //定义一个窗口类结构
以上代码是定义一些对象变量 以备后期使用
//初始化窗口类各项
wndclass.style = CS_HREDRAW | CS_VREDRAW; //这种形式的往往是位标记 用按位或操作符可以将多个位标记组合起来
wndclass.lpfnWndProc = WndProc ; //函数指针 指向窗口过程(该行代码也就是用于将窗口类与特定窗口过程进行关联)
wndclass.cbClsExtra = 0 ; //设置一个类的额外空间
wndclass.cbWndExtra = 0 ; //设置一个窗口的额外空间
wndclass.hInstance = hInstance ; //设置一个应用程序实例句柄 这里为WinMain参数中的hInstance
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION); //基于该窗口类的所有窗口的图标 这里使用LoadIcon函数返回一个图标句柄
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW); //基于该窗口类的所有窗口的鼠标指针,这里使用LoadCursor函数返回一个鼠标指针句柄
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH); //设置客户区的背景颜色 这里调用GetStockObject函数并传入颜色常量值
//默认函数返回一个HGDIOBJ类型 这里将其强制转换成HBRUSH类型
wndclass.lpszMenuName = NULL ; //指定窗口类的菜单 因该小程序不带菜单则设为NULL
wndclass.lpszClassName = szAppName ; //设置一个窗口类的名称
以上代码是对窗口类各项设置指定值。
//失败检测及窗口类注册
if (!RegisterClass (&wndclass))
{
MessageBox (NULL, TEXT ("This program requires Windows NT!"),
szAppName, MB_ICONERROR) ;
return 0 ;
}
因为像Windows 98系统基本是不支持Unicode版本的函数的,如果有如下预编译语句:#define UNICODE 则RegisterClass将被替换为RegisterClassW,在Win98中是不支持的,会导致该函数失败返回 如果不进行处理的话,在后面将会出现未知错误,所以为了保险起见,可以用上面的代码进行检测,如果RegisterClass函数失败返回0(False)则!0为True并执行代码块里的MessageBox函数提示用户。当程序运行在Win98系统里,注册函数调用则会失败,执行代码块内提示语句,到这里可能会有人问,如果有#define UNICODE这行语句,则MessageBox不将替换成MessageBoxW了吗?而Win98系统不是不支持UNICODE版本的函数吗,怎么能在Win98系统里通过这个提示用户呢?我上面也说了,Windows 98系统里基本上是不支持Unicode版本的函数,但没说所有的Unicode版本函数都不支持,而MessageBoxW()函数则是Win98系统中所支持的少有的Unicode版本函数的其中一个,所以可以通过该函数来提示用户。
//创建窗口
hwnd = CreateWindow (
szAppName, //窗口类名 用于将该创建的窗口与指定窗口类进行关联
TEXT ("The Hello Program"), //窗口标题
WS_OVERLAPPEDWINDOW, //窗口风格
CW_USEDEFAULT, //窗口的左上角与屏幕左上角的X位置 这里使用默认值
CW_USEDEFAULT, //窗口的左上角与屏幕左上角的Y位置 这里使用默认值
CW_USEDEFAULT, //窗口宽度
CW_USEDEFAULT, //窗口高度
NULL, //这里是父类窗口的句柄,没有则为NULL
NULL, //这里是窗口菜单句柄 没有则为NULL
hInstance, //应用程序实例句柄 这里还是WinMain参数中的hInstance
NULL //创建参数
);
以上代码是创建一个基于某个窗口类的窗口,其中实参WS_OVERLAPPEDWINDOW实际上是一个#define 其定义如下:
#define WS_OVERLAPPEDWINDOW (WS_OVERLAPPED | \
WS_CAPTION | \
WS_SYSMENU | \
WS_THICKFRAME | \
WS_MINIMIZEBOX | \
WS_MAXIMIZEBOX)
也就是说,WS_OVERLAPPEDWINDOW是多个窗口风格标记位通过按位或运算符进行组合起来多种风格。
基于窗口类的窗口已经创建了,实际上,执行CreateWindow函数后,窗口已经在内部被创建,也就是说系统为这个窗口分配了内存用于存放窗口的一些信息,但是如果需要将该窗口显示到屏幕上,还需要两条语句:
ShowWindow (hwnd, iCmdShow);
UpdateWindow (hwnd);
ShowWindow函数第一个参数为要显示的窗口的句柄,第二个参数为显示模式,如:正常显示 最大化显示 最小化显示等
以下列出了最为普遍的三个显示模式:
SW_SHOWNORMAL 正常显示
SW_MAXIMIZE 最大化显示
SW_MINIMIZE 最小化显示
还有很多不同的显示模式,这里就不一一列举了,如果大家有需要的话,可以MSDN上看看: ShowWindows
接着我们看下以下代码:
//消息循环
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
这就是学习Windows程序经常听到的:消息循环语句 因为可以说Windows程序是一个消息响应程序,所以理解消息循环对理解Windows程序是很有帮助的。
Windows系统为每一个运行的Windows程序都维护了一个“消息队列”,当输入事件发生后,如:鼠标点击窗口上按钮,拖动窗口等,系统就会将这个事件转换成“消息”然后放到应用程序的“消息队列”中,“消息队列”只是用于存放消息的地方,而要取出这些消息并响应,就需要上面的消息循环语句了。
理解消息循环之前,我们先来看下MSG类型(消息结构)其定义类似如下:
typedef struct tagMSG{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
DWORD lPrivate;
}MSG;
其中POINT是另外一种结构 其定义类似于:
typedef struct tagPOINT{
LONG x;
LONG y;
}POINT;
这个结构主要是用于存放当消息被触发后鼠标指针的位置坐标。
消息结构(MSG)中各参数:
hwnd 是消息指向的窗口的句柄
message 存放消息的标识符 其类型是UINT即unsigned int 消息标识符以WM为前缀 每一个标识符都是一个数字
wParam 一个32位的“消息参数” 该参数的含义和取值取决于实现
lParam 一个32位的“消息参数” 该参数的含义和取值取决于实现
time 消息进入队列的时间 其类型是DWORD即unsigned long
pt 一个结构体 存放触发消息时的鼠标位置坐标
到这里相信大家已经了解MSG消息结构了,再来说下Windows系统中消息循环的机制,程序通过GetMessage函数从“消息队列”中取出一个消息,并将这个消息用于填充指定的MSG消息结构中的各个字段,如上面消息循环代码中的:GetMessage (&msg, NULL, 0, 0);函数就是将取出的消息用于填充参数中的msg 消息结构的各个字段,后面几个参数都是NULL或0,说明该程序希望接收由该程序所创建的所有窗口发来的消息。因为GetMessage函数的返回值类型为BOOL值,所以如果取出的消息不是VM_QUIT(其值为0x0012 “消息”实际上就是一个整型值 通过查看VM_QUIT等窗口消息标识符的定义可知)则返回非0值即true 并执行循环体内语句,如果取出的是VM_QUIT消息,则GetMessage函数返回0即false,终止循环,结束程序。虽然该函数返回的是BOOL值,若调用GetMessage函数的时候遇到了错误,则会返回-1,如hwnd窗口句柄无效等。
而且返回值可能是-1,所以上述消息循环代码并不推荐,因为如果返回的-1而-1是非零值即true,就也会进入循环体执行块内语句,这样可能会导致一些未知的问题,所以最好将消息循环代码改成如下形式:
BOOL bRet;
while((bRet = GetMessage(&msg,NULL,0,0)) != 0){
if(bRet == -1){
//错误处理代码 通常是直接退出程序
}else{
TranslateMessage (&msg);
DispatchMessage (&msg);
}
}
TranslateMessage (&msg);函数用于一些额外的处理,将虚拟按键信息转换成字符消息,执行完毕后,调用DispatchMessage (&msg);函数,该函数将消息传送到指定窗口句柄所关联的窗口过程程序中用于处理该消息。
那么窗口过程又是如何处理消息的呢?下面看如下窗口过程代码:
//窗口过程
LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
HDC hdc ; //定义一个设备环境句柄的变量
PAINTSTRUCT ps ; //定义一个PAINTSTRUCT的结构体变量
RECT rect ; //定义一个RECT的结构体变量
switch (message)
{
case WM_CREATE: //处理WM_CREATE消息
PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC) ;
return 0 ;
case WM_PAINT: //处理WM_PAINT消息
hdc = BeginPaint (hwnd, &ps) ;
GetClientRect (hwnd, &rect) ;
DrawText (hdc, TEXT ("Hello, Windows 98!"), -1, &rect,
DT_SINGLELINE | DT_CENTER | DT_VCENTER) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_DESTROY: //处理WM_DESTROY消息
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, message, wParam, lParam); //用于处理以上没有的窗口消息
}
首先,我们需要知道的是,一个应用程序可以包含多个窗口过程,但是每一个窗口过程都需要与某一个窗口类进行关联,而当基于窗口类创建的窗口触发了消息后,都将由该窗口类关联的窗口过程进行处理该消息。
窗口过程是由程序开发者自己所编写的,但是编写的所有窗口过程函数,都得如下形式定义:LRESULT CALLBACK WndProc (HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 其中WndProc是窗口过程函数的名称,这个名称可以随便写,但是必须遵循名称定义规则,而且与其他名称不重复即可。
窗口过程函数中的参数列表一共有4个参数,这4个参数都与MSG消息结构中的前4个参数一一对应,参数hwnd指的是消息所要处理的窗口句柄,参数message顾名思义就是用于接收消息类型的(标识符)第3和第4个参数是32位的消息参数,用于提供关于该消息更丰富的信息,所包含的内容随消息类型而异。
该程序通过使用switch-case来对相应消息进行处理,当窗口过程函数参数message接收的是一个WM_CREATE消息的话,则会执行PlaySound (TEXT ("hellowin.wav"), NULL, SND_FILENAME | SND_ASYNC);播放音频语句并返回0。
实际上,WM_CREATE消息也是一般程序所处理的第一个消息,当执行CreateWindow函数并处理了一些必要操作后(此时所创建的窗口句柄已经生成)Windows系统将自动调用与该创建的窗口关联的窗口过程函数并进行处理WM_CREATE消息。
当窗口过程函数所接收的消息是WM_PAINT,则过程函数始终将首先调用BeginPaint函数最后调用EndPaint函数,调用BeginPaint函数将一个窗口句柄和一个PAINTSTRUCT结构体类型指针作为参数,PAINTSTRUCT结构体主要记录用于重绘窗口时的一些信息,当指定窗口背景尚未被擦除时,则Windows会对其进行擦除,使用的画刷是WNDCLASS窗口类中的hbrBackground字段所代表的,在上述程序中,也就是用白色画刷擦除窗口背景。当该函数执行完毕后,将返回一个设备环境句柄给指定的hdc变量。
接着将执行GetClientRect (hwnd, &rect);函数,该函数也接受2个参数,一个是窗口句柄,另一个是RECT结构体类型的指针,RECT是由4个long型变量组成的矩形结构体,其定义类似如下:
typedef struct tagRECT
{
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT
函数将设置这4个字段,left和top字段始终被设置为0,此时bottom和right就可以分别以像素为单位表示当前已经更新的窗口尺寸宽和高了。
接着调用DrawText 函数,该函数在窗口中绘制一个文本,该函数有5个参数,第一个参数是指示设备环境句柄,这里正是上面调用BeginPaint函数所返回的,第二个参数指示需要文本输出的内容,第三个参数指示该内容是以0为结尾的字符串,第4个参数类型是RECT的矩形结构指针,指示需要在什么范围内进行绘制,这里是上面调用GetClientRect函数时所设置的rect矩形结构,最后一个参数是多个标记位进行组合的,说明绘制的这个文本是以单行垂直水平居中显示的,三个标记为及代表的函数分别为:
DT_SINGLELINE 单行显示
DT_CENTER 居中
DT_VCENTER 垂直居中
DrawText函数调用完毕后,将在窗口中间绘制一个单行指定内容的文本,然后调用EndPaint函数,该函数将使上面的设备环境句柄失效。
当点击窗口右上方的关闭按钮或应用程序菜单选项中的关闭选项卡 试图关闭窗口时,将触发WM_DESTROY消息,该消息将调用PostQuitMessage函数,然后系统会向消息队列发送一条WM_QUIT消息,然后立即返回执行return 0;语句,当消息循环获取到系统所发送过来的WM_QUIT消息后,将导致GetMessage函数返回0,循环终止,执行WinMain中的return语句并结束程序。
总结:
1.什么是消息?
消息是一个整型值,可以通过窗口过程函数中的message参数的类型可知(UINT类型),并且消息是Windows所发出的一个通知。
2.什么是消息队列?
消息队列是Windows对消息的接收所采取的一项措施,假设没有消息队列,那么某个消息正在处理的话,紧接着又产生了几条消息,那么后来产生的消息怎么处理呢?丢弃掉?肯定是不合理的,所以Windows采用消息队列可以将所产生消息加入到消息列队中,当某个消息执行完毕后,该消息从消息队列中删除,紧接着再通过消息循环处理队列中剩余的其他消息。
3.什么是消息循环?
消息循环是用于循环取出消息队列中的消息,并将消息执行一系列操作后发送到消息所关联的窗口过程中进行处理。
4.什么是窗口过程?
窗口过程是一个函数,用于处理Windows程序中所产生的一系列消息。