深入C++ Builder之编写自己的元件-深入分析VCL继承、消息机制

来源:互联网 发布:sql 游标 遍历 编辑:程序博客网 时间:2024/05/02 04:20

这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多VCL源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章。所以所有言论都是个人观点、经验的描述,仅供参考。

你可转载,拷贝,但必须加入作者署名Aweay,如果用于商业目的,必须经过作者同意。

系统要求

如果你想一起跟着做的话,那么你应该看看这里,否则你可以直接跳过。
C++ Builder6 + updata4 (上帝造人的工具,以下简称BCB)
Windows2k or higher (必要)
作者强烈建议你使用WinNT,BCB在Win9x下有非常多的问题,而且非常不稳定,就算你不在乎这个,还有一个非常致命的问题,BCB的帮助文件在Win9x下显示不完全(因为BCB的帮助索引关键字数量超过Win9x的限制),这样非常难于参考帮助。
Delphi6 :( (必要)
什么?是不是写错了,完全没有写错,如果你要深入VCL查看源代码的话,在没有比用Delphi6更合适的了,在全部安装Delphi6后,把VCL Source的目录加入Search Path中,这样你可以在编辑器中按住Ctrl键,点击鼠标直接跳转到源代码处非常方便,比什么grep好用多了。

起步

对于VCL的消息机制,大家可以参考CKER的
http://www.csdn.net/develop/read_article.asp?id=8131

重复的内容我就不介绍了,但是对于编写元件来说上面的消息机制还是很模糊,而且很多时候并不是用那些方法来处理消息的,还有就是元件特有的CM_XXXXXXXX消息如何处理呢?如何加入自己的事件呢?这些问题我会在后面的讨论中做详细介绍。

站在巨人的肩膀

编写元件的第一件事情就是确定我们从那里继承的问题,选取一个好的祖先类是编写一个好的元件的第一步,那么到底如何选取他山之石呢?一般性的规则是这样的:
1.对于有界面的显示的,需要处理键盘事件的,又不是容器的组件从TCustomControl继承
2.对于有界面的显示的,需要不处理键盘事件的,需要处理鼠标事件的从TGraphicsControl继承
3.对于没有界面显示的,类似与TOpenDialog/TXpMenu这样的控件从TComponent继承
4.如果你想扩展某个指定的控件,比如TPanel,你最好从TCustomPanel继承,而不要从TPanel直接继承。

注意上面第4条规则,基本上所有组件都有TCustomXXX的父类,这也是VCL鼓励的继承对象,原因在于你可以定制元件属性的可见性,最重要的是他们的构造函数和析构函数是虚拟的。

这篇文章主要针对1,2规则的元件进行介绍,3,4相对简单就不作深入讨论了。

画出自己

元件要显示在窗体上,必须以一定的样子出现,那么可定要画出自己,大家都知道处理WM_PAINT消息就可以了,从CKER的文章里,我们可以得出很多方法来处理这个消息,比如:
__fastcall WndProc(TMessage msg)
{
switch(msg->msg)
{
case WM_PAINT:
//我们的处理代码
...
}

或者干脆用消息映射的宏,但这些都不是最好的方法。

从TControl以后的组件都有Paint这个虚拟方法,我们只要重载这个方法就可以自动绘制,相当于处理了WM_PAINT,这是因为:
procedure TGraphicControl.WMPaint(var Message: TWMPaint);
begin
if Message.DC <> 0 then
begin
Canvas.Lock;
try
Canvas.Handle := Message.DC;
try
Paint;
finally
Canvas.Handle := 0;
end;
finally
Canvas.Unlock;
end;
end;
end;
以上代码片断说明了这一点,据我所研究过的专业级组件都是通过重载这个函数来绘制自己的。

注意上面的代码片断就是用我上面提到的方法(装delphi6)按了几次鼠标左键得到的,是不是很实惠,^_^。

在Paint方里我们可以自由绘制,在后面的文章里我会交大家如何高效率绘制。

在很多时候,我们需要重绘自己,比如我前几天给网友做的划线的组件,当线的宽度改变时我们必须重绘自己,否则无法反映属性的改变,我见很多朋友使用 repaint()方法,这也不是最好的方法,我们应该用Invalidate(),为什么?留给大家看源代码吧,就算复习上面的知识了。
代码演示:
void __fastcall TLine::SetLineWidth(int value)
{
//TODO: Add your source code here
if(FLineWidth!=value)
{
FLineWidth=value;
Invalidate();
}
}



这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多VCL源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章。所以所有言论都是个人观点、经验的描述,仅供参考。

你可转载,拷贝,但必须加入作者署名Aweay,如果用于商业目的,必须经过作者同意。

DYNAMIC函数

在上篇文章我们讲到了如何绘制元件,但是光绘制元件还是不够的,一个元件不光要有样子,还要能够处理、相应用户输入,这就需要我们来处理键盘鼠标事件。

处理键盘鼠标事件同样像我们处理系统消息一样,比如WM_KEYDOWN等这样的消息,所以你可以同样像在Form中处理消息那样重载WndProc或者编写消息映射宏,所有的这些方法都可以用到元件中,但是我们没有不用,也没有必要这么做。VCL已经提我们预留了很多接口函数,我们只需要重载相应虚拟函数就可以完成相应事件的处理。

如果你要处理鼠标事件,那么你需要重写下列函数:

MouseDown

MouseUp

MouseMove

Click

如果你需要处理键盘,那么你需要处理下列函数:

KeyPress

KeyDown

KeyUp

对于键盘虚拟函数,你需要注意派生类必须继承自TCustomeControl(直接或间接,准确的说,也不一定)才能使用上面的虚拟函数。

上面的函数看起来和我们在设计Form时的函数差不多,但是他们有一个明显的差别,如果你重载他们似乎不是那么容易,如果你看VCL源代码,它只写一个overide关键字来标示函数属性,但是在C++里这样就不行了,为什么呢?

我们先来看一下在VCL中的源代码:

procedure KeyDown(var Key: Word; Shift: TShiftState); dynamic;

procedure KeyUp(var Key: Word; Shift: TShiftState); dynamic;

procedure KeyPress(var Key: Char); dynamic;

基本上所有的这些虚拟(动态)函数都是这样申明的,可以在C++并没有dynamic这个关键字啊?为了解释这个问题,我们先来了解一下什么时dynamic函数:

在Delphi中,函数有virtual和dynamic两种多态方式,对于virtual和我们C++中的virtual没有区别,而dynamic则不一样,dynamic仅维护一份虚拟方法表,当一个基类有多个多态函数,而这个基类又有可能被多次继承的时候,使用dynamic申明的函数可以减少代码长度,不过dynamic和virtual的行为都是差不多的,virtual的速度要比dynamic快一些,dynamic要比virtual代码长度小。

如果你想深入研究一下这个问题,可以参考以下文章:

http://www.csdn.net/develop/Read_Article.asp?Id=14729

或者去borland的新闻组提问,一定能得到满意的答复。

我们知道在标准C++中并没有dynamic关键字,所以为了C++ Builder能使用VCL的类,Borland做了扩充,我们可以直接使用DYNAMIC宏来实现动态函数,所以如果我们要在自己的派生类中重写这些消息处理函数,我们需要这样:

DYNAMIC void __fastcall MouseMove(Classes::TShiftState Shift, int X, int Y);

现在我们已经能画出元件,能处理元件的消息,基本上我们已经可以编写一些简单的元件了。在继续下面的内容之前,也作为这篇文章的结束,我们来实际编写一个简单的元件。这里我们假设你已经有了一定编写元件的经验,如果你看不懂相应代码也没有关系,我会在后面的文章做详细讨论。

下面时我以前帮网友写的一个简单的矢量绘图板的元件的源代码,它实现可以在元件上画出矢量直线,包括选取和重新调整:

(h头文件)

//---------------------------------------------------------------------------

#ifndef VecCanvasH

#define VecCanvasH

//---------------------------------------------------------------------------

#include <SysUtils.hpp>

#include <Classes.hpp>

#include <Controls.hpp>

#include "Shape.h"

typedef void __fastcall (__closure *THoverShapeEvent)(TObject* Sender,int Index);

typedef void __fastcall (__closure *TShapeSelectedEvent)(TObject* Sender,int Index);

enum ToolType{ttSelect,ttLine,ttRect};

//---------------------------------------------------------------------------

class PACKAGE TVecCanvas : public TCustomControl

{

private:

TList* ShapeList;

CShape* CurrentShape;

bool Down;

TPoint LastPos;

TPoint NewPos;

THoverShapeEvent FOnHoverShape;

ToolType FTool;

TShapeSelectedEvent FOnShapeSelected;

DYNAMIC void __fastcall MouseDown(TMouseButton Button, Classes::TShiftState Shift, int X, int Y);

DYNAMIC void __fastcall MouseUp(TMouseButton Button, Classes::TShiftState Shift, int X, int Y);

DYNAMIC void __fastcall MouseMove(Classes::TShiftState Shift, int X, int Y);

void __fastcall DoHoverShape(int index);

void __fastcall SetTool(ToolType value);

void __fastcall DoShapeSelected(int index);

protected:

void __fastcall Paint();

public:

__fastcall TVecCanvas(TComponent* Owner);

__fastcall ~TVecCanvas();

int __fastcall ShapeAtPos(int x, int y);

__published:

__property Color;

__property OnClick;

__property THoverShapeEvent OnHoverShape = { read=FOnHoverShape, write=FOnHoverShape };

__property Align;

__property OnMouseMove;

__property OnMouseDown;

__property OnMouseUp;

__property ToolType Tool = { read=FTool, write=SetTool };

__property TShapeSelectedEvent OnShapeSelected = { read=FOnShapeSelected, write=FOnShapeSelected };

};

对于上面的头文件,高手肯定一眼就明白了,对于初学者如果比较困难的话,或者干脆看不懂,你或许应该去看一看其他一些关于设计元件入门的文章,当然,我这里还是简要的做一下解释:

可能很多人都会问的问题就是为什么申明了OnClick就会有OnClick事件呢?它怎么知道鼠标点击了?答案是继承了父类的申明和实现,我只是负责把它表露出来,以便元件使用着能使用。这里又回到了我们第一篇文章那个话题,如果你要设计一个TButtonEx元件,你可以从TButton继承,也可以从 TCustomButton(假设有)继承,如果从TButton继承,那么所有的属性和方法都不用重新申明,TButton有的,TButtonEx都有,但是TButtonEx真的需要全部的TButton的属性和事件吗?当然不一定,我们只需要导出你感兴趣或者用户感兴趣的属性和事件就可以了。

还有就是__property关键字,这里我不打算详细介绍它,我想你应该去看看帮助或者其他什么东西。头文件里还有其他的一些东西,我们在下面再详细讨论,现在我们来看一下cpp文件。

//---------------------------------------------------------------------------

#endif

(cpp文件)

//---------------------------------------------------------------------------

#include <vcl.h>

#pragma hdrstop

#include "VecCanvas.h"

#pragma package(smart_init)

//---------------------------------------------------------------------------

// ValidCtrCheck is used to assure that the components created do not have

// any pure virtual functions.

//

static inline void ValidCtrCheck(TVecCanvas *)

{

new TVecCanvas(NULL); //保证元件中没有纯虚函数,否则不能生成元件

}

//---------------------------------------------------------------------------

__fastcall TVecCanvas::TVecCanvas(TComponent* Owner)

: TCustomControl(Owner)

{

ShapeList=new TList();

CurrentShape=NULL;

Down=false;

FTool=ttLine;

}

//---------------------------------------------------------------------------

namespace Veccanvas

{

void __fastcall PACKAGE Register() //包的注册函数

{

TComponentClass classes[1] = {__classid(TVecCanvas)};

RegisterComponents("Draw Suite", classes, 0);

}

}

//---------------------------------------------------------------------------

__fastcall TVecCanvas::~TVecCanvas()

{

//TODO: Add your source code here

delete ShapeList;

}

void __fastcall TVecCanvas::MouseDown(TMouseButton Button,

Classes::TShiftState Shift, int X, int Y)

{

//TODO: Add your source code here

if(Button!=mbLeft)

return;

switch(FTool)

{

case ttLine:

LastPos.x=X;

LastPos.y=Y;

NewPos.x=X;

NewPos.y=Y;

Down=true;

case ttSelect:

{

int sh=ShapeAtPos(X,Y);

if(sh!=-1)

DoShapeSelected(sh);

break;

}

}

}

void __fastcall TVecCanvas::MouseUp(TMouseButton Button,

Classes::TShiftState Shift, int X, int Y)

{

//TODO: Add your source code here

Down=false;

if(Button==mbLeft)

{

switch(FTool)

{

case ttLine:

CShape* sh= new CLine();

sh->Start=LastPos;

sh->End=TPoint(X,Y);

sh->Type=stLine;

ShapeList->Add(sh);

Canvas->MoveTo(LastPos.x,LastPos.y);

Canvas->LineTo(X,Y);

break;

}

}

}

void __fastcall TVecCanvas::MouseMove(Classes::TShiftState Shift,

int X, int Y)

{

//TODO: Add your source code here

if(!Down)

{

int sh=ShapeAtPos(X,Y);

if(sh!=-1)

DoHoverShape(sh);

return;

}

switch(FTool)

{

case ttLine:

Canvas->Pen->Mode=pmNotXor;

Canvas->MoveTo(LastPos.x,LastPos.y);

Canvas->LineTo(NewPos.x,NewPos.y);

NewPos=TPoint(X,Y);

Canvas->MoveTo(LastPos.x,LastPos.y);

Canvas->LineTo(NewPos.x,NewPos.y);

Canvas->Pen->Mode=pmCopy;

break;

}

}

void __fastcall TVecCanvas::Paint() //简单重载了Paint函数

{

//TODO: Add your source code here

//Canvas->Brush->Color=Color;

//Canvas->FillRect(this->ClientRect);

for(int n=0;n<ShapeList->Count;n++)

{

CShape* sh=(CShape*)ShapeList->Items[n];

sh->Draw(Canvas);

}

}

void __fastcall TVecCanvas::DoHoverShape(int index)

{

//处理自定义事件

if(FOnHoverShape)

{

FOnHoverShape(this,index);

}

}

int __fastcall TVecCanvas::ShapeAtPos(int x, int y)

{

//TODO: Add your source code here

for(int n=0;n<ShapeList->Count;n++)

{

CShape* sh=(CShape*)ShapeList->Items[n];

if(sh->IsPartOf(TPoint(x,y)))

return n;

}

return -1;

}

void __fastcall TVecCanvas::SetTool(ToolType value)

{

//TODO: Add your source code here

if(FTool!=value)

{

FTool=value;

}

}

void __fastcall TVecCanvas::DoShapeSelected(int index)

{

//TODO: Add your source code here

if(FOnShapeSelected)

{

FOnShapeSelected(this,index);

}

}

上面的代码基本上运用了上面的消息处理知识和绘制的知识,所以很容易看懂,注意这里:

typedef void __fastcall (__closure *THoverShapeEvent)(TObject* Sender,int Index);

typedef void __fastcall (__closure *TShapeSelectedEvent)(TObject* Sender,int Index);

是我自己定义的消息处理,这也是我们下面文篇将要讨论的问题-自定义消息的处理和CM_XXXX消息


题外话

很多朋友看了我的前两篇文章后,纷纷来信说能不能介绍一些元件入门的基础知识,因为他们根本找不到相关资料,并询问我是如何知道这些知识的。诚然,网上确实没有这方面的介绍资料,更何况大家是学BCB的,对于Delphi的源代码学习起来更是困难,对于作者来说也不比大家知道多少,我认为最好的方式就是看VCL源代码和去Borland的新闻组提问,至少我是这样解决问题的,希望你也可以。

这里是Borland新闻组地址,如果你英文够好,他们基本是有问必答的:

forums.borland.com

对于那些想学习基础元件知识的朋友,我会在这系列文章的最后部分专门安排2篇文章作为礼物送给你们,一篇是我会实际分析一个专业级元件,来个源代码解剖,把所有细节展示给大家,第二篇是我会实际编写一个简单使用的组件,并介绍全过程,希望大家喜欢。

更多消息处理

已经写了2篇文章了,怎么还是消息处理?是的,编写元件就是处理消息和表露事件,对于一般的消息处理,前面2篇文章介绍的内容已经足够用了,但是很多时候这还是不够的,比如如果在设计时期你更改了元件的Font属性,而你又想根据字体重新绘制。很明显传统的Windows消息处理其不到丝毫作用,这样的消息通常是WM_XXXX的形式。如果你研究过VCL源代码,你会发现很多CN_XXXX和CM_XXXX这样的消息,如果你要完成我上面提到的消息处理,这些消息可以帮助完成任务。

其实,VCL存在一些非API消息以供其内部使用,为什么要这样做呢?这要从WM_COMMAND & WM_NOTIFY消息说起,我们说WM_COMMAND消息并不是直接发给实际产生消息的窗体,而是发送到它的父窗体。但是父窗体几乎不可能用通常方法处理这些根本不知道如何处理的消息,于是父窗体把这个消息加上CN_BASE在分发到实际的子窗体中,然后由实际的子窗体处理。

比如TBitBtn元件为了在按钮表面绘制图象,处理了CN_DRAWITEM消息,这个消息处理函数是这样写的:

FCanvas.Handle := DrawItemStruct.hDC;

R := ClientRect;

… //省略一部分

if IsDown then

OffsetRect(R, 1, 1);

TButtonGlyph(FGlyph).Draw(FCanvas, R, Point(0,0), Caption, FLayout, FMargin,

FSpacing, State, False, DrawTextBiDiModeFlags(0));

if IsFocused and IsDefault then

begin

R := ClientRect;

InflateRect(R, -4, -4);

FCanvas.Pen.Color := clWindowFrame;

FCanvas.Brush.Color := clBtnFace;

DrawFocusRect(FCanvas.Handle, R);

end;

FCanvas.Handle := 0;

可以看出这和通常处理Paint的方法差不多,其实都是在HDC上作图。如果你学习过SDK的话,其实我们可以自己处理WM_NOTIFY消息来处理那些由控件产生的消息,只不过VCL替我们封装了一下而已。

还有一些消息是VCL内部控件而产生的,这类消息通常是CM_XXXX的格式,比如CM_FONTCHANGED这个消息就是当字体改变的时候触发,详细的定义你可以在Controls.pas文件中找到,这里就不再详细介绍了

对于上面的CM_FONTCHANGED消息,通常是这样处理的:

procedure TBitBtn.CMFontChanged(var Message: TMessage);

begin

inherited;

Invalidate;

end;

通过上面的讨论,得出一个结论,所有CN/CM消息都可以自己处理,但是他们没有对应的虚函数,所以我们只好用老方法,所以消息映射宏在这里是最好得解决方案,比如像这样:

BEGIN_MESSAGE_MAP

VCL_MESSAGE_HANDLER(CN_DRAWITEM, TWMDrawItem, CNDrawItem)

END_MESSAGE_MAP(TCustomControl)

Void __fastcall CNDrawItem(TWMDrawItem Msg);

定义自己的消息

在上篇文章的结束,我示范了一段元件代码,如果你还记忆犹新的话:

typedef void __fastcall (__closure *THoverShapeEvent)(TObject* Sender,int Index);

typedef void __fastcall (__closure *TShapeSelectedEvent)(TObject* Sender,int Index);

是否还记得上面的代码?

大概来说那是函数指针的申明,对于初学者来说,上面的申明真的很晦涩,我来解释一下:THoverShapeEvent是一个函数指针,该函数的返回值是void , 调用类型是__fastcall,有2个行参,分别是TObject*和int,关键在于红色的__closure关键字,什么意思?

在BCB的帮助我我找到了如下说明:

The keyword __closure was added to support the VCL and is used when declaring event handler functions.

就是如此简单,几乎没有提供任何信息,只知道__closure提供对事件处理函数的支持,下面我来详细介绍一下:

不知道你有没有写过这样的代码:

我们设计了一个类,比如遍历磁盘,有一个数据成员是回调函数指针,当我们遍历磁盘的的函数找到了一个文件时调用这个回调函数,通常情况下,我们这个回调函数需要申明在类的外面,那么还是指针需要这样申明:

typedef void __fastcall (*BDCallBack)(String path,int type);

但是这显然不符合OO设计原则,如果你想把一个类的成员函数指定为这个成员函数,那么你将需要这样申明:

typedef void __fastcall (base::* BDCallBack)(String path,int type);

同时你需要这样赋值:

BDCallBack m=&bass::func;

语法越来越晦涩了,这还不是最重要的,如果有很多类的成员函数都需要指定为回调函数呢?你需要为每一个类申明一个类似的函数指针,我想你已经崩溃了。

__closure这个时候就有用武之地了,如果你这样申明:

typedef void __fastcall (__closure *BDCallBack)(String path,int type);

那么所有问题都解决了,它可以方便的透过对象直接访问成员函数,在所有的类中你都可以这样做:

class A

{

BDCallBack func;

Void DoSometing()

{

func(“Find it”,0);

}

};

class B

{

Funcb()

{

A a;

a.func=this.callback;

}

void __fastcall callback(String path,int type)

{

}

}

上面的代码是不是很简捷,但这跟编写元件有什么关系呢?

我们还是以上篇文章的例子为例:

if(!Down)

{

int sh=ShapeAtPos(X,Y);

if(sh!=-1)

DoHoverShape(sh);

return;

}

这段代码很好的说明了问题,可以看出我们需要表露的事件同样是一个回调函数,我是这样调用的:

void __fastcall TVecCanvas::DoHoverShape(int index)

{

//TODO: Add your source code here

if(FOnHoverShape) //如果属性被赋值,及有相应处理函数

{

FOnHoverShape(this,index);

}

}

我是这样赋值的:

__property THoverShapeEvent OnHoverShape = { read=FOnHoverShape, write=FOnHoverShape };

可见事件处理器其实是一个属性,BCB会自动把On开头属性当作事件对待,所以这个属性就出现在Event列表里了。

我们来总结一下:我们自定义的事件实际上就是回调函数,在相应需要触发的地方调用由元件用户指定了的回调函数,一句话道破了自定义事件的真谛,但是却花了一大篇文章来解释它的原理,即使如此,我仍然相信由很多朋友没有真正了解这其中的奥秘,如果是这样,你需要看看什么是CallBack函数,属性如何定义等等这样的文章。

最后,这这篇文章的结尾,我留下自己的Email:

sineysan@163.com

如果大家有什么问题可以来信与我讨论:


0 0