Windows SDK编程(Delphi版)之消息处理
Windows的目的是使那些熟悉系统基本知识的人能够坐下来,不必进行任何预训练,就能实际运行任何应用程序。为实现此目的,Windows向用户提供 了一些始终不变的接口。理论上来说,如果用户能运行起Windows,那么也就能运行那些基于那种接口的所有程序。而我们作为程序员的职责就是用 Windows提供的这些接口来开发基于Windows的应用程序。Windows提供的这些接口,我们通俗的称为Win32 SDK API。
为许多操作系统编写程序时,是你的程序使其与操作系统发生作用。比如在DoS程序中,是程序在要求做诸如是输入和输出这样的工作。不同的是,用传统的方法 编写的程序调用操作系统,操作系统却不调用你的程序。不过在大多数情况下,windows以相反的方式进行工作,既Windows会来调用你的程序。其过 程为:Windows一直处于等待状态,直到由Windows发送一条消息,该消息通过由Windows调用的特殊函数传送到用户的程序中。只要程序接收 到一条消息,它就会产生相应的动作,这便是Windows应用程序的的驱动方式,我称它为消息驱动模型。
Windows编程基础
入口函数WinMain:
以前学过C的人都晓得,C中的入口就是main()函数,任何程序开始,都由main进入开始执行,在Windows的编程中,也一样有一个入口函数,本 函数与C的入口函数差不多,不过是WinMain,所有的Windows程序都由Winmain入口开始往下执行。另外,WinMain有一个调用约定, 必须制定为WINAPI约定。和C的main返回一样,返回整形。Windows的标准语言是C,所以这个说明是针对于C版本的,那么我们Delphi程 序员呢?如果有人看过Delphi的工程文件dpr里面的内容的话,就应该晓得,Delphi的入口是由dpr文件的begin end之间入口开始往下执行。这个我们可以看着为Delphi的编译器的一个魔法,它将WinMain函数在Delphi的内部给我们已经制定好了,编译 的时候自动进入Winmain,然后再Winmain中进入begin end之间开始我们的窗口过程,所以我们可以将begin end这个看做为WinMain的一个子部分,那么就可以简单的理解为这个就相当于是一个WinMain了,同时也不用我们自己去声明一个WinMain 这样的入口函数,而只需在工程文件的begin .. end之间写我们的代码就可。
窗口过程
首先 窗口过程是由Windows调用的,而不是由我们程序自己调用的函数。 所有的Windows程序必须包括一些特殊的函数,他们不由我们程序自己调用,而是由Windows操作系统来调用。这个函数通常被称为窗口过程或者窗口 函数。当Windows需要向程序中传递一条消息时,Windows将调用窗口函数来处理这个消息。窗口函数在它的参数内接受消息。所有的窗口函数的返回 类型为LRESULT,调用约定为CALLBACK。LRESULT实际上就是一个整形,在Delphi中可以用LongInt来表示。调用约定 CallBack表示此过程属于系统回调函数,在Delphi中用Stdcall约定就行。实际上凡是关乎操作系统的相关函数都指定为Stdcall调用 约定。一个窗口函数过程内部,我们通常能够看到一个很大的Case end这样的结构。用来标记对不同的消息做不同的处理。Windows的消息有很多很多个,很多时候,我们不必为我们关心的消息提供消息处理过程,此时我 们就可以使用Windows的默认处理过程为DefWindowProc函数,用这个函数就可以按照系统的默认方式进行处理。
窗口类
这里说的窗口类,是指窗口的类型和样式,而不是我们面向对象中所说的那个类了,这个需要分解清楚。Windows的应用程序都是由一个个的窗口组成的。而 要生成这些窗口,我们必须先注册一个窗口类给系统,这样以后创建窗口的时候,就会根据你所给定的样式等信息进行创建了。
消息循环
在前面说了Windows是消息驱动程序运行的,在每个应用程序中,都有一个自己的消息队列,从应用程序的消息队列中不断的取回消息构建了消息循环运转。 所以所有的应用程序都必须在内部建立一个消息循环,此循环从应用程序的消息队列中读取任何未处理的消息,然后将它送还给Windows,这样以该消息作为 参数的就会被对应的窗口过程调用以对该消息进行处理。
通过前面几点的了解,应该大致的晓得了一个Windows应用程序所具备的几个要素和步骤了:
1、声明入口函数(这个在Delphi中没有)
2、定义一个窗口类
3、注册窗口类
4、创建窗口
5、显示窗口
6、开始消息循环。
通过这6个步骤,就基本上能够实现一个Windows的视窗应用程序了。下面我来给一个样本程序
program Project1;
uses
WIndows,
messages;
//窗口过程
function WndProc(hwnd: THandle;MsgId: Longint;wParam: WParam;lParam: LParam): LRESULT;stdcall;
var
dc: HDC;
r: TRect;
str: string;
hi: Word;
PaintStruct: TPaintStruct;
begin
Case msgId of
WM_DESTROY:
begin
PostQuitMessage(0);{释放的时候,发送退出消息循环,程序结束}
result :=1;
exit;
end;
result := DefWindowProc(Hwnd,MsgId,WParam,LParam);
exit;
end;
end;
procedure WinMain(HthisInstance: LongInt);
var
msg: Tmsg;
MainHwnd: THandle;
WndClass: TWNdClassex;
begin
WndClass.cbSize := Sizeof(WNdClass);//指定结构大小
WndClass.hInstance := HThisInstance;//指定宿主为当前应用程序的实例
WndClass.lpszClassName :='DxWindow';//指定类名
WndClass.lpfnWndProc := @WndProc;//指定窗口过程
WndClass.style :=0;//CS_VREDRAW or CS_HREDRAW;//指定样式为普通样式
WndClass.hIcon := LoadIcon(0,IDI_Application);//普通图标32*32大小的
WndClass.hIconSm := LoadIcon(0,IDI_WINLogo);//指定小图标16*16的
WndClass.hCursor := LoadCursor(0,IDC_Arrow);//指定光标
WndClass.lpszMenuName :=nil;//指定菜单
WndClass.cbClsExtra :=0;
WndClass.cbWndExtra :=0;
WndClass.hbrBackground := CreateSolidBrush(RGB(236,233,216));//GetStockObject(White_Brush);
if RegisterClassex(WndClass) <>0then
begin
MainHwnd := CreateWindowEx(0,WndClass.lpszClassName,'测试窗口标题',WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,0,0,hTHisInstance,nil);
if MainHwnd <>0then
begin
ShowWindow(MainHwnd,sw_ShowNormal);
UpdateWindow(MainHwnd);
while GetMessage(msg,0,0,0) do
begin //这里开始消息循环
TranslateMessage(msg);
DIspatchMessage(msg);
end;
ExitCOde := Msg.wParam;//退出
end;
end;
end;
begin
WinMain(Hinstance);
end.
本程序,就是一个最基本的Windows程序,本程序不包含任何功能,仅仅是显示一个窗口。现在来分解一下这个程序。由于Delphi中不必用 WinMain,所以我自己在内部构建了一个WinMain函数,传递一个参数HthisInstance,本参数表示当前应用程序的实例,实际上 Delphi运行启动程序的时候,这个实例已经通过Delphi内部包装的WinMain函数给反馈回来了,这个实例句柄,我们也可以通过 GetModuleHandle(0)来获得。那么这个参数的目的有什么作用呢?还是需要将WinMain的原型拿过来分析说明一下!WinMain的原 型为:
int WINAPI WinMain(Hinstance hThisInst,Hinstance hPrevInst,LPSTR lpszArgs,int nWinMode);
hThisInst和hPrevInst都是句柄类型,hThisInst指程序的当前实例,因为Windows是个多任务操作系统,一次可以同时 运行相同程序的多个实例,所以用这个实例句柄来标记到底属于哪个。hPrevInst这个我们可以不同管他,在我们的系统中,他始终为nil,他存在的唯 一理由就是与Win3.1时代的程序兼容,意思是指前一个程序实例。lpszArgs指定为命令行参数,就想ping 127.0.0.1这个里面的127.0.0.1这样的就是参数,对应着Delphi的paramstr(1)等。nWinMode参数保存的值决定如何 显示窗口。
定义窗口类。我这里用的是TWndClassEx,在Windows中声明如下
tagWNDCLASSEXA = packed record
cbSize: UINT; //指定本类结构体大小
style: UINT; //指定为窗口样式
lpfnWndProc: TFNWndProc; //指定窗口过程
cbClsExtra: Integer; //附加的信息
cbWndExtra: Integer;
hInstance: HINST;//所属的应用程序实例
hIcon: HICON; //图标32*32,大图标
hCursor: HCURSOR; //光标
hbrBackground: HBRUSH; //背景画布对象句柄
lpszMenuName: PAnsiChar;//菜单资源名
lpszClassName: PAnsiChar;//类名
hIconSm: HICON;//小图标16*16
end;
每个Windows应用程序都有两个与其相关的图标,一个是标准尺寸(32*32),另一个是小图标,当应用程序被最小化时,使用小图标。当应用程序快捷方式,以及在硬盘中显示时,显示标准图标。这里我通过了
LoadIcon这个函数来加载一个图标资源。原型为
HICON LoadIcon(Hinsance hinst,LPCSTR IconResName);
本函数将返回一个图标句柄。hInst指定包含图标的实例模块,我这里指定的是0,0表示调用系统的,如果指定为我们本应用程序的实例的话,这个返回是会失败的哦,因为我们应用程序内部并不包含对应的图标资源。第二个参数指定为图标资源名。系统默认的有
IDI_APPLICATION 缺省图标
IDI_ERROR 错误符号
IDI_INFORMATION 信息
IDI_QUESTION 问号
IDI_WARNING 感叹号
IDI_WINLOGO 窗口标志
读取鼠标光标,用LoadCursor,用法与LoadIcon差不多,一些系统的默认光标样式
IDC_ARROW 缺省箭头指针
IDC_CROSS 十字线
IDC_IBEAM 垂直工字型
IDC_WAIT 沙漏
窗口的背景,通过使用API函数来获得背景画刷的句柄。我这里用的是CreateSolidBrush,目的是创建一个画刷的GDI对 象,CreateSolidBrush中的参数指定为画刷的颜色。另外,还有一种方式就是通过GetStockObject函数来获得一个系统内部的画刷 对象。GetStockObject函数用于获得一些标准显示对象的句柄,包括画刷,画笔和字符字体等。原型为
HGDIOBJ GetStockObject(int Object);
汗水返回object所指定的对象的句柄(HGDIOBJ表示一个GDI句柄)
一些系统的内置画刷:
BLACK_BRUSH 黑色
DKGRAY_BRUSH 黑灰
HOLLOW_BRUSH
LIGRAY_BRUSH 浅灰
WHITE_BRUSH 白色
比如,可以尝试,将我上面的CreateSolidBrush换成GetStockObject(WHITE_BRUSH)这样的看一下效果。
一旦,当窗口类型,指定好了之后,我们就可以通过使用RegisterClassEx来注册一个窗口类了。
ATOM RegisterClassEx(wndclassex: TWNDCLASSEX)
ATOM表示一个原子类型,表示全系统唯一。
创建窗口
通过上面的步骤,注册成功了之后,就可以依据注册的窗口类来创建一个窗口了,创建窗口我用了CreateWindowEX,
function CreateWindowEx(dwExStyle: DWORD; lpClassName: PChar;
lpWindowName: PChar; dwStyle: DWORD; X, Y, nWidth, nHeight: Integer;
hWndParent: HWND; hMenu: HMENU; hInstance: HINST; lpParam: Pointer): HWND;
参数一指定了窗口的扩展样式,我这里啥扩展都没,所以指定为0,参数二指定为类名,这个名字必须要和我们注册的窗口类的名字一样,所以直接指定为 WndClass.lpszClassName,参数三指定为窗口标题,参数4指定为窗口的样式,我这里指定的是层叠样式窗口,其常见类型为:
WS_ORERLAPPED 边框重叠窗口
WS_MAXIMIZEBOX 最大化
WS_MINIMIZEBOX 最小化
WS_SYSMENU 系统菜单
WS_HSCROLL 水平滚动
WS_VSCROLL 垂直滚动
尔后的参数就是指定了窗口的位置,宽度和高度,我这里都是指定了CW_USEDEFAULT采用了系统默认的。如果窗口创建成功,将会返回创建的窗口句柄,否则返回0。
创建了窗口之后,窗体并不会被显示,要显示窗口,我们需要嗲用ShowWindow函数,本函数参数一指定要显示的窗口句柄,参数2指定为显示方式,显示方式有下面几种形式:
SW_HIDE 隐藏
SW_MINIMIZE 最小化
SW_MAXIMIZE 最大化
SW_RESTORE 恢复为正常大小
ShowWindows函数,返回窗口的前一个显示状态,如果显示了窗口则返回非0值,如果没显示,则返回0
最后,调用了UpdateWindow函数,本函数的目的是告诉Windows向用户的应用程序发送一条消息,该消息是需要更新一下主窗口。
最后便是消息循环了。消息循环是所有Windows应用程序的组成部分,它的作用就是接受并处理Windows发送的消息。运行程序时,它不断的被系统发 送消息。直到所有这些消息都被读取以及处理。否则就一直保存在应用程序的消息队列里。每次应用程序准备好去读取另一个消息时。就必须调用 GetMessage。原型为
BOOL GETMessage(msg: tagMsg;hwnd: THandle;min,max: UINT);
msg指定消息结构体,这个在windows单元中有定义,在本结构体中有包含hwnd窗口句柄,用来标石采用哪个窗口过程处理。也包含有消息 id,同时表示采用窗口过程的那个消息处理方式处理。其中的WPARAM和lparam是消息的附加信息。time指定了消息的发送时间,以毫秒为单位指 定,pt包含鼠标的坐标。在应用程的消息队列中如果没有一条消息,则GetMessage调用会向Windows回传一个控制命令(这个以后再说)。
GetMessage的hwnd参数将指定所获得的消息传给哪个窗口。一个应用程序可能有很多个窗口,而用户也许只想接收某个具体窗口的消息,如果想接收 指向所有应用程序的所有消息,则指定为0。其他两个参数指定了要接收的消息的范围,通常希望接收所有的消息,所以一般都指定为0.当用户程序结束 时,getmssage返回0,此时消息循环接收。否则返回非0,如果发生错误,则返回-1(至于错误如何处理,请大家思考一下)。然后消息循环段内,有 两个函数
TranslateMessage和DispatchMessage。这两个函数,第一个函数是将系统产生的虚拟键代码转成字符消息。可能并不是所 有程序都需要调用它,但是打部分程序还是需要处理按键信息的。一旦读取并转换了消息,通过DispatchMessage就将消息再派遣回 Windows,于是Windows保存该消息,直到能将其传递给程序的窗口uhanshu为止。一旦消息循环结束,WinMain也就结束,程序也随之 结束。
最后就是窗口函数,我在上面定义的窗口函数为WndProc,可见我在这个函数中仅仅处理了一个唯一的消息就是窗口释放的时候,发送了一个 PostQuitMessage的处理。该函数将发送一个WM_QUIT的消息给应用程序,然后GetMessage获得WM_QUIT的时候,就会为 False,从而退出循环结束消息。其他的消息处理,我全部都是调用的默认处理过程DefWindowProc来对消息进行处理。
之前,已经讲了如何进行SDK编程,以及SDK运行的一些机理条件,同时还给出了一个SDK的Delphi编程模板,大家可以通过那个模板来实现一个简单的SDK框架。
这回就记录一下SDK里面的消息处理。Windows的应用处理核心就在消息上,而这个消息的处理就在对应的窗口的窗口过程中,之前,我给的模板中,就有 一些消息处理,只是很少,比如说WM_DESTROY,这个消息就是在窗口释放的时候会触发,然后我发送了一个退出程序的消息。
Windows是一个交互式及时响应操作系统,那么每步的操作都会产生大量的消息。每条消息由一个唯一的32位整型值来表示,并且每条消息相当于某个事 件。例如WM_LBUTTONDOWN表示鼠标左键按下,WM_LBUTTONUP表示鼠标左键弹起等等。WM_LBUTTONDOWN这个在C中实际 上,都被叫做宏名,是用#define定义的,而在Delphi中就是我们定义的常量,用Const关键字标记,一般的系统消息,我们可以在 Messages单元中看到。比如一些常用的消息:
WM_CHAR,WM_PAINT,WM_MOVE,WM_CLOSE,WM_LBUTTONDOWN,WM_LBUTTONUP等。每条消息,都 伴随着其他两个值,这两个值获得与具体消息相关的消息。其中一个值是WPARAM类型,另一个是LPARAM类型。实际上这两个类型就是LongInt类 型。通常,他们保存着消息的一些附加信息,比如保存光标或者鼠标的坐标、按键值或与系统相关的值。
消息都是由程序的窗口函数来处理的,窗口函数有4个参数,以前说过,分别为消息所服务的窗口的句柄、消息本身和两个附加信息wparam,lparam。 有时,附加消息被译成两个包含wparam和lparam参数的字。为了便于访问wParam和lParam,在C中有LOWORD和HiWORD这两个 宏,而在Delphi中也可以使用loword()强制转型与hiWOrd这个函数来实现一样的功能。比如x := loword(lparam); y := hiword(lparam);
分别返回低位和高位。
现在来举例一些常见的Windows消息,比如我们用键盘输入时产生的按键消息,WM_Char,实际上WM_CHAR是被 TranslateMessage这个转换过来的(这个在之前已经说过)。当我们每按键一次,WM_CHAR消息就被送到活动窗口中,然后系统会调用本活 动窗口的窗口过程来处理这个按键值。每次发送WM_CHAR时,wParam中就包含了所按键的ASCII码值。loword(lparam)中包含由于 持续按键锁导致的重复按键的次数。HIWORD(lparam)中的每一个位表示的信息为:
位 意义
15 如果正被释放则为1,如果正被按下则为0
14 如果在发送消息前则为1,否则为0
13 ALT被按下时为1,否则为0
12 ~9 Windows使用,我们不管
8 如果按下的键是由增强键盘提供的扩充键则为1,否则0
7~0 依赖于生产厂家的键盘代码(既扫描代码)
通过上面的这个,可以知道WParam中保存的就是我们需要的ASCII代码,那么就可以通过这个来获得用户的按键信息了。于是,在那个模板的窗口过程中,我们可以添加一个消息的处理过程为
WM_CHAR:
begin
MessageBox(hwnd,pchar('消息:您按下了 '+inttostr(wParam)+' ASCII:'+CHAR(wParam)),'消息',64);
end;
这样,就能够获得你输入的按键的编码和按键信息了。
现在,再来看看另一个消息,WM_Paint,这个消息主要是在绘制界面的时候产生,通过对这个消息进行处理,可以在界面上绘制出任何我们想要的效 果。现在说他的目的就是将我们上面说的按键信息通过这个WM_PAINT绘制显示在界面上,而且让他一直存在!为什么,我这里会说一直存在!这个与 Windows的一个显示机制有关系,这个后面再说。WM_PAINT的主要目的是用来绘制界面的,那么这个绘制如何来与界面屏幕进行交互呢?这里有另外 一个概念就是设备描述表。通过设备描述表,用户的程序就能够和屏幕之间建立一个链路。所谓的设备描述表实际上是一个描述窗口输出显示环境的结构体,它包括 了设备驱动程序和不同的现实参数,如当前的字体类型等。所以,我们要想将用户的按键信息显示在界面上,那么首先,我们需要在界面屏幕与程序之间建立一个链 路,也就是要获得界面的设备描述表,获得设备界面描述表有几种方式。不过在WM_PAINT消息中,获得对应窗口的设备描述表,我们只能使用 BeginPaint这个API函数。其原型为:
HDC BeginPaint(Hwnd hwnd,LPPAINTSTRUCT lpPS);
Delphi的为
function BeginPaint(hWnd: HWND; var lpPaint: TPaintStruct): HDC; stdcall;
这个函数,就返回一个hwnd句柄所对应的窗口的设备描述表句柄,如果函数成功返回句柄,失败,返回0第二个参数返回时所对应的结构体中将包含程序用来重画窗口的信息。
typedef struct tagPAINTSTRUCT{
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
Bool fIncUpdate;
Bool rgbReserved[32];
}PAINTSTRUCT;
Delphi中对应的结构在Windows单元中,可以看到,中hdc就包含被重画的窗口设备描述表句柄。此DC也是通过BeginPaint() 返回的。如果需要擦出窗口的背景,则fErase为非0值。不过只要在窗口创建时指定了一个背景画刷,就可以忽略fErase,系统会自动擦除窗口。 RECT类型是一个指定矩形区左上角和右下角的结构体。一旦我们获得了设备描述表,就可以像窗口中输出内容和绘制我们的信息。完成我们信息的绘制之后,必 须调用EndPaint来释放设备描述表。
BOOL EndPaint(HWND hwnd,CONST PAINTSTRUCT *lpPS);
EndPaint返回非0值(它不可能失败)。hwnd指定重画窗口的句柄,第二个参数就是beginPaint的第二个参数。这里有两点需要主 要,首先就是WM_PAINT中必须使用BeginPaint函数来获得设备描述表,然后就是使用BeginPaint获得设备描述表之后,必须使用 EndPaint来释放设备描述表。
另外获得设备描述表还有一个方法GetDC(HWND hwnd)用来获得指定窗口的设备描述表,释放设备描述表使用ReleaseDC(Hwnd,DC)。在不使用设备描述表的时候,释放它是必须的,因为设 备描述表的数目只由自由内存的大小来决定,其数目有限,如果用户程序都不释放设备描述表,那么最终可用的设备描述表就会被耗尽,从而导致获得设备描述失 败。
通过上面的描述,那么现在,我们就可将我们的按键信息一直显示在界面上,直到用户按下下一个按键位置。实现方式如下:
先定义一个LastChar: longint=-1,在winMain的时候初始化为-1,表示无效值
WM_CHAR:
begin
//MessageBox(hwnd,pchar('消息:您按下了 '+inttostr(wParam)+' ASCII:'+CHAR(wParam)),'消息',64);
LastCHar := wParam;
r.Left :=30;
r.Top :=20;
r.Bottom :=50;
r.Right :=250;
Dc := GetDC(hwnd);
FillRect(Dc,r,GetStockObject(WHITE_BRUSH));
DrawText(Dc,Pchar('ASCII: '+inttostr(LastCHar)+' 字符:'+CHar(LastCHar)),-1,r,DT_CENTER or DT_SINGLELINE or DT_VCENTER);
ReleaseDC(Hwnd,Dc)
end;
此时将WM_CHar换成这样,然后用户在按键的时候,就会在界面上显示用户的按键信 息了。里面用到了FillRect这个函数,这个函数是填充区域,使用它的目的是刷新界面为白色,去掉上次绘制的文字信息。但是现在这个有个问题,就是当 用户将程序最小化在恢复的时候,我们记录的文字信息就丢失了。前面,我已经说过,这是由于Windows的显示机制决定的,Windows并不帮我们恢复 窗口内所包含的记录,所以这种情况下,我们必须自己保存记录,然后重新显示窗口的内容,窗口每次恢复的时候,都会向用户的程序发送一个WM_Paint消 息,每次接收到这个消息,他都必须重新显示窗口中的内容,于是我们需要在窗口中处理我们自己的绘制消息。这里实际上在模板中,我已经写了一个hello word的wm_Paint模板,现在我们只用将WM_CHar中的稍微修改,搬到WM_Paint的实现中去,就能够实现永久记录了。
WM_PAINT:
begin
GetWindowRect(hwnd,r1);
r.Left :=0;
r.Top :=0;
r.Right := r1.Right - r1.Left;
r.Bottom := r1.Bottom - r1.Top;
Dc := BeginPaint(hwnd,PaintStruct);
try
SetBkMode(Dc,TRANSPARENT);
DrawText(Dc,'Hello World!',-1,r,DT_CENTER or DT_SINGLELINE or DT_VCENTER);
if LastCHar >-1then
begin
r.Left :=30;
r.Top :=20;
r.Bottom :=50;
r.Right :=250;
Dc := GetDC(hwnd);
FillRect(Dc,r,GetStockObject(WHITE_BRUSH));
DrawText(Dc,Pchar('ASCII: '+inttostr(LastCHar)+' 字符:'+CHar(LastCHar)),-1,r,DT_CENTER or DT_SINGLELINE or DT_VCENTER);
ReleaseDC(Hwnd,Dc)
end;
finally
EndPaint(hwnd,PaintStruct);
end;
end;
然后还有一些其他的非常 多的消息,其处理过程都是根据自己的需要,在窗口过程中的Case结构中添加需要的消息,加上自己的处理过程就好了。我这里不再一一讲述,具体的消息参考 可以去找《Microsoft Win32程序员参考大全(五)----消息、结构和宏》这本书看看!里面记录了所有Windows的消息信息以及参考资料。

浙公网安备 33010602011771号