OTA: Visual design of Wizards

By: Ondrej Kelle

Abstract: This article describes how to implement OpenTools wizards as data modules and creators as components so they can be desinged visually in the IDE.


Introduction

Delphi's OpenTools API can be used to extend your favorite development environment to be more productive in your everyday work. For a code-generating wizard you need to write a few classes, implementing some of the OTA interfaces, like IOTAWizard, IOTAProjectCreator, IOTAModuleCreator, IOTAFile and so on. If you have done this a few times, you know that this work involves several more or less simple tasks that are always the same for each wizard. Never send a human to do a machine's job. This article is here to show you another way; a Delphi way.

The framework

IOTANotifer: TNotifierModule

We are going to create a framework which will enable us to design our wizards the way we're used to work in the Delphi IDE: by setting properties in the Object Inspector and writing event handling code. Our wizard implementation classes will be derived from TDataModule so they can serve as non-visual containers for our creator components. If our wizards need other components like menus, actions, or database access components we can put them on the data module, too, and enjoy automatic loading of their properties from the .dfm stream.

First, all wizards need to implement IOTANotifier interface. Let's write its implementation class derived from TDataModule:

Listing 1: DMNotifier.pas

The implementation of IUnknown interface is very similar to that of TInterfacedObject. However, there is a difference in the internal FUseRefCount variable: Reference counting is only active if CreateInterfaced constructor was used to create the instance. We will use this constructor when creating the actual wizard instance in our Register procedures. At design time, we don't want to (and, from my experience with Delphi 6, we cannot) use reference counting. Instead, we want our instance to behave just like any standard data module or component.
Implementation of IOTANotifier interface is also simple: we will just call the appropriate events.

Note that for IOTAWizard implementations, Delphi will never call AfterSave, BeforeSave and Modified methods; therefore OnAfterSave, OnBeforeSave and OnModified events have no meaning and I have only included them for the sake of completeness. Perhaps future versions of Delphi will find a meaningful use for them.


IOTAWizard: TWizardModule

Listing 2: DMWizard.pas

This class is derived from TNotifierModule and publishes a few useful properties: IDString, State and WizardName. The IOTAWizard implementation methods GetIDString, GetState and GetName then simply return values of these properties, respectively. Execute is left empty and virtual, so that descendants can override it. There are several other classes included in this unit:

  • TCreator - base creator component, implements IOTACreator
  • TModuleCreator - module creator component, implements IOTAModuleCreator
  • TProjectCreator - project creator component, implements IOTAProjectCreator, IOTAProjectCreator50
  • TModuleFile - internal class used by TModuleCreator, implements IOTAFile


  • TModuleCreator is a component with a few useful published properties which can be saved to the .dfm stream and edited in the Object Inspector:
    
        property AncestorName: string read FAncestorName write FAncestorName;
        property ImplFileName: string read FImplFileName write FImplFileName;
        property IntfFileName: string read FIntfFileName write FIntfFileName;
        property FormName: string read FFormName write FFormName;
        property MainForm: Boolean read FMainForm write FMainForm default False;
        property ShowForm: Boolean read FShowForm write FShowForm default True;
        property ShowSource: Boolean read FShowSource write FShowSource default True;
        property SourceForm: TStrings read FSourceForm write SetSourceForm;
        property SourceImpl: TStrings read FSourceImpl write SetSourceImpl;
        property SourceIntf: TStrings read FSourceIntf write SetSourceIntf;
    
    
    How are they used? Delphi calls an appropriate method of IOTAModuleCreator when a new module is to be created in the IDE:

  • NewFormFile: the creator should return the new module's .dfm stream
  • NewImplSource: the creator should return the new module's .pas source code
  • NewIntfSource: this is only used in C++Builder, I believe it's for .hpp header files


  • These three methods are basically the same, so let's just have a glance at NewImplSource:
    
    function TModuleCreator.NewImplSource(const ModuleIdent, FormIdent, AncestorIdent: string): IOTAFile;
    begin
      if FSourceImpl.Text = '' then
        Result := nil
      else
        Result := TModuleFile.Create(Self, mstImpl, ModuleIdent, FormIdent, AncestorIdent);
    end;
    
    
    Does it make sense? Let's have a look at what TModuleFile does when Delphi requests the new source:
    
    function TModuleFile.GetSource: string;
    begin
      Result := '';
      if Assigned(FCreator) then
      begin
        case FSourceType of
          mstForm:
            Result := Format(FCreator.FSourceForm.Text, [FModuleIdent, FFormIdent, FAncestorIdent]);
          mstImpl:
            Result := Format(FCreator.FSourceImpl.Text, [FModuleIdent, FFormIdent, FAncestorIdent]);
          mstIntf:
            Result := Format(FCreator.FSourceIntf.Text, [FModuleIdent, FFormIdent, FAncestorIdent]);
        end;
        // remove trailing CRLF
        if (Result[Length(Result) - 1] = #13) and (Result[Length(Result)] = #10) then
          Delete(Result, Length(Result) - 1, 2);
        if Assigned(FCreator.OnGetSource) then
          FCreator.OnGetSource(FCreator, FSourceType, Result);
      end;
    end;
    
    
    That means that the creator's ModuleIdent, FormIdent and AncestorIdent are used to format SourceImpl.Text to obtain the resulting source code. SourceImpl (TStrings property) should therefore contain %s placeholders whereever substitution is appropriate. Here is an example of what SourceImpl property might contain to create a simple new form unit:
    
    
    unit %0:s;
    
    uses
      Classes, SysUtils;
    
    interface
    
    type
      T%1:s = class(TForm)
      private
        procedure SomeProc;
      end;
    
    var
      %1:s: T%1:s;
    
    implementation
    
    {$R .dfm}
    
    procedure T%1:s.SomeProc;
    
    begin
      
    end;
    
    end.
    
    
    

    IOTAMenuWizard: TMenuWizardModule

    Derived from TWizardModule, it additionally implements IOTAMenuWizard interface which is used to create a menu wizard. This type of wizard can be invoked using its menu item under Delphi's Help menu.

    Listing 3: DMMenuWizard.pas

    IOTARepositoryWizard: TRepositoryWizardModule

    Also derived from TWizardModule, it additionally implements IOTARepositoryWizard interface which is used to create a repository wizard. This type of wizard can be invoked from the FileNew... dialog.

    Listing 4: DMRepositoryWizard.pas

    Registration

    We need to register these new data modules with the IDE designer so that our new published properties will appear in the Object Inspector:
    
    procedure Register;
    begin
      RegisterCustomModule(TNotifierModule, TDataModuleCustomModule);
      RegisterCustomModule(TWizardModule, TDataModuleCustomModule);
      RegisterCustomModule(TMenuWizardModule, TDataModuleCustomModule);
      RegisterCustomModule(TRepositoryWizardModule, TDataModuleCustomModule);
      RegisterComponents('Wizards', [TModuleCreator, TProjectCreator]);
    end;
    
    

    Wizard creation wizards

    The basic framework is ready. Our data modules have the needed published properties which can be edited in the Object Inspector. What we need now is a few wizards to create new instances of our datamodules and components for us according to our requirements. They will be repository wizards so we can invoke them from the FileNew... dialog. Why not use the data module framework we have just put together? Here they are:

    Add-in wizard creator wizard: TAddInWizardRepository

    This repository wizard will create a new add-in wizard for you and add its unit to the currently active project by default. Add-in wizards do not automatically get a menu item or any other user interface element that can be used to invoke them. If you write an add-in wizard, you have to write such code yourself. Because the wizard itself is a data module, you can easily put components like menu items, actions or image lists on it and set their properties.

    Listing 5: WizardAddInWizard.pas
    Listing 6: WizardAddInWizard.dfm

    Menu wizard creator wizard: TMenuWizardRepository

    This repository wizard will create a new menu wizard for you and add its unit to the currently active project by default. Menu wizards automatically get a menu item under Delphi main menu's Help item. This can be a real time saver if all you need is a quick look at something inside Delphi's process: just open your design time package, add a new menu wizard (FileNewWizardsMenu Wizard) and fill in a simple ShowMessage line in the Execute method. Compile/install the package. The new menu item is there.

    Listing 7: WizardMenuWizard.pas
    Listing 8: WizardMenuWizard.dfm

    Unit/Form wizard creator wizard: TFormWizardRepository

    This repository wizard will create a new unit or form wizard for you and add its unit to the currently active project by default.

    Listing 9: WizardFormWizard.pas
    Listing 10: WizardFormWizard.dfm

    Project creator wizard: TProjectWizardRepository

    This repository wizard will create a new project wizard for you and add its unit to the currently active project by default.

    Listing 11: WizardProjectWizard.pas
    Listing 12: WizardProjectWizard.dfm

    Wizard package creator wizard: TPackageWizardRepository

    This repository wizard will create a new design time package for you which can be used to hold several wizards in one place. It will also add the new package to the currently active project group by default. The new package will be marked as design time only and assigned a default description. Appropriate required package references will be added to the new package's requires clause: If you choose to use our data module framework, the package will depend on WizardUtil.dcp where our base classes are declared. This is sufficient to compile the package; designide.dcp and vcl.dcp are included implicitly because WizardUtil.dcp requires them. If you choose not to use the data module framework, only designide.dcp will be added.
    Having several wizards in one package enables you to quickly enable/disable a group of wizards by checking/unchecking the package in the ComponentInstall Packages... dialog.
    Before creating the new package, this wizard will prompt you with the following modal form where you can select the type of your first wizard to be included in it:

    New Wizard form

    If you uncheck the 'With Data module' checkbox, the new wizard will not use a data module; instead, it will be a classic code-only wizard (derived from TInterfacedObject) you already know.

    Listing 13: WizardPackageWizard.pas
    Listing 14: WizardPackageWizard.dfm

    New Wizards page

    You can download the related source code here:

    CodeCentral ID 17106: Visual design of OTA wizards (Delphi 6)

    For Delphi 4-5, there is an older version here:

    CodeCentral ID 15941: Visual design of OTA wizards (Delphi 4-5)

    The installation instructions are simple:

  • install WizardUtil.dpk
  • install WizardWizards.dpk


  • Enjoy! I hope you'll find this stuff useful. All comments, problem reports and ideas for improvement are welcome.

    TOndrej

    Server Response from: ETNASC01