Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Dr.Bob's Delphi Notes Dr.Bob's Delphi Clinics
 How To Write Delphi and C++Builder Wizards
See Also: Delphi Papers and Columns

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.
Delphi 1 and 2 simply add the Wizard the the Component Library, but Delphi 3 needs to add it to a package - the DCLUSR30 package by default:

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:

TGenericExpert
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:

TSysInfoExpert
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;

 implementation
The 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):

Actions
  • CloseProject
    returns True if no project open, or if the currently open project can be closed.
  • OpenProject
    returns True if the named project can be opened. You have to pass an empty string to create a new project and main form.
  • OpenProjectInfo
    returns True if the named project file can be opened. This routine bypasses all the normal project load features (such as loading a desktop file, showing the source code, etc), and simply opens the .DPR and .OPT files.
  • SaveProject
    returns True if the project is unmodified, if there is no project open, or if the open project can be saved.
  • CloseFile
    returns True if the specified file is not currently open, or if it can be closed.
  • OpenFile
    returns True if the specified file is already open or can be opened.
  • ReloadFile
    returns True if the file is already open and was reloaded from disk. (NOTE: This will not perform any checking of the current editor state).
  • RefreshBuffers
    causes the IDE to check the time/date stamp of each open file to determine if the file has changed on disk. If so, the file is re-read.
  • ModalDialogBox
    used by non-VCL DLL's to present a dialog box which is modal. Note that DLLs written using VCL can simply call a form's ShowModal function.
  • CreateModule
    Will create new module from memory images of the source and, optionally, the form file.
      The TCreateModuleFlags are:
    • cmAddToProject: Add the new module to the currently open project.
    • cmShowSource: Show the source file in the top-most editor window.
    • cmShowForm: If a form is created, show it above the source.
    • cmUnNamed: Will mark the module as unnamed which will cause the SaveAs dialog to show the first time the user attempts to save the file.
    • cmNewUnit: Creates a new unit and adds it to the current project. NOTE: all other parameters are ignored.
    • cmNewForm: Creates a new form and adds it to the current project. NOTE: all other parameters are ignored.
    • cmMainForm: If the module includes a form, make it the main form of the currently open project. Only valid with the cmAddToProject option.
    • cmMarkModified: Will insure that the new module is marked as modified.

    Informational

  • GetParentHandle
    returns a HWND which should be used as the parent for windows created by the client.
  • GetProjectName
    returns a fully qualified path name of the currently open project file, or an empty string if no project is open.
  • GetUnitCount
    returns the current number of units belonging to the project.
  • GetUnitName
    returns a fully qualified name of the specified unit.
  • GetFormCount
    returns the current number of forms belonging to the project.
  • GetFormName
    returns a fully qualified name of the specified form file.
  • GetCurrentFile
    returns a fully qualified name of the current file, which could either be a form or unit (.PAS). Returns a blank string if no file is currently selected.
  • IsFileOpen
    returns True if the named file is currently open.
  • GetNewModuleName
    Automatically generates a valid Filename and Unit identifier. Uses the same mechanism as used by the IDE.

    Component library interface

  • GetModuleCount
    Returns the number of currently installed modules in the component library.
  • GetModuleName
    Returns then name of the module given its index.
  • GetComponentCount
    Returns the number of components installed in a particular module.
  • GetComponentName
    Returns the name of the component given its module index and index in that module.

    Error handling

  • RaiseException
    This will cause an Exception to be raised with the IDE with the string passed to this function. NOTE: This will cause the stack to unwind and control will NOT return to this point. It is the resposibility of the Library to be sure it has correctly handled the error condition before calling this procedure.
  • 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: Actions
  • CreateModuleEx
    New extended form of CreateModule. This will return a TIModuleInterface. All CreateModes from CreateModule are supported with only the following differences:
    • cmExisting: Will create an existing module from the given file system.
    • AncestorClass: This must specify an existing base class in the project. (use the cmAddToProject flag to add a module to the project first).

    Informational

  • EnumProjectUnits
    Calls EnumProc once for each unit in the project.

    Editor

  • GetModuleInterface
    Return an module interface associated with the given file. This function returns the same interface instance for a given module, only the reference count is adjusted. The user of this interface owns it and must call release when finished.
  • GetFormModuleInterface
    returns an module interface associated with the given form.

    Menu

  • GetMainMenu
    Returns an interface to the IDE's main menu. See TIMainMenuIntf for details.

    Notification

  • AddNotifier
    Registers an instance of a descendant to TIAddIn- Notifier.
  • RemoveNotifier
    Removes a registered instance of a TIAddInNotifier.

    Configuration Access

  • GetBaseRegistryKey
    returns a string representing the full path to Delphi's base registry key. This key is relative to HKEY_CURRENT_USER.

    Virtual File System

  • The RegisterFileSystem and UnRegisterFileSystem methods seem to be present to prepare for future porting and operating/filesystem specific issues. Nice to know Borland is working on that as well!

    Pascal String Handling

  • The Pascal string handling functions are provided for IDE add-in writers to use a language other than Pascal. (C or C++, for example). Add-in writers using Delphi will never need to use these functions, but we'll focus on them for the sake of completeness:
    • NewPascalString: Allocates and returns a pascal long string from the provided PChar (char*, in C). Passing an empty string or nil for the PChar will return nil for the string (Pascal's equivalent of an empty string).
    • FreePascalString: Attempts to free the given Pascal string by decrementing the internal reference count and releasing the memory if the count returns to zero.
    • ReferencePascalString: Increments the reference count of the given Pascal string. This allows the calling function to manually extend the lifetime of the string. A corresponding call to FreePascalString must be made in order to actually release the string's memory.
    • AssignPascalString: Assigns one Pascal string to another. Never directly assign Pascal strings to each other. Doing so will orphan memory and cause a memory leak. The destination may be referencing another string, so the reference count of that string must be decremented. Likewise, the reference count of the source string must be incremented.
  • 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:

    TFileOpenExpert
    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.
    Note that Delphi sometimes gives an error message when you open just any file as your new project, when the header (the first few lines) doesn't contain a "uses" clause (which my AUTOEXEC.BAT seldom does).

    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.

    TFileOpenExpert
    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:

    TFileOpenProjectExpert = class(TFileOpenExpert)
    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.
    We do need to change something else, though, we need to register them both:
      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.
    The TFileOpenExpert Execute method must use the try-except structure as well, of course:
      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.
    InitExpert takes three arguments. The first one is the ToolServices of Delphi itself (remember that Delphi itself is the one to call our InitExpert function). If we need ToolServices later on, we must make a copy of it now! The second argument of InitExpert is the RegisterProc callback function of Delphi, which we need to call with the created instance of our Wizard. The third argument of InitExpert is a reference (variable) argument of type TExpertTerminateProc which we need to assign to our exported clean-up procedure (DoneExpert, in this case). For the FileOpen DLL Wizard, the resulting code is as follows:
     {$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:

    TCompilerOutputExpert
    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.

    Delphi will read this at startup, and will get its information regarding Wizards that must be loaded from it. To install a DLL Wizard, we need to modify the Wizards section to add a key with as value the path where the DLL Wizard can be found. If the DLL cannot be found, or the registration inside the DLL fails, then Delphi will tell us so.

    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:

    We use the short filename "DrBobX.DPK", and the long description "Dr.Bob's Experts & Wizards Package'.

    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.
    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).
    The Caption is the text that we'll see in the menu, something like '&Dr.Bob''s Wizard' or some useful text. The Name is the name of the VCL menu item component. It's not clear whether or not this name should be actually the same as the component name that holds the menu item that we've just created. And in that case, we should probably need to use some unique component name as well. For now, I've used DrBobItem, which seems pretty unique (so far). The final string is a Hint, which is not used at this time by the IDE anyway, so I leave it blank for now. Then, we can add a ShortCut key, a help context and groupindex. As menuflags I always use enabled and visible, but we can pick from a set of them (see a previous listing). Finally, we need to assign an OnClick event that gets fired when the menu item for our expert is selected. This is the place where previously our Execute method would kick in. Only the Execute method is no longer of the required type, since we need a method of type TIMenuClickEvent here. In short, the call to InsertItem could be as follows:
      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};
    

    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!
    After we've installed our first AddIn Expert (in the usual way), we can start Delphi up again and see our first AddIn Expert as part of the Tool 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.0xC++BuilderDelphi 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.DestroyMenuItem
    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.
    Notice 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...

    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.


    This webpage © 2000-2017 by Bob Swart (aka Dr.Bob - www.drbob42.com). All Rights Reserved.