Extending Delphi beyond Tools API
Posted: (EET/GMT+2)
Have you ever missed a feature in Delphi, but have found that you cannot do that with the Tools API? Well, I personally miss a simple feature in the Watch List: the local menu doesn't have a command to open the Evaluate/Modify dialog. Of course, I hoped that the Tools API would help me, but it didn't. So I figured out a simple way to do things, with a little help from the Tools API. This document is about that quick and dirty solution.
Because the Win32 environments are secure, you cannot simply hook the correct window, and then modify its menu as you wish. Instead, you must first find a way to get yourself into the address space of the process. Fortunately, this is not a difficult as it seems. The solution is a Tools API Expert DLL.
First, I wrote a simple Expert framework. We need to know the window class name of the Watch List. WinSight (came with Delphi) helps: TWatchWindow. Because Delphi is written with Delphi iself, ie. with VCL components, the next step was to get the name of the TPopupMenu component used for the Watch Window's local menu.
This involved a bit hacking: I fired up Resource Explorer*, opened DELPHI32.EXE, and browsed to the RCData/TWATCHWINDOW resource structure. Then I scrolled the hex dump, and at some obscure offset I found the name of the VCL component for the local menu: WatchMenu.
Now everything seemed simple: when our Expert is initialized, we would just find the Watch List, get a pointer to the WatchMenu component, and add the items. Unfortunately, that wasn't the right way to go. The first problem was to get the Watch List window handle: Experts are initialized before the Watch List even exists.
Of course, more hacking was required. I figured that if I post (not send, big difference!) a message to the Delphi's window procedure, the message is handled only after Delphi has completed the intialization. Then we only had to catch the message ourselves. But how could we hook the window procedure? Easy: We already are in the address space of Delphi!
The solution was like this:
Const
cm_DelphiInitDone = $D000;
Var
DelphiWindow : hWnd;
PreviousWndProc : TFarProc;
Function TemporaryWndProc(hWindow : hWnd; Msg,WParam,
LParam : Integer) : Integer; StdCall;
Begin
If (Msg = cm_DelphiInitDone) Then Begin
ModifyWatchWindowMenu;
SetWindowLong(DelphiWindow,gwl_WndProc,
LongInt(PreviousWndProc));
Result := 0;
End
Else
Result := CallWindowProc(PreviousWndProc,hWindow,
Msg,WParam,LParam);
End;
Procedure WorkWithWindows; { called when Expert is initialized }
Begin
DelphiWindow := FindWindow('TAppBuilder','Delphi 2.0');
If (DelphiWindow = 0) Then Begin
ShowError('FindWindow failed.');
DelphiWindow := ToolServices.GetParentHandle;
End;
If (DelphiWindow = 0) Then ShowError('DelphiWindow is zero!');
PreviousWndProc := Pointer(GetWindowLong(DelphiWindow,gwl_WndProc));
SetWindowLong(DelphiWindow,gwl_WndProc,LongInt(@TemporaryWndProc));
PostMessage(DelphiWindow,cm_DelphiInitDone,0,0);
End;
It might seem that the first call to FindWindow in WorkWithMenus is a bit risky, but actually it seems quite safe. Delphi hasn't yet loaded any projects which could change the caption. However, I added a error recovery with the ToolServices.GetParentHandle call. However, this fails if the Watch Window hasn't been created when our message is processed.
The TemporaryWndProc handles our custom message which we post. When we receive the message, we know that Delphi initialization is done and we can modify the Watch List window. Modifying the Watch List was a tricky task, as you can see:
Function ModifyWatchWindowMenu : Boolean;
Var
W : hWnd;
C : TWinControl;
M : TPopupMenu;
S : String;
A : TAtom;
Begin
Result := False;
W := FindWindow('TWatchWindow','Watch List');
S := 'Delphi'+IntToHex(GetCurrentProcessID,8);
A := GlobalFindAtom(PChar(S));
If (A <> 0) Then Begin
C := Pointer(GetProp(W,MakeIntAtom(A)));
If (C <> nil) Then Begin
With TForm(C) do Begin { typecast to form without questions }
Caption := 'Watch List - Hello from DLL!';
M := TPopupMenu(FindComponent('WatchMenu'));
If (M <> nil) Then Begin
With M do Begin
Items.Add(NewLine);
Items.Add(NewItem('E&valuate/Modify...',
ShortCut(vk_F4,[ssCtrl]),False,True,
Handler.WatchLocalMenuClick,0,
'WatchMenuEvaluateModify'));
Result := True;
End;
End
Else ShowError('Didn''t get WatchMenu!');
End;
End;
End
Else ShowError('Didn''t get atom!');
End;
(Please forgive stupid variable names.) Finding the Watch List window handle is trivial. But the more challenging task is the get the instance of the TForm responsible for the window. Fortunately, the Delphi VCL uses internally atoms, which it uses when it needs to get a pointer to the object from a window handle (just what we need to do).
But the problem is that we cannot simply get the handle of the needed atom, because the Controls unit defines them in the implementation section, which means that we cannot access them. (If you have the source code, you could simply move the variable definitions to the interface section and live with that.) Instead, we have to copy the Delphi behaviour: Delphi creates an atom named "DelphiXXXXXXXX" where the X's are replaced by the process ID of the current Delphi instance. When we have the atom name, we can get the handle to it. And with the atom handle, it is simple to get the pointer to the form with a Win32 API GetProp call.
Now that we have the actual form component, we can do almost whatever we want with it. Note that we cannot simply use constructs like OurWatchListForm.WatchMenu because we don't know the actual definition of the form. Instead, we must use the FindComponent function to get pointers to the components on the form.
For example, I took the pointer to the local menu described earlier. Then I added my new item to the menu. Note that "Handler.WatchLocalMenuClick" is our own message handler which I describe next.
I wanted the local menu command to call the Evaluate/Modify command in the Delphi's main Run menu. However, this was trickier than I thought. The Tools API has a command to get the method pointer to a main menu command which would - in theory - be simple to call (see code in comments below). However, I couldn't get it to work. Using the definition "TIMenuClickEvent = procedure (Sender: TIMenuItemIntf) of object;" simply resulted in error message "Calling conventions differ" or "Type mismatch". Why this happens still escapes me (maybe I should change the level of this document to beginner).
However, I found another solution: SendMessage. I figured that I could simply send a message to the Delphi main window simulating a menu click. Using WinSight again, I found that the Run|Evaluate/Modify code was $66. Thus:
Procedure TMyEventHandler.WatchLocalMenuClick(Sender : TObject);
Var W : hWnd;
{
Tried these, but they didn't work:
Var
MI : TIMenuItemIntf;
(* E : TNotifyEvent; *)
E : TIMenuClickEvent;
Function F : TIMenuClickEvent;
Begin
Result := MI.GetOnClick;
End;
Begin
ShowError('Hello!');
MI := ToolServices.GetMainMenu.FindMenuItem('RunEvalModItem');
(* E := MI.GetOnClick; *)
E := F;
End;
}
Begin
W := FindWindow('TAppBuilder',nil);
If (W = 0) Then ShowError('W is zero!')
Else SendMessage(W,wm_Command,$66,0); { $66 = Run|Evaluate/Modify }
End;
The problem here is that almost any project could be loaded (window title can vary), so we simply find the first window with the class TAppBuilder. This will lead to a problem if more than one copy of Delphi is loaded. Anyway, when we have the handle, simulating a click is simple.
Now, the "expert" seemed ready. However, a single problem still required solving. The problem was with the Watch List. If the Watch list is not visible when Delphi is started, the window has not been created, and our menu modification will fail. Note that if the user closes the Watch List, it is simply hidden, not destroyed. So we need a way to process the Watch List only after it has been created. How could that be done?
The solution is quite simple. If the first try to modify the window fails, we have to hook the View|Watch List command, and modify the window after that. And we can even use the previously defined TemporaryWndProc! A simple extenstion to it:
Function TemporaryWndProc(hWindow : hWnd; Msg,WParam,
LParam : Integer) : Integer; StdCall;
Begin
If (Msg = cm_DelphiInitDone) Then Begin
If ModifyWatchWindowMenu Then
SetWindowLong(DelphiWindow,gwl_WndProc,LongInt(PreviousWndProc));
Result := 0;
End
Else If ((Msg = wm_Command) And (WParam = $3E)) Then Begin
{ $3E = View|Watch List }
Result := CallWindowProc(PreviousWndProc,hWindow,Msg,WParam,LParam);
ModifyWatchWindowMenu; { must succeed }
SetWindowLong(DelphiWindow,gwl_WndProc,LongInt(PreviousWndProc));
End
Else
Result := CallWindowProc(PreviousWndProc,hWindow,Msg,WParam,LParam);
End;
Once again, a bit hacking was recuired to ge the command of the View|Watch List ($3E). Note that if the Watch List modification at cm_DelphiInitDone fails, we simply keep the window procedure hooked until we get the correct View menu command. If the first try succeeds, we unhook our window procedure, and it never gets called again.
Having got this far was refreshing. I now had a way to extend Delphi that worked, at least on my computer ;-). If you forgive me the arcane variable names, sometimes buggy operation and lack of professional error handling, as a conclusion I could say that there seems only be a very little you cannot do with a little help from the Tools API.
If you have problems getting the code of this How To to work, please mail me. The most vulnerable part of this method seems to be to get the window handles. But if it works for you, this document has reached its goal.
*) Note: Resource Explorer is a sample program which comes with Delphi. You can find it from the \Delphi 2.0\Demos\ResXplor directory.