技术文章 > 深入C++ Builder之编写自己的元件(3)

深入C++ Builder之编写自己的元件(3)

2018-08-18 23:44

文档管理软件,文档管理系统,知识管理系统,档案管理系统的技术资料:

深入C++ Builder之编写自己的元件-深入分析VCL继承、消息机制(3)
这篇文章提及内容可能大家已经在很多地方看到过了,作者也是如此,只不过还看了很多VCL源代码,加上自己实际编写元件的经验,拼凑了这么一篇文章。所以所有言论都是个人观点、经验的描述,仅供参考。
你可转载,拷贝,但必须加入作者署名Aweay,如果用于商业目的,必须经过作者同意。
题外话
很多朋友看了我的前两篇文章后,纷纷来信说能不能介绍一些元件入门的基础知识,因为他们根本找不到相关资料,并询问我是如何知道这些知识的。诚然,网上确实没有这方面的介绍资料,更何况大家是学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
如果大家有什么问题可以来信与我讨论: