深入BCB理解VCL的消息机制

来源:互联网 发布:电影人间喜剧 知乎 编辑:程序博客网 时间:2024/05/19 12:18

引子:本文所谈及的技术内容都来自于Internet的公开信息。由笔者在闲暇之际整理
后,贴出来以飴网友,姑且妄称原创。每次在国外网站上找到精彩文章的时候,心中都
会暗自叹息,为什么在中文网站难以觅得这类文章呢?其实原因大家都明白。

时至今日,学习Windows编程的兄弟们都知道消息机制的重要性。所以理解消息机制也
成了不可或缺的功课。大家都知道,Borland的C++ Builder以及Delphi的核心是VCL。
作为Win32平台上的开发工具,封装Windows的消息机制当然也是必不可少的。那么,在
C++ Builder中处理消息的方法有哪些呢?它们之间的区别又在哪里?如果您很清楚这
些,呵呵,对不起啦,请关掉这个窗口。如果不清楚那就和我一起深入VCL的源码看个
究竟吧。

方法1:使用消息映射(Message Map)重载TObject的Dispatch虚成员函数
这个方法大家用的很多。形式如下


BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER( … … )
END_MESSAGE_MAP( … )

但这几句话实在太突兀,C++标准中没有这样的定义。不用讲,这显然又是宏定义。它
们到底怎么来的呢?CKER第一次见到它们的时候,百思不得其解。嘿嘿,不深入VCL,
怎么可能理解?

在/Borland/CBuilder5/Include/Vcl找到sysmac.h,其中有如下的预编译宏定义:


#define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) /
{ /
switch (((PMessage)Message)->Msg) /
{

#define VCL_MESSAGE_HANDLER(msg,type,meth) /
case msg: /
meth(*((type *)Message)); /
break;

// NOTE: ATL defines a MESSAGE_HANDLER macro which conflicts with VCL's
macro. The
// VCL macro has been renamed to VCL_MESSAGE_HANDLER. If you are not using
ATL,
// MESSAGE_HANDLER is defined as in previous versions of BCB.

#if !defined(USING_ATL) && !defined(USING_ATLVCL) &&
!defined(INC_ATL_HEADERS)
#define MESSAGE_HANDLER VCL_MESSAGE_HANDLER
#endif // ATL_COMPAT

#define END_MESSAGE_MAP(base)
default: /
base: ispatch(Message); /
break; /
} /
}

这样对如下的例子:
BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint)
END_MESSAGE_MAP(TForm1)

在预编译时,就被展开成如下的代码
virtual void __fastcall Dispatch(void *Message)
{
switch (((PMessage)Message)->Msg)
{
case WM_PAINT:
OnPaint(*((TMessage *)Message));
//消息响应句柄,也就是响应消息的成员函数,在Form1中定义
break;
default:
TForm1: ispatch(Message);
break;
}

}

这样就很顺眼了,对吧。对这种方法有两点要解释一下:
1.virtual void __fastcall Dispatch(void *Message)
这个虚方法的定义最早可以在TObject的定义中找到。打开BCB的帮助,查找TForm的方
法(Method),你会发现这里很清楚的写着Dispatch方法继承自TObject。如果您关心
VCL的继承机制的话,您会发现TObject是所有VCL对象的基类。TObject的抽象凝聚了
Borland的工程师们的心血。如果有兴趣。您应该好好查看一下TObject的定义。
很显然,所有TObject的子类都可以重载基类的Dispatch方法,来实现自己的消息调
用。如果Dispatch方法找不到此消息的定义,会将此消息交由TObject:DefaultHandler
方法来处理。抽象基类TObject的DefaultHandler方法实际上是空的。同样要由继承子
类重载实现它们自己的消息处理过程。


2.很多时候,我见到的第二行是这样写的:
MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint)
在这里,您可以很清楚地看到几行注解,意思是ATL中同样包含了一个MESSAGE_HANDLER
的宏定义,这与VCL发生了冲突。为了解决这个问题,Borland改用
VCL_MESSAGE_HANDLER。当您没有使用ATL的时候,MESSAGE_HANDLER将转换成
VCL_MESSAGE_HANDLER。但如果用了ATL就会有问题。所以我建议您始终使用
VCL_MESSAGE_HANDLER的写法,以免出现问题。

 

方法2:重载TControl的WndProc方法
还是先谈谈VCL的继承策略。VCL中的继承链的顶部是TObject基类。一切的VCL组件和对
象都继承自TObject。

打开BCB帮助查看TControl的继承关系:
TObject->TPersistent->TComponent->TControl

原来TControl是从TPersistent类的子类TComponent类继承而来的。TPersistent抽象基
类具有使用流stream来存取类的属性的能力。

TComponent类则是所有VCL组件的父类。
这就是所有的VCL组件包括您的自定义组件可以使用dfm文件存取属性的原因(当然要是
TPersistent的子类,我想您很少需要直接从TObject类来派生您的自定义组件吧)。

TControl类的重要性并不亚于它的父类们。在BCB的继承关系中,TControl类的是所有
VCL可视化组件的父类。实际上就是控件的意思吧。所谓可视化是指您可以在运行期间
看到和操纵的控件。这类控件所具有的一些基本属性和方法都在TControl类中进行定
义。

TControl的实现在/Borland/CBuilder5/Source/Vcl/control.pas中可以找到。(可能
会有朋友问你怎么知道在那里?使用BCB提供的Search -> Find in files很容易找到。
或者使用第三方插件的grep功能。)

好了,进入VCL的源码吧。说到这里免不了要抱怨一下Borland。哎,为什么要用pascal
实现这一切……:-(

TControl继承但并没有重写TObject的Dispatch方法。反而提供了一个新的方法
WndProc。一起来看看Borland的工程师们是怎么写的吧。

procedure TControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
begin
//由拥有control的窗体来处理设计期间的消息
if (csDesigning in ComponentState) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and (Form.Designer <> nil) and
Form.Designer.IsDesignMsg(Self, Message) then Exit;
end
//如果需要,键盘消息交由拥有control的窗体来处理
else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
begin
Form := GetParentForm(Self);
if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit;
end
//处理鼠标消息
else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST)
then
begin
if not (csDoubleClicks in ControlStyle) then
case Message.Msg of
WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
end;
case Message.Msg of
WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);
WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:
begin
if FDragMode = dmAutomatic then
begin
BeginAutoDrag;
Exit;
end;
Include(FControlState, csLButtonDown);
end;
WM_LBUTTONUP:
Exclude(FControlState, csLButtonDown);
end;
end
// 下面一行有点特别。如果您仔细的话会看到这个消息是CM_VISIBLECHANGED.
// 而不是我们熟悉的WM_开头的标准Windows消息.
// 尽管Borland没有在它的帮助中提到有这一类的CM消息存在。但很显然这是BCB的
// 自定义消息。呵呵,如果您对此有兴趣可以在VCL源码中查找相关的内容。一定会有
不小的收获。
else if Message.Msg = CM_VISIBLECHANGED then
with Message do
SendDockNotification(Msg, WParam, LParam);
// 最后调用dispatch方法。
Dispatch(Message);
end;

看完这段代码,你会发现TControl类实际上只处理了鼠标消息,没有处理的消息最后都
转入Dispatch()来处理。
但这里需要强调指出的是TControl自己并没有获得焦点Focus的能力。TControl的子类
TWinControl才具有这样的能力。我凭什么这样讲?呵呵,还是打开BCB的帮助。很多朋
友抱怨BCB的帮助实在不如VC的MSDN。毋庸讳言,的确差远了。而且这个帮助还经常有
问题。但有总比没有好啊。

言归正传,在帮助的The TWinControl Branch 分支下,您可以看到关于TWinControl类
的简介。指出TWinControl类是所有窗体类控件的基类。所谓窗体类控件指的是这样一
类控件:

1. 可以在程序运行时取得焦点的控件。

2. 其他的控件可以显示数据,但只有窗体类控件才能和用户发生键盘交互。

3. 窗体类控件能够包含其他控件(容器)。

4. 包含其他控件的控件又称做父控件。只有窗体类控件才能够作为其他控件的父控
件。

5. 窗体类控件拥有句柄。

除了能够接受焦点之外,TWinControl的一切都跟TControl没什么分别。这一点意味着
TWinControl可以对许多的标准事件作出响应,Windows也必须为它分配一个句柄。并且
与这个主题相关的最重要的是,这里提到是由BCB负责来对控件进行重画以及消息处
理。这就是说,TWinControl封装了这一切。

似乎扯的太远了。但我要提出来的问题是TControl类的WndProc方法中处理了鼠标消
息。但这个消息只有它的子类TWinControl才能够得到啊!?

这怎么可以呢……Borland是如何实现这一切的呢?这个问题实在很奥妙。为了看个究
竟,再次深入VCL吧。

还是在control.pas中,TWinControl继承了TControl的WndProc方法。源码如下:

procedure TWinControl.WndProc(var Message: TMessage);
var
Form: TCustomForm;
KeyState: TKeyboardState;
WheelMsg: TCMMouseWheel;
begin
case Message.Msg of
WM_SETFOCUS:
begin
Form := GetParentForm(Self);
if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit;
end;
WM_KILLFOCUS:
if csFocusing in ControlState then Exit;
WM_NCHITTEST:
begin
inherited WndProc(Message);
if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient(
SmallPointToPoint(TWMNCHitTest(Message).Pos)), False) <> nil) then
Message.Result := HTCLIENT;
Exit;
end;
WM_MOUSEFIRST..WM_MOUSELAST:
//下面这一句话指出,鼠标消息实际上转入IsControlMouseMsg方法来处理了。
if IsControlMouseMsg(TWMMouse(Message)) then
begin
if Message.Result = 0 then
DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
Exit;
end;
WM_KEYFIRST..WM_KEYLAST:
if Dragging then Exit;
WM_CANCELMODE:
if (GetCapture = Handle) and (CaptureControl <> nil) and
(CaptureControl.Parent = Self) then
CaptureControl.Perform(WM_CANCELMODE, 0, 0);
else
with Mouse do
if WheelPresent and (RegWheelMessage <> 0) and
(Message.Msg = RegWheelMessage) then
begin
GetKeyboardState(KeyState);
with WheelMsg do
begin
Msg := Message.Msg;
ShiftState := KeyboardStateToShiftState(KeyState);
WheelDelta := Message.WParam;
Pos := TSmallPoint(Message.LParam);
end;
MouseWheelHandler(TMessage(WheelMsg));
Exit;
end;
end;
inherited WndProc(Message);
end;

鼠标消息是由IsControlMouseMsg方法来处理的。只有再跟到IsControlMouseMsg去看看
啦。源码如下:
function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
var
//TControl出现啦
Control: TControl;
P: TPoint;
begin
if GetCapture = Handle then
begin
Control := nil;
if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then
Control := CaptureControl;
end else
Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);
Result := False;
if Control <> nil then
begin
P.X := Message.XPos - Control.Left;
P.Y := Message.YPos - Control.Top;
file://TControl的Perform方法将消息交由WndProc处理。
Message.Result := Control.Perform(Message.Msg, Message.Keys,
Longint(PointToSmallPoint(P)));
Result := True;
end;
end;

原来如此,TWinControl最后还是将鼠标消息交给TControl的WndProc来处理了。这里出
现的Perform方法在BCB的帮助里可以查到,是TControl类中开始出现的方法。它的作用
就是将指定的消息传递给TControl的WndProc过程。
结论就是TControl类的WndProc方法的消息是由TWinControl类在其重载的WndProc方法
中调用IsControlMouseMsg方法后使用Peform方法传递得到的。

由于这个原因,BCB和Delphi中的TControl类及其所有的派生类都有一个先天的而且是
必须的限制。那就是所有的TControl类及其派生类的Owner必须是TWinControl类或者
TWinControl的派生类。Owner属性最早可以在TComponent中找到,一个组件或者控件是
由它的Owner拥有并负责释放其内存的。这就是说,当Owner从内存中释放的时候,它所
拥有的所有控件占用的内存也都被释放了。Owner最好的例子就是Form。Owner同时也负
责消息的分派,当Owner接收到消息的时候,它负责将应该传递给其所拥有的控件的消
息传递给它们。这样这些控件就能够取得处理消息的能力。TImage就是个例子:你可以
发现Borland并没有让TImage重载TControl的WndProc方法,所以TImage也只有处理鼠标
消息的能力,而这种能力正是来自TControl的。

唧唧崴崴的说了一大堆。终于可以说处理消息的第二种方法就是重载TControl的
WndProc方法了。例程如下:

void __fastcall TForm1::WndProc(TMessage &Message)
{
switch (Message.Msg)
{
case WM_CLOSE:
OnCLOSE(Message); // 处理WM_CLOSE消息的方法
break;
}
TForm::WndProc(Message);
}

乍看起来,这和上次讲的重载Dispatch方法好象差不多。但实际上还是有差别的。差别
就在先后次序上,从前面TControl的WndProc可以看到,消息是先交给WndProc来处理,
最后才调用Dispatch方法的啦。
这样,重载WndProc方法可以比重载Dispatch方法更早一点点得到消息并处理消息。

好了,这次就说到这里。在您的应用程序里还有没有比这更早得到消息的办法呢?有,
下次再说。

 


方法3 自TApplication的方法
不用我多废话,大家都知道TApplication在BCB中的重要性。在BCB的帮助中指出:
TApplication、TScreen和TForm构成了所有BCB风格的Win32 GUI程序的脊梁,他们控制
着您程序的行为。TApplication类提供的属性和方法封装了标准Windows程序的行为。
TApplication表现了在Windows操作系统中创建、运行、支持和销毁应用程序的基本原
理。因此,TApplication大大简化了开发者和Windows环境之间的接口。这正是BCB的
RAD特性。

TApplication封装的标准Windows行为大致包括如下几部分:
1> Windows 消息处理
2> 上下文关联的在线帮助
3> 菜单的快捷键和键盘事件处理
4> 异常处理
5> 管理由操作系统定义的程序基础部分,如:MainWindow 主窗口、 WindowClass 窗
口类 等。

一般情况下,BCB会为每个程序自动生成一个TApplication类的实例。这部分源码可以
在yourproject.cpp文件中见到(这里假定您的工程名称就叫yourproject.bpr)。

当然TApplication是不可见的,他总是在您的Form背后默默的控制着您的程序的行为。
但也不是找不到蛛丝马迹。如果您新建一个程序(New Application),然后不作任何改
动,编译运行的话,你会发现程序窗体的Caption是Form1,但在Windows的状态条上的
Caption确写着project1的字样。这就是TApplication存在的证据。当然,这只是一种
臆测,实战的方法应该打开BCB附带的WinSight来查看系统的进程。您可以清楚的看到
TApplication类的存在,他的大小是0(隐藏的嘛),然后才是TForm1类。

好了,既然TApplication封装了消息处理的内容。我们就研究一下TApplication的实际
动作吧。实际上消息到达BCB程序时,最先得到它们的就是TApplication对象。经由
TApplication之后,才传递给Form的。以前的方法都是重载TForm的方法,显然要比本
文所提到的方法要晚一些收到消息。对您来说,是不是希望在第一时间收到消息并处理
它们呢?

要清楚的知道TApplication的处理机制还是深入VCL源码。首先看一看最最普通的一段
代码吧。

#include
#pragma hdrstop
USERES("Project1.res");
USEFORM("Unit1.cpp", Form1);
//--------------------------------------------------------------
WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
try
{
// 初始化Application
Application->Initialize();
// 创建主窗口,并显示
Application->CreateForm(__classid(TForm1), &Form1);
// 进入消息循环,直到程序退出
Application->Run();
}
catch (Exception &exception)
{
Application->ShowException(&exception);
}
return 0;
}

短短的几行代码就可以让您的BCB程序自如运行。因为一切都已经被VCL在后台封装好
了。Application->Run()方法进入程序的消息循环,直到程序退出。一起跟进VCL源码
看个究竟吧。
TApplication的定义在forms.pas中。

procedure TApplication.Run;
begin
FRunning := True;
try
AddExitProc(DoneApplication);
if FMainForm <> nil then
begin
// 设置主窗口的显示属性
case CmdShow of
SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
end;
if FShowMainForm then
if FMainForm.FWindowState = wsMinimized then
Minimize else
FMainForm.Visible := True;
// 看见了吧,这里有个循环,直到Terminated属性为真退出。Terminated什么意思,
就是取消,结束
repeat
HandleMessage
until Terminated;
end;
finally
FRunning := False;
end;
end;

消息处理的具体实现不在Run方法中,很显然关键在HandleMessage方法,看看这函数名
字-消息处理。只有跟进HandleMessage瞧瞧喽。
procedure TApplication.HandleMessage;
var
Msg: TMsg;
begin
if not ProcessMessage(Msg) then Idle(Msg);
end;

咳,这里也不是案发现场。程序先将消息交给ProcessMessage方法处理。如果没什么要
处理的,就转入Application.Idle方法“程序在空闲时调用的方法”。
呼呼,再跟进ProcessMessage方法吧。

function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
begin
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end
else
FTerminate := True;
end;
end;

哎呀呀,终于有眉目了。ProcessMessage采用了一套标准的Windows API 函数
PeekMessage .... TranslateMessage;DispatchMessage。
有人说:Application->OnMessage = MyOnMessage; //不能响应SendMessage的消息,
但是可以响应PostMessage发送的消息,也就是消息队列里的消息

SendMessage和PostMessage最主要的区别在于发送的消息有没有通过消息队列。

原因就在这里。ProcessMessage使用了PeekMessage(Msg, 0, 0, 0, PM_REMOVE) 从消
息队列中提取消息。然后先检查是不是退出消息。不是的话,检查是否存在OnMessage
方法。如果存在就转入OnMessage处理消息。最后才将消息分发出去。

这样重载Application的OnMessage方法要比前两种方法更早得到消息,可以说是最快速
的方法了吧。举个例子:

void __fastcall TForm1::MyOnMessage(tagMSG &Msg, bool &Handled)
{
TMessage Message;
switch (Msg.message)
{
case WM_KEYDOWN:
Message.Msg = Msg.message;
Message.WParam = Msg.wParam;
Message.LParam = Msg.lParam;
MessageDlg("You Pressed Key!", mtWarning, TMsgDlgButtons() << mbOK, 0);
Handled = true;
break;
}
}

void __fastcall TForm1::FormCreate(TObject *Sender)
{
Application->OnMessage = MyOnMessage;
}

现在可以简短的总结一下VCL的消息机制了。
标准的BCB程序使用Application->Run()进入消息循环,在Application的
ProcessMessage方法中,使用PeekMessage方法从消息队列中提取消息,并将此消息从
消息队列中移除。然后ProcessMessage 方法检查是否存在Application->OnMessage方
法。存在则转入此方法处理消息。之后再将处理过的消息分发给程序中的各个对象。至
此,WndProc方法收到消息,并进行处理。如果有无法处理的交给重载的Dispatch方法
来处理。要是还不能处理的话,再交给父类的Dispatch方法处理。最后Dispatch方法实
际上将消息转入DefaultHandler方法来处理。

嘿嘿,实际上,你一样可以重载DefaultHandler方法来处理消息。但是太晚了一点。我
想没有人愿意最后一个处理消息吧...:-)

写到这里似乎可以结束了。但如果您看过上一篇的话,一定会注意到
Application->HookMainWindow方法。这又是怎么一回事呢?

如果您打算使用Application->OnMessage来捕获所有发送至您的应用程序的消息的话,
您大概要失望了。原因已经讲过,它无法捕获使用SendMessage直接发送给窗口的消
息,因为这不通过消息队列。您也许会说我可以直接重载TApplication的WndProc方
法。呵呵,不可以。因为TApplication的WndProc方法被Borland申明为静态的,从而无
法重载。显而易见,这么做的原因很可能是Borland担心其所带来的副作用。那该如何
是好呢?

查看TApplication的WndProc的pascal源码可以看到:

procedure TApplication.WndProc(var Message: TMessage);
... // 节约篇幅,此处与主题无关代码略去
begin
try
Message.Result := 0;
for I := 0 to FWindowHooks.Count - 1 do
if TWindowHook(FWindowHooks[I]^)(Message) then Exit;
... // 节约篇幅,此处与主题无关代码略去

WndProc方法一开始先调用HookMainWindow挂钩的自定义消息处理方法,然后再调用缺
省过程处理消息。这样使用HookMainWindow就可以在WndProc中间接加入自己的消息处
理方法。使用这个方法响应SendMessage发送来的消息很管用。最后提醒一下,使用
HookMainWindow挂钩之后一定要对应的调用UnhookMainWindow卸载钩子程序。给个例子

void __fastcall TForm1::FormCreate(TObject *Sender)
{
Application->HookMainWindow(AppHookFunc);
}

bool __fastcall TForm1::AppHookFunc(TMessage &Message)
{
bool Handled ;
switch (Message.Msg)
{
case WM_CLOSE:
mrYes == MessageDlg("Really Close??",
mtWarning,
TMsgDlgButtons() << mbYes << mbNo,
0) ? Handled = false : Handled = true ;
break;
}
return Handled;
}

void __fastcall TForm1::FormDestroy(TObject *Sender)
{
Application->UnhookMainWindow(AppHookFunc);
}

void __fastcall TForm1::Button1Click(TObject *Sender)
{
SendMessage(Application->Handle,WM_CLOSE,0,0);
}

这样,将本文中的两种方法相结合,您就可以自如的处理到达您的应用程序的各种消息
了。

 
原创粉丝点击