自动规范控件前缀命名的专家
作者:陈省
在编程过程中对代码进行规范的命名,可以使编出的代码便于理解和维护,尤其对于大型软件,由于代码量极其巨大,规范命名就更为重要。记得在我刚开始编程的时候,对代码的规范命名很不以为然,认为实在是有点浪费时间,所以写出的程序中的控件基本上是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控件添加了前缀,在随书所附的源代码中还提供了一个完整版本的专家,可以提供对全部标准控件前缀自动命名功能。专家有待改进的是应提供一个编辑前缀对照表的界面,这样就可以添加对新的或第三方控件的支持了,这个问题留给读者去完成。
|