DIL - de Delphi Expert |
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.
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.