实际上,本不想造车,可官方的车太破,而 Jam software (原Softgem)的 VirtualTreeView(后面称老VT吧) 又不知道 FMX 版要等到什么时候,本着求人不如求己的基本原则,编写了这个组件。不过,感谢的话是必需说的,这个组件的核心思想实际上与老VT的基本一致。不过我没有照扒别人东西的习惯,虽然它的设计已经很好用,但我还是想按照自己的想法设计这个 TQVirtualTreeView(下面称新VT吧)。
首先,这个组件分成了三个级别,然后定义好接口并分别实现它:
- 数据管理
- 内容呈现
- 内容编辑
这么分层以后,接下来就是完成各自的组成部分。
一、数据管理这方面
每个结点对应的是一个 TQVTNode 对象,老VT是用的结构体,我用的是对象,而且是基于接口设计的,这样子释放和引用的事也省心一些,虽然理论上对象存在占用空间更大一占,创建速度秒慢一点的问题,但通过算法的优化,这些实际上都不算什么核心的问题。
使用老VT的时候,我们知道要设置 NodeDataSize,以便老 VT 分配 PVirtualNode 所占用的内存时,将附加数据内容考虑进去,保存起来。新 VT 换种设计思路,提供了一个扩展属性 Exts 来管理扩展列表,让你可以扩展任何基于接口的数据内容,而使用接口的原因就是内存的自动管理,我们不需要去关心内存释放的问题。当然,如果你扩展数据是记录或者普通对象怎么办?新 VT 给你提供了一个 TQSimpleInterface 模板类来做一个简单的封装。
我们回到 Exts 这个属性,它实际是 TList<IInterface> 类型,所以增删啥的我就不说了,新 VT 提供了额外的方法方便你检索扩展成员:
function ExtByType(const IID: TGuid; var AValue): Boolean; overload; function ExtByType(const AClass: TClass): TObject; overload; function ExtByType(const AClass: TClass; var AValue): Boolean; overload; function ExtByName(const AName: String): IInterface;
这几个接口,都会返回找到的第一个。注意第四个接口 ExtByName 要求你添加的扩展数据支持 IQVTNamedExt 接口,以但能够让函数得到扩展数据的名称。第三个可以认为是第二个的重载,第一个是根据接口 ID 直接去找对应的接口。告诉你一个小秘密,由于 TQVTNode 重载了 QueryInterface 接口,所以实际上,如果你懒的可以,你可以直接使用 Delphi 自身的一些对接口的支持来访问扩展数据,比如我们的扩展数据有一个类型是 IPropertyData:
procedure TForm1.vtPropsFocusChanged(Sender: TQVirtualTreeView; ANode: TQVTNode; ACol: Integer); var AData:IPropertyData; begin AData:=ANode as IPropertyData; if Assigned(AData) then ... end
和
procedure TForm1.vtPropsFocusChanged(Sender: TQVirtualTreeView; ANode: TQVTNode; ACol: Integer); var AData:IPropertyData; begin if ANode.ExtByType(IPropertyData,AData) then ... end
两种用法都是可以的。通过扩展数据的方式,而不是像老 VT 绑定死到 PVirtualNode 结构上,带来的优势是更灵活方便,缺点是需要额外分配更多的内存(列表对象和每个数据成员)和性能的略微下降,但我确定除非你写的很烂,否则你不会感觉到也们的速度区别。
二、内容呈现
内容的绘制这块实际上就是解决怎么将这个树画出来的问题。老VT提供了一个 TVirtualDrawTree 和一个 TVirtualStringTree,新 VT 在这方面的做法是为每个单元格的绘制请求一个 IQVTDrawer 接口,每个单元格怎么绘制的问题交给 IQVTDrawer 的实现去解决。所以在新 VT 中,绘制这个树的正常过程变成了:
- 确定要绘制的结点列表,不可见的结点就不需要画了嘛。
- 绘制控件的大外框;
- 绘制每个结点,包括他们的边框
- 绘制标题
好吧,有人可能说太麻烦了,我还得自己实现 IQVTDrawer 去画呀?当然不能这样,自己画当然是要毫不犹豫的支持的,但大部分情况下,我们不能让你自己去做这个费劲的事。所以新 VT 默认了很多类型的绘制器,你直接设置 TQVTNode 的 DrawerType 属性就好了。当然了,如果默认的绘制器无法满足你 BT 的需求,你有两种方式可以自绘:
- 直接自己实现 IQVTDrawer 接口,然后在新 VT 的 OnGetCellDrawer 事件中,返回这个接口的实例;
- 在窗体上放置一个 TQVTCustomDrawer 组件,然后响应 OnDraw 事件完成绘制。
这里牵涉到的许多绘制的各种参数,比如颜色/样式啥的怎么知道呢?这就需要通过另一个接口 IQVTCellData 来获取。这个接口是同样有两种方式来处理:
- 直接自己实现 IQVTCellData 接口,然后在新 VT 的 OnGetCellData 事件中,返回这个接口的实例;
- 在窗体上放置 TQVTCustomCell 子类的组件,然后响应对应的事件来返回需要的数据,比如 TQVTCustomTextCell 就要求你实现 OnGetText 事件以便用于支持 IQVTTextCellData 接口。系统默认提供的类型满足一般的需求应该没有多大问题。
三、内容编辑
一颗只能显示不能修改的树是不完美的,内容编辑控件要求实现的接口是 IQVTInplaceEditor,你可以在 OnGetCellEditor 事件中返回自定义的编辑器实现,当然默认的实现也为你准备好了,你可以将组件放置到窗体上,然后为每列的 Editor 来绑定对应的编辑器。
到这一步还差一点,你的内容就可以编辑了,你还需要设置新 VT 的 Options ,将 toEditable 选项启用。下面说下新 VT 内容编辑内部的实际流程:
- 当前如果处于编辑中,调用 EndEdit 结束上一个的编辑;
- 如果当前单元格处理可以编辑状态,触发 BeforeEdit 事件通知;
- 获取单元格对应的 IQVTInplaceEditor 实现(默认赋列的 Editor 属性,然后调用 OnGetCellEditor 事件给用户调整的机会;
- 调用 IQVTInplaceEditor.BeginEdit 来尝试进入编辑状态,如果返回 False,则退出编辑状态;
- 调用 IQVTInpalceEditor.SetBounds 调整编辑控件的位置,然后调用 IQVTInpalceEditor.Show 显示编辑控件供用户编辑;
- 完成后,调用 EndEdit 结束编辑或者调用 CancelEdit 取消编辑。调用 EndEdit 如果成功,会触发 AfterEdit 事件,CancelEdit 目前没有触发事件。
好了,既然牵涉到编辑,那么要编辑的内容从何而来,到何而去是要解决的另一个问题,同样它和内容呈现一样,是通过 IQVTCellData 的子接口来实现的,不同的编辑器类型要求实现不同的接口,具体控件就不在这里一一说明了。
综上所述,那么我们一般使用 TQVirtualTreeView 需要的步骤就很清楚了:
- 放一个 TQVirtualTreeView 控件,并添加各列需要的 IQVTCellData 和 IQInplaceEditor 组件;
- 修改 TQVirtualTreeView.Header.Columns,然后添加列并设置相应的 CellData/Editor 以及 DrawerType 属性(注意默认不显示标题,要显示标题,设置 Header.Options 包含 hoVisible );
- 实现各个 CellData 对应的事件,以返回相应的数据。在设计时,可以设置 CellData 的 DefaultText 属性,并设置 TQVirtualTreeView.RootNodeCount 属性来预览效果。
- 一般情况下,还需实现 TQVirtualTreeView.OnInit 方法以便建立结点与你内部数据之间的关联,当然有些场景下是不需要的。
好了,本篇教程只是一个入门的简单介绍,以方便你查看相应示例。具体的代码还是需要你自己去深入研究的,毕竟师傅领进门,修行在个人,这个控件不会有太多教程(主要是没时间),有需要的朋友自己研究下,有问题可以帮忙修改下反馈给我。
给大家提供了两个 Demo,位于 Demos\Delphi\FMX\VirtualTreeView 目录下。