深入VCL 理解BCB的消息机制

<script language="JavaScript" type="text/javascript"> </script> VCL BCB <>


 

 


 

方法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()方法。反而提供了一个新的方法就是xycleo提到的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方法更早一点点得到消息并处理消息。




 

<script language="JavaScript" type="text/javascript"> </script> VCL BCB <>

 

 

 

时至今日,学习Windows编程的兄弟们都知道消息机制的重要性。所以理解消息机制也成了不可或缺的功课。

  大家都知道,Borland的C++ Builder以及Delphi的核心是VCL。作为Win32平台上的开发工具,封装Windows的消息机制当然也是必不可少的。

  那么,在C++ Builder中处理消息的方法有哪些呢?它们之间的区别又在哪里?如果您很清楚这些,呵呵,对不起啦,请关掉这个窗口。 如果不清楚那就和我一起深入VCL的源码看个究竟吧。『注:BCB只有Professional和Enterprise版本才带有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.
file://
#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::Dispatch(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:
Form1::Dispatch(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的写法,以免出现问题。

 
 

<script language="JavaScript" type="text/javascript"> </script> C++Builer线


 

 

 

 还在Dos时代,人们就在寻求一种多任务的实现。于是出现了TSR类型的后台驻留程序,比较有代表性的有Side Kick、Vsafe等优秀的TSR程序,这类程序的出现和应用确实给用户使用计算机带来了极大的方便,比如Side Kick,我们编程可以在不用进编辑程序的状态下,一边编辑源程序,一边编译运行,非常方便。但是,Dos单任务操作系统的致命缺陷注定了在Dos下不可能开发出真正的多任务程序。进入Windows3.1时代,这种情况依然没有根本的改变,一次应用只能做一件事。比如数据库查询,除非应用编得很好,在查询期间整个系统将不响应用户的输入。

  进入了Windows NT和Windows 9x时代,情况就有了彻底的改观,操作系统从真正意义上实现了多任务(严格地说,Win9x还算不上)。一个应用程序,在需要的时候可以有许多个执行线程,每个线程就是一个小的执行程序,操作系统自动使各个线程共享CPU资源,确保任一线程都不能使系统死锁。这样,在编程的时候,可以把费时间的任务移到后台,在前台用另一个线程接受用户的输入。对那些对实时性要求比较高的编程任务,如网络客户服务、串行通信等应用时,多线程的实现无疑大大地增强了程序的可用性和稳固性。

  在Windows NT和Windows 9x中,多线程的编程实现需要调用一系列的API函数,如CreateThread、ResumeThread等,比较麻烦而且容易出错。我们使用Inprise公司的新一代RAD开发工具C++Builder,可以方便地实现多线程的编程。与老牌RAD工具Visual Basic和Delphi比,C++Builer不仅功能非常强大,而且它的编程语言是C++,对于系统开发语言是C的Windows系列操作系统,它具有其它编程语言无可比拟的优势。利用C++Builder提供的TThread对象,多线程的编程变得非常简便易用。那么,如何实现呢?且待我慢慢道来,让你体会一下多线程的强大功能。

  1. 创建多线程程序:

  首先,先介绍一下实现多线程的具体步骤。在C++Builder中虽然用Tthread对象说明了线程的概念,但是Tthread对象本身并不完整,需要在TThread下新建其子类,并重载Execute方法来使用线程对象。在C++Builder下可以很方便地实现这一点。

  在C++Builder IDE环境下选择菜单File|New,在New栏中选中Thread Object,按OK,接下来弹出输入框,输入TThread对象子类的名字MyThread,这样C++Builder自动为你创建了一个名为TMyThread的TThread子类。同时编辑器中多了一个名为Unit2.cpp的单元,这就是我们创建的TMyThread子类的原码,如下:

  #include
  #pragma hdrstop
  
  #include “Unit2.h”
  #pragma package(smart_init)
  //---------------------
  // Important: Methods and properties of objects in VCL can only be
  // used in a method called using Synchronize, for example:
  //
  // Synchronize(UpdateCaption);
  //
  // where UpdateCaption could look like:
  //
  // void __fastcall MyThread::UpdateCaption()
  // {
  // Form1->Caption = “Updated in a thread”;
  // }
  //--------------------
  __fastcall MyThread::MyThread(bool CreateSuspended)
   : TThread(CreateSuspended)
  {
  }
  //--------------------
  void __fastcall MyThread::Execute()
  {
   //---- Place thread code here ----
  }
  //---------------------

  其中的Execute()函数就是我们要在线程中实现的任务的代码所在处。在原代码中包含Unit2.cpp,这个由我们创建的TMyThread对象就可以使用了。使用时,动态创建一个TMyThread 对象,在构造函数中使用Resume()方法,那么程序中就增加了一个新的我们自己定义的线程TMyThread,具体执行的代码就是Execute()方法重载的代码。要加载更多的线程,没关系,只要继续创建需要数量的TMyThread 对象就成。

  以上我们初步地实现了在程序中创建一个自定义的线程,并使程序实现了多线程应用。但是,多线程应用的实现,并不是一件简单的工作,还需要考虑很多使多个线程能在系统中共存、互不影响的因素。比如,程序中公共变量的访问、资源的分配,如果处理不当,不仅线程会死锁陷入混乱,甚至可能会造成系统崩溃。总的来讲,在多线程编程中要注意共享对象和数据的处理,不能忽视。因此,下面我们要讲的就是多线程中常见问题:

 2. 多线程中VCL对象的使用

  我们都知道,C++Builder编程是建立在VCL类库的基础上的。在程序中经常需要访问VCL对象的属性和方法。不幸的是,VCL类库并不保证其中对象的属性和方法是线程访问安全的(Thread_safe),访问VCL对象的属性或调用其方法可能会访问到不被别的线程所保护的内存区域而产生错误。因此,TThread对象提供了一个Synchronize方法,当需要在线程中访问VCL对象属性或调用方法时,通过Synchronize方法来访问属性或调用方法就能避免冲突,使各个线程之间协调而不会产生意外的错误。如下所示:

  void __fastcall TMyThread::PushTheButton(void)
  
  {
   Button1->Click();
  }
  
  void __fastcall TMyThread::Execute()
  {
   ...
   Synchronize((TThreadMethod)PushTheButton);
   ...
  }

  对Button1-〉Click()方法的调用就是通过Synchronize()方法来实现的,它可以自动避免发生多线程访问冲突。在C++Builder中,虽然有一些VCL对象也是线程访问安全的(如TFont、TPen、TBrush等),可以不用Sychronize()方法对它们的属性方法进行访问调用以提高程序性能,但是,对于更多的无法确定的VCL对象,还是强烈建议使用Synchronize()方法确保程序的可靠性。

  3. 多线程中公共数据的使用

  程序设计中难免要在多个线程中共享数据或者对象。为了避免在多线程中因为同时访问了公共数据块而造成灾难性的后果,我们需要对公共数据块进行保护,直到一个线程对它的访问结束为止。这可以通过临界区域(Critical Section)的使用来实现,所幸的是在C++Builder中,给我们提供了一个TCriticalSection对象来进行临界区域的划定。该对象有两个方法,Acquire()和Release()。它设定的临界区域可以保证一次只有一个线程对该区域进行访问。如下例所示:

  class MyThread : public TThread
  {
   ...
  private:
  TCriticalSection pLockX;
  int x;
  float y;
  ...
  };
  void __fastcall MyThread::Execute()
  {
  ...
  pLockX->Acquire();//Here pLockX is a Global CriticalSection variable.
  x++;
  y=sin(x);
  pLockX->Release();
  ...
  }

  这样,对公共变量x,y的访问就通过全局TCriticalSection 对象保护起来,避免了多个线程同时访问的冲突。

  4. 多线程间的同步

  当程序中多个线程同时运行,难免要遇到使用同一系统资源,或者一个线程的运行要依赖另一个线程的完成等等,这样需要在线程间进行同步的问题。由于线程同时运行,无法从程序本身来决定运行的先后快慢,使得线程的同步看起来很难实现。所幸的是Windows系统是多任务操作系统,系统内核为我们提供了事件(Event)、Mutex、信号灯(semaphore)和计时器4种对象来控制线程间的同步。在C++Builder中,为我们提供了用于创建Event的TEvent 对象供我们使用。

  当程序中一个线程的运行要等待一项特定的操作的完成而不是等待一个特定的线程完成时,我们就可以很方便地用TEvent对象来实现这个目标。首先创建一个全局的TEvent对象作为所有线程可监测的标志。当一个线程完成某项特定的操作时,调用TEvent对象的SetEvent()方法,这样将设置这个标志,其他的线程可以通过监测这个标志获知操作的完成。相反,要取消这个标志,可以调用ResetEvent()方法。在需要等待操作完成的线程中使用WaitFor()方法,将一直等待这个标志被设置为止。注意WaitFor()方法的参数是等待标志设置的时间,一般用INFINITE表示无限等待事件的发生,如果其它线程运行有误,很容易使这个线程死住(等待一个永不发生的事件)。

  其实直接用Windows API函数也可以很方便地实现事件(Event)、信号灯(semaphore)控制技术。尤其是C++Builder,在调用Windows API方面有着其它语言无可比拟的优势。所用的函数主要有:CreateSemaphore()、CreateEvent()、WaitForSingleObject()、ReleaseSemaphore()、SetEvent()等等,这里就不赘述了。

  本文结合Inprise(Borland)公司开发的强大的RAD工具C++Builder的编程,对Windows下的多线程编程作了比较全面的介绍。其实多线程的实现并不神秘,看了本文,你也可以编出自己的多线程程序,真正体会多任务操作系统的威力。

  附:本文是本人在使用C++Builder一年来的一些实践体会。在完成自己的项目的同时,发现对多线程的编程一般的书籍都介绍得比较少,而实际应用中,多线程编程又是如此的重要,因此,本文通过对多线程编程比较全面的介绍,愿能达到抛砖引玉之效。

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值