Delphi制作数据感知控件之浮想联翩
王维康 编写
知识点
本文共有6个关于控件的知识点:
1、基本控件的制作;2、感知属性的添加;3、关联控件的销毁处理;
4、事件方法的赋值; 5、属性页的制作; 6、组件包设计思想
关于制作感知控件的文章有不少,但涉及的内容大都比较单一,读者只能依照文章的陈述按部就班地操作,无法很好的理解控件的制作机制。本文试图通过讲解一个控件的制作流程来着重阐述制作感知控件的思路和编程思想,让读者真正了解VCL控件的制作机制,而非仅仅达到了解此控件的制作方法为目的,希望能使读者在阅读以后达到“一叶落而知天下秋”的效果,哈,夸张了。
本文以制作一个类似于DBEdit的控件为例。首先使用组件向导从TEdit下继承下来,命名TMyDataBaseEdit,单元名为MyDataBaseEdi.pas,安装在一个新的包文件中,起名为MyDataEditStd60.Dpk。
包的命名没有规则,但是我们建议遵守包的命名约定:包的命名与包的版本相关,包的名称前面几个字符通常表示作者或公司名,也可以是控件的一个描述词,后面紧跟的Std表示运行期包,Dsgn表示设计期包,然后是版本号。关于包的设计方式我们将随后详细说明。
数据感知
准备就绪,我们开始编辑控件的功能,首先添加数据源感知属性 .DataSource,实现此属性只需Pubished域添加一句话:
property DataSource: TDataSource read GetDataSource write SetDataSource;按下shift +ctrl+ C组合键(complete class at cursor)完成属性的自动结构化功能,private域自动添加两个函数:
procedure SetDataSource(const Value: TDataSource);
function GetDataSource: TDataSource;
这样,安装组件到面板,可以看到该组件已经拥有了.DataSource属性,并实现感知TDataSource控件功能。怎么样,简单吧,呵呵。其实我们由此可以得到:控件属性的感知只不过是将它的一个属性声明为将要感知的控件类而已,如果感知Image控件,则:
property MyImage:TImage read GetImage write SetImage;
但是,假如我们要将一个控件作为这个控件的子属性,即将这两个控件的代码合并,则不能单单需要上述那一句话了:
首先,去掉添加控件的注册函数;
其次,将其从TComponent继承改为从TPersistent继承下来。大家可试试看。
事件赋值
不过,该控件并不能真正与数据库相连,只是一个样子而已,要真正实现功能,我们需要添加代码,这时候我们用到了一个重要的类TfieldDataLink。
它是控件内部的数据联接对象,从TDataLink继承下来,它的作用是与TDataSource组件相互通信,连接单个字段进行数据提取。我们将要处理这个对象的OnDataChange事件,这样,当字段或记录有所改变时就会得到通知,进行相应的数据处理。Ok,我们声明对象并创建:
private域声明 FDataLink:TFieldDataLink;构造函数中创建
constructor TMyDataBaseEdit.Create(AOwner: TComponent);
begin
inherited;
FDataLink := TFieldDataLink.Create;
FDataLink.OnDataChange := DataChange;
end;
DataChange是我们private域声明的一个过程:procedure DataChange(Sender: TObject);这里面实现了我们这个控件的实际功能,并和FDataLink.OnDataChange事件相连。创建成功之后我们实现GetDataSource、SetDataSource函数过程:
function TMyDataBaseEdit.GetDataSource: TDataSource;
begin
Result := FDataLink.DataSource;
end;
procedure TMyDataBaseEdit.SetDataSource(const Value: TDataSource);
begin
if not (FDataLink.DataSourceFixed and (csLoading in ComponentState)) then
begin
FDataLink.DataSource := Value;
end;
if Value <> nil then
begin
Value.FreeNotification(Self);
end;
end;
关联控件的销毁
实现了上述代码,这样数据才真正的与控件关联起来,要个性化处理数据就要添加DataChange过程的代码了。
那么Value.FreeNotification(Self)代码是什么意思呢?请大家想一想:我们的组件需要和DataSource控件和DataSet控件相组合才能实现数据库数据的读写,那么当我们删除其中一个时,如果其他两个控件不知道,那么是不是会出现异常呢?答案是肯定的。
那么我们怎样才能做到通知其他组件呢?Yes, Value.FreeNotification(Self)就是做这个工作的!FreeNotification(self)将会把我们的组件置入其通知对象列表中,被撤消时,它会依次调用通知对象列表中所有对象的Notification方法,我们只需要在组件中重载它就行:
protected
{ Protected declarations }
procedure Notification(AComponent: TComponent;Operation: TOperation);override;
代码实现如下:
procedure TMyDataBaseEdit.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited Notification(AComponent, Operation);
if (Operation = opRemove) and (FDataLink <> nil) and
(AComponent = DataSource) then DataSource := nil;
end;
这样,当关联的参考控件被删除时,控件就会得到消息,来处理事件,防止异常的发生,否则将会导致Delphi开发环境的不稳定甚至死机,切记!
同时,不要忘了重载Destroy函数:
destructor TMyDataBaseEdit.Destroy;
begin
inherited;
FDataLink.Free;
FDataLink := nil;
end;
确保使用完之后销毁FDataLink的实例,释放空间。无论是写组件还是写程序我们都要严格注意声明的对象,一定要在使用完之后释放!
添加DataField属性
我们使用此控件的功能目的是想连接好数据库组件后,通过选择字段名,将其字段值显示在Edit.Text中。那么我们还缺一个属性.DataField。
published域声明:
property DataField: string read GetFieldName write SetFieldName;
实现如下:
function TMyDataBaseEdit.GetFieldName: string;
begin
Result := FDataLink.FieldName;
end;
procedure TMyDataBaseEdit.SetFieldName(const Value: string);
begin
FDataLink.FieldName := Value;
end;
OK,这样数据库中的字段就将列在下拉框中了。
下面我们填写我们的数据处理过程:
procedure TMyDataBaseEdit.DataChange(Sender: TObject);
begin
if FDataLink.Field <> nil then
begin
Text := FDataLink.Field.Text;
end;
end;
哈,现在基本上可以运行起来了。现在,我们将Table,DataSource连接起来,来看看当前记录发生改变时,MyDataBaseEdit控件的Edit框中显示所选字段的字段值
添加属性页(About)
现在基本功能已经具备了,下面我们给它加上一个属性页,我们这里只是做一个About对话框演示,开发者可根据自己的实际功能要求自行定制。
我们实现的效果是要求:在属性编辑浏览器中出现一项About属性,右侧显示:(About)… 点击“…”按钮弹出一个对话框。
下面我们开始新建一个Package,按照命名规则起名:MyDataEditDsgn60.Dpk,然后新建一个Form,作为About对话框,我们可以根据需要自行设计界面。我们新建一个类TAboutEditor使其从TPropertyEditor下继承过来:
TAboutEdit = class(TPropertyEditor)
FFrmAbout: TfrmAbout;
function GetAttributes: TPropertyAttributes;override;
function GetValue: string;override;
procedure Edit;override;
end;
注意:将全局的Form变量删掉,重新在TAboutEdit类中声明一个新的私有变量。
覆盖Edit函数:
procedure TAboutEdit.Edit;
begin
FFrmAbout := TfrmAbout.Create(Application);
FFrmAbout.ShowModal;
FFrmAbout.Free
end;
用来创建销毁窗体对象。
覆盖GetAttributes函数:
function TAboutEdit.GetAttributes: TPropertyAttributes;
begin
Result := [paDialog,paReadOnly];
end;
告诉IDE将以何种工作方式进行显示,关于TPropertyAttributes可以参考帮助看看,这里不再赘述。我们这里是以只读对话框的方式显示的,Object Inspector将在About属性旁边出现一个省略号按钮。当用户单击这个按钮,就调用Edit方法。
覆盖GetValue函数,是为了省略号按钮旁出现“(About)”的字样,并且只读。
function TAboutEdit.GetValue: string;
begin
Result := '(About)'
end;
好了,工作基本完成,如果你还想进一步控制,还可以在研究TPropertyEditor类的代码。这里就不细说了。
也许你产生疑问了:这样能行了吗?控件怎么知道这个Package的属性方法,没关联呀。对了,要想把编辑器和控件关联起来还要2项工作:
1、 控件增加一个string 类型的”About”属性
2、 在编辑器的单元里注册属性编辑器
第一步,很简单,大家早就熟悉增加一个属性的方法了:
private
FAbout: string
Published
Property About: string read FAbout write FAbout;
第二步,也只是在属性编辑器单元增加一个注册的全局过程函数:
procedure Register
beging
//只有一句话:
RegisterPropertyEditor(TypeInfo(string),TMyDataBaseEdit,'About',TAboutEdit);
end;
现在,我来解释一下RegisterPropertyEditor函数:它用来注册一个属性编辑器,将控件中的一个属性和编辑器关联起来。
l 第一个参数:TypeInfo(string),参数原型:PropertyType:PTypeInfo它是一个指针,指向要编辑的属性的运行期类型信息,我们通过TypeInfo()获取。
l 第二个参数:TMyDataBaseEdit,参数原型:Component: Calss,用于指定这个属性编辑器所作用的组件类。
l 第三个参数是一个字符串,用于指定被作用属性的名称。
l 第四个参数用于指定属性编辑器的类型。
假如第二个参数设为nil,第三个参数设为空串,会如何呢?嘿嘿,所有string类型的属性全部变成About框了,哈哈。
大家注意,我们在这里只是简单的做了一个About对话框,目的是为了能够让大家快速清晰地了解属性编辑器的设计原理,我们也可以把它做成一个复杂的有返回值的对话框,这样我们就可以真正用对话框来编辑控件的属性了。这就要看各位的实际需要了,但万便不离其宗,呵呵。
有兴趣的话大家还可以研究一下Delphi的Source/Property Editors目录下的StringsEdit单元。(关于属性编辑器更详细的各种类型、定义可以参考《Delphi5开发人员指南》第三部分“关于组件的开发”或仔细研究DesignEditors单元)
包的设计思想
在进行编辑器设计的时候我们新建了一个Package组件包。组件包,大家应该都比较了解它了,它类似于Dll,不过只是在Delphi和CBuilder环境中通用。很好的利用此包,可以使我们的程序模块清晰,能最大限度的代码重用,使程序的体积尽量减小,而且可以在这两种语言环境中互用。这里就不多说了,否则跑题太远,有兴趣的话大家可以跟我联系,共同探讨。
现在,我来说一下为什么要和控件的包分开,单独新建一个Package组件包。
首先,分块划分好管理,这不用多说了,一个运行期包,一个设计期包,使应用程序的体积变小,省得所有关于的单元全部链接到执行程序中去(Delphi5及前版本),即使你只在程序中静态地进行链接,混合运行时和设计时的代码也将使你的代码膨胀。因为你的设计时代码在运行时不会被执行,但是编译器并不知道,所以把它也一起链接进去了。
其实重要的一个原因是,Delphi6版本以来,VCl结构对Delphi5及前版本有所调整,Borland用DesignIntf替换了DsgnIntf,而且属性编辑器也被放进DesignEditors、DesignMenus、DesignWindows和其它的一些设计文件里。特别是DesignEditors使用了其它的一个名叫Proxies的IDE文件。而Proxies被编译进了DesignIDE.bpl文件中。DesignIDE.bpl已经不再是一个可以分发的文件,我们只能在开发环境中使用,不用说,这时候编译应用程序时就出现“找不到文件”的问题了。这时,我们将运行期包和设计期包分开,将设计期包加入designide.dcp文件,分别编译包文件,就能保证正确无误,编译通过!
所以我们遵守一些约定,将使我们的代码结构达到最优化,一般设置如下:
设计时包应该包括:
1、 所有的注册声明(最好放在一个单独的单元中);
2、 所有的属性编辑器;
3、 所有的组件编辑器;
4、 将需要DesignIDE支持的包。
注:组件编辑器是通过在组件上弹出快捷菜单,象Table等组件。它的制作比较类似,是从TComponentEditor下继承来的。这里不再多讲了。
运行时包应该包括:
1、 组件本身。
2、 组件可能在运行时调用的窗体(属性编辑器用到的)。
Package部分由于版本不同而对不同的版本要进行不同的设计,Delphi 6.0的编辑器版本是VER140,Delphi 5.0的编辑器版本是VER130,如果我们想适应不同的版本需要使用 {$IFDEF VER140}。。。{$ELSE}。。。{$ENDIF};在Uses域进行控制,这点大家稍加注意。
以上,是本人关于Delphi制作数据感知控件的经验之谈,希望能给各位一点启发,在开发的路上少走弯路。有认识不严密的地方,欢迎大家斧正,共同探讨!我的Email:weikang_wang@163.com