Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
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).
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.
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).
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).
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
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).
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"