Delphi IDE扩展开发向导

http://blog.csdn.net/huangjacky/archive/2009/12/26/5083093.aspx

作者:Borland(由CnPack翻译)
来源:www.CnPack.org

以后文章中没有写我的开场白:技术交流,DH讲解 的都是转载的,知道作者名字的,我会写出来,不知道也没有办法了.
但是好文章还要拿出来和大家分享的.

一、 概  述 

1、 前言
Delphi的IDE扩展是一般程序员很少涉足的领域,不管是网上还是书店里,这方面的资料都是鲜有所见。Delphi7自带的帮助文件是我们最容易找到的资料,为了方便CnPack开发组成员以及对IDE扩展感兴趣的朋友对这一领域有更多的认识,我花了点时间把Delphi7中IDE扩展部分的帮助翻译成中文发布,希望对大家有所帮助。
2、 术语列表
以下是本文档翻译时使用的术语对照表:
插件(Add-in),以设计期包或DLL形式被设计期的IDE调用的扩展工具。
专家(Wizard),实现了IOTAWizard接口的IDE插件工具。
仓库专家(Repository Wizard),用来创建新的单元、窗体或工程的专家。
包(Package),Delphi中使用的特殊的动态链接库。
设计期包(Design-time Package),被编译为允许IDE在设计期装载的包。
运行期包(Runtime Package),被编译为允许DLL在运行期调用的包。
接口(Interface),Delphi中使用的COM风格的接口。
通知器(Notifier),由用户实现特定接口并由IDE在特定事件中调用的用户对象。
创建器(Creator),由用户实现特定接口的用于创建新的单元、窗体或工程的用户对象。
工程(Project),Delphi中的Project。
单元(Unit),Delphi中的Unit。
模块(Module),对应着一组在IDE中打开的逻辑上关联的文件集,可以是一个单元、包含窗体的单元、工程文件等对象。
编辑器(Editor),IDE中用来设计和编辑模块的对象。
其它如IDE、DLL、Action、Tools API这样的术语则沿用英文,不作翻译。

二、 扩展Delphi的IDE
1、 IDE扩展
通过使用Open Tools API(通常缩写为Tools API),你可以用你自己的菜单项、工具栏按钮、动态的窗体创建专家以及更多的东西来扩展和定制IDE。Tools API是一套超过100个用于关联以及控制IDE的接口,包括主菜单、工具栏、主Action列表以及图像列表、源代码编辑器内部缓冲区、键盘宏及键盘绑定、窗体设计器中的窗体及其上面的组件、调试器和正在被调试的进程、代码完成、消息视图,以及任务列表。
使用Tools API是一件容易的事情,只要写几个实现了特定接口的类,并调用由另一些接口提供的服务即可。你的Tools API代码必须编译并作为一个设计期包或DLL装载到设计期的IDE中。这样,编写一个Tools API扩展有些类似编写一个属性或组件编辑器。在阅读这份材料之前,请确信你已经对基本的 用包来工作 和 注册组件 比较熟悉了。

下面这些主题描述了怎样使用Tools API:
Tools API概述
编写一个专家类
获得Tools API服务
对文件和编辑器的操作
创建窗体和工程
IDE的专家事件通知
2、 Tools API概述
所有的Tools API声明都在这一个单元里:ToolsAPI。要使用Tools API,你通常要引用designide这个包,这意味着你需要将你的Tools API插件作为设计期包或使用了运行期包的DLL来构建。关于包和库的问题,参阅 安装专家包。
编写一个Tools API扩展的主要接口是IOTAWizard,故大部分IDE插件都称为专家(Wizards)。在绝大多数情况下,C++Builder和Delphi的专家可以通用。你可以在Delphi中编写和编译一个专家,然后在C++Builder中使用,反之亦然。这种共用的工作最好在同一个IDE版本号之间,但同样也可能编写一个专家并且他们能在两种产品的将来版本里都可使用。要使用Tools API,你可以编写一个专家类并实现在ToolsAPI单元中定义的一个或多个接口。专家可以利用在Tools API中提供的服务,每个服务都是一个提供一组相关函数的接口,接口的实现部分被隐藏在IDE里面。Tools API只公布了接口,你可以利用它们来编写你的专家,而不必关心那些接口的实现细节。这些不同的接口提供了对源代码编辑器、窗体设计器、调试器等的访问。怎样在你的专家中使用这些接口包含的服务,参阅 获得Tools API服务。
这些服务和其它的接口被划分为两个基本的分类,你可以按照用为类型名称的前缀来区分它们:
NTA(native tools API)本地的Tools API允许直接访问实际的IDE对象,如IDE的TMainMenu对象。当使用这些接口时,专家必须引用Borland的包,这意味着专家将限制于特定的IDE版本中。这类专家可以放在一个设计期包或使用了运行期包的DLL中。
OTA(open tools API)开放的Tools API不需要引用包,只能通过接口访问IDE。在理论上,如果你能支持Delphi的函数调用约定以及类似于AnsiString这样的Delphi类型,则你能够使用任何支持COM风格接口的语言来编写专家,但是几乎所有的Tools API功能都只能通过OTA接口获得。如果一个专家只使用OTA接口,则它有可能写成一个不依赖于特定IDE版本的DLL。
Tools API有两种类型的接口:一种是作为程序员的你必须实现的接口,另一种是IDE已经实现了的接口。大部分的接口属于后者的分类:接口定义了IDE的功能而隐藏了真正的实现。你必须实现的接口可分为以下三类:专家(Wizards)、通知器(Notifiers)以及创建器(Creators):
在前面的主题中提到,一个专家类要实现IOTAWizard接口以及可能的派生接口。
通知器是Tools API中另一种类型的接口。IDE使用通知器在某些你关注的事情发生时回调你的专家。你可以编写一个类来实现通知器接口,并使用Tools API注册该通知器,当用户打开一个文件、编译源代码、修改窗体、开始调试会话及其它情况时,IDE会回调你的通知器对象。通知器的介绍见 IDE的专家事件通知。
创建器是你必须实现的另一种类型的接口。Tools API使用创建器来创建新的单元、工程或文件,或用来打开一个已存在的文件。关于创建器的内容请查看 创建窗体和工程 部分。
其它的重要接口是模块(Module)和编辑器(Editor)。一个模块接口代表了一个打开的单元,包含一个或多个文件。一个编辑器接口代表一个打开的文件。不同类型的编辑器接口提供给你对IDE中不同方面的访问:源代码编辑器(Source Editor)对应着源代码文件,窗体设计器(Form Designer)对应着窗体文件,另外还有工程资源(Project Resource)对应资源文件。关于模块和编辑器的内容请查看 对文件和编辑器的操作 部分。
3、 编写一个专家类
一共有四种类型的专家,专家的类型依赖于专家类所实现的接口。下面的表格描述了这四种类型的专家:

四种类型的专家
接口 描述
IOTAFormWizard 用来创建新的单元、窗体或其它文件
IOTAMenuWizard 自动增加到Help菜单中
IOTAProjectWizard 用来创建一个新的应用程序工程。
IOTAWizard 不适合放在其它分类中的各种专家

这四种类型的专家区别仅在于用户怎样调用专家:
菜单型专家(Menu Wizard)将增加到IDE的Help菜单中。当用户选择该菜单项时,IDE将调用该专家的Execute方法。普通的专家表现得更为灵活,故菜单型专家通常只在原型和调试时使用。
窗体和工程专家又叫仓库专家,因为他们被放在对象仓库(Object Repository)中。用户在新建项目对话框中调用这些专家,用户也能在对象仓库(通过选择Tools|Repository菜单项)中看到这些专家。用户可以为一个窗体专家选中“New Form”检查框,这将通知IDE当用户从主菜单中选择“File|New Form”时,将自动调用这个窗体专家。用户同样可以选择“Main Form”检查框,这将通知IDE使用这个窗体专家来生成新应用程序默认的主窗体。用户还可以为一个工程专家选择“New Project”检查框,当用户选择“File|New Application”时,IDE将调用选择的工程专家。
第四种类型的专家用于不能放到其它分类时的情况。一个普通的专家自身不能做任何事,取而代之的是,你必须自己定义专家怎样被调用。

Tools API并不对专家作任何强制性的约束,比如并不是必须要有个工程专家才能创建工程。你可以很容易地写一个工程专家来创建一个窗体以及写一个窗体专家来创建工程(如果你确实想要这样做的话)。
下面的主题详细说明了怎样实现和安装专家:
实现专家接口
安装专家包

4、 实现专家接口
每一个专家类至少必须实现IOTAWizard接口,同样,也要求实现它的父接口:IOTANotifier和IInterface。 窗体和工程专家必须实现他们的所有父接口,即:IOTARepositoryWizard、IOTAWizard、IOTANotifier和IInterface。
你对IInterfac的实现必须遵循Delphi接口的一般规则,这同样也是COM接口的规则。即,QueryInterface执行类型匹配,_AddRef和_Release管理引用计数。你可能会想使用一个公共的基类来简化专家和通知器类的编写。出于这个考虑,ToolsAPI单元定义了一个类,TNotifierObject,它实现了IOTANotifier接口并使用了空方法体。
尽管专家继承自IOTANotifier,而且因此必须实现它定义的所有函数,但IDE通常并不使用这些函数,所以你的实现可以为空(它们在TNotifierObject中实现)。因此,当你编写你的专家类时,你只需要声明并实现那些在专家接口中引入的方法就行了,默认使用TNotifierObject对IOTANotifier的实现。
5、 安装专家包
类似于其它的设计期包,一个专家包(Wizard Package)也必须实现一个Register函数。(关于Register函数的详细说明见 注册组件。)在Register函数中,通过调用RegisterPackageWizard,你可以注册任意多的专家,并传递一个专家对象作为唯一的参数,如下所示:
procedure Register;
begin
RegisterPackageWizard(MyWizard.Create);
RegisterPackageWizard(MyOtherWizard.Create);
end;

同样,你也可以注册属性编辑器、组件等等,作为同一个包的一部分。
请记住,设计期包是Delphi主程序的一部分,这意味着所有窗体的名称在整个应用程序和所有其它的设计期包中都应该是唯一的。这是使用包方式主要的一个缺点:你并不知道其它人会怎样命名他们的窗体。
在开发中,安装包专家类似于其它的设计期包:在包管理器中点击Install按钮。IDE将编译和连接该包并尝试装载它。如果装载包成功,IDE会显示一个对话框通知你。
(译注:如果是DLL类型的专家,需要在注册表中注册,例如在Delphi7中注册一个名称MyWizard的专家,可以在注册表HKEY_CURRENT_USER\Software\Borland\Delphi\7.0\Experts中增加一个字符串项:MyWizard,值为DLL的完整路径文件名)
6、 获得Tools API服务
为了做一些有用的工作,专家需要访问IDE:它的编辑器、窗体、菜单等等,这些是服务接口的任务。Tools API包括很多的服务,例如用Action服务执行文件Action操作,用编辑器服务访问源代码编辑器,用调试器服务访问调试器,等等。下面的表格总结了所有的服务接口:

Tools API服务接口
接口 描述
INTAServices 提供对本地IDE对象的访问:主菜单、Action列表、图像列表和工具栏。
IOTAActionServices 实现基本的文件操作:打开、关闭、保存和重装载文件。
IOTACodeCompletionServices 提供对代码完成的访问,允许专家安装自定义的代码完成管理器。
IOTADebuggerServices 提供对调试器的访问。
IOTAEditorServices 提供对源代码编辑器及其内部缓冲区的访问。
IOTAKeyBindingServices 允许专家注册自定义的键盘绑定。
IOTAKeyboardServices 提供对键盘宏和绑定的访问。
IOTAKeyboardDiagnostics 切换按键调试。
IOTAMessageServices 提供对消息视图(Message View)的访问。
IOTAModuleServices 提供对打开的文件的访问。
IOTAPackageServices 查询已安装的包及他们的组件的名称。
IOTAServices 其它的服务。
IOTAToDoServices 提供对To-Do列表的访问,允许专家安装自己的To-Do列表管理器。
IOTAToolsFilter 注册工具过滤通知器(Tools Filter Notifiers)。
IOTAWizardServices 注册及删除专家。

要使用服务接口,使用在SysUtils单元中定义的全局的Supports函数将BorlandIDEServices变量转换为目标服务接口。例如:
procedure set_keystroke_debugging(debugging: Boolean);
var
diag: IOTAKeyboardDiagnostics
begin
if Supports(BorlandIDEServices, IOTAKeyboardDiagnostics, diag) then
diag.KeyTracing := debugging;
end;

如果你的专家频繁地使用一个特定的服务,你可以将这个服务的指针作为一个数据成员保存在你的专家类中。
下面的主题讨论了使用Tools API服务接口来工作时一些特定的事项:
使用本地IDE对象
调试专家
接口版本号

7、 使用本地IDE对象
专家可以完全地访问IDE的主菜单、工具栏、Action列表和图像列表。(注:IDE的很多弹出菜单不能直接通过Tools API来访问。)
对IDE本地对象的操作以INTAServices接口为起点,你可以使用这个接口来增加图像到图像列表,增加Action到Action列表,添加菜单项到主菜单,以及在工具栏上添加按钮。你也可以关联Action到菜单项和工具栏按钮。当专家释放的时候,它必须清除那些由它自己创建的对象,但是不能删除它增加到图像列表中的图像,因为删除图像可能会打乱所有在该专家之后增加的其它图像的索引号。
下面的主题阐述了如何执行这些操作:
增加图像到图像列表
增加Action到Action列表
删除工具栏按钮
专家操作的是IDE中真实的TMainMenu、TActionList、TImageList和TToolBar对象,故你可以象在编写其它应用程序那样写代码。这同样意味着你有很大的机会让IDE崩溃或者禁用掉一些重要的功能,例如删除文件菜单。调试专家 讨论了当你发现类似这样的问题时,怎样调试你的专家的方法。
8、 增加图像到图像列表
假如你打算增加一个菜单项来调用你的专家,你同样也会允许用户增加一个工具栏按钮来调用这个专家。第一个步骤是增加一个图像到IDE的图像列表,然后你增加的图像的索引号就可以在Action中使用,随后也就可在菜单项和工具栏中使用。使用图像编辑器(Image Editor)创建一个包含16X16位图资源的资源文件,然后在你的专家构造器中加上下面的代码:
constructor MyWizard.Create;
var
Services: INTAServices;
Bmp: TBitmap;
ImageIndex: Integer;
begin
inherited;
Supports(BorlandIDEServices, INTAServices, Services);
{ Add an image to the image list. }
Bmp := TBitmap.Create;
Bmp.LoadFromResourceName(HInstance, 'Bitmap1');
ImageIndex := Services.AddMasked(Bmp, Bmp.TransparentColor,
'Tempest Software.intro wizard image');
Bmp.Free;
end;

请确认使用你在资源文件中指定的名称或ID来装载位图。你必须选择一个颜色来作为图像的背景颜色。如果你不想要背景颜色,可以选择一个在位图中不存在的颜色。
9、 增加Action到Action列表
在 增加图像到图像列表 中获得的图像索引号可以用来创建Action,如下所示。专家使用OnExecute和OnUpdate事件。在专家中常用的方法是在OnUpdate事件中启用或禁用Action,请确认OnUpdate事件能很快的返回,否则用户将会发现在装载你的专家后,IDE变得慢如蜗牛。Action的OnExecute事件类似于专家的Execute方法。如果你使用菜单项来调用一个窗体或工程专家,你甚至可能希望让OnExecute直接调用Execute方法。
NewAction := TAction.Create(nil);
NewAction.ActionList := Services.ActionList;
NewAction.Caption := GetMenuText();
NewAction.Hint := 'Display a silly dialog box';
NewAction.ImageIndex := ImageIndex;
NewAction.OnUpdate := action_update;
NewAction.OnExecute := action_execute;

菜单项可以设置它的Action属性为新创建的Action。创建一个新的菜单项时比较复杂的部分是要知道它应该被插入到哪儿。下面的例子查找View菜单,并且创建一个新的菜单项作为第一项插入到View菜单下。(通常,依赖于绝对位置不是个好主意:你无法知道还有哪些其它的专家会将自己插入到菜单中。另外,将来版本的Delphi也可能会调整菜单项的顺序。一个更好的方法是使用特定的名称查找指定的菜单项。下面的简单例子仅用来说明概念。)
for I := 0 to Services.MainMenu.Items.Count - 1 do
begin
with Services.MainMenu.Items[I] do
begin
if CompareText(Name, 'ViewsMenu') = 0 then
begin
NewItem := TMenuItem.Create(nil);
NewItem.Action := NewAction;
Insert(0, NewItem);
end;
end;
end;

增加Actoin到IDE的Action列表中后,用户就能在定制工具栏时看到这个Action了。用户可以选择Action并把它作为按钮增加到工具栏上。这在你的专家被卸载时会带来一些问题:所有这些指向已经不存在的Action的工具栏按钮和它们的OnClick事件句柄都将被悬空。为了防止访问违规错误,你的专家必须查找所有引用了它的Action的工具栏按钮,并且删除它们。
10、 删除工具栏按钮
此处没有直接的函数来从工具栏中删除按钮,你必须自己发送CM_CONTROLCHANGE消息。消息的第一个参数是要修改的控件,第二个参数为零表示从工具栏中删除(非零是添加)。删除工具栏按钮后,专家析构器删除Action和菜单项,删除这些对象将自动把它们从IDE的ActionList和MainMenu中移除。
procedure remove_action (Action: TAction; ToolBar: TToolBar);
var
I: Integer;
Btn: TToolButton;
begin
for I := ToolBar.ButtonCount - 1 downto 0 do
begin
Btn := ToolBar.Buttons[I];
if Btn.Action = Action then
begin
{ Remove "Btn" from "ToolBar" }
ToolBar.Perform(CM_CONTROLCHANGE, WPARAM(Btn), 0);
Btn.Free;
end;
end;
end;
destructor MyWizard.Destroy;
var
Services: INTAServices;
Btn: TToolButton;
begin
Supports(BorlandIDEServices, INTAServices, Services);
{ Check all the toolbars, and remove any buttons that use this action. }
remove_action(NewAction, Services.ToolBar[sCustomToolBar]);
remove_action(NewAction, Services.ToolBar[sDesktopToolBar]);
remove_action(NewAction, Services.ToolBar[sStandardToolBar]);
remove_action(NewAction, Services.ToolBar[sDebugToolBar]);
remove_action(NewAction, Services.ToolBar[sViewToolBar]);
remove_action(NewAction, Services.ToolBar[sInternetToolBar]);
NewItem.Free;
NewAction.Free;
end;

11、 调试专家
Tools API为你的专家与IDE之间的交互提供了极大的灵活性。然而,这种灵活性也带来了风险,很容易因为空指针和访问违规导致错误。
当使用本地Tools API编写专家时,你编写代码的可能会导致IDE崩溃掉,还有可能当你编写完专家安装后却发现它不能象你预期那样工作,这就要求对设计期代码进行调试。不过这个问题很容易解决,因为专家被安装在Delphi自身中,你只需要简单的在“Run|Parameters…”菜单项中设置专家包的宿主应用程序为Delphi可执行文件(delphi32.exe)即可。
当你想要或需要调试一个包时,先不要安装它。取而代之的是,从菜单栏中选择“Run|Run”,这将会重新启动一个新的Delphi的实例。在新的实例中,从菜单栏中选择“Components|Install Package…”并选中已经编译好的包。回到原来的Delphi进程实例中,现在你将看到在专家的源代码中,那些小蓝点意味着你可以在源代码中设置断点了。(如果没有,打开你的编译设置确认设置了允许调试;确认安装了正确的包;另外还可以打开进程模块来确保你装载的.bpl文件确实是你想要的。)
这种方式下,你不能调试进入VCL、CLX或RTL的代码,但是你可以全面地调试你自己的专家,对于查找是哪里出错来说,已经足够了。
12、 接口版本号
如果你细心地查看过某些接口,例如IOTAMessageServices,你会发现他们从另一个名字很接近的接口继承而来,例如IOTAMessageServices50,而后者又继承自IOTAMessageServices40。使用这些版本号可以帮助你的代码隔离不同Delphi发布版本之间的变化。
Tools API遵循COM的基本规则,即接口和它的GUID永远不会修改。如果一次新的发布为某个接口增加了一些功能,Tools API将声明一个新的接口并继承自旧的接口。保留旧的GUID附加在原来未变化的接口上,而新的接口则使用一个新的GUID。使用旧的GUID的专家还可以继续工作。
Tools API还会修改接口的名称以保持源代码的兼容性。要想知道他们是怎样工作的,很重要的一点是要区别Tools API中两种不同类型的接口:由Borland实现的接口以及由用户实现的。
如果是由IDE实现的接口,名称会保留给最新版本的接口。因为新功能并不会影响到原来的代码,而旧的接口则在后面加上旧的版本号。
对一个由用户实现的接口来说,接口中新的成员需要在你的代码中用新的函数来实现,因此名称被保留给旧接口使用,而新的接口在其后面加上版本号。
以消息服务来举例,Delphi 6引入了一个新的功能:消息分组。因此,基本的消息服务接口需要一些新的成员函数。这些函数将在一个新的接口类中声明,新接口将继续使用IOTAMessageServices的名称,而旧的消息服务接口改名为IOTAMessageServices50(表示版本号5)。旧的IOTAMessageServices的GUID与新的IOTAMessageServices50的GUID相同,因为它们的成员函数是一致的。
IOTAIDENotifier接口是一个由用户实现接口的例子。Delphi 5增加了新的overload函数:AfterCompile和BeforeCompile。已经使用IOTAIDENotifer编写的代码不需要修改,但是如果要使用新功能,新的代码则需要修改,改为覆盖从IOTAIDENotifier50接口中继承来的新函数。Delphi 6中并没有为这个接口增加新的功能,所以当前版本使用的是IOTAIDENotifier50。
一个建议的规则是当编写新代码时使用最后面的派生类。而如果你只是想在新的Delphi版本中重新编译的话,保留原来旧的代码不变就行了。
13、 对文件和编辑器的操作
理解Tools API怎样操作文件是很重要的。这里主要的接口是IOTAModule,每一个模块对应着一组逻辑上关联的打开的文件。例如,一个简单的模块可能对应着一个单独的单元文件(Unit)。一个模块可以有一个或多个编辑器,每个编辑器对应着一个文件,比如源代码(.pas)或窗体(.dfm或.xfm)文件。编辑器接口反映了IDE编辑器状态,故专家也可以看到用户所见到的修改过的代码和窗体,甚至那些用户还没有保存过的修改。
下面的主题提供了关于模块和编辑器接口的内容:
使用模块接口
使用编辑器接口
14、 使用模块接口
要获得模块接口,先要从模块服务(IOTAModuleServices)开始。你可以查询模块服务来获得所有打开的模块,通过一个文件或窗体的名字来检索模块,或者打开一个文件以获得它的模块接口。
针对不同类型的文件,有不同类型的模块,例如工程、资源以及类型库。可以尝试将一个模块接口转换成特定类型的模块接口以判断该模块是否属于那种类型。例如,以下是一种获得当前工程组接口的方法:
{ Return the current project group, or nil if there is no project group. }
function CurrentProjectGroup: IOTAProjectGroup;
var
I: Integer;
Svc: IOTAModuleServices;
Module: IOTAModule;
begin
Supports(BorlandIDEServices, IOTAModuleServices, Svc);
for I := 0 to Svc.ModuleCount - 1 do
begin
Module := Svc.Modules[I];
if Supports(Module, IOTAProjectGroup, Result) then
Exit;
end;
Result := nil;
end;

15、 使用编辑器接口
每一个模块都有至少一个编辑器接口。有些模块拥有好几个编辑器,比如一个源代码(.pas)文件和一个窗体描述(.dfm)文件。所有的编辑器都实现了IOTAEditor接口,可以尝试将一个编辑器转换成另一个类型的编辑器接口以判断它是否属于那种类型。例如,要想获得一个单元的窗体编辑器接口,你可以这样做:
{ Return the form editor for a module, or nil if the unit has no form. }
function GetFormEditor(Module: IOTAModule): IOTAFormEditor;
var
I: Integer;
Editor: IOTAEditor;
begin
for I := 0 to Module.ModuleFileCount - 1 do
begin
Editor := Module.ModuleFileEditors[I];
if Supports(Editor, IOTAFormEditor, Result) then
Exit;
end;
Result := nil;
end;

编辑器接口给予你对编辑器内部状态的访问。你可以查看用户正在编辑的源代码或窗体上的组件,修改源代码、组件、属性、修改当前源代码及窗体编辑器中选择的内容,以及完成差不多所有最终用户能执行的操作。
使用窗体编辑器接口,专家能访问到窗体上所有的组件。每一个组件(包括根窗体或数据模块)都有一个关联的IOTAComponent接口。专家可以查看或修改组件的大多数属性。如果你需要完全控制组件,你可以将IOTAComponent接口转换为INTAComponent。本地组件接口允许你的专家直接访问TComponent指针。这一点在当你需要读取或修改一个Class类型的属性时是非常有用的,比如TFont的属性,只有在通过NTA风格的接口时才能访问。
16、 创建窗体和工程
Delphi已经提供了非常多的窗体和工程专家,当然你也可以编写你自己的。对象仓库(Object Repository)允许你创建可以在工程中使用的静态模板,而专家提供了更强大的功能,因为它是动态的。专家可以提示用户并根据用户不同的选择以创建不同类型的文件。
窗体或工程专家可以创建一个或多个新文件。通常,最好是创建未命名、未保存的模块,以代替真实的文件,这样当用户保存它们时,IDE会提示用户选择文件名。专家使用创建器(Creator)对象来创建这些模块。
创建器类实现一个继承自IOTACreator的创建器接口。专家传递一个创建器对象给模块服务的CreateModule方法,IDE会根据创建模块时需要的参数来回调创建器对象。
例如,一个创建新窗体的窗体专家通常会实现GetExisting并返回False,以及实现GetUnnamed并返回True。此时将创建一个没有名字的模块(这样用户在保存文件时必须选择一个文件名)并且没有已存在的文件与之对应(这样用户甚至在没有做任何改动时也需要保存文件)。创建器的其它方法将告诉IDE要创建的文件是什么类型的(例如工程、单元或窗体)、提供文件的内容或返回窗体的名称、父类的名称以及其它一些重要信息。附加的回调方法使得专家可以增加新的模块到新创建的工程,或增加组件到新创建的窗体中。
要创建新的文件,在窗体或工程专家中通常需要实现为新文件提供内容。要做到这点,可以写一个实现了IOTAFile接口的新类。如果你的专家只需要使用默认文件内容的话,你也可以在需要返回IOTAFile接口的函数中返回nil。
例如,假定你的组织要求在每个单元的最上面有一个标准的注释块,你可以在对象仓库中使用静态模板,但是你需要手动修改该注释块来反映其作者和创建日期。代替的方法是,你可以使用一个创建器来在文件创建时动态地填写这些内容。
编写专家的第一个步骤是创建一个新的单元和窗体。大多数的创建器函数返回零、空字符串或其它的缺省值,以通知Tools API使用它自己默认的实现来创建新的单元或窗体。覆盖GetCreatorType方法以通知Tools API将创建哪一种类型的模块:单元或是窗体。要创建单元,返回sUnit,要创建窗体,返回sForm。为了简化代码,这里使用一个简单的类将构造器参数作为创建器的类型,通过保存创建器类型在一个数据成员中,这样GetCreatorType返回这个值就可以了。然后,实现NewImplSource以及NewIntfSource来返回需要生成的文件的内容。
TCreator = class(TInterfacedObject, IOTAModuleCreator)
public
constructor Create(const CreatorType: string);
{ IOTAModuleCreator }
function GetAncestorName: string;
function GetImplFileName: string;
function GetIntfFileName: string;
function GetFormName: string;
function GetMainForm: Boolean;
function GetShowForm: Boolean;
function GetShowSource: Boolean;
function NewFormFile(const FormIdent, AncestorIdent: string): IOTAFile;
function NewImplSource(const ModuleIdent, FormIdent, AncestorIdent: string): IOTAFile;
function NewIntfSource(const ModuleIdent, FormIdent, AncestorIdent: string): IOTAFile;
procedure FormCreated(const FormEditor: IOTAFormEditor);
{ IOTACreator }
function GetCreatorType: string;
function GetExisting: Boolean;
function GetFileSystem: string;
function GetOwner: IOTAModule;
function GetUnnamed: Boolean;
private
FCreatorType: string;
end;

TCreator的大部分方法返回0、nil或空字符串。Boolean类型的方法返回True,除了GetExisting,它要返回False。最有趣的方法是GetOwner,它返回一个指向当前工程模块的接口指针,如果没有工程则返回nil。此处没有简单的方法来获得当前工程或当前工程组。代替的是,GetOwner必须遍历所有打开的模块,如果找到一个工程组,它将是唯一被打开的工程组,此时GetOwner就可以返回它的当前工程了。否则,该方法返回它找到的第一个工程模块,如果没有打开的工程则返回nil。
function TCreator.GetOwner: IOTAModule;
var
I: Integer;
Svc: IOTAModuleServices;
Module: IOTAModule;
Project: IOTAProject;
Group: IOTAProjectGroup;
begin
{ Return the current project. }
Supports(BorlandIDEServices, IOTAModuleServices, Svc);
Result := nil;
for I := 0 to Svc.ModuleCount - 1 do
begin
Module := Svc.Modules[I];
if Supports(Module, IOTAProject, Project) then
begin
{ Remember the first project module}
if Result = nil then
Result := Project;
end
else if Supports(Module, IOTAProjectGroup, Group) then
begin
{ Found the project group, so return its active project}
Result := Group.ActiveProject;
Exit;
end;
end;
end;

创建器在NewFormSource中返回nil,以产生一个默认的窗体文件。比较有趣的方法是NewImplSource和NewIntfSource,它们会创建一个IOTAFile接口的实现来返回文件的内容。
TFile类实现了IOTAFile接口,它返回-1作为文件的日期(这将意味着文件不存在),并以字符串返回文件的内容。为了简化TFile类,这里由创建器来生成字符串,TFile简单地把它传进去。
TFile = class(TInterfacedObject, IOTAFile)
public
constructor Create(const Source: string);
function GetSource: string;
function GetAge: TDateTime;
private
FSource: string;
end;
constructor TFile.Create(const Source: string);
begin
FSource := Source;
end;
function TFile.GetSource: string;
begin
Result := FSource;
end;
function TFile.GetAge: TDateTime;
begin
Result := TDateTime(-1);
end;

为了让文件内容更容易修改,你可以把它存储为资源,不过出于简化的目的,这个例子在专家中用硬编码的方法来实现。下面的例子生成了源代码,并假定有一个窗体。你也可以很容易地增加对普通单元的判断:检查FormIdent,如果它为空,创建一个普通的单元,否则创建一个窗体单元。这个例子中生成的代码框架跟IDE默认的基本一致(当然在最顶上增加了注释块),但是你可以按照你的意愿任意扩充它。
function TCreator.NewImplSource(
const ModuleIdent, FormIdent, AncestorIdent: string): IOTAFile;
var
FormSource: string;
begin
FormSource :=
'{ ----------------------------------------------------------------- ' + #13#10 +
'%s - description'+ #13#10 +
'Copyright © %y Your company, inc.'+ #13#10 +
'Created on %d'+ #13#10 +
'By %u'+ #13#10 +
' ----------------------------------------------------------------- }' + #13#10 + #13#10;
return TFile.Create(Format(FormSource, ModuleIdent, FormIdent,
AncestorIdent));
}

最后一步是创建两个窗体专家:一个使用sUnit创建器类型,另一个使用sForm。考虑更好的用户化需要,你还可以使用INTAServices来增加菜单项到File|New菜单中来调用这些专家。菜单项的OnClieck事件句柄直接调用专家的Execute函数。
一些专家需要允许或禁用菜单项,这依赖于IDE中发生的事件。例如,一个用于源代码控制系统集成的专家,需要在IDE中没有打开文件时禁用掉它的Check In菜单项,此时你可以通过在你的专家中 使用通知器 来实现。
17、 IDE的专家事件通知
编写一个表现良好的专家的重要标志是专家是否具有对IDE事件的响应能力。特别是,那些保留了模块接口的专家必须知道用户在什么时候关闭了模块,以释放模块接口。要做到这点,专家需要一个通知器,这意味着你必须编写一个通知器类。
所有的通知器类实现一个或多个通知器接口。通知器接口定义回调方法,专家使用Tools API注册通知器对象,当某些重要事件发生时,IDE回调通知器。
每一个通知器接口都派生自IOTANotifier,尽管对一个特定通知器而言,并非IOTANotifier中所有的方法都会用到。下表列出了所有的通知器接口,并为每个接口给出了一个概要的描述。

通知器接口
接口 描述
IOTANotifier 所有通知器的抽象基类
IOTABreakpointNotifier 在调试器中触发或切换一个断点
IOTADebuggerNotifier 在调试器中运行一个程序,或增加/删除断点
IOTAEditLineNotifier 跟踪源代码编辑器中代码行的变更
IOTAEditorNotifier 修改或保存源代码文件,或在编辑器中选择文件
IOTAFormNotifier 保存窗体,或修改窗体或窗体上的组件(或数据模块)
IOTAIDENotifier 装载工程,安装包,以及其它的全局IDE事件
IOTAMessageNotifier 在消息视图中增加或删除一个标签(消息组)
IOTAModuleNotifier 切换、保存或重命名模块
IOTAProcessModNotifier 在调试器中装载一个进程
IOTAProcessNotifier 在调试器中创建或销毁线程和进程
IOTAThreadNotifier 在调试器中切换线程的状态
IOTAToolsFilterNotifier 调用工具过滤器

要了解怎样使用通知器,先查看一下 创建窗体和工程 中的例子。该例子示范了模块创建器,通过创建一个专家来增加注释块到每个源代码文件中。注释块中包含了单元的初始名称,但是用户几乎总是会使用不同的文件名来保存文件。在这个例子中,如果专家能自动更新注释以匹配真实的文件名的话,会让用户感觉更为友好。
要做到这一点,你需要一个模块通知器。专家保存CreateModule返回的模块接口,并使用它来注册模块通知器。当用户修改或保存文件时,模块通知器将接收到通知,不过这些事件在这个专家中意义不大,所以并没有被实现,AfterSave及相关的函数只是空方法体。重要的函数是ModuleRenamed,当用户使用一个新名称来保存文件时IDE将会调用它。模块通知器类的声明如下:
TModuleIdentifier = class(TNotifierObject, IOTAModuleNotifier)
public
constructor Create(const Module: IOTAModule);
destructor Destroy; override;
function CheckOverwrite: Boolean;
procedure ModuleRenamed(const NewName: string);
procedure Destroyed;
private
FModule: IOTAModule;
FName: string;
FIndex: Integer;
end;

编写通知器的一个方法是在它的构造器中自动注册它自身,并在析构器中反注册通知器。在模块通知器的例子中,当用户关闭文件时,IDE会调用Destroyed方法。此时,通知器必须反注册它自身并释放它保存的模块接口。IDE释放它保存的通知器接口,将导致它的引用计数变为零并释放掉通知器对象。因此,你编写析构器时必须考虑到:通知器有可能已经被反注册过了。
constructor TModuleNotifier.Create( const Module: IOTAModule);
begin
FIndex := -1;
FModule := Module;
{ Register this notifier. }
FIndex := Module.AddNotifier(self);
{ Remember the module's old name. }
FName := ChangeFileExt(ExtractFileName(Module.FileName), '');
end;
destructor TModuleNotifier.Destroy;
begin
{ Unregister the notifier if that hasn't happened already. }
if Findex >= 0 then
FModule.RemoveNotifier(FIndex);
end;
procedure TModuleNotifier.Destroyed;
begin
{ The module interface is being destroyed, so clean up the notifier. }
if Findex >= 0 then
begin
{ Unregister the notifier. }
FModule.RemoveNotifier(FIndex);
FIndex := -1;
end;
FModule := nil;
end;

当用户重命名文件时,IDE会回调通知器的ModuleRenamed函数。这个函数传入新的文件名作为参数,专家可以使用它来更新注释块。要编辑源代码缓冲区,专家需要使用编辑位置接口。专家先定位到正确的位置,双重检查确定找到的是否正确的文本,并用新文件名替换掉该文本。
procedure TModuleNotifier.ModuleRenamed(const NewName: string);
var
ModuleName: string;
I: Integer;
Editor: IOTAEditor;
Buffer: IOTAEditBuffer;
Pos: IOTAEditPosition;
Check: string;
begin
{ Get the module name from the new file name. }
ModuleName := ChangeFileExt(ExtractFileName(NewName), '');
for I := 0 to FModule.GetModuleFileCount - 1 do
begin
{ Update every source editor buffer. }
Editor := FModule.GetModuleFileEditor(I);
if Supports(Editor, IOTAEditBuffer, Buffer) then
begin
Pos := Buffer.GetEditPosition;
{ The module name is on line 2 of the comment.
Skip leading white space and copy the old module name,
to double check we have the right spot. }
Pos.Move(2, 1);
Pos.MoveCursor(mmSkipWhite or mmSkipRight);
Check := Pos.RipText('', rfIncludeNumericChars or rfIncludeAlphaChars);
if Check = FName then
begin
Pos.Delete(Length(Check)); // Delete the old name.
Pos.InsertText(ModuleName); // Insert the new name.
FName := ModuleName; // Remember the new name.
end;
end;
end;
end;

如果用户在模块名之前插入了附加的注释会怎样呢?如果是这样的话,在模块名确定后,你需要使用一个编辑行通知器来保持对行号的跟踪。要做到这点,可以使用IOTAEditLineNotifier和IOTAEditLineTracker接口。
在编写通知器时你需要非常谨慎,你必须确保没有通知器的生存期能超过它所在的专家。例如,如果用户原来使用专家创建了一个新的单元,然后缷载了专家,此时还会有一个通知器附加在该单元上面。结果将是不可预料的,不过大多数情况下,IDE将会崩溃。因此,专家需要跟踪它创建的所有通知器,并且在专家释放前必须反注册每一个通知器。另一方面,如果用户首先关闭了文件,模块通知器接收到了一个Destroyed通知,这意味着通知器必须反注册它自己并且释放所有对模块的引用,此时通知器也必须将自身从主专家的通知器列表中删除掉
以下是专家Execute函数的最终版本。它创建了一个新的模块,使用模块接口来创建模块通知器,然后保存模块通知器到一个接口列表中(TInterfaceList)。
procedure DocWizard.Execute;
var
Svc: IOTAModuleServices;
Module: IOTAModule;
Notifier: IOTAModuleNotifier;
begin
{ Return the current project. }
Supports(BorlandIDEServices, IOTAModuleServices, Svc);
Module := Svc.CreateModule(TCreator.Create(creator_type));
Notifier := TModuleNotifier.Create(Module);
list.Add(Notifier);
end;

专家的析构器遍历接口列表并反注册列表中的每一个通知器。简单地让接口列表释放掉通知器接口是不够的,因为IDE还保留了同样的接口。你必须告诉IDE释放这些通知器接口来释放通知器对象。在这个例子中,析构器欺骗通知器让它们以为它们的模块被释放掉了。在更复杂的场合中,你会发现最好是为通知器类单元写一个反注册函数。
destructor DocWizard.Destroy; override;
var
Notifier: IOTAModuleNotifier;
I: Integer;
begin
{ Unregister all the notifiers in the list. }
for I := list.Count - 1 downto 0 do
begin
Supports(list.Items[I], IOTANotifier, Notifier);
{ Pretend the associated object has been destroyed.
That convinces the notifier to clean itself up. }
Notifier.Destroyed;
list.Delete(I);
end;
list.Free;
FItem.Free;
end;

专家的其它地方处理注册专家、安装菜单以及诸如此类的其它事务。

原文地址:https://www.cnblogs.com/chulia20002001/p/1894428.html