Delphi Clinic C++Builder Gate Training & Consultancy Delphi Notes Weblog Dr.Bob's Webshop
Bob Swart (aka Dr.Bob) - Medical Officer
 UK-BUG DIL - Developer Information Library
 

Delphi DIL Wizardry
If you're like me, a developer working with Inprise/Borland tools most of the day, then you must also know the additional value of DIL - the Developers Information Library, collected and distributed by the UK-BUG Borland User Group. Not a single day goes by without DIL helping me at least once with a problem or component that I'm looking for. And being an impatient guy, I want DIL to be available at all times - ready when I want it to, so during a recent UK-BUG meeting in London I suddenly got a great idea: why not integrate DIL into the Delphi IDE, by writing a little "wrapper" Wizard around it, making it available from the Delphi Help menu.

Delphi IDE Experts and Wizards
Actually, the words Expert and Wizard mean the same for Delphi. They were called Experts up to and including Delphi 2, but then C++Builder started calling them Wizards, which is also used in Delphi 3 now. I'll use and mix the two terms always (hope you don't mind).
There are two ways to write Experts and Wizards. The easy way is to use my Wizard Wizard (available on my website) and let it generate the code for us. The more instructive way is to take a look at the ExptIntf.int and ToolIntf.int files in the Delphi 3/Source/ToolsAPI directory, and try to do it ourselves.
It all boils down to deriving a new class, say TDILWizard, from the abstract base class TIExpert, and override a number of functions that specify the new behaviour.
The following code snippet shows the seven method we need to override in this case.

  uses
    ShareMem, { in case of a DLL Wizard }
    VirtIntf, ExptIntf, ToolIntf, Windows, SysUtils, Forms;

  Type
    TDILWizard = class(TIExpert)
    public
      function GetStyle: TExpertStyle; override;
      function GetIDString: string; override;
      function GetName: string; override;
      function GetAuthor: String; override;
      function GetMenuText: string; override;
      function GetState: TExpertState; override;

      procedure Execute; override;
    end {TDILWizard};
The Delphi Open Tools API offers four kinds of Wizards, but only one ends up in the Help menu, which is the Standard style Wizard. We can use this information to return esStandard from GetStyle. Apart from esStandard, we can also use esProject (for Project Experts), esForm (for Form Experts) and esAddIn (for special AddIn Experts). You can read more about them in my detailed "Delphi Wizards" article available on my website at www.drbob42.com, but for now we'll concentrate on the Standard Delphi-DIL Wizard:
  function TDILWizard.GetStyle: TExpertStyle;
  begin
    Result := esStandard
  end {GetStyle};
The next function we need to override, specifies the internal ID-string for the Wizard. This one needs to be unique among all installed Wizards in Delphi. Usually, this means we can best use a scheme like "CompanyName.WizardName.WizardType", where you can substitute your personal name if you're not writing a Company specific Wizard. The fact that we include the WizardType here, means that we can actually make a single Wizard available as different Wizard types without having to think of a difficult new ID string (we only have to change the WizardType part of the GetIDString method).
In the current case, we can use the UKBUG.BobSwart.TDILWizard.esStandard ID:
  function TDILWizard.GetIDString: String;
  begin
    Result := 'UKBUG.BobSwart.TDILWizard.esStandard'
  end {GetIDString};
Next, we need to specify a name for the Wizard. The name is just another way to identify the Wizard among all other Wizards installed in the Delphi IDE, although it doesn't have to be unique (so if we make the Delphi-DIL Wizard available as esStandard and esAddIn Wizard, we don't actually need to change the GetName result.
In this particular situation, we can just return the class/type name TDILWizard:
  function TDILWizard.GetName: String;
  begin
    Result := 'TDILWizard'
  end {GetName};
Wizards can specify who the author of this particular Wizard is (and some tools can show this information if you want), which is the returnvalue of the GetAuthor method:
  function TDILWizard.GetAuthor: String;
  begin
    Result := 'Bob Swart (aka Dr.Bob - www.drbob42.com)'
  end {GetAuthor};
We get a little bit more to the point with the GetMenuText method. Since we're implementing an esStandard Wizard which will end up in the Delphi Help menu, we need to specify the exact menutext. Note that we can include ampersends in this string, which will end up as underline characters (underlining the next character).
In our particular situation for the Delphi-DIL Wizard, let's just return UK-BUG &DIL here:
  function TDILWizard.GetMenuText: String;
  begin
    Result := 'UK-BUG &DIL'
  end {GetMenuText};
Menus can he enabled or disable, and checked or not (checked). We can dynamically specify this using the GetState method. Note that I said "dynamically", since we can return a different value each time this method gets called (for example based upon the amount of free disk space, for a disk/file managing Wizard).
For the UK-BUG DIL Wizard, I just want to have it available at all times, returning a set with only esEnabled in it:
  function TDILWizard.GetState: TExpertState;
  begin
    Result := [esEnabled];
  end {GetState};
Finally, we get to the part where the Wizard gets executed (whenever the end-user clicks on the "UK-BUG DIL Wizard" menu item. Here, we can do anything we want, including executing a WinExec API with DIL.EXE as argument (note that I used the exact path where DIL was stored here):
  procedure TDILWizard.Execute;
  begin
    try
      WinExec('c:\program files\developer information library\dil.exe', SW_NORMAL);
    except
      // if we have one, just eat the exception
    end
  end {Execute};
Although we're done with the Wizard, we still need to install it. This can be done as DCU Wizard with a call to RegisterLibraryExpert, passing TDILWizard.Create as argument (yes, passing an instance of a justly created Wizard here), or as a DLL Wizard by implementing and exporting the following method (and don't forget to specify the "stdcall" keyword here, like I did before supper during the UK-BUG meeting):
  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(TDILWizard.Create);
    except
      HandleException
    end
  end {InitExpert};

  exports
    InitExpert name ExpertEntryPoint;
Installation
If you created a DCU Wizard, with a call to RegisterLibraryExpert inside a Register procedure, all you need to do is add the unit with this source code to a package (the Dhi 3 user package for example) and recompile to get the Wizard loaded.
If you are using the above InitExpert function, you need to add a new String Value to the registry at HKEY_CURRENT_USER\Software\Borland\Delphi\3.0\Experts, with a name that doesn't really matter, but a value that points to the Wizard DLL, such as C:\DIL\WIZARD.DLL.

Enhancements
Now that we have the first version of the DIL Wizard, I want to make a few enhancements to this Wizard in order to make it respond more easily to my fingertips.

ShortCut
First of all, I'd like to start the DIL Search Engine with a short-cut key, such as [Shift]+F1. This is not possible when DIL is installed as "standard" Wizard in the Help menu (simply because no shortcut-key is assigned to this menuitem). The obvious solution is to turn the standard Wizard into a AddIn Wizard. I will omit most of the source code, and only show the important parts here (you can always download the full Wizard source code from my website later).
The esAddIn style Wizard needs to override the constructor to be able to define a new menuitem (in this case, a menuitem in the Tools menu, where we use [Shift]+F1 as shortcut-key.

  constructor TDrBobDIL.Create;
  var
    MainMenu: TIMainMenuIntf;
    MainItem: TIMenuItemIntf;
    MenuItem: TIMenuItemIntf;
  begin
    inherited Create;
    NewMenuItem := nil;
    if ToolServices <> nil then
    try
      MainMenu := ToolServices.GetMainMenu;
      if MainMenu <> nil then { main menu }
      try
        MenuItem := MainMenu.FindMenuItem('ToolsOptionsItem');
        if MenuItem <> nil then
        try
          MainItem := MenuItem.GetParent;
          if MainItem <> nil then
          try
            NewMenuItem :=
              MainItem.InsertItem(MenuItem.GetIndex+1,
                                 'UK-BUG &DIL',
                                 'DrBobDIL1','',
                                  ShortCut(VK_F1,[ssShift]),0,0,
                                 [mfEnabled, mfVisible], OnClick)
          finally
            MainItem.DestroyMenuItem
          end
        finally
          MenuItem.DestroyMenuItem
        end
      finally
        MainMenu.Free
      end
    except
      HandleException
    end
  end {Create};
This will make sure that the "UK-BUG DIL" menu item shows in the Tools menu, and that we can activate the DIL Search Engine whenever we hit [Shift]+F1 (as opposed to hitting [Ctrl]+F1 to get the Delphi on-line help). The only difference with the Delphi on-line help is that the latter is context-sensitive (i.e. it responds to the current keyword we're on in the code editor for example).
Surely, it would be nice to be able to have the current keyword in the editor be entered in the DIL Search Engine "Find keyword" editbox. And in order to do that, we need to do two things: first obtain the current keyword (using the Editor Interface), and then put that keyword inside the DIL editbox (by sending some keyboard "keystrokes" to the editbox inside the DIL Window itself).
To get a pointer to the current editor, and specifically to the current keyword, we first need to get a handle to the current module (using ToolServices.GetModuleInterface), supplying the current open filename as parameter (which is ToolServices.GetCurrentFile). Using the module handle, we can call GetEditorInterface to get a handle to the editor, and then GetView on that handle to get the current cursor position in the editor view. We also need the editor handle to call CreateReader, so we can actually obtain the data (using GetText) reading around the cursor position. Then, we need to strip all characters that are not in IdentSet before and after the current cursor position until we're finally left with the current keyword. If I lost you already, take a look at the source code below (I'm sure it won't help much, but combined with what I said above you might just get the idea):
  procedure TDrBobDIL.Execute;
  var
    ModIntf: TIModuleInterface;
    EditIntf: TIEditorInterface;
    EditView: TIEditView;
    EditRead: TIEditReader;
    FileName: ShortString;

  const
    IdentSet = ['A'..'Z','0'..'9','.'];
  var
    HWnd: THandle;
    EditPos: TEditPos;
    CharPos: TCharPos;
    Position: LongInt;
  begin
    try
      FileName := ToolServices.GetCurrentFile;
      if Pos('.PAS',UpperCase(FileName)) > 0 then
      begin
        ModIntf := ToolServices.GetModuleInterface(FileName);
        if ModIntf <> nil then
        try
          EditIntf := ModIntf.GetEditorInterface;
          if EditIntf <> nil then
          try
            EditView := EditIntF.GetView(0);
            if EditView <> nil then
            try
              EditPos := EditView.CursorPos;
              EditView.ConvertPos(True,EditPos,CharPos);
              Position := EditView.CharPosToPos(CharPos)
            finally
              EditView.Free
            end
            else Position := 0;
            EditRead := EditIntF.CreateReader;
            if EditRead <> nil then
            try
              repeat
                FileName[0] := Chr(EditRead.GetText(Position,@FileName[1],255));
                Dec(Position)
              until (Position = 0) or not (UpCase(FileName[1]) in IdentSet);
              Delete(FileName,1,1); { remove leading space character }
              Position := 0;
              repeat
                Inc(Position)
              until not (UpCase(FileName[Position]) in IdentSet);
              Delete(FileName,Position,255);
            finally
              EditRead.Free
            end
          finally
            EditIntf.Free
          end
        finally
          ModIntf.Free
        end
      end;
      ....
At this point, we've found the current keyword. Now, it's time to find the DIL Search Engine (if it's already loaded), or load it directly, and enter the keyword inside the "Find" editbox.
      ....
      HWnd := FindWindowEx(0,0,'TfmDilSearch',nil);
      if HWnd <> 0 then
      begin
        SetForeGroundWindow(HWnd);
        BringWindowToTop(HWnd);
        SetFocus(HWnd);
        SendKeys(FileName)
      end
      else
        if WinExec('c:\program files\developer information library\dil.exe',
                    SW_NORMAL) > 32 then
          SendKeys(FileName)
    except
      HandleException
    end
  end {Execute};
The SendKeys is something special; that's not a standard Windows API or Delphi function. In fact, I used DIL itself to find some code that would enable me to send keystrokes to a give window, and ended up with the following code (thanks Phil):
    procedure SimulateKeyDown(Key : byte);
    begin
      keybd_event(Key, 0, 0, 0)
    end;

    procedure SimulateKeyUp(Key : byte);
    begin
      keybd_event(Key, 0, KEYEVENTF_KEYUP, 0)
    end;

    procedure SimulateKeystroke(Key : byte; extra : DWORD);
    begin
      keybd_event(Key, extra, 0, 0);
      keybd_event(Key, extra, KEYEVENTF_KEYUP, 0)
    end;

    procedure SendKeys(s: String);
    var
      i: integer;
      flag: bool;
      w: word;
    begin
      flag := not GetKeyState(VK_CAPITAL) and 1 = 0;
      if flag then SimulateKeystroke(VK_CAPITAL, 0);
      for i := 1 to Length(s) do
      begin
        w := VkKeyScan(s[i]);
        if ((HiByte(w) <> $FF) and (LoByte(w) <> $FF)) then
        begin
          if HiByte(w) and 1 = 1 then SimulateKeyDown(VK_SHIFT);
          SimulateKeystroke(LoByte(w), 0);
          if HiByte(w) and 1 = 1 then SimulateKeyUp(VK_SHIFT)
        end;
      end;
      if flag then SimulateKeystroke(VK_CAPITAL, 0);
    end;
There's even one more feature that Phil himself added after I wrote the above code: not everyone has the DIL.EXE program installed at the default location (at c:\program files\developer information library), but the location of DIL can be found in the registry. I leave this as an exercise for the readers (full source code can be found on the DIL Knowledge CD at "\Developer Information Library\DrBobDIL.dpr"


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