自动规范控件前缀命名的专家 作者:陈省 在编程过程中对代码进行规范的命名,可以使编出的代码便于理解和维护,尤其对于大型软件,由于代码量极其巨大,规范命名就更为重要。记得在我刚开始编程的时候,对代码的规范命名很不以为然,认为实在是有点浪费时间,所以写出的程序中的控件基本上是IDE起什么名,我就用什么名,由于刚开始写的程序比较短,影响还不是太明显。后来,有一回写了一个稍微长了一点的程序,过了一段时间我又需要对它进行修改,把程序翻出来一看就傻了眼了,程序中一共用到了10个按钮,名字从button1一直排到了button10,每个按钮还定义了一堆事件,当时那个代码把我看得这个头晕呀,没办法只好重新修改,忙了半天比起完全推倒了重写,根本没节省多少时间。从那以后,我就比较注意这方面了,每生成一个控件都加上一个前缀(关于前缀的缩写Borland曾经写过一个规范,有人翻译成了中文,在网上可以查到。本文的附录中列出了基本的前后缀列表)。 后来虽然重视了,但是程序写多了,每加一个控件都要改前缀,工作量还是很大的,并经常会忘,而且很容易写错。有了OTA后,能不能利用向导在控件生成的时候来自动生成控件的前缀? 仔细研究一下ToolsApi.pas就会发现这是完全可能的,前面提到过IOTAFormNotifier接口提供了一个ComponentRenamed方法,当编程时向窗体添加删除或修改控件名称时,IDE都会调用这个方法。这就意味着通过编写一个实现了IOTAFormNotifier接口的TNotifierObject的子类,就可以在ComponentRenamed方法中获得IDE的通知,进而自动为新加的控件添加前缀使之符合需要的命名规范。要实现的FormNotifier类声明如下: TPreFFormNotifier = class( TNotifierObject, IOTANotifier, IOTAFormNotifier ) private FFileName : String; public constructor Create( FileName : String ); destructor Destroy; override; procedure FormActivated; procedure FormSaving; //ComponentRenamed方法是关键!!! procedure ComponentRenamed(ComponentHandle: TOTAHandle; const OldName, NewName: string); { IOTAModuleNotifier } end; 要添加IOTAFormNotifier需要调用IOTAFormEditor.AddNotifier方法,而只有当文件打开后才能获得对应文件的IOTAFormEditor接口,同时在文件关闭前又必须删除挂在对应模块上的Notifier(否则会引发异常),这就意味着FormNotifier的生存期是在文件打开和关闭之间,也就意味着必须在合适的时间添加和删除FormNotifier,幸运的是IDENotifier提供了FileNotification 方法,参数NotifyCode是TOTAFileNotification类型的集合类型,类型定义如下: TOTAFileNotification = (ofnFileOpening, ofnFileOpened, ofnFileClosing, ofnDefaultDesktopLoad, ofnDefaultDesktopSave, ofnProjectDesktopLoad, ofnProjectDesktopSave, ofnPackageInstalled, ofnPackageUninstalled); 集合值意义如表3.3: 表3.3
| 值 | 意 义 | | ofnFileOpening | 通知向导文件正被打开 | | ofnFileOpened | 通知向导文件已经被打开 | | ofnFileClosing | 通知向导文件正在关闭 | | ofnDefaultDesktopLoad | 通知向导缺省桌面加载 | | ofnDefaultDesktopSave | 通知向导缺省桌面保存 | | ofnProjectDesktopLoad | 通知向导项目桌面配置加载 | | ofnProjectDesktopSave | 通知向导项目桌面配置保存 | | ofnPackageInstalled | 通知向导系统安装完包 | | ofnPackageUninstalled | 通知向导系统卸载完包 |
这里需要响应ofnFileOpened和ofnFileClosing事件,所以需要实现IOTAIDENotifier接口,对象的接口声明如下: TPreFIdeNotifier = class( TNotifierObject, IOTANotifier, IOTAIDENotifier) public constructor Create; destructor Destroy; override; { IOTAIDENotifier } procedure FileNotification(NotifyCode: TOTAFileNotification; const FileName: string; var Cancel: Boolean); procedure BeforeCompile(const Project: IOTAProject; var Cancel: Boolean); overload; procedure AfterCompile(Succeeded: Boolean); //overload; end; 对应的FileNotification方法的示意流程如下: procedure TPreFIdeNotifier.FileNotification( NotifyCode: TOTAFileNotification; const FileName: string; var Cancel: Boolean); var Module : IOTAModule; Editor : IOTAEditor; FormEditor : IOTAFormEditor; FormNotifier : TPrefFormNotifier; FormNotifierI, ListI, I : Integer; begin Case NotifyCode of ofnFileOpened : begin { 获得对应于相应文件的IOTAModule接口 } Module := ( BorlandIDEServices as IOTAModuleServices ).FindModule (FileName); { 遍历相应的全部文件} for i := 0 to Module.GetModuleFileCount - 1 do begin { 获得FileEditor } Editor := Module.GetModuleFileEditor(i); if Editor.QueryInterface( IOTAFormEditor, FormEditor ) = S_OK then begin { 如果FileEdiotr是一个FormEditor的话添加Notifier } FormNotifier := TPrefFormNotifier.Create( FileName ); FormNotifierI := FormEditor.AddNotifier ( FormNotifier ); if FormNotifierI < 0 then begin FormNotifier.Free; end else begin NotifierList.AddObject(FileName, Pointer(FormNotifierI)); end; end end; end; ofnFileClosing : begin if NotifierList.Find(FileName, ListI) then begin Module := ( BorlandIDEServices as IOTAModuleServices ).FindModule (FileName); { 获得Notifier在列表中的索引,利用索引值可以删除相应的Notifier} FormNotifierI := Integer(NotifierList.Objects[ListI]); for i := 0 to Module.GetModuleFileCount - 1 do begin Editor := Module.GetModuleFileEditor(i); if Editor.QueryInterface( IOTAFormEditor, FormEditor ) = S_OK then begin FormEditor.RemoveNotifier ( FormNotifierI ); NotifierList.Delete(ListI); end; end; end; end; end; end; 注意上面的代码,由于当窗体关闭时,必须确保每一个Notifier都被正确地释放。为此,需要建立一个Notifier列表来管理添加后的Notifier,这里用了一个TStringList来进行管理,注意储存Notifier索引的字符串列表必须是排序好的。另外Notifier列表需要声明为一个全局变量,并且注意不能在类中初始化列表,这样很容易造成重复创建,我们要在单元的initialization和finalization部分进行列表的初始化和释放工作,代码如下: var Index : Integer; NotifierList : TStringList; ////////////////////////////////////////////////////// initialization NotifierList := TStringList.Create; NotifierList.Sorted := True; Index := (BorlandIDEServices as IOTAServices).AddNotifier(TPreFIdeNotifier.Create); finalization (BorlandIDEServices as IOTAServices).RemoveNotifier(Index); NotifierList.Free; end. 当一个新窗体建立或老窗体被打开后,有可能要把文件另存,而事先我们已经把文件名储存在字符串列表中,现在文件名变了,原来保存的文件名就无效了。为了处理这种情况,就还需要添加一个IOTAModuleNotifier消息通知器,它的ModuleRenamed方法使我们能够截获文件重命名的消息,这时就可以用新的文件名替换字符串列表中老的文件名。同时为了保证接口IOTAModuleNotifier可以使用同前面的FormNotifier类的同样的文件名称等私有变量,要把前面的类定义修改如下: type TPreFFormNotifier = class(TNotifierObject, IOTANotifier, IOTAFormNotifier, IOTAModuleNotifier) private FFileName: string; FOldFileName: string; FOldName: string; FNewName: string; FRenaming: Boolean; //FModifing:Boolean; public constructor Create(FileName: string); destructor Destroy; override; procedure FormActivated; procedure FormSaving; //ComponentRenamed方法是关键!!! procedure ComponentRenamed(ComponentHandle: TOTAHandle; const OldName, NewName: string); procedure Modified; { IOTAModuleNotifier } function CheckOverwrite: Boolean; procedure ModuleRenamed(const NewName: string); end; ModuleRenamed方法实现流程如下: constructor TPreFFormNotifier.Create(FileName: string); begin FFileName := FileName; FOldFileName := FileName; //FModifing:=False; end; ///////////////////////////////////////////////////////////////// procedure TPreFFormNotifier.ModuleRenamed(const NewName: string); var ListI: Integer; FormNotifierI: Integer; ModNotifierI: Integer; begin //定位原来的文件名,删除原来文件名,添加新的文件名 ShowMessage(format('module renaming from %s to %s', [FOldFileName, NewName])); if FormNotifierList.Find(FOldFileName, ListI) then begin FormNotifierI := Integer(FormNotifierList.Objects[ListI]); FormNotifierList.Delete(ListI); FormNotifierList.AddObject(NewName, Pointer(FormNotifierI)); if ModNotifierList.Find(FOldFileName, ListI) then begin ModNotifierI := Integer(FormNotifierList.Objects[ListI]); ModNotifierList.Delete(ListI); ModNotifierList.AddObject(NewName, Pointer(ModNotifierI)); end; end; FOldFileName := NewName; FFileName := NewName; inherited; end; 现在本专家程序的大体流程已经确定下来了,但还有一个问题要说明:ComponentRenamed方法的声明procedure ComponentRenamed (ComponentHandle: TOTAHandle;const OldName, NewName: string);中NewName和OldName参数为什么定义为const类型而不是Var,这岂不是意味着无法在IDE调用ComponentRenamed方法中修改它了吗,解决办法是:TNotifierObject有个方法叫Modified,可以在这里对控件的名称进行修改,先要重新定义一个Modified方法(注意由于Borland声明Modified方法为静态的,所以不需要重载)。 现在我们回头来研究一下ComponentRename方法,只需要在Component中记录当前控件改名的状态,然后在Modified方法里修改控件名示意如下: procedure TPreFFormNotifier.ComponentRenamed(ComponentHandle: TOTAHandle; const OldName, NewName: string); begin //当前处于改名状态 FRenaming := true; //记录老控件名,实际上没什么用,因为Modified方法是在IDE已经改完控件名之后才被 //调用,这时用FindComponent找到的控件名已经是NewName了。 FOldName := OldName; //重要!!!在Modified方法中会用到 FNewName := NewName; //ShowMessage(Format('rename from %s to %s',[OldName,NewName])); end; procedure TPreFFormNotifier.Modified; var Module: IOTAModule; Editor: IOTAEditor; FormEditor: IOTAFormEditor; OTAComponent: IOTAComponent; Component: IComponent; I: Integer; TempName: string; ModifiedName: string; begin if FOldName = FNewName then Exit; //当新建控件时,会出现这种情况 try if FRenaming then begin Module := (BorlandIDEServices as IOTAModuleServices).FindModule(FFileName); for i := 0 to Module.GetModuleFileCount - 1 do begin Editor := Module.GetModuleFileEditor(i); if Editor.QueryInterface(IOTAFormEditor, FormEditor) = S_OK then begin //查找改名后的控件 OTAComponent := FormEditor.FindComponent(FNewName); if OTAComponent = nil then Exit else begin //如果控件类型是Tbutton,则在控件前加上前缀"Btn" if OTAComponent.GetComponentType = 'TButton' then begin Component := OTAComponent.GetIComponent; if Component = nil then Exit else begin ShowMessage('FNewName:' + FNewName); if Pos('btn', FNewName) = 1 then begin ShowMessage('will not modify ' + FNewName); Exit; end; TempName := 'btn' + FNewName; //应该将TempName中的数字去掉 ModifiedName := (FormEditor as INTAFormEditor).FormDesigner.UniqueName(TempName); FNewName:=ModifiedName; OTAComponent.SetPropByName('Name', ModifiedName); end; end; end; end end; end; finally FRenaming:=false; end; end; 到此基本上大功告成了,剩下的问题就是如何提供控件类型及前缀对照表供Modified方法去使用,大家可以下载Delphi程序编码规范来自己写,另外本文提供了一个对照表文件,示例如下: … TMainMenu=mm TPopupMenu=pm #TMainMenuItem=mmi // I think this should be TMenuItem TMenuItem=mmi TPopupMenuItem=pmi TLabel=lbl TEdit=edt TMemo=mem TButton=btn TCheckBox=chk TRadioButton=rb TListBox=lb TComboBox=cb TScrollBar=scb TGroupBox=gb TRadioGroup=rg TPanel=pnl TCommandList=cl … 最后,上面的例子只是对Tbutton控件添加了前缀,在随书所附的源代码中还提供了一个完整版本的专家,可以提供对全部标准控件前缀自动命名功能。专家有待改进的是应提供一个编辑前缀对照表的界面,这样就可以添加对新的或第三方控件的支持了,这个问题留给读者去完成。
|