Bob Swart (aka Dr.Bob)
DIL - de Delphi Expert

In een eerdere column heb ik al eens de Developer's Information Library (DIL) van de UK-BUG besproken, en op de afgelopen CttM heeft de Delphi OplossingsCourant (namens UK-BUG) zelfs ruim 200 CD's uitgedeeld. Er zullen dus inmiddels al flink wat mensen zijn die met DIL aan de slag zijn gegaan. Speciaal voor hen ontwikkelen we nu een heuse Wizard die DIL integreerd in de Delphi IDE zelf.

Wie de DIL CD al geïnstalleerd heeft kent het nut ervan inmiddels. En ik moet zeggen dat er haast geen dag voorbij gaat zònder dat ik DIL op de een of andere manier raadpleeg. Een nadeel hierbij was het feit dat ik DIL steeds apart moest opstarten, en niet direct vanuit Delphi in de DIL Knowledge Base kon zoeken. Ik schrijf hier "was" en "moest" - verleden tijd inderdaad, want inmiddels heb ik DIL volledig geïntegreerd met de Delphi IDE (het Help menu) door middel van een kleine Expert/Wizard wrapper om DIL.

Delphi IDE Experts en Wizards
Het begrip Expert en Wizard betekent overigens voor Delphi hetzelfde. Het begon allemaal met Experts (en de TIExpert class) in Delphi, maar C++Builder veranderde de term plots in Wizards (maar bleef de interne class TIExpert noemen), en vanaf Delphi 3 is deze terminologie ongewijzigd overgenomen. We maken dus Wizards door TIExpert te implementeren. Even wennen, maar verder valt het wel mee.
Om een Wizard te maken moeten we de zojuist genoemde TIExpert class (gedefinieerd in EXPTINTF.PAS) nemen en daar een eigen class van afleiden, bijvoorbeeld TDILWizard. Dit is nodig omdat TIExpert een zgn. abstract base class is; het bevat de (interface) definitie waar een Wizard aan moet voldoen, maar niet de implementatie - die moeten we nu juist in onze TDILWizard aanbrengen door elke (abstracte) method van TIExpert te overriden en te implementeren. De class definitie van TDILWizard ziet er dan als volgt uit:

  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};
De Delphi Open Tools API kent vier verschillende soorten Wizards (Standard, Project, Form en AddIn), maar er is er maar eentje die in het Help menu komt, en dat is de Standard stijl. We moeten hiervoor de waarde "esStandard" terug laten geven door de GetStyle methode. In een toekomstig nummer van SDGN magazine zal ik terugkomen op de drie andere soorten Wizards (die iets uitgebreider zijn in hun mogelijkheden), maar in dit artikel richt ik me op de Standard Wizards die in het Help menu komen te hangen.
  function TDILWizard.GetStyle: TExpertStyle;
  begin
    Result := esStandard
  end {GetStyle};
Na de GetStyle method, moeten de we GetIDString method implementeren. Deze method moet een unieke ID string van de Wizard opleveren. Uniek wil zeggen: uniek ten opzichte van alle geïnstalleerde Wizards in Delphi op deze machine (dus het hoeft niet wereldwijd uniek te zijn, zoals een GUID, als je maar zeker weet dat je nooit twee Wizards kan hebben met dezelfde GetIDString, want dan gaat het goed mis). Als richtlijn geldt de conventie "bedrijfsnaam.wizardnaam.wizardtype", waarbij je natuurlijk ook je eigen naam kunt gebruiken als je Wizards voor persoonlijk gebruik maakt. Het toevoegen van wizardtype heeft tot voordeel dat je dan een Wizard zowel als esStandard als esAddIn aan de Delphi IDE kunt toevoegen (ook voor aenzelfde Wizard moet ieder type dus een unieke ID-string opleveren). In ons geval kunnen we TAS-AT.BobSwart.TDILWizard.esStandard als ID gebruiken:
  function TDILWizard.GetIDString: String;
  begin
    Result := 'TAS-AT.BobSwart.TDILWizard.esStandard'
  end {GetIDString};
Vervolgens moeten we een naam voor de Wizard zelf bedenken. Dit is gewoon een naam om de Wizard te kunnen identificeren tussen de andere Wizards die in de Delphi IDE geladen zijn. Ik geef hier meestal gewoon de typename zelf terug, dus TDILWizard in dit geval:
  function TDILWizard.GetName: String;
  begin
    Result := 'TDILWizard'
  end {GetName};
Sinds Delphi 3 bevat de TIExpert class ook een GetAuthor method waarmee we de auteur van de Wizard kunnen aangeven. In dit geval is dat uiteraard mijn eigen naam (en website URL):
  function TDILWizard.GetAuthor: String;
  begin
    Result := 'Bob Swart (aka Dr.Bob - www.drbob42.com)'
  end {GetAuthor};
Tot zover de "informatieve"-methods. Vanaf nu komen we wat meer ter zake, met de GetMenuText method (die overigens alleen relevant is voor esStandard Wizards). Aangezien we een Standard Wizard maken die zichtbaar zal worden onder het Help-menu, moeten we nu de tekst string teruggeven die onder het Help-menu vertoond gaat worden. We kunnen de & gebruiken als underscore (dus "UK-BUG &DIL" wordt netjes "UK-BUG DIL"):
  function TDILWizard.GetMenuText: String;
  begin
    Result := 'UK-BUG &DIL'
  end {GetMenuText};
Afgezien van de menu-tekst zelf, kunnen we ook aangeven of de menu-optie enabled of disabled moet zijn met de GetState method. Het mooie hiervan is dat deze method iedere keer opnieuw wordt aangeroepen, en we kunnen dus dynamisch bepalen of het menu enabled of disabled is (bijvoorbeeld op basis van de vrije schijfruimte kunnen we een disk/file manager Wizard schrijven). Onze UK-BUG DIL Wizard wil ik gewoon altijd aktief (enabled) hebben, dus geef ik esEnabled terug:
  function TDILWizard.GetState: TExpertState;
  begin
    Result := [esEnabled];
  end {GetState};
En dan komen we tenslotte bij het deel van de Wizard waar de daadwerkelijke code staat die uitgevoerd wordt (als de gebruiker het "UK-BUG DIL Wizard" menu item selecteerd). Hier kunnen we alles doen, inclusief bijvoorbeeld het opstarten via WinExec van de DIL executable (die normaal gesproken in c:\program files\developer information library\dil.exe staat - anders moet je hier zelf even het juiste pad opgeven):
  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};
Tot nu toe hebben we gezien hoe we een Wizard kunnen laten samenwerken met de Delphi IDE. Wat we echter nog niet gezien hebben is hoe we de Wizard ook daadwerkelijk kunnen installeren in de IDE. De makkelijkste manier om dit te doen is als zgn. DCU Wizard, waarbij de Wizard in feite als een DCU file aan een package wordt toegevoegd, die we vervolgens makkelijk in Delphi kunnen laden. We moeten hierbij een aanroep naar RegisterLibraryExpert doen, en daarbij als argument onze TDILWizard.Create meegeven (ja, dit is inderdaad een dynamische instantie van onze eigen Wizard die we hier maken een meegeven). Als alternatief voor een DCU Wizard kunnen we ervoor kiezen om de Wizard in een DLL te stoppen (die kunnen we dan ook makkelijker debuggen, trouwens). Hiervoor moeten we de function InitExpert implementeren, zoals hieronder te zien is. Merk op dat we hierbij de TExpertRegisterProc als callback function van de Delphi IDE krijgen. Vergelijkbaar met de wijze waarop de als DCU Wizard zelf de RegisterLibraryExpert kunnen aanroepen (omdat we dan al direkt meegelinkt zijn met een package in Delphi zelf):
  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;

Installatie
Voor een DCU Wizard, die dus RegisterLibraryExpert aanroept binnen de Register procedure, hoeven we alleen maar de unit toe te voegen aan een package, en dit package laden in Delphi. Da's alles. Voor een DLL Wizard die bovenstaande InitExpert function gebruikt moeten we een string value in de registry toevoegen bij HKEY_CURRENT_USER\Software\Borland\Delphi\5.0\Experts, met een willekeurige naam, maar een waarde die wijst naar de locatie van de Wizard DLL, zoals bijvoorbeeld C:\DIL\WIZARD.DLL. Het resultaat: DIL is beschikbaar in het Delphi Help menu. Altijd onder handbereik. Uiteraard hadden we DIL ook gewoon aan het Tools menu kunnen toevoegen, maar het gaat om het idee, nietwaar?

ShortCut
Toch zit het me niet helemaal lekker. Of liever gezegd: ik vind DIL in het Delphi Help menu handig, maar nog niet handig genoeg. Nu moet ik iedere keer helemaal naar het Help menu, DIL opstarten, en dan kan ik nog eens beginnen om ik te tikken waar ik naar opzoek ben. Veel liever zou ik natuurlijk een DIL Knowledge Base hebben die zich gedraagd als de on-line help: selecteer een identifier, druk op Ctrl+F1 en de on-line help komt op met de uitleg die je zoekt. Voor DIL zou ik bijvoorbeeld na Shift+F1 meteen DIL voor me willen zien, met in de "search box" de naam van de huidige identifier waar ik op sta in de editor. Dat klink haast te mooi om waar te zijn, maar ja, ik werk niet voor niks bij TAS Advanced Technologies - dat zijn we wel gewend.

Wie tot nu heeft mee zitten typen: gooi de source code maar weer weg. In plaats van een esStandard Wizard gaan we nu een esAddIn Wizard maken. Ik zal niet alle source code laten zien (die is te downloaden van mijn website), maar met name naar de interesssante onderdelen kijken. We beginnen met de Shift+F1 shortcut. Dit is niet mogelijk als esStandard Wizard (dan is het helemaal niet mogelijk om een shortcut op te geven), maar wel bij een AddIn Wizard. Bij dit soort Wizards, moeten we de constructor zelf overriden om een nieuw menu item aan te maken (in dit geval ergens in het Tools menu), en daarbij een shortcut meegeven:

  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};
Bovenstaande code zorgt ervoor dat het "UK-BUG DIL" menu item in het Tools menu erbij komt, en dat we het tevens de DIL Search Engine kunnen aktiveren met de Shift-F1 shortcut.
Nu moeten we nog zorgen dat we de DIL Search Engine meteen laten beginnen met het huidige keyword; net als de on-line help. Hiervoor moeten we nog twee dingen doen: het huidige keyword (in de editor) zien te achterhalen, en vervolgens dit keyword (als keystrokes) naar de editbox binnen het DIL Window sturen. Om met het makkelijke te beginnen: de Delphi ToolServices (beschikbaar voor iedere Wizard) geeft ons de mogelijkheid om met onze "tengels" aan de inhoud van de editor te komen. Dat gat als volgt: eerst moeten we een handle naar de huidige module ophalen (met ToolServices.GetModuleInterface), waarbij we de huidige bestandsnaam als argument moeten meegeven (en die krijgen we weer met ToolServices.GetCurrentFile). Met behulp van dem odule handle kunnen we GetEditorInterface aanroepen om een handle naar de editor zelf te krijgen, waarna GetView ons de huidige cursor positie binnen de editor view geeft. Vervolgens moeten we nog de editor handle gebruiken bij een aanroep naar CreateReader, zodat we daadwerklijk de inhoud van de editor kunnen lezen (met GetText - is iedereen er nog?).
Gezien het feit dat ik alleen maar de huidige positie van de cursor krijg, en ik natuurlijk op een willekeurige plek binnen een keyword (of identifier) kan staan, zorg ik dat ik terugloop en verder loop tot ik zowel aan het begin als aan het eind een niet-identifier teken heb staan. Alles daartussen is dus onderdeel van het keyword waarnaar ik de DIL Search Engine wil laten zoeken.
  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;
      ....
Als we hier zijn aangekomen hebben we inmiddels het juiste keyword gevonden. Het is nu zaak om te kijken of de DIL Search Engine al draait (zodat we die weer "aktief" moeten maken) of om DIL opnieuw te laden, en het gevonden keyword in de "Find" editbox te stoppen.
      ....
      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};
De SendKeys routine is een beetje bijzonder - niet een standaard Windows API of Delphi funktie. Ik moet zelfs toegeven dat ik DIL heb gebruikt om een stukje code te zoeken dat mij in staat stelt om keystrokes naar een bepaald Window te sturen. Het resultaat staat hieronder (DIL zelf heeft dus meegewerkt aan de DIL Delphi Wizard, zou je kunnen zeggen):
  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;
Tot slot nog een laatste feature die in van Phil Goulson (UK-BUG) vermeld kreeg: niet iedereen zal DIL op de default locatie geïnstalleerd hebben (c:\program files\developer information library). Gelukkig kan de juiste locatie van DIL altijd in de registry teruggevonden worden. Ik laat dit als een oefening voor de lezer. Denk eraan dat je altijd de laatste versie van de DIL Delphi Wizard kan vinden op de UK-BUG pagina van mijn website. Wie nog geen DIL heeft, kan daar ook meer informatie vinden over DIL zelf.


This webpage © 1999-2005 by webmaster drs. Robert E. Swart (aka - www.drbob42.com). All Rights Reserved.