How to write Delphi Wizards
TAddInMenuListExpert
Let's move on to our fourth and final kind of Experts: esAddIn Experts. These Experts are quite different from the previous ones. We can no longer rely on the Delphi IDE itself to query us and obtain a meun text or repository icon. In fact, we must "build" and "handle" our interface by ourselves. This means that - apart from GetStyle, GetIDString and GetName - the usual TIExpert functions are useless for us now. However, in order to be able to compile without warnings that tell me an "instance of a class containing abstract method is created", we still need to override every function; if only to do and return nothing.
The actual interface part of an esAddIn Expert is provided by the TIMainMenuIntf class, that we can find in the TOOLINTF.PAS file:
type
TIMainMenuIntf = class(TInterface)
public
function GetMenuItems: TIMenuItemIntf; virtual; stdcall; abstract;
function FindMenuItem(const Name: string): TIMenuItemIntf; virtual; stdcall; abstract;
end;
The TIMainMenuIntf class represents the Delphi main menu. We can actually get a list of menu items by calling GetMenuItems (which returns the top level menus), and we can search for a specific menu item with FindMenuItem, as long as we know the exact VCL component name of the particular item (i.e. not the name of the menu as it appears in the menubar, but the name of the menu item component itself!)
Once we have a list of menuitems, or we have found one particular menu items, we have a much more powerful component in our hands: the TIMenuItemIntf - an Expert's interface to menu items, with which we can add our own menu item(s) into the Delphi menu system!
type
TIMenuFlag =
(mfInvalid,mfEnabled,mfVisible,mfChecked,mfBreak,mfBarBreak,mfRadioItem);
TIMenuFlags = set of TIMenuFlag;
TIMenuClickEvent = procedure (Sender: TIMenuItemIntf) of object;
TIMenuItemIntf = class(TInterface)
public
function DestroyMenuItem: Boolean; virtual; stdcall; abstract;
function GetIndex: Integer; virtual; stdcall; abstract;
function GetItemCount: Integer; virtual; stdcall; abstract;
function GetItem(Index: Integer): TIMenuItemIntf; virtual; stdcall; abstract;
function GetName: string; virtual; stdcall; abstract;
function GetParent: TIMenuItemIntf; virtual; stdcall; abstract;
function GetCaption: string; virtual; stdcall; abstract;
function SetCaption(const Caption: string): Boolean; virtual; stdcall; abstract;
function GetShortCut: Integer; virtual; stdcall; abstract;
function SetShortCut(ShortCut: Integer): Boolean; virtual; stdcall; abstract;
function GetFlags: TIMenuFlags; virtual; stdcall; abstract;
function SetFlags(Mask, Flags: TIMenuFlags): Boolean; virtual; stdcall; abstract;
function GetGroupIndex: Integer; virtual; stdcall; abstract;
function SetGroupIndex(GroupIndex: Integer): Boolean; virtual; stdcall; abstract;
function GetHint: string; virtual; stdcall; abstract;
function SetHint(Hint: string): Boolean; virtual; stdcall; abstract;
function GetContext: Integer; virtual; stdcall; abstract;
function SetContext(Context: Integer): Boolean; virtual; stdcall; abstract;
function GetOnClick: TIMenuClickEvent; virtual; stdcall; abstract;
function SetOnClick(Click: TIMenuClickEvent): Boolean; virtual; stdcall; abstract;
function InsertItem(Index: Integer; Caption, Name, Hint: string; virtual; stdcall; abstract;
ShortCut, Context, GroupIndex: Integer; Flags: TIMenuFlags;
EventHandler: TIMenuClickEvent): TIMenuItemIntf; virtual; stdcall; abstract;
end;
The TIMenuItemIntf class is created by Delphi.
This is simply a virtual interface to an actual menu item found in the IDE.
It is the responsibility of the client to destroy all menu items which it created.
Failure to properly clean-up will result in unpredictable behaviour, according to the comments in the source code of the class TIMenuItemIntf.
Using functions that returns a TIMenuItemIntf should be done with care.
Unless created by a particular add-in tool, we should not keep the menu items for long, since the underlying actual VCL TMenuItem may be destroyed without any notification (in which case we're holding a pointer to nowhere).
In practice, this only pertains to Delphi created menus or menus created by other add-in tools.
It is also the responsibility of the user to free these interfaces when finished.
Any attempt to modify a menu item that was created by Delphi will fail.
The most important functions are DestroyMenuItem, which needs to be called whenever we get a menu item from Delphi (i.e. allocated by Delphi, in for example the GetParent or GetItem functions).
Any menu item can have submenus.
The function GetItemCount will return the number of submenus.
Using GetItem we can walk through the list of menu items (warning: GetItem is zero-based, so start by counting from 0 to GetItemCount - 1, otherwise you'll get an index out of bounds exception).
All TIMenuItemIntfs we get from GetItem must be freed by calling DestroyMenuItem on them again.
To get the true VCL component name of a menu item, we need to call the GetName method.
This function is important, since we need the actual name to be able to search for menu items in the main menu (with the FindMenuItem function).
Actually, it seems that we would need a list of names first, before we can actually search for a unique one.
Any menu item has a menu parent, and we can get the parent menu item by calling GetParent (obviously).
A parent menu is important, since we must use the parent to be able to install a menu item next to another (in practice this means that the parent gets another child).
Using the GetCaption and SetCaption methods we can get and set the actual captions of the menu items.
This may be useful, but can be very confusing (although we can only modify menu captions that are not part of the Delphi IDE skeleton - i.e. we can modify the text for the Database Expert, but we cannot modify the File menus).
Using GetShortCut and SetShortCut we can get and set the shortcuts for the individual menu items.
Again, we can not really modify the pre-existing Delphi IDE menu shortcuts, but only the added ones.
Other functions include GetFlags and SetFlags, to get and set the state of the menu item; GetGroupIndex and SetGroupIndex, to get and set the GroupIndex property of a TMenuItem (useful for specifying values for grouped radio menus); GetContext and SetContext to get and set the help context ID of a TMenuItem; and finally GetHint and SetHint that do not work at all (the IDE seems to simply ignore them at this time).
There is one more really important method left: InsertItem.
This is the API that creates and inserts our new sub menu item into the menu of the Delphi IDE.
The function takes a lot of arguments, so let's have another look:
function InsertItem(Index: Integer;
Caption, Name, Hint: String;
ShortCut, Context, GroupIndex: Integer;
Flags: TIMenuFlags;
EventHandler: TIMenuClickEvent): TIMenuItemIntf; virtual; stdcall; abstract;
The index is the place where the new menu item should be placed (in the list of the Parent's menu items).
If the index is less than zero or equal or bigger then GetItemCount, then the new menu item is actually appended to the bottom of the list (since the list is zero-based).
DrBobItem := Tools.InsertItem(ToolsTools.GetIndex+1,
'&Dr.Bob''s Expert',
'DrBobItem','',
ShortCut(Ord('D'),[ssCtrl]),0,0,
[mfEnabled, mfVisible], OnClick);
The last two methods of TIMenuItemIntf are the GetOnClick and SetOnClick methods, who can be used to get and set the OnClick method (useful in case we want to do something else based on a special condition).
With this additional information, it's time to add code to our AddIn Expert. What would be the best place (and time) to add our AddIn Expert to the Delphi IDE menu? Well, a constructor would seem a fine place (and time) to me. But TIExpert doesn't have a constructor! OK, so let's define one! And while we're at it, let's override the destructor as well to make sure we clean up the MenuItem that we'll create in the constructor in the first place:
type
TBAddInExpert = class(TIExpert)
public
constructor Create; virtual;
destructor Destroy; override;
function GetStyle: TExpertStyle; override;
function GetIDString: String; override;
function GetName: String; override;
function GetAuthor: String; override;
function GetMenuText: string; override;
function GetState: TExpertState; override;
function GetGlyph: HICON; override;
function GetComment: string; override;
function GetPage: string; override;
{ Expert Action }
procedure Execute; override;
protected
procedure OnClick(Sender: TIMenuItemIntf); virtual;
private
MenuItem: TIMenuItemIntf;
end {TBAddInExpert};
| GetStyle: | esAddIn |
| GetIDString: | DrBob.TBAddInExpert |
| GetName: | AddIn Wizard |
| GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
| GetMenuText: | |
| GetState: | [] |
| GetGlyph: | 0 |
| GetPage (win32): | |
| GetComment: | |
The code for the Create constructor, Destroy destructor and OnClick event handler is as follows:
constructor TBAddInExpert.Create;
var
Main: TIMainMenuIntf;
ToolsTools: TIMenuItemIntf;
Tools: TIMenuItemIntf;
begin
inherited Create;
MenuItem := nil;
if ToolServices <> nil then
begin
Main := ToolServices.GetMainMenu;
if Main <> nil then { we've got the main menu }
try
ToolsTools := Main.FindMenuItem('ToolsToolsItem');
if ToolsTools <> nil then { we've got the suh-menuitem }
try
Tools := ToolsTools.GetParent;
if Tools <> nil then { we've got the Tools menu }
try
MenuItem := Tools.InsertItem(ToolsTools.GetIndex+1,
'&Dr.Bob''s Expert',
'DrBob','',
ShortCut(Ord('D'),[ssCtrl]),0,0,
[mfEnabled, mfVisible], OnClick)
finally
Tools.DestroyMenuItem
end
finally
ToolsTools.DestroyMenuItem
end
finally
Main.Free
end
end
end {Create};
destructor TBAddInExpert.Destroy;
begin
if MenuItem <> nil then MenuItem.DestroyMenuItem;
inherited Destroy
end {Destroy};
procedure TBAddInExpert.OnClick(Sender: TIMenuItemIntf);
begin
MessageDlg('Hello Nashville!', mtInformation, [mbOk], 0)
end {OnClick};
Note that I've looked for the menuitem called 'ViewPrjMgrItem', which is a rather funny looking name.
How did I know what menuname to look for in the first place?
Well, sit tight, because we're about to find out all names of all menu items in the Delphi 2.0x, 3 and C++Builder IDE menu!
Menu Names We've seen a generic but pretty useless esAddIn Expert so far. In order to write truly more useful experts, we need to do something special inside the OnClick method like show an interesting form in which a lot of things can happen. But first, let's dig a little bit deeper in the main menu of the Delphi IDE. Now that we have the power, let's use it to get a list of VCL names for the individual menu items, so we don't need to look for one if we need it. To do this, I've modified the Create constructor of the AddIn Expert (and called it the new AddInMenuList Expert) to walk through the menu items of the main menu and print their names on a file as follows:
constructor TAddInMenuListExpert.Create;
var Main: TIMainMenuIntf;
MenuItems: TIMenuItemIntf;
ToolsTools: TIMenuItemIntf;
Tools: TIMenuItemIntf;
var i,j: Integer;
f: System.Text;
begin
inherited Create;
if ToolServices <> nil then
try
Main := ToolServices.GetMainMenu;
if Main <> nil then { we've got the main menu }
try
MenuItems := Main.GetMenuItems;
if MenuItems <> nil then
try
System.Assign(f,'C:\MENUS.D3');
System.Rewrite(f);
writeln(f,MenuItems.GetName,' -',MenuItems.GetItemCount);
for i:=0 to Pred(MenuItems.GetItemCount) do
begin
Tools := MenuItems.GetItem(i);
if Tools <> nil then { we've got a sub-menu }
try
writeln(f,' ',Tools.GetName);
for j:=0 to Pred(Tools.GetItemCount) do
begin
ToolsTools := Tools.GetItem(j);
if ToolsTools <> nil then { sub-sub-menu }
try
writeln(f,' ',ToolsTools.GetName);
finally
ToolsTools.DestroyMenuItem
end
end
finally
Tools.DestroyMenuItem
end
end
finally
System.Close(f);
MenuItems.DestroyMenuItem
end
finally
Main.Free
end
except
{ HandleException }
end
end {Create};
The resulting list is pretty impressive, and gives us a good idea of which VCL menu item component names are used (and can be used as argument to the FindMenuItem function of the MainMenu).
| Delphi 2.0x | C++Builder | Delphi 3 | |
| FileMenu |
FileNewItem FileNewApplicationItem FileNewFormItem FileNewDataModuleItem FileOpenItem FileClosedFilesItem FileSaveItem FileSaveAsItem FileSaveProjectAs FileSaveAllItem FileCloseItem FileCloseAllItem N6 FileUseUnitItem FileAddItem FileRemoveItem FilePrintItem FileExitItem |
FileNewItem FileNewApplicationItem FileNewFormItem FileNewDataModuleItem FileNewUnitItem N16 FileOpenItem OpenProjectItem FileClosedFilesItem N17 FileSaveItem FileSaveAsItem FileSaveProjectAs FileSaveAllItem FileCloseItem FileCloseAllItem N6 FileUseUnitItem FilePrintItem FileExitItem |
FileNewItem FileNewApplicationItem FileNewFormItem FileNewDataModuleItem FileOpenItem FileClosedFilesItem FileSaveItem FileSaveAsItem FileSaveProjectAs FileSaveAllItem FileCloseItem FileCloseAllItem N6 FileUseUnitItem FileAddItem FileRemoveItem N8 FilePrintItem FileExitItem |
| EditMenu |
EditUndoItem EditRedoItem N15 EditCutItem EditCopyItem EditPasteItem EditDeleteItem EditSelectAll N14 EditAlignGridItem EditFrontItem EditBackItem EditAlignItem EditSizeItem EditScaleItem EditTabOrderItem CreationOrderItem EditLockControlsItem N5 EditObjectItem |
EditUndoItem EditRedoItem N15 EditCutItem EditCopyItem EditPasteItem EditDeleteItem EditSelectAll N14 EditAlignGridItem EditFrontItem EditBackItem EditAlignItem EditSizeItem EditScaleItem EditTabOrderItem CreationOrderItem EditLockControlsItem N5 EditObjectItem |
EditUndoItem EditRedoItem N15 EditCutItem EditCopyItem EditPasteItem EditDeleteItem EditSelectAll N14 EditAlignGridItem EditFrontItem EditBackItem EditAlignItem EditSizeItem EditScaleItem EditTabOrderItem CreationOrderItem EditLockControlsItem N17 EditAddToInterfaceItem |
| SearchMenu |
SearchFindItem SearchReplaceItem SearchAgainItem SearchIncrementalItem SearchGoToItem SearchCompErrItem SearchFindErrItem SearchSymbolItem |
SearchFindItem SearchReplaceItem SearchAgainItem SearchIncrementalItem SearchGoToItem SearchCompErrItem SearchFindErrItem |
SearchFindItem SearchFileFindItem SearchReplaceItem SearchAgainItem SearchIncrementalItem SearchGoToItem SearchCompErrItem SearchFindErrItem SearchSymbolItem |
| ViewsMenu |
ViewPrjMgrItem ViewPrjSourceItem ViewObjInspItem ViewAlignItem ViewBrowserItem ViewBreakpointsItem ViewCallStackItem ViewWatchItem ViewThreadsItem ViewCpuItem ViewCompListItem ViewWindowListItem N1 ViewToggleFormItem ViewUnitItem ViewFormItem N3 ViewNewEditorItem N2 ViewSpeedBarItem ViewPaletteItem ViewSwapSourceFormItem |
ViewPrjMgrItem ViewPrjSourceItem ViewMakeFileItem ViewObjInspItem ViewAlignItem ViewCompListItem ViewWindowListItem N18 ViewCallStackItem ViewThreadsItem ViewCpuItem ViewBreakpointsItem ViewWatchItem N1 ViewToggleFormItem ViewUnitItem ViewFormItem N3 ViewNewEditorItem N2 ViewSpeedBarItem ViewPaletteItem ViewSwapSourceFormItem |
ViewPrjMgrItem ViewPrjSourceItem ViewObjInspItem ViewAlignItem ViewBrowserItem ViewBreakpointsItem ViewCallStackItem ViewWatchItem ViewThreadsItem ViewModulesItem ViewCpuItem ViewCompListItem ViewWindowListItem N1 ViewToggleFormItem ViewUnitItem ViewFormItem ViewTypeLibraryItem N3 ViewNewEditorItem N2 ViewSpeedBarItem ViewPaletteItem ViewSwapSourceFormItem |
| ProjectMenu |
ProjectAddItem ProjectRemoveItem ProjectAddRepositoryItem N10 ProjectCompileItem ProjectBuildItem ProjectSyntaxItem ProjectInformationItem N11 ProjectOptionsItem |
ProjectAddItem ProjectRemoveItem ProjectAddRepositoryItem N10 ProjectCompileUnitItem ProjectMakeItem ProjectBuildItem N11 ProjectInformationItem |
ProjectAddItem ProjectRemoveItem ImportTypeLibraryItem ProjectAddRepositoryItem N10 ProjectCompileItem ProjectBuildItem ProjectSyntaxItem ProjectInformationItem N5 ProjectDepOptItem ProjectDeployItem N11 ProjectOptionsItem |
| RunMenu |
RunRunItem RunParametersItem N4 RunStepOverItem RunTraceIntoItem RunTraceToSourceItem RunGotoCursorItem RunShowCSIPItem RunPauseItem RunResetItem RunAddWatchItem RunAddBreakItem RunEvalModItem |
RunRunItem RunParametersItem N4 RunStepOverItem RunTraceIntoItem RunTraceToSourceItem RunGotoCursorItem RunShowCSIPItem RunPauseItem RunResetItem RunInspectItem RunEvalModItem N19 RunAddWatchItem RunAddBreakItem |
RunRunItem RunParametersItem RunRegisterComItem RunUnregisterComItem N4 RunStepOverItem RunTraceIntoItem RunTraceToSourceItem RunGotoCursorItem RunShowCSIPItem RunPauseItem RunResetItem RunAddWatchItem RunAddBreakItem RunEvalModItem |
| ComponentMenu |
ComponentNewItem ComponentInstallItem N7 ComponentOpenLibraryItem ComponentRebuildItem N8 ComponentPaletteItem |
ComponentNewItem ComponentInstallItem N7 ComponentOpenLibraryItem ComponentRebuildItem N8 ComponentPaletteItem |
ComponentNewItem AddtoPackage1 ComponentImportAXCItem N16 ComponentInstallCompositeItem N7 InstallPackagesItem ComponentPaletteItem |
| DatabaseMenu |
Borland_DbExplorerMenu Borland_FormExpertMenu |
Borland_DbExplorerMenu Borland_FormExpertMenu |
Borland_DbExplorerMenu Borland_FormExpertMenu |
| ToolsMenu |
ToolsOptionsItem ToolsGalleryItem |
ToolsToolsItem |
ToolsOptionsItem ToolsGalleryItem ToolsToolsItem |
| Options1 |
ProjectOptionsItem ToolsOptionsItem ToolsGalleryItem |
||
| HelpMenu |
HelpContentsItem N13 HelpTopicSearchItem HelpUsingHelpItem HelpAPIItem HelpAboutItem |
HelpContentsItem KeywordSearch N20 ProgGuideItem VclRefItem RtlRefItem N13 HelpAboutItem |
HelpContentsItem HelpTopicSearchItem HelpWhatsNew HelpGettingStarted HelpUsingPascal HelpDevelopingApps HelpObjCompRef N13 HelpBorlandPage HelpDelphiPage HelpProgramsPage N18 HelpUsingHelpItem HelpAPIItem HelpAboutItem |
If we want to write esAddIn Wizards that are compatible between Delphi 2.0x, C++Builder and Delphi 3, then we must make sure to pick an "entry" MenuItemName that exists in all three versions, so we can get a handle to an existing Menu item. Note that some menu items may not be available in all editions of Delphi 2.x, C++Builder or Delphi 3 (I used the Client/Server versions of all three to get the above table). Of course, you can always re-create this list yourself with the TBAddInMenu Wizard.