您的位置:首页 > 理论基础 > 数据结构算法

理解Windows消息机制

2008-07-20 16:31 302 查看
本文绝大部分内容来源于《C++ Builder 高级应用开发指南》,清华大学出版社出版,李幼仪、甘志 编著,版权归他们所有。作者仅仅对其进行了拷贝,不享有除学习之外的任何权利。
本文以及作者其他系列文章中出现的所有程序代码均经过严格的验证,如果没有特别说明即是在Win XP SP2 + C++ Builder 6.0的环境下测试通过。

一、消息的基本概念

消 息(Message)指的就是Windows操作系统发给应用程序的一个通告,它告诉应用程序某个特定的事件发生了。比如,用户单击鼠标或按键都会引发 Windows系统发送相应的消息。最终处理消息的是应用程序的窗口函数,如果程序不负责处理的话系统将会作出默认处理。

从数据结构的角度来说,消息是一个结构体,它包含了消息的类型标识符以及其他的一些附加信息。比如对于鼠标单击产生的消息而言,它就包含了窗口句柄、此消息的常量标识符、鼠标的位置坐标等相关信息。

Windows 系统定义了许多消息常量,包括标准的Windows消息、通知消息、命令消息等等。这些消息常量通常具有XX_YYYY的形式,其他XX通常代表消息的类 型,而后面的YYYY通常是这个消息所对应事件的英文缩写。比如WM_LBUTTONDOWN代表的事件就是按下了鼠标左键。

二、Windows的消息系统

Windows的消息系统由3个部分组成:

消息队列。Windows操作系统本身维护了一个系统消息队列,而对于每一个正在执行的Windows应用程序,系统会为其建立一个应用程序消息队列。应用程序可以从这个消息队列中获取消息,然后分派给对应的窗口。

消息循环。Windows应用程序中都包含了一段称作“消息循环(也称消息泵)”的代码,用来从消息队列中检索消息并把他们分发到相应的窗口函数中。正是这个消息循环使得一个应用程序能够响应外部的各种事件,所以消息循环往往是一个Windows应用程序的核心部分。

窗 口函数。最终为了处理各种消息,Windows应用程序所创建的每个窗口(广义,包括实际窗口、控件等诸如此类的的内容)都会在系统中注册一个相应的窗口 函数,此窗口函数从形式上看一个巨大的switch语句,用以处理由消息循环发送到该窗口的各种消息。窗口函数是一种回调函数(Callback Function),也就是说,它是由Windows操作系统负责调用的,而应用程序本身不能调用它。

Windows操作系统中的消息从发生到被处理一般有5个步骤:
(1)系统发生了一个事件。
(2)Windows系统把事件翻译为对应的消息,并把它放到消息队列中。
(3)应用程序从消息队列中获取消息,然后把它封装在TMsg结构中。
(4)应用程序通过消息循环把消息分派给对应的窗口函数。
(5)窗口函数负责最终处理这个消息。

三、C++ Builder中的OnMessage事件

C++ Builder为了方便消息处理,进一步把常用的消息封装成了各种相应事件,这样程序员通常情况下就无需考虑消息的具体细节,只要编写相应的事件处理函数即可。这就是所谓的基于事件的Windows程序开发。

在C++ Builder开发的应用程序中,任何窗体接收到一个Windows消息都会触发一次OnMessage事件,所以可以通过响应TApplication对象的OnMessage事件来捕获任何发送给本程序的Windows消息。

OnMessage事件的处理函数原型如下:
typedef void __fastcall (__closure *TMessageEvent)(tagMSG &Msg,bool &Handled);

这个函数有两个参数,其中参数Msg表示的是被截获的消息,而参数Handled则用来指示本消息是否已经处理完成。在程序中可以通过设置参数Handled为true以避免后续过程处理这个消息,反之亦然。

需 要注意的是,OnMessage事件仅仅接受发送到消息队列中的消息,而直接使用API函数 SendMessage()发送给窗口函数的消息将不会被截获到。另外,当程序运行的时候,OnMessage事件被触发的频率一般非常高,所以这个事件 处理函数中的代码执行事件将直接影响到整个程序的运行效率。

下面我们通过一个实例来演示OnMessage事件的处理过程。在这个范例中,程序将累计发生的OnMessage事件次数并显示在窗体上。

我们在主窗体上放置一个Label组件,命名为Label1,用于显示消息发生次数。
程序中全部采用默认名字。

在窗体类头文件Unit1.h中添加以下代码:
private: // User declarations
long num;
void __fastcall AppMessage(tagMSG &Msg,bool &Handled);
然后在Unit1.cpp中添加以下代码:
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
num=0;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::FormCreate(TObject *Sender)
{
Application->OnMessage=AppMessage;
}
//---------------------------------------------------------------------------
void __fastcall TForm1::AppMessage(tagMSG &Msg,bool &Handled)
{
num++;
Label1->Caption=AnsiString(num);
Handled=false;
}
//---------------------------------------------------------------------------
其中Application->OnMessage=AppMessage;的作用是把事件处理函数AppMessage()和OnMessage事件联系起来。Handled=false;是为了让后续过程能继续处理消息,以避免窗口无法正常接收消息而引起死锁。

最后按F9运行程序,可以看到窗体上显示出了消息事件发生的次数,当你移动鼠标或按下按键的时候它都会不断地变化。

四、利用消息映射截获消息

C++ Builder提供了一种消息映射机制,通过消息映射程序能将特定的Windows消息与对应的消息处理函数联系起来,当窗口捕获到这个消息的时候就会自动调用对应的处理函数。

在程序中使用这样的消息映射一般需要以下三个步骤:
(1)声明消息映射表,把某些消息的处理权交给自定义的消息处理函数。

这 样的消息映射列表应该位于一个组件类的定义中,它以一个没有参数的BEGIN_MESSAGE_MAP宏开始,以END_MESSAGE_MAP宏结束。 END_MESSAGE_MAP宏的唯一参数应该是组件的父类的名字。通常情况下这个所谓的父类指的就是TForm。在宏 BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间可以插入一个或者是多个MESSAGE_HANDLER宏。

MESSAGE_HANDLER宏将一个消息句柄和一个消息处理函数联系在了一起。它有三个参数:Windows消息名、消息结构体名和对应的消息处理函数名。其中,消息结构体名既可以是通用的消息结构体TMessage,也可以是特定的消息结构体比如TWMMouse。

在使用消息映射的时候应该注意以下两点:
一个窗口类定义中只能有一个消息映射列表;
消息映射必须位于它所引用的所有消息处理函数声明的后面。
一个典型的消息映射声明代码如下:

BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(Windows消息名,消息结构体名,消息处理函数名)
MESSAGE_HANDLER(Windows消息名,消息结构体名,消息处理函数名)
...
END_MESSAGE_MAP

(2)在窗口类中声明消息处理函数

注意,这里的消息处理函数的名字和参数都必须和对应的MESSAGE_HANDLER宏一致。
一个典型的消息处理函数声明如下:
void __fastcall 消息处理函数名(消息结构体名 &Message)

(3)实现消息处理函数

消 息处理函数的编制和普通的窗口类成员函数没有太大的差异,唯一不同的是,通常在此函数的最后都要加上一 句:TForm::Dispatch(&Message),以完成VCL对于消息的默认处理。如果没有这条语句,消息将会被完全拦截;这样可能造 成某些情况下VCL类因得不到消息而无法正常工作。

下面我们通过一个实例来演示如何通过消息映射在程序中截获Windows消息。在这个范例中,程序将强制设置主窗体的最大、最小尺寸。

我们仍然采用默认命名,将主窗体Form1的Height和Width属性分别设置为150和250。在示例程序中我们将要限制窗体的最大长、宽分别为300和200,最小长、宽分别为200和100。

编辑窗体类的头文件Unit1.h如下:
class TForm1 : public TForm
{
__published: // IDE-managed Components
TLabel *Label1;
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
void __fastcall WMGetMinMaxInfo(TWMGetMinMaxInfo &Msg);
BEGIN_MESSAGE_MAP
MESSAGE_HANDLER(WM_GETMINMAXINFO,TWMGetMinMaxInfo,WMGetMinMaxInfo)
END_MESSAGE_MAP(TForm)
};

在窗体文件Unit1.cpp中只需要实现一个函数,其他函数全部不写任何代码。
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WMGetMinMaxInfo(TWMGetMinMaxInfo &Msg)
{
//当用户单击最大化按钮时,限制其大小
Msg.MinMaxInfo->ptMaxSize.x=300;
Msg.MinMaxInfo->ptMaxSize.y=200;
//设定窗体最大化时左上角的屏幕坐标为当前窗体的位置
Msg.MinMaxInfo->ptMaxPosition.x=Left;
Msg.MinMaxInfo->ptMaxPosition.y=Top;
//当用户用鼠标拖动改变窗体尺寸时,限制其最大值
Msg.MinMaxInfo->ptMaxTrackSize.x=300;
Msg.MinMaxInfo->ptMaxTrackSize.y=200;
//当用户用鼠标拖动改变窗体尺寸时,限制其最小值
Msg.MinMaxInfo->ptMinTrackSize.x=200;
Msg.MinMaxInfo->ptMinTrackSize.y=100;
//显示当前窗体大小尺寸
Label1->Caption="Width="+AnsiString(Width)+" Height="+AnsiString(Height);
}
//---------------------------------------------------------------------------
程序代码中的TWMGetMinMaxInfo结构的声明在头文件Messages.hpp中,MINMAXINFO结构在WinUser.h中有详细说明。
F9运行程序后完全符合我们预先的设想。
关于消息映射的实现细节,我们可以通过在头文件$(BCB)/Include/Vcl/sysmac.h中的这些宏的代码来搞清楚。

五、重载WndProc()函数

在 上一节我们学会了使用消息映射来捕获或屏蔽某些特定的消息,当然,那种方法并不是唯一的,我们还可以通过重载窗口函数WndProc()来实现。因为系统 会在调用函数Dispatch()分派消息之前调用窗口函数WndProc(),所以我们可以通过重载此函数得到一个在分派消息之前就处理消息的机会。

这个用于处理消息的窗口函数原型如下:
virtual void __fastcall WndProc(Message::TMessage &Message);

下面我们就通过一个实例来演示如何重载它,要实现的目标和上面那个程序一样,只发代码就可以了,工程设置也是一样的。

编辑窗体类的头文件Unit1.h如下:
class TForm1 : public TForm
{
__published: // IDE-managed Components
TLabel *Label1;
private: // User declarations
void __fastcall WndProc(TMessage &Message);
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};

编辑窗体函数文件Unit1.cpp如下:
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::WndProc(TMessage &Message)
{
if(Message.Msg == WM_GETMINMAXINFO)
{
LPMINMAXINFO lpmni = (LPMINMAXINFO) Message.LParam;

lpmni->ptMaxSize.x=300;
lpmni->ptMaxSize.y=200;

lpmni->ptMaxPosition.x=Left;
lpmni->ptMaxPosition.y=Top;

lpmni->ptMaxTrackSize.x=300;
lpmni->ptMaxTrackSize.y=200;

lpmni->ptMinTrackSize.x=200;
lpmni->ptMinTrackSize.y=100;

Label1->Caption="Width="+AnsiString(Width)+" Height="+AnsiString(Height);
}
TForm::WndProc(Message);
}
//---------------------------------------------------------------------------
程 序首先判断被处理的消息是否为WM_GETMINMAXINFO,如果是才进行后续处理。代码LPMINMAXINFO lpmni = (LPMINMAXINFO) Message.LParam;的作用是把消息附带的MINMAXINFO类型的结构体的地址赋值给一个工作变量lpmni。这个MINMAXINFO类 型的结构体被用来存储有关窗体最大化、最小化以及窗体位置的信息。对于WM_GETMINMAXINFO而言,这个指针被存储在TMessage结构的 LParam成员域中。
按F9运行程序,运行结果同样符合了我们的预期。

六、非标准消息

1,通知消息
通知消息(Notification message)指的是,当一个窗体的子控件发生了一些事情后,它通知给其父窗体的消息。需要注意的是,通知消息只发生在一些标准的Windows控件上,包括按钮、编辑框、列表框、组合列表框、树状视图控件、列表控件等。
通知消息如同标准的Windows消息一样,也可以通过消息映射等方法来进行处理。

2,自定义消息
自 定义消息一般有两种方式:直接定义消息常量或调用API函数RegisterWindow-Message()向系统注册一个消息。其中比较简洁的方式是 直接在程序中定义消息的数值,通常将其定义为WM_USER+XXX或者是WM_APP+XXX。Windows对于消息编号的分配情况通常如下所示:
范围 说明
0到WM_USER-1 系统消息
WM_USER到0x7FFF 为用户的窗体类保留整数类型消息
WM_APP到0xBFFF 为应用程序保留的消息
0xC000到0xFFFF 为应用程序保留的字符类型消息,由函数RegisterWindowMessage()分配
0xFFFF以上 为将来的系统应用保留

一个典型的消息常量定义如下:
#define WM_MYNOTIFY (WM_APP+100)

这种直接定义消息的方式虽然简洁,但在有的情况下,如果有大量的组件和应用使用这种方式来定义自己的消息编号时就有可能引起冲突。此时,可以通过函数RegisterWindowMessage()来获取一个系统中唯一的消息编号。
函数RegisterWindowMessage()的原型定义如下:
UINT RegisterWindowMessage(LPCTSTR lpString);
这 个函数需要传输一个以null结束的字符串,并且返回一个范围是0xC000~0xFFFF的消息常量。使用它的好处在于,对于任何给定的字符串都可以得 到一个系统中惟一的消息常量,从而可靠地避免了自定义消息之间可能的冲突。当然,这个函数的返回值只有在程序运行时才有意义。

3,VCL内部消息
VCL自身也使用了许多内部的消息,它们在一般的程序开发中很少用到,但是在编写自己的组件时却非常有用。这些VCL内部消息都具有CM_XXXX的形式,用于处理VCL内部的事务。

七、自己发送消息

在C++ Builder中提供了几种自己发送消息的途径:使用函数TControl::Perform()或者API函数SendMessage()和 PostMessage()向特定窗体发送消息,或者是使用函数TWinControl::Broadcast()和API函数 BroadcastSystemMessage()广播消息。

1,Perform()
Perform()函数适用于所有由TControl类派生的对象,可以通过这个函数直接向这些组件发送消息,而这个组件对于这个消息的反应就如同它真的从系统接收到了这个消息一样。函数Perform()的原型如下:
int __fastcall Perform(Cardinal Msg,int WParam,int LParam);
其中参数Msg表示的就是这个消息的标识符,比如WM_LBUTTONBLCLK。另外两个代表的是此消息的两个附带参数,它们对于不同的消息具有不同的含义。
调用了此函数之后,要等到消息处理之后才返回。
在 同一个应用程序的不同窗体和控件之间使用Perform()是非常便捷的。但这个函数是TControl类的成员函数。也就是说,程序必须知道这个接收消 息的控件类的实例。而在许多情况下程序并不知道接收消息的窗体的实例而只是知道它的句柄,比如在不同程序的窗体之间发送消息就属于这种情况。这时就要使用 下面的两个函数了。

2,SendMessage()和PostMessage
这两个函数的功能基本一样,它们都可以用来向一个特 定的窗口句柄发送消息。主要的区别是:函数SendMessage()直接把一个消息发送给窗口函数,等消息被处理之后才返回;而函数 PostMessage()则只是把消息发送到消息队列然后就立即返回。它们的用法与Perfoem()基本相似,不过多了个hWnd参数来表示目标窗口 的句柄。

3,Broadcast()和BroadcastSystemMessage()
函数Broadcast()适用于所有由TWinControl类派生的对象,它可以向窗体上所有的子控件广播消息,原型如下:
void __fastcall Broadcast(void *Message);
它只有一个参数,指向被广播的TMessage类型的消息结构体。

函 数Broadcast()只能向C++ Builder应用程序中的指定窗体上所有的子控件广播消息,如果要向系统中的其他应用程序或者窗体广播消息,它就无能为力了。这时可以使用API函数 BroadcastSystemMessage(),这个函数可以向任意的应用程序或者组件广播消息。它的原型如下:
long BroadcastSystemMessage(
DWORD dwFlags,
LPDWORD lpdwRecipients,
UINT uiMessage,
WPARAM wParam,
LPARAM lParam);
更详细的用法请查阅帮助文档。

当然,在程序中需要给系统中的所有进程广播消息的情况是很少见的,所以它的使用并不多。但是在程序内部给一个窗体的所有子控件广播消息的情况则较为普遍,特别是当窗体的子控件很多的情况下,用广播消息的方式显得非常便捷。(全文完)
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息