Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
Delphi and C++Builder are truly open development environments, in that they have interfaces to enable us to integrate our own tools and experts within their IDE. This article will focus on writing and integrating Wizards (previously called Experts) with Delphi. The resulting (32-bits) Wizards will be compatible with Delphi 2.0x, Delphi 3 and C++Builder.
Delphi has four kinds of Wizards: Project Experts, Form Experts, Standard Experts and (32-bits only) AddIn Experts. The first two can be found in the Repository, Standard Experts can be found under the Help menu (like the Database Form Expert), while AddIn Experts have to provide their own menu-interface with the Delphi IDE (typicaly anywhere in the menu except for the Help Menu, which seems to be reserved for Standard Experts only).
Project and Form Experts can be activated whenever you create a new Project or Form (just like Project and Form Templates).
Standard and AddIn Experts are the other kind of Wizards that generally do not create a new project or form, but provide some kind of information, or only create a new file or unit.
If you've ever tried an Wizard, you know what power and ease they can bring to you.
The Project Expert develops an entire project for you based on your specific preferences (like for example the Application Wizard).
The Form Experts develop custom forms that are added to your current project.
The Database Form Expert, for example, generates a form that displays data from an external database.
These example Wizards are not just external tools that can be started from Delphi, they actually communicate with Delphi and are an integrated part of the development environment.
While this is not so strange for the existing Delphi Experts (after all, they were developed and added by the same team that developed Delphi in the first place, and we all know Delphi's IDE is written in Delphi), it sounds intriguing at least to know that we, too, can write a Delphi Wizard that is able to communicate with Delphi in the same way.
Could we actually write an Wizard that also opens files in the IDE, that can be used to start a new project from scratch?
Yes, all this is possible, and more, as we will see shortly!
1. TIExpert Interface
The major reason why everybody thinks writing custom Wizards is difficult, is because they are not documented.
Not in the manuals or on-line Help, that is (they are documented in my book The Revolutionary Guide to Delphi 2 and in my column in The Delphi Magazine).
If you take a look at the documentation and source code on your harddisk, you'll find some important files and even two example Wizards that are installed automatically by Delphi itself.
The important example files can be found in the DOC, SOURCE\VCL or SOURCE\TOOLSAPI subdirectories, and the main files are EXPTINTF.PAS, TOOLINTF.PAS, VIRTINTF.PAS and SHAREMEM.PAS.
The first one shows how to derive and register our own Wizard, while the second one shows how to use the tool-services of Delphi to make the integration with the IDE complete.
In order to start working on a custom wizard, we have to take a look at the abstract base class definition TIExpert in EXPTINTF.PAS, which is as follows for the 32-bits versions of Delphi:
Type TExpertStyle = (esStandard, esForm, esProject, esAddIn); TExpertState = set of (esEnabled, esChecked); TIExpert = class(TInterface) public { Expert UI strings } function GetIDString: string; virtual; stdcall; abstract; function GetName: string; virtual; stdcall; abstract; function GetAuthor: string; virtual; stdcall; abstract; function GetStyle: TExpertStyle; virtual; stdcall; abstract; function GetMenuText: string; virtual; stdcall; abstract; function GetState: TExpertState; virtual; stdcall; abstract; function GetGlyph: HICON; virtual; stdcall; abstract; function GetComment: string; virtual; stdcall; abstract; function GetPage: string; virtual; stdcall; abstract; { Launch the Expert } procedure Execute; virtual; stdcall; abstract; end;
2. TGenericExpert: Hello, World!
If we want to derive our own Wizard, say TGenericExpert, we have to derive it from the abstract base class TIExpert, which has seven or nine abstract member functions (GetStyle, GetName, GetComment, GetGlyph, GetState, GetIDString and GetMenuText, and for the 32-bits versions of Delphi also GetAuthor and GetPage) and one member procedure Execute.
Since TIExpert is an abstract base class, we need to override every function we need for any particular Wizard.
unit Generic; interface uses Windows, ExptIntf; Type TGenericExpert = class(TIExpert) public { Expert Style } function GetStyle: TExpertStyle; override; { Expert Strings } 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; end; procedure Register; implementation uses Dialogs; { The implementation details of TGenericExpert will follow in the text } procedure Register; begin RegisterLibraryExpert(TGenericExpert.Create) end {Register}; end.Let's have a closer look at our generic Wizard from this listing. Since TIExpert is an abstract base class, we need to override every function we need for our TGenericExpert. First of all, we need to specify the style of the Wizard with the GetStyle method that can return one of three (or four) possible values: esStandard to tell the IDE to treat the interface to this Wizard as a menu item on the Help menu, esForm to tell the IDE to treat this Wizard interface in a fashion similar to form templates, or esProject to tell the IDE to treat this interface in a fashion similar to project templates. For 32-bits Delphi Wizards only, we can also return esAddIn here, to indicate that this is a special klind of Wizard that handles all its own interfaceing to the IDE through the TIToolServices interface. For our TGenericExpert, a Standard type Wizard that only shows a MessageDlg to say hello to the world, we can use the esStandard style.
function TGenericExpert.GetStyle: TExpertStyle; begin Result := esStandard end {GetStyle};The GetIDString should be unique to all Wizards that could be installed. By convention, the format of the string is: CompanyName.ExpertFunction, like Borland.Expert or DrBob.GenericExpert.
function TGenericExpert.GetIDString: String; begin Result := 'DrBob.TGenericExpert' end {GetIDString};After we've set the style of the Wizard, all we need to do is fill the other options accordingly. The GetName must return a unique descriptive name identifying this Wizard, like 'Generic Wizard'.
function TGenericExpert.GetName: String; begin Result := 'Generic Wizard' end {GetName};If the style is esForm or esProject, then - for 32-bits versions of Delphi only - we need to return a valid name for the Author. In this case, the style is esStandard, so we can return an empty string instead. For an esForm or esProject style Wizard the name would be displayed in the Object Repository of the 32-bits versions of Delphi.
{$IFDEF WIN32} function TGenericExpert.GetAuthor: String; begin Result := 'Bob Swart (aka Dr.Bob)' { although not needed for esStandard } end {GetAuthor}; {$ENDIF}If style is esForm or esProject then GetGlyph should return a handle to a bitmap (for Delphi 1) or icon (for Delphi 2.0x and 3) to be displayed in the form or project list boxes or dialogs. This bitmap should have a size of 60x40 pixels in 16 colours. The icon should be 32x32 in 16 colours. Again, since the style is just esStandard for our TGenericExpert, we can return 0 here. We can even combine the 16- and 32-bit version of GetGlyph here (0 is a valid value to indicate that an icon or bitmap is empty). Note that if we return a 0 when a bitmap or icon is needed, Delphi will use the default image.
{$IFDEF WIN32} function TGenericExpert.GetGlyph: HICON; {$ELSE} function TGenericExpert.GetGlyph: HBITMAP; {$ENDIF} begin Result := 0 { not needed for esStandard } end {GetGlyph};If style is esForm or esProject then GetComment should return a 1 or 2 line sentence describing the function of this Wizard. Since the style is esStandard, we can return an empty string.
function TGenericExpert.GetComment: String; begin Result := '' { not needed for esStandard } end {GetComment};If style is esForm or esProject then - only for 32-bits versions of Delphi - using GetPage we can specify the name of the page in the Object Repository where to place our Wizard. If we don't specify a name here, then the Wizard just gets added to the Default Form or Project page. Since we're writing an esStandard Expert, we don't need to supply a page name, so we can return an empty string again.
{$IFDEF WIN32} function TGenericExpert.GetPage: String; begin Result := '' { not needed for esStandard } end {GetPage}; {$ENDIF}If style is esStandard then GetMenuText should return the actual text to display for the menu item, like 'Generic Wizard'. Since this function is called each time the parent menu is pulled-down, it is even possible to provide context sensitive text.
function TGenericExpert.GetMenuText: String; begin Result := '&Generic Wizard...' end {GetMenuText};If the style is esStandard then GetState returning esChecked will cause the menu to display a checkmark. This function is called each time the Wizard is shown in a menu or listbox in order to determine how it should be displayed. We just leave it esEnabled for now.
function TGenericExpert.GetState: TExpertState; begin Result := [esEnabled] end {GetState};Finally, the Execute method is called whenever this Wizard is invoked via the menu, form gallery dialog, or project gallery dialog. Note that Execute is never called for an esAddIn style Wizard (this kind of Wizard will handle all its own interfacing to the IDE through the upcoming TIToolServices interface). The style will determine how the Wizard was invoked. In this case, we just call a MessageDlg in the Execute method to indicate that the Wizard is actually alive.
procedure TGenericExpert.Execute; begin MessageDlg('Hello Nashville!', mtInformation, [mbOk], 0) end {Execute};To install our first Wizard, all we need to do is act like it's a new component: For Delphi 1.0, pick Options | Install, for Delphi 2.0x and 3 select Component | Install, and add it to the list of installed components.
After we click onthe OK-button to add the generic unit with our GenericExpert to the DCLUSR30 package, we need to confirm that Delphi needs to rebuild the package:
After the package is rebuilt and installed into the Delphi 3 IDE again, we can inspect the Package Editor and see that the generic unit is now part of it. This simple example already illustrates that packages are not limited to components, but can contain Wizards as well.
When Delphi is done with compiling and linking COMPLIB.DCL or DCLUSR30.DPL, you can find our first new Wizard in the Help menu:
Just select the "Generic Wizard" and it will show the world that it's alive:
As we can see, only the Execute method contains any significant code, and this will remain so for all Wizards to come. In order to avoid that we have to print a long listing in this paper for an Wizard where only one method is relevant, I'll propose the following technique: let's use a table to define the nine methods, and only specify the Execute method in detail. Our TGenericExpert would then become the following:
GetStyle: | esStandard |
GetIDString: | DrBob.TGenericExpert |
GetName: | Generic Wizard |
GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
GetMenuText: | &Generic Wizard... |
GetState: | [esEnabled] |
GetGlyph: | 0 |
GetPage (win32): | |
GetComment: |
With only the Execute method outlined in detail (see previous listing). We will use this notation in the rest of this session.
3. TSysInfoExpert
Instead of just popping up a MessageDlg, we can show any form we'd like.
In fact, this is just were the fun starts.
Generally, we can consider our Wizard to consist of two parts: the Wizard engine and the form interface.
We've just seen how to write the Wizard engine, and we all know how to write form interfaces, so let's put these two together and write our first something-more-than-trivial information Wizard.
The information that I want the Wizard to present can be found in the SysUtils unit, and consists of the country specific informatin regarding currency and date/time formatting constants.
In the on-line help we can find which constants are defined in SysUtils, but we can't see their value.
This is unfortunate, since most of the time Delphi is of course up-and-running while we're developing, so SysUtils is active as well (remember: Delphi is written in Delphi!) and should know about these values.
So, using the Dialog Expert, we can create a Multipage dialog, using a TabbedNotebook, and give the three pages the names "Currency", "Date" and "Time". Next, we must drop a label on each of the pages, set autosize for each label to false, and make them about as big as the entire notebook (so multiple lines can be viewed). The source code for the form merely consists of putting the right values on the right places when the form is created (in the OnCreate handler), so nothing complex at all for the interface side of the SysInfo Wizard. The engine of TSysInfoExpert is as follows:
GetStyle: | esStandard |
GetIDString: | DrBob.TSysInfoExpert |
GetName: | SysInfo Wizard |
GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
GetMenuText: | &SysInfo Wizard... |
GetState: | [esEnabled] |
GetGlyph: | 0 |
GetPage (win32): | |
GetComment: |
The Execute method of the SysInfo Wizard is almost as easy, since all we need to do is to create, show and free the form with the desired information. That's it. The source code for the Execute procedure is as follows:
procedure TSysInfoExpert.Execute; begin with TSysInfoForm.Create(nil) do begin ShowModal; Free end end {Execute};And presto! Our first "useful" Wizard, showing information at design time that isn't available any other way:
This is only the first of many examples where we will see an Wizard engine that will show an interface form to show (or get) information to the user. One source of information to provide (or actions to apply) can be obtained from the so-called toolservices interface Delphi offers us in the TIToolServices class.
4. ToolServices
We've seen some generic but in fact pretty much useless Wizard so far.
In order to write truly more useful Wizards, we need to do something special inside the Execute method, like show a (more) interesting form in which a lot of things can happen, a bit like we introduced with the TSysInfoExpert.
Did you ever feel the need to load some project other than a .DPR file in the IDE?
No?
Never written any DLLs in Delphi?
Well, I often have the need to open a .PAS or any file with an extension other than .DPR inside the IDE as my project.
In fact, my need is so big, I want to write an Wizard to help me in browsing over my disk and directories in search for a certain file to open as a new project.
But is this possible?
To answer this question, we need to take a look at TOOLINTF.PAS from Delphi 1.0, the file that contains the definition of TIToolServices (the "I" stands for Interface again), which is as follows:
unit ToolIntf; interface uses WinTypes, VirtIntf; Type TIToolServices = class(TInterface) public { Action interfaces } function CloseProject: Boolean; virtual; export; abstract; function OpenProject(const ProjName: string): Boolean; virtual; export; abstract; function OpenProjectInfo(const ProjName: string): Boolean; virtual; export; abstract; function SaveProject: Boolean; virtual; export; abstract; function CloseFile(const FileName: string): Boolean; virtual; export; abstract; function SaveFile(const FileName: string): Boolean; virtual; export; abstract; function OpenFile(const FileName: string): Boolean; virtual; export; abstract; function ReloadFile(const FileName: string): Boolean; virtual; export; abstract; function ModalDialogBox(Instance: THandle; TemplateName: PChar; WndParent: HWnd; DialogFunc: TFarProc; InitParam: LongInt): Integer; virtual; export; abstract; function CreateModule(const ModuleName: string; Source, Form: TIStream; CreateFlags: TCreateModuleFlags): Boolean; virtual; export; abstract; { Project/UI information } function GetParentHandle: HWND; virtual; export; abstract; function GetProjectName: string; virtual; export; abstract; function GetUnitCount: Integer; virtual; export; abstract; function GetUnitName(Index: Integer): string; virtual; export; abstract; function GetFormCount: Integer; virtual; export; abstract; function GetFormName(Index: Integer): string; virtual; export; abstract; function GetCurrentFile: string; virtual; export; abstract; function IsFileOpen(const FileName: string): Boolean; virtual; export; abstract; function GetNewModuleName(var UnitIdent, FileName: string): Boolean; virtual; export; abstract; { Component Library interface } function GetModuleCount: Integer; virtual; export; abstract; function GetModuleName(Index: Integer): string; virtual; export; abstract; function GetComponentCount(ModIndex: Integer): Integer; virtual; export; abstract; function GetComponentName(ModIndex,CompIndex: Integer): string; virtual; export; abstract; {function InstallModule(const ModuleName: string): Boolean; virtual; export; abstract; function CompileLibrary: Boolean; virtual; export; abstract; } { Error handling } procedure RaiseException(const Message: string); virtual; export; abstract; end; implementationThe Tool services object is created on the application (Delphi/C++Builder) side, and is passed to the VCS/Expert Manager DLL during initialization. Note that the application (Delphi/C++Builder) is responsible for creating and freeing the interface object, and the client should never free the interface.
The following ToolServices functions are available to the client (for Delphi 1.0 as well as 2.0x and 3):
TIToolInterface for Delphi 2.0x and 3
Delphi 2.0x and 3 have an expanded Open Tools API (compared to Delphi 1.x), which is not only reflected in a few new methods for TIExpert, but especially for TIToolServices.
The following additional methods are new and for the 32-bits versions of Delphi only (methods that are shared with Delphi 1.0 have been left out for now):
TIToolServices = class(TInterface) public { Action interfaces } function CreateModuleEx(const ModuleName, FormName, AncestorClass, FileSystem: string; Source, Form: TIStream; CreateFlags: TCreateModuleFlags): TIModuleInterface; virtual; stdcall; abstract; { Project/UI information } function EnumProjectUnits(EnumProc: TProjectEnumProc; Param: Pointer): Boolean; virtual; stdcall; abstract; { Virtual File system interfaces } function RegisterFileSystem(AVirtualFileSystem: TIVirtualFileSystem): Boolean; virtual; stdcall; abstract; function UnRegisterFileSystem(const Ident: string): Boolean; virtual; stdcall; abstract; function GetFileSystem(const Ident: string): TIVirtualFileSystem; virtual; stdcall; abstract; { Editor Interfaces } function GetModuleInterface(const FileName: string): TIModuleInterface; virtual; stdcall; abstract; function GetFormModuleInterface(const FormName: string): TIModuleInterface; virtual; stdcall; abstract; { Menu Interfaces } function GetMainMenu: TIMainMenuIntf; virtual; stdcall; abstract; { Notification registration } function AddNotifier(AddInNotifier: TIAddInNotifier): Boolean; virtual; stdcall; abstract; function RemoveNotifier(AddInNotifier: TIAddInNotifier): Boolean; virtual; stdcall; abstract; { Pascal string handling functions } function NewPascalString(Str: PChar): Pointer; virtual; stdcall; abstract; procedure FreePascalString(var Str: Pointer); virtual; stdcall; abstract; procedure ReferencePascalString(var Str: Pointer); virtual; stdcall; abstract; procedure AssignPascalString(var Dest, Src: Pointer); virtual; stdcall; abstract; { Configuration Access } function GetBaseRegistryKey: string; virtual; stdcall; abstract; end;The following ToolServices functions are available to the client for 32-bits versions of Delphi only:
5. TFileOpenDialogExpert
Well, with the TIToolServices class we sure have a lot of information and power at our disposal, don't we!?
Let's try to do something easy that doesn't get us into trouble right away.
The API that I'm looking for that will allow me to open any file as a new project seems to be OpenProject, which takes a fully qualified filename as only argument.
The engine for the first version of the TFileOpenExpert is as follows:
GetStyle: | esStandard |
GetIDString: | DrBob.TFileOpenExpert.standard |
GetName: | FileOpen Wizard |
GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
GetMenuText: | &FileOpen Wizard... |
GetState: | [esEnabled] |
GetGlyph: | 0 |
GetPage (win32): | |
GetComment: |
So, all we need to do in the Execute procedure, is somehow get the fully qualified filename, and call ToolServices.OpenProject with it. The easiest way seems to be using a TOpenDialog, which we can create and execute on the fly, so the code of Execute looks as follows now:
procedure TFileOpenExpert.Execute; begin with TOpenDialog.Create(nil) do begin if Execute then ToolServices.OpenProject(FileName); Free end end {Execute};Well, it's almost what we want. We forgot to initialise the filter of the TOpenDialog, so we need to do that as well. Other than that, we can indeed open up new projects, but we don't close the files of any previous projects that were loaded. So, we need to call SaveProject and CloseProject prior to calling our OpenProject.
procedure TFileOpenExpert.Execute; begin with TOpenDialog.Create(nil) do begin Title := GetName; { name of Wizard as OpenDialog caption } Filter := 'All Files (*.*)|*.*'; Options := Options + [ofShowHelp, ofPathMustExist, ofFileMustExist]; HelpContext := 0; if Execute then begin ToolServices.SaveProject; ToolServices.CloseProject; ToolServices.OpenProject(FileName) end; Free end end {Execute};This time it works as we want; we get a TOpenDialog in which we can select any file we'd want. And once we've selected one and hit that OK button, the current project is saved and closed before the selected file is opened as our new project.
Standard FileOpen Wizard
The FileOpenExpert from last section looks like any TOpenDialog in action; not really breathtaking, but still we have our first Standard FileOpen Wizard:
If we install this TFileOpenExpert in COMPLIB.DCL or DCLUSR30.DPL like just another component, we get to see it in the Help menu topics. Like the TGenericExpert and the TSysInfoExpert, we've now created another Delphi Standard Expert; one that can be really useful at times:
Project FileOpen Wizard
Now that we've seen a Standard Expert, let's go and try to make it a Project Wizard.
After all, the FileOpen Wizard is opening the selected file as a new project, so it actually is already a Project Expert (or at least acts that way).
Remember the Database Form Expert of Delphi?
This one can be found in the gallery as a Form Expert and in the Help menu as a Standard Expert.
It seems to be both.
I would like to be able to use my FileOpen Expert not only as a Standard Expert but also as a Project Expert.
In that case we have to modify the Wizard functions to include both the esStandard and esProject styles (the result is in the next listing).
We have to override a fourth method to make it all work: the GetIDString needs to return a truly unique ID string for both the Standard and the Project Expert.
Even though the two are essentially the same, we need to return two special IDs.
If you don't, DELPHI will just GPF when you try to install the Wizards (with no real clue as to where it when wrong).
Which leads back to rule #1 for every serious Component Builder: always have a backup of COMPLIB.DCL or DCLUSR30.DPL at hand when you start to play with components and Wizards.
If you don't you might corrupt it beyond repair (at which time you'll have to reinstall the original from your CD-ROM - unless you had made that copy at a safe place).
The base class for our TFileOpenExpert is still the standard expert, while the TFileOpenProjectExpert is derived from it.
GetStyle: | esStandard |
GetIDString: | DrBob.TFileOpenExpert.standard |
GetName: | FileOpen Wizard |
GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
GetMenuText: | &FileOpen Wizard... |
GetState: | [esEnabled] |
GetGlyph: | 0 |
GetPage (win32): | |
GetComment: |
We need a new way to express the fact that the TFileOpenProjectExpert is derived from the TFileOpenExpert, and only needs to override a few of the methods. I've decided to use the notation below:
GetStyle: | esProject |
GetIDString: | DrBob.TFileOpenExpert.project |
GetGlyph: | LoadBitMap(HInstance, MakeIntResource('FILEOPEN')) or LoadIcon(HInstance, MakeIntResource('FILEOPEN')) |
GetPage (win32): | Project |
GetComment: | This Project Experts opens any file as new project in the IDE |
In order to be able to use the LoadBitMap statement, we must link a resource file with $R compiler directive (like {$R FILEOPEN.RES}) to our project, which contains a 60 x 40 x 16 colour bitmap for Delphi 1.0 or a 32 x 32 x 16 colour icon for the 32-bits versions of the Wizard. Since 16-bit and 32-bit resources are different, we need a construct like the following:
{$IFDEF WIN32} {$R FILEOPEN.R32} {$ELSE} {$R FILEOPEN.R16} {$ENDIF}Also note that we don't need to override the Execute method again, since the Execute of TFileOpenExpert is of course the same as the one for TFileOpenProjectExpert.
procedure Register; begin RegisterLibraryExpert(TFileOpenExpert.Create); RegisterLibraryExpert(TProjectOpenExpert.Create) end {Register};Now, if we install the Wizards with Options | Install, we get both a Standard FileOpen Wizard in the Help menu and the Project FileOpen Wizard in the Gallery. If we enable the gallery from the environments options, we can open any source file every time we start a new project!
6. DLL Wizards
So far, we've seen how to write our own Standard and Project Expert, and we've created a nice FileOpen Wizard that integrates in the COMPLIB.DCL or DCLUSR30.DPL file.
However, we remember the Application and Dialog Experts that came as a DLL.
How do we put an Wizard in a DLL?
First of all, we need to write a DLL and not a unit, which leads to the following code (note that I've used compiler directives to be able to switch from a DCU Wizard to a DLL Wizard based on one compiler options).
{$A+,B-,D-,F-,G+,I-,K+,L-,N-,P-,Q-,R-,S+,T-,V-,W-,X+,Y-} {$IFDEF DLL} library FileOpen; {$M 32768,0} {$ELSE} unit FileOpen; interface {$ENDIF}Also, when using the 32-bits versions of Delphi, we need to make sure that we use the ShareMem unit as well (very important!). Now, before we continue, it is important to realise that a DLL is different from a DCU in that it must deal with its own exceptions. You are not allowed to let any exception escape the DLL. Even if you think that a Delphi application will be able to deal with this exception (which it will not), your DLL may not even be servicing a Delphi application, but an application written in an entire other language (remember: everyone may do a LoadLibrary of your DLL, and try to call the APIs inside). So, the number one rule for writing DLL Wizards, or any DLLs at all for that matter, is to handle your own exceptions. For this, we need our own exception handler, which is defined as follows (an example on which this was based can be found in EXPTDEMO.DPR in the C:\DELPHI\DEMOS\EXPERTS directory of Delphi 1.0):
procedure HandleException; begin if Assigned(ToolServices) then ToolServices.RaiseException(ReleaseException) end {HandleException};Which also results in the use of try-except blocks for every method that gets called by something from the outside (all methods of our Wizards, that is):
function TFileOpenExpert.GetStyle: TExpertStyle; begin try Result := esStandard except HandleException end end {GetStyle};Note that when using this technique, Delphi will give us warnings (if enabled) that these functions now may have an undefined result. This might indeed occur if an exception is raised before the assignment to Result could take place or succeed. However, in that case it was the assignment itself that raised the exception, so I don't really mind.
procedure TFileOpenExpert.Execute; begin try if (ToolServices = nil) then MessageDlg('ToolServices not available!', mtError, [mbOk], 0) else begin if not Assigned(FileOpenForm) then { Create Wizard Form } FileOpenForm := TFileOpenForm.Create(Application); if (FileOpenForm.ShowModal = idOk) then { Action! } ToolServices.OpenProject(FileOpenForm.FileListBox.FileName) else MessageBeep($FFFF) end except HandleException end end {Execute};Finally, a DLL Wizard is added (installed and registered) to Delphi in a somewhat different way than a DCU Wizard. For one, it doesn't get integrated into the Component Palette or Package (so we don't need the register procedure anymore), but it'll stay an independent DLL, and can only communicate with Delphi by using a copy of ToolServices. To get this copy, we must define a function called InitExpert that gets called whenever the DLL Wizard is initialised by Delphi.
{$IFNDEF DLL} procedure Register; begin RegisterLibraryExpert(TFileOpenExpert.Create); RegisterLibraryExpert(TProjectOpenExpert.Create) end {Register}; {$ELSE} procedure DoneExpert; export; begin if Assigned(FileOpenForm) then FileOpenForm.Free; FileOpenForm := nil end; function InitExpert(ToolServices: TIToolServices; RegisterProc: TExpertRegisterProc; var Terminate: TExpertTerminateProc): Boolean; export; begin ExptIntf.ToolServices := ToolServices; { Save for our local usage!! } if ToolServices <> nil then Application.Handle := ToolServices.GetParentHandle; Terminate := DoneExpert; if (@RegisterProc <> nil) then Result := RegisterProc(TFileOpenExpert.Create) AND RegisterProc(TProjectOpenExpert.Create) end {InitExpert}; exports InitExpert name ExpertEntryPoint; begin {$ENDIF} end.Note that we call the function RegisterProc twice, once for the Standard FileOpen Wizard, and once for the Project FileOpen Wizard. Of course, we must make sure that the DCU Wizards are 'un-installed' before we try to install this DLL Wizard, otherwise the duplicate GetIDStrings will cause trouble in the internal Delphi Wizard manager part of the IDE (assuming that's what it's called)...
Compiler Output Wizard
Another useful Wizard would be a Compiler Output "grabber" like this:
The Wizard form above is not important, but the implemenation is based on the FileOpen DLL Wizard:
GetStyle: | esStandard |
GetIDString: | DrBob.TCompilerOutputExpert |
GetName: | Compiler Output Wizard |
GetAuthor (win32): | Bob Swart (aka Dr.Bob) |
GetMenuText: | &Compiler Output Wizard... |
GetState: | [esEnabled] |
GetGlyph: | 0 |
GetPage (win32): | |
GetComment: |
procedure TStandardExpert.Execute; begin try with TDCC32OutputForm.Create(Application) do try ShowModal finally Free end except HandleException end end {Execute};The Compiler Output Wizard is actually quite a handy Wizard that could be useful inside the Delphi 2.0x and C++Builder environments as well. So, let's keep this Wizard in mind for the upcoming esAddIn Experts when porting to these environments
Installation
The installation of DLL Wizards is different from the installation of .DCU Wizards.
Since the DLL is entirely self-containting, it does not have to be linked with COMPLIB.DCL or DCLUSR30.DPL on a binary level, but rather in a dynamic way (the name Dynamic Link Library should indicate something to that end, right?).
We install the FILEOPEN.DLL Wizard by adding a new entry in the Registry at the Wizards section.
DLL vs DCU
Now that we've seen DCU Wizards as well as DLL Wizards, it's time to investigate the difference between these two; which one would be more efficient or pleasant to use?
For starters, a DCU Wizard is smaller (it doesn't need to have the entire RTL and VCL linked with itself) and has a tight integration with the Component Library/Package (but a DCU Wizard won't be able to dynamically install DLL Wizards, as we'll see later).
On the other hand, a DLL Wizard is bigger, less integrated with Delphi itself (we have no GetClass API, as we'll see later) but a DLL Wizard can be installed rather easy by modifying DELPHI.INI or the registry and in fact will be able to be installed dynamically and install other Project/Form DLL Wizards!
In short, we need some more examples to see when a DCU Wizard is the best choice and when a DLL Wizard is the only one that will work.
Until this point, all Wizards that we've written could have been a DLL Wizard just as easy as a DCU Wizard.
Packages
Apart from installing DCU Wizards in the DCLUSR30 package, we can of course also use a special package for all our Wizards, like DrBobX.DPL.
Just select File | New and pick the "package" type of new item:
The generated source code for the DrBobX.DPK package file is as follows (note the Generic and Form Wizards that are already contained within):
package DrBobX; {.$R *.RES} {$ALIGN ON} {$ASSERTIONS ON} {$BOOLEVAL OFF} {$DEBUGINFO ON} {$EXTENDEDSYNTAX ON} {$IMPORTEDDATA ON} {$IOCHECKS ON} {$LOCALSYMBOLS ON} {$LONGSTRINGS ON} {$OPENSTRINGS ON} {$OPTIMIZATION ON} {$OVERFLOWCHECKS OFF} {$RANGECHECKS OFF} {$REFERENCEINFO OFF} {$SAFEDIVIDE OFF} {$STACKFRAMES OFF} {$TYPEDADDRESS OFF} {$VARSTRINGCHECKS ON} {$WRITEABLECONST ON} {$MINENUMSIZE 1} {$IMAGEBASE $00400000} {$DESCRIPTION 'Dr.Bob''s Experts & Wizards Package'} {$DESIGNONLY} {$IMPLICITBUILD ON} requires vcl30; contains Generic, Form; end.One of the great advantages of package-Wizards compared to DLL wizards is that a package can be loaded and unloaded on demand, without having to exit Delphi itself!
7. TSplashFormExpert
So far, we've seen and build our own Standard Experts and Project Experts.
It isn't really hard to make a Standard Expert into a Project Expert.
But what about the third kind of Expert; Form Experts?
This one doesn't open up a file as a project, it usually creates something, a form and corresponding code.
Remember the Dialog Expert and Database Form Expert?
How do they work, and more important, can we do the same thing all by ourselves?
As a useful example of a Form Expert, I've come up with the idea of a Splash Form Expert.
Every state-of-the-art application needs a Splash Screen, right?
Why not automate the process and have a Splash Form Expert that generates one for us?
All we need is a bitmap and a little border around it.
If we want to, we can always make it more complex afterwards.
First, let's look at what the resulting should look like.
Just a plain form with a Panel and Bitmap should do it.
Make sure the panel shows a nice border, and size the form enough to fit around the bitmap and panel (you don't want to resize/stretch the bitmap unless absolutely necessary, of course).
I found the most convenient way to add a Splash Screen to my application is to use the following technique.
The unit below must be included in the uses clause of the main .DPR file, and will then get activated when the application starts (see the initialization part?).
The FormDeactivate method will then make sure that this Splash Screen is shown right until the first main form comes up (and gets activated).
At that time, the Splash Screen is freed.
Note that this technique will only work for large, slow or database programs, i.e. programs that will take some seconds to show the main form.
Otherwise, the Splash Screen will only flicker to be replaced almost immediately by the main form.
unit SplashFm; interface uses Forms, Classes, Controls, StdCtrls, ExtCtrls; type TSplash = class(TForm) Panel1: TPanel; Image1: TImage; procedure FormDeactivate(Sender: TObject); end; implementation {$R *.DFM} var Splash: TSplash; procedure TSplash.FormDeactivate(Sender: TObject); begin Screen.Cursor := crDefault; Splash.Free; end; initialization Screen.Cursor := crAppStart; Splash := TSplash.Create(nil); Splash.Show; Splash.Update end.Since the Panel is wrapped around the bitmap, and the Form is sized to fit around the Panel, all we need is an Wizard that enables us to select a bitmap from somewhere, and generates the code above with a corresponding .DFM form file.
Actually, the Splash Form Wizard is part of Dr.Bob's Collection of Delphi/C++Builder Wizards (and was used to generate the Splash Screen for the collection itself):
8. 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.
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.
9. TCompilerOutputExpert
Now, it's time to write an esAddIn Wizard, just like the two examples above, but this time we respond to the OnClick event by creating and showing the Compiler Output Wizard, like we did earlier in this paper.
Since we'll be porting this last Wizard of this paper from Delphi 3 to Delphi 2 and C++Builder, let's include the entire code to look closely at the potential porting issues involved:
library AddIn; uses ShareMem, VirtIntf, ExptIntf, ToolIntf, Menus, Forms, Classes, Dcc32Out; { EXPERT TYPE DEFINITION } Type TBDcc32OutputExpert = 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; protected procedure OnClick(Sender: TIMenuItemIntf); virtual; private MenuItem: TIMenuItemIntf; end {TBDcc32OutputExpert}; { EXPERT SUPPORT FUNCTION } procedure HandleException; begin if Assigned(ToolServices) then ToolServices.RaiseException(ReleaseException) end {HandleException}; { EXPERT IMPLEMENTATION } function TBDcc32OutputExpert.GetStyle: TExpertStyle; begin Result := esAddIn end {GetStyle}; function TBDcc32OutputExpert.GetIDString: String; begin try Result := 'DrBob.TCompilerOutputExpert' except HandleException end end {GetIDString}; function TBDcc32OutputExpert.GetName: String; begin try Result := 'Compiler Output Wizard' except HandleException end end {GetName}; function TBDcc32OutputExpert.GetAuthor: String; begin try Result := 'Bob Swart (aka Dr.Bob)' { although not needed for esAddIn } except HandleException end end {GetAuthor}; constructor TBDcc32OutputExpert.Create; var Main: TIMainMenuIntf; ProjectBuildItem: TIMenuItemIntf; Project: TIMenuItemIntf; begin inherited Create; MenuItem := nil; if ToolServices <> nil then try Main := ToolServices.GetMainMenu; if Main <> nil then { we've got the main menu } try ProjectBuildItem := Main.FindMenuItem('ProjectBuildItem'); if ProjectBuildItem <> nil then try Project := ProjectBuildItem.GetParent; if Project <> nil then try MenuItem := Project.InsertItem(ProjectBuildItem.GetIndex+1, '&Dr.Bob''s Compiler Output Wizard...', 'DrBob','', ShortCut(Ord('D'),[ssCtrl]),0,0, [mfEnabled, mfVisible], OnClick); finally Project.DestroyMenuItemNotice the fact that InitExpert got exported twice? One as name 'INITEXPERT0016' and once as name 'INITEXPERT0017'. The former is the name that's used by Delphi 2.0x and C++Builder, while the latter is the name used by Delphi 3. If we don't export the InitExpert function by the right name, then Delphi will just disregard the Wizard and tells us that it's the wrong version. So, by exporting InitExpert twice under both names (or maybe even as 'INITEXPERT0018' to prepare for Delphi 4), we've taken the first obstacle. But don't party yet; there are more to come...
end finally ProjectBuildItem.DestroyMenuItem
end finally Main.Free end except HandleException end end {Create}; destructor TBDcc32OutputExpert.Destroy; begin try if MenuItem <> nil then MenuItem.DestroyMenuItem; inherited Destroy except HandleException end end {Destroy}; procedure TBDcc32OutputExpert.OnClick(Sender: TIMenuItemIntf); begin try with TDCC32OutputForm.Create(Application) do try ShowModal finally Free end except HandleException end end {OnClick}; { DLL EXPERT INTERFACE } procedure DoneExpert; begin // cleanup end {DoneExpert}; function InitExpert(ToolServices: TIToolServices; RegisterProc: TExpertRegisterProc; var Terminate: TExpertTerminateProc): Boolean; stdcall; begin Result := True; try ExptIntf.ToolServices := ToolServices; { Save! } if ToolServices <> nil then Application.Handle := ToolServices.GetParentHandle; Terminate := DoneExpert; Result := RegisterProc(TBDcc32OutputExpert.Create); except HandleException end end {InitExpert}; exports InitExpert name 'INITEXPERT0016', { Delphi 2.0x & C++Builder } InitExpert name 'INITEXPERT0017'; { Delphi 3 } begin end.
10. Wizard Compatibility Issues
Delphi Wizards and Expert that use ToolServices must include the ShareMem unit as first unit in their main project uses clause.
This is due to the fact that a special DELPHIMM.DLL Memory Manager DLL must be loaded and installed as the Wizard's memory manager before anything related to "memory" has happened.
This includes working with any class, Long Strings or other heap related operations.
So far so good.
However, C++Builder also uses a memory manager for the same purposes: called BCBMM.DLL.
These DLLs are not 100% compatible, so if we have a esStandard Wizard laoding in C++Builder, asking for ToolServices.ProjectName and DELPHIMM.DLL is loaded (instead of BCBMM.DLL) then we get a lot of Access Violations.
The same is true when we try it the other way around.
So, we must somehow override the ShareMem unit, find out - when our Wizard is loading - which IDE is loading the Wizard: Delphi or C++Builder, and manually (i.e. explicitly) load the required DLL and install it as our new memory manager by hand.
There is one problem left: how do we detect the right IDE?
Remember that we cannot use any class or Long String operation, since we're installing the new memory manager (and this is something that can only be done once each session).
The solution is to use the
function GetCommandLine: PChar;define in kernel32.dll. SInce this function returns the command-line stored in a PChar, we can just walk through this command-line and look for 'BCB'. If we found 'BCB', then we can conclude that we're loaded by Borland C++Builder. Otherwise, we just believe it's Delphi:
unit ShareMem; { (c) 1997 by Bob Swart (aka Dr.Bob - http://www.drbob42.com } interface const Delphi: Boolean = True; { can be used outside the unit as well } Version: Integer = 2; { C++Builder = 2 } SysGetMem: function(Size: Integer): Pointer = nil; SysFreeMem: function(P: Pointer): Integer = nil; SysReallocMem: function(P: Pointer; Size: Integer): Pointer = nil; GetHeapStatus: function: THeapStatus = nil; GetAllocMemCount: function: Integer = nil; GetAllocMemSize: function: Integer = nil; DumpBlocks: procedure = nil; implementation uses Windows; const Handle: THandle = 0; const SharedMemoryManager: TMemoryManager = ( GetMem: nil; FreeMem: nil; ReallocMem: nil); function GetCommandLine: PChar; stdcall; external 'kernel32.dll' name 'GetCommandLineA'; var P: PChar; i: Integer; initialization P := GetCommandLine; i := 0; repeat Inc(i) until (P[i] = #0) or ((P[i] = 'B') and (P[i+1] = 'C') and (P[i+2] = 'B')); Delphi := P[i] = #0; if not Delphi then begin Handle := LoadLibrary('BCBMM.DLL'); { if Handle = 0 then MessageBox(HWnd(0),'Error: could not load BCBMM.DLL', nil,MB_OK or MB_ICONHAND); } @DumpBlocks := GetProcAddress(Handle, 'DumpBlocks'); @SysGetMem := GetProcAddress(Handle, '@System@SysGetMem$qqri'); @SysFreeMem := GetProcAddress(Handle, '@System@SysFreeMem$qqrpv'); @SysReallocMem := GetProcAddress(Handle, '@System@SysReallocMem$qqrpvi'); end else { Delphi } begin if (FindWindow('TApplication', 'Delphi 2.0') > 0) then Version := 2; if (FindWindow('TApplication', 'Delphi 3') > 0) then Version := 3; Handle := LoadLibrary('DELPHIMM.DLL'); { if Handle = 0 then MessageBox(HWnd(0),'Error: could not load DELPHIMM.DLL', nil,MB_OK or MB_ICONHAND); } @SysGetMem := GetProcAddress(Handle, 'SysGetMem'); @SysFreeMem := GetProcAddress(Handle, 'SysFreeMem'); @SysReallocMem := GetProcAddress(Handle, 'SysReallocMem'); end; @GetHeapStatus := GetProcAddress(Handle, 'GetHeapStatus'); @GetAllocMemCount := GetProcAddress(Handle, 'GetAllocMemCount'); @GetAllocMemSize := GetProcAddress(Handle, 'GetAllocMemSize'); SharedMemoryManager.GetMem := @SysGetMem; SharedMemoryManager.FreeMem := @SysFreeMem; SharedMemoryManager.ReallocMem := @SysReallocMem; SetMemoryManager(SharedMemoryManager); finalization FreeLibrary(Handle) end.With this new ShareMem unit, we can compile a Wizard with Delphi, and use it in the C++Builder IDE without any problems!
Note that in case we're loaded by Delphi, we even try to find out the version number of Delphi, i.e. whether we're loaded by Delphi 2.0x or Delphi 3.
This is important for the following portability issue regarding Delphi/C++Builder Wizards: the version of the TInterface class.
For Delphi 2.0x and C++Builder, TInterface.GetVersion returns 2.
For Delphi 3, however, TInterface.GetVersion returns 3.
And if we just compile a Wizard with Delphi 3, we can't run it in Delphi 2 and vice versa.
Reminds me a bit of the InitExpert export declaration, remember?
Only this time it may be a bit harder to fix...
The only way I could make thing work, was by hacking into VIRTINTF.PAS, and making sure TInterface.GetVersion returns the version number that ShareMem detected when it was looking for the Delphi or C++Builder IDE:
unit VirtIntf; interface type TInterface = class private FRefCount: Longint; public constructor Create; procedure Free; function AddRef: Longint; virtual; stdcall; function Release: Longint; virtual; stdcall; function GetVersion: Integer; virtual; stdcall; end; { TIStream - This provides a pure virtual interface to a physical stream } TIStream = class(TInterface) public function Read(var Buffer; Count: Longint): Longint; virtual; stdcall; abstract; function Write(const Buffer; Count: Longint): Longint; virtual; stdcall; abstract; function Seek(Offset: Longint; Origin: Word): Longint; virtual; stdcall; abstract; function GetModifyTime: Longint; virtual; stdcall; abstract; procedure SetModifyTime(Time: Longint); virtual; stdcall; abstract; procedure Flush; virtual; stdcall; abstract; end; function ReleaseException: string; implementation uses SysUtils, ShareMem; { TInterface } constructor TInterface.Create; begin inherited Create; FRefCount := 1; end; procedure TInterface.Free; begin if Self <> nil then Release; end; function TInterface.AddRef: Longint; begin Inc(FRefCount); Result := FRefCount; end; function TInterface.Release: Longint; begin Dec(FRefCount); Result := FRefCount; if FRefCount = 0 then Destroy; end; function TInterface.GetVersion: Integer; begin Result := ShareMem.Version end; { Exception handling } function ReleaseException: string; begin Result := Exception(ExceptObject).Message; end; end.This work only when either Delphi 2.0x or Delphi 3 is loaded. When both are loaded, then the reported GetVersion will be 3. So, when Delphi 3 is loaded, it's not possible to load Delphi 2.0x as well, since any Wizard using the above units will detect the Delphi IDE, but with the "3" version, which will result in a verion-failure when trying to load the Wizards. Oh well, who wants to use Delphi 3 and Delphi 2.0x at the same time anyway?
FWIW, with the new SHAREMEM.PAS and VIRTINTF.PAS I've compiled my Collection of Delphi/C++Builder Wizards - using Delphi 3 - into a single binary-compatible DRBOB.DLL (of roughly 800 Kbytes).
11. Conclusion
We've seen that we can use Delphi to extend the Delphi and C++Builder IDEs themselves with four kinds of Experts: Standard, Project, Form and AddIn Experts.
We've even seen how to write compatible Wizards that can be re-used and shared by the different development environments.
Wizards can extend and greatly enhance the way we work with the RAD environment. If everything is within our reach, and connected to each other, then it's clear productivity will increase. Custom written Wizards can help you support yourself and those around you.
Bibliography
If you want more interesting and technical information on the Open Tools API of Delphi,
then you should check out my articles in The Delphi Magazine or the book The Revolutionary Guide to Delphi 2, ISBN 1-874416-67-2 published by WROX Press.