Launch HTML Help as Separate Process - delphi

I am using XE7 64 and I am looking for a strategy to solve several problems I am having when displaying HTMLHelp files from within my applications (I have added the HTMLHelpViewer to my uses clause). The issues are the following: 1) Ctrl-c does not copy text from topics; 2) The helpviewer cannot be accessed when a modal dialog is active.
The source of the problems are presumably attributable to the htmlhelpviewer running in the same process as the application. Is there a way to have the built-in htmlhelpviewer launch a new process? If not, then will I need to launch HH.EXE with Createprocess?

You could launch the help file viewer as a separate process, but I think that will make controlling it even more complex. My guess is that the supplied HTML help viewer code is the root cause of your problems. I've always found that code to be extremely low quality.
I deal with that by implementing an OnHelp event handler that I attach to the Application object. This event handler calls the HtmlHelp API directly. I certainly don't experience any of the problems that you describe.
My code looks like this:
unit Help;
interface
uses
SysUtils, Classes, Windows, Messages, Forms;
procedure ShowHelp(HelpContext: THelpContext);
procedure CloseHelpWindow;
implementation
function RegisterShellHookWindow(hWnd: HWND): BOOL; stdcall; external user32;
function DeregisterShellHookWindow(hWnd: HWND): BOOL; stdcall; external user32;
procedure ShowHelp(HelpContext: THelpContext);
begin
Application.HelpCommand(HELP_CONTEXTPOPUP, HelpContext);
end;
type
THelpWindowManager = class
private
FMessageWindow: HWND;
FHelpWindow: HWND;
FHelpWindowLayoutPreference: TFormLayoutPreference;
function ApplicationHelp(Command: Word; Data: THelpEventData; var CallHelp: Boolean): Boolean;
protected
procedure WndProc(var Message: TMessage);
public
constructor Create;
destructor Destroy; override;
procedure RestorePosition;
procedure StorePosition;
procedure StorePositionAndClose;
end;
{ THelpWindowManager }
constructor THelpWindowManager.Create;
function DefaultRect: TRect;
var
i, xMargin, yMargin: Integer;
Monitor: TMonitor;
begin
Result := Rect(20, 20, 1000, 700);
for i := 0 to Screen.MonitorCount-1 do begin
Monitor := Screen.Monitors[i];
if Monitor.Primary then begin
Result := Monitor.WorkareaRect;
xMargin := Monitor.Width div 20;
yMargin := Monitor.Height div 20;
inc(Result.Left, xMargin);
dec(Result.Right, xMargin);
inc(Result.Top, yMargin);
dec(Result.Bottom, yMargin);
break;
end;
end;
end;
begin
inherited;
FHelpWindowLayoutPreference := TFormLayoutPreference.Create('Help Window', DefaultRect, False);
FMessageWindow := AllocateHWnd(WndProc);
RegisterShellHookWindow(FMessageWindow);
Application.OnHelp := ApplicationHelp;
end;
destructor THelpWindowManager.Destroy;
begin
StorePositionAndClose;
Application.OnHelp := nil;
DeregisterShellHookWindow(FMessageWindow);
DeallocateHWnd(FMessageWindow);
FreeAndNil(FHelpWindowLayoutPreference);
inherited;
end;
function THelpWindowManager.ApplicationHelp(Command: Word; Data: THelpEventData; var CallHelp: Boolean): Boolean;
var
hWndCaller: HWND;
HelpFile: string;
DoSetPosition: Boolean;
begin
CallHelp := False;
Result := True;
//argh, WinHelp commands
case Command of
HELP_CONTEXT,HELP_CONTEXTPOPUP:
begin
hWndCaller := GetDesktopWindow;
HelpFile := Application.HelpFile;
DoSetPosition := FHelpWindow=0;//i.e. if the window is not currently showing
FHelpWindow := HtmlHelp(hWndCaller, HelpFile, HH_HELP_CONTEXT, Data);
if FHelpWindow=0 then begin
//the topic may not have been found because the help file isn't there...
if FileExists(HelpFile) then begin
ReportError('Cannot find help topic for selected item.'+sLineBreak+sLineBreak+'Please report this error message to Orcina.');
end else begin
ReportErrorFmt(
'Cannot find help file (%s).'+sLineBreak+sLineBreak+'Reinstalling the program may fix this problem. '+
'If not then please contact Orcina for assistance.',
[HelpFile]
);
end;
end else begin
if DoSetPosition then begin
RestorePosition;
end;
HtmlHelp(hWndCaller, HelpFile, HH_DISPLAY_TOC, 0);//ensure that table of contents is showing
end;
end;
end;
end;
procedure THelpWindowManager.RestorePosition;
begin
if FHelpWindow<>0 then begin
RestoreWindowPosition(FHelpWindow, FHelpWindowLayoutPreference);
end;
end;
procedure THelpWindowManager.StorePosition;
begin
if FHelpWindow<>0 then begin
StoreWindowPosition(FHelpWindow, FHelpWindowLayoutPreference);
end;
end;
procedure THelpWindowManager.StorePositionAndClose;
begin
if FHelpWindow<>0 then begin
StorePosition;
SendMessage(FHelpWindow, WM_CLOSE, 0, 0);
FHelpWindow := 0;
end;
end;
var
WM_SHELLHOOKMESSAGE: UINT;
procedure THelpWindowManager.WndProc(var Message: TMessage);
begin
if (Message.Msg=WM_SHELLHOOKMESSAGE) and (Message.WParam=HSHELL_WINDOWDESTROYED) then begin
//need cast to HWND to avoid range errors
if (FHelpWindow<>0) and (HWND(Message.LParam)=FHelpWindow) then begin
StorePosition;
FHelpWindow := 0;
end;
end;
Message.Result := DefWindowProc(FMessageWindow, Message.Msg, Message.wParam, Message.lParam);
end;
var
HelpWindowManager: THelpWindowManager;
procedure CloseHelpWindow;
begin
HelpWindowManager.StorePositionAndClose;
end;
initialization
if not ModuleIsPackage then begin
Application.HelpFile := ChangeFileExt(Application.ExeName, '.chm');
WM_SHELLHOOKMESSAGE := RegisterWindowMessage('SHELLHOOK');
HelpWindowManager := THelpWindowManager.Create;
end;
finalization
FreeAndNil(HelpWindowManager);
end.
Include that unit in your project and you will be hooked up to handle help context requests. Some comments on the code:
The implementation of the OnHelp event handler is limited to just my needs. Should you need more functionality you'd have to add it yourself.
You won't have TFormLayoutPrefernce. It's one of my preference classes that manages per-user preferences. It stores away the window's bounds rectangle, and whether or not the window was maximised. This is used to ensure that the help window is shown at the same location as it was shown in the previous session. If you don't want such functionality, strip it away.
ReportError and ReportErrorFmt are my helper functions to show error dialogs. You can replace those with calls to MessageBox or similar.

Based on David's comments that he calls HtmlHelp directly and does not encounter the problems noted above, I tried that approach and it solved the problems. Example of calling HTMLHelp directly to open a topic by id:
HtmlHelp(Application.Handle,'d:\help study\MyHelp.chm',
HH_HELP_CONTEXT, 70);

Related

DELPHI "the object invoked has disconnected from its clients"

When excel runs, the data file that I process is opened. (and my program stops if I close this file)
unit UBImportFromExcel;
interface
uses
MainDM, Contnrs, DB, Classes, UGeneral, BSYSTMGlobals,
SysUtils, ADODB, BDataSet, NThermo, ComObj, Graphics, Forms, ShellAPI, Windows, Math, DBClient;
type
TBImportFromExcel = class
private
ExcelApp, ExcelWorkbook, ExcelWorksheet,
Unassigned : Variant;
iStartingRow : Integer;
iSheetCount : Integer;
iRowCount : Integer;
public
function ReadCell(iRow : Integer; iColumn : Integer) : string;
procedure Worksheet(iWorksheetIndex : integer = 1);
protected
published
property StartingRow : Integer read iStartingRow write iStartingRow;
property SheetCount : Integer read iSheetCount;
property RowCount : Integer read iRowCount;
constructor Create(sFile : string); virtual;
destructor Destroy; override;
end;
implementation
{ TBImportFromExcel }
constructor TBImportFromExcel.Create(sFile : string);
begin
ExcelApp := CreateOleObject('Excel.Application');
ExcelWorkbook := ExcelApp.WorkBooks.Open(sFile);
iSheetCount := ExcelWorkbook.Worksheets.Count;
iStartingRow := 2;
end;
destructor TBImportFromExcel.Destroy;
begin
ExcelWorkbook.Close(False);
ExcelWorksheet := Unassigned;
ExcelWorkbook := Unassigned;
ExcelApp := Unassigned;
inherited;
end;
function TBImportFromExcel.ReadCell(iRow, iColumn: Integer): string;
var
oCell : OleVariant;
begin
oCell := ExcelWorksheet.Cells[iRow, iColumn];
Result := oCell.Value;
end;
procedure TBImportFromExcel.Worksheet(iWorksheetIndex: integer);
begin
ExcelWorksheet := ExcelWorkbook.Worksheets.Item[iWorksheetIndex];
ExcelWorksheet.Activate;
ExcelWorksheet.Select;
iRowCount := ExcelWorksheet.UsedRange.Rows.Count;
end;
end.
If the excel file opened by itself is closed, I get the error in the header and my program is breaking.
sorry for my bad english. createoleobject cannot create a standalone object. why?
If I have understood you correctly, I think I may have a solution to your problem.
As well as Visible, Excel's OLE object has two other relevant properties:
DisplayAlerts which determines whether alerts produced by the Exccel application
are displayed on-screen; and
Interactive, which determines whether the Excel object (created using CreateOleObject)
can interact with an instance of Excel started by the user via the Windows gui. If it is set to False,
the Excel OLE instance is completely "insolated" from the user's desktop instance.
Both of these are documented by MS, see here
for the documentation for Interactive.
So I created a simple test app with just two buttons and two checkboxes and this code
procedure TForm1.Start;
begin
sFile := ExtractFilePath(Application.ExeName) + 'Test.Xlsx';
ExcelApplication := CreateOleObject('Excel.Application');
ExcelApplication.Interactive := cbInteractive.Checked;
ExcelApplication.Visible := cbVisible.Checked;
ExcelApplication.DisplayAlerts := ExcelApplication.Visible; // no point in displaying alerts if Excel app is not visible
ExcelWorkbook := ExcelApplication.WorkBooks.Open(sFile);
end;
procedure TForm1.Stop;
begin
ExcelWorkBook.Close; // Needed to hide Excel if it was started with Visible = True
ExcelApplication.Quit;
ExcelWorkBook := Unassigned;
ExcelApplication := Unassigned;
end;
procedure TForm1.btnStartClick(Sender: TObject);
begin
Start;
end;
procedure TForm1.btnStopClick(Sender: TObject);
begin
Stop;
end;
Setting the cbInteractive checkbox to False does indeed appear to isolate the OLE-created instance from any interactively-created one.
Anyway, have an experiment with that and see if it meets your needs.

Detecting changes in an editable TWebBrowser

I'm loading an HTML local file into TWebBrowser as follows:
procedure TForm1.FormCreate(Sender: TObject);
begin
WebBrowser1.Navigate('file:///C:\Tmp\input.html');
end;
In the TWebBrowser.OnDocumentComplete event handler I'm making it editable:
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
(WebBrowser1.Document as IHTMLDocument2).designMode := 'on';
end;
I need to be notified as soon as the user applies any changes through the TWebBrowser (i.e: he writes something...) but I can't see any OnChanged or similar event handler.
I've tried capturing WM_PASTE and WM_KEYDOWN but my code is never executed:
TMyWebBrowser = class(TWebBrowser)
public
procedure WM_Paste(var Message: TWMPaste); message WM_PASTE;
procedure WM_KeyDown(var Message: TWMKeyDown); message WM_KEYDOWN;
end;
...
procedure TMyWebBrowser.WM_Paste(var Message: TWMPaste);
begin
inherited;
ShowMessage('Paste');
end;
procedure TMyWebBrowser.WM_KEYDOWN(var Message: TWMKeyDown);
begin
inherited;
ShowMessage('KeyDown');
end;
I've also tried setting the WindowProc property but without any success.
To capture changes to the document in design mode you should use its IMarkupContainer2 interface to register an IHTMLChangeSink via RegisterForDirtyRange method. The process is pretty simple - implement IHTMLChangeSink, obtain IMarkupContainer2 from WebBrowser1.Document and call its RegisterForDirtyRange method, but there's a catch.
When you change the designMode of IHTMLDocument2, TWebBrowser control reloads the current document and it loses all registered change sinks. Therefore you should register it after putting the document in design mode. After that you receive change notifications via IHTMLChangeSink.Notify method.
But there's another catch. Since entering the design mode causes reloading of the document and that in turn causes changing the readyState property of the document to 'loading' and then consecutively to 'complete'. Your change sink will receive those readyState change notifications. Note that TWebBrowser.OnDocumentComplete is not invoked after entering design mode. That's why you should ignore any notifications until the document is fully reloaded in design mode.
Another minor complication is that RegisterForDirtyRange creates a cookie that you need to maintain in order to unregister the change sink. Since you need a class to implement IHTMLChangeSink anyway, it could also encapsulate the design mode state and change registration.
uses
System.SysUtils, SHDocVw, MSHTML;
const
DesignMode: array[Boolean] of string = ('off', 'on');
type
TWebBrowserDesign = class(TInterfacedObject, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TProc;
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
public
constructor Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
destructor Destroy; override;
end;
constructor TWebBrowserDesign.Create(WebBrowser: TWebBrowser; const AOnChange: TProc);
begin
inherited Create;
if not Assigned(WebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(WebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
FHTMLDocument2.designMode := DesignMode[True];
if Supports(WebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0
else
_Release;
end;
FOnChange := AOnChange;
end;
destructor TWebBrowserDesign.Destroy;
begin
if Assigned(FMarkupContainer2) and (FDirtyRangeCookie <> 0) then
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
if Assigned(FHTMLDocument2) then
FHTMLDocument2.designMode := DesignMode[False];
inherited;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange();
end;
Note the call to _Release after registering the change sink. This is to "prevent" markup container from holding strong reference to TWebBrowserDesign instance. That allows you to control design mode using the lifetime of TWebBrowserDesign instance:
type
TForm1 = class(TForm)
{ ... }
private
FWebBrowserDesign: IInterface;
{ ... }
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
FWebBrowserDesign := TWebBrowserDesign.Create(WebBrowser1, procedure
begin
ButtonSave.Enabled := True;
end);
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
FWebBrowserDesign := nil;
ButtonSave.Enabled := False;
end;
Alternatively you can implement change sink as a component.
type
TWebBrowserDesign = class(TComponent, IHTMLChangeSink)
private
FDirtyRangeCookie: LongWord;
FDocumentComplete: Boolean;
FHTMLDocument2: IHTMLDocument2;
FMarkupContainer2: IMarkupContainer2;
FOnChange: TNotifyEvent;
FWebBrowser: TWebBrowser;
procedure EnterDesignMode;
procedure ExitDesignMode;
function GetActive: Boolean;
procedure SetActive(const Value: Boolean);
procedure SetWebBrowser(const Value: TWebBrowser);
{ IHTMLChangeSink }
function Notify: HResult; stdcall;
protected
procedure Notification(AComponent: TComponent; Operation: TOperation); override;
public
destructor Destroy; override;
published
property Active: Boolean read GetActive write SetActive;
property OnChange: TNotifyEvent read FOnChange write FOnChange;
property WebBrowser: TWebBrowser read FWebBrowser write SetWebBrowser;
end;
destructor TWebBrowserDesign.Destroy;
begin
ExitDesignMode;
inherited;
end;
procedure TWebBrowserDesign.EnterDesignMode;
begin
if not Assigned(FWebBrowser) then
raise Exception.Create('Web browser control missing.');
if not Supports(FWebBrowser.Document, IHTMLDocument2, FHTMLDocument2) then
raise Exception.Create('No HTML document loaded.');
try
FHTMLDocument2.designMode := DesignMode[True];
if Supports(FWebBrowser.Document, IMarkupContainer2, FMarkupContainer2) then
begin
if FMarkupContainer2.RegisterForDirtyRange(Self, FDirtyRangeCookie) <> S_OK then
FDirtyRangeCookie := 0;
end;
except
ExitDesignMode;
raise;
end;
end;
procedure TWebBrowserDesign.ExitDesignMode;
begin
if Assigned(FMarkupContainer2) then
begin
if FDirtyRangeCookie <> 0 then
begin
FMarkupContainer2.UnRegisterForDirtyRange(FDirtyRangeCookie);
FDirtyRangeCookie := 0;
end;
FMarkupContainer2 := nil;
end;
if Assigned(FHTMLDocument2) then
begin
FHTMLDocument2.designMode := DesignMode[False];
if not (csDestroying in ComponentState) then
FHTMLDocument2 := nil; { causes AV when its hosting TWebBrowser component is destroying; I didn't dig into details }
end;
FDocumentComplete := False;
end;
function TWebBrowserDesign.GetActive: Boolean;
begin
Result := Assigned(FHTMLDocument2);
end;
procedure TWebBrowserDesign.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if (Operation = opRemove) and (AComponent = FWebBrowser) then
WebBrowser := nil;
end;
function TWebBrowserDesign.Notify: HResult;
begin
Result := S_OK;
if not FDocumentComplete then
FDocumentComplete := FHTMLDocument2.readyState = 'complete'
else if Assigned(FOnChange) then
FOnChange(Self);
end;
procedure TWebBrowserDesign.SetActive(const Value: Boolean);
begin
if Active <> Value then
begin
if Value then
EnterDesignMode
else
ExitDesignMode;
end;
end;
procedure TWebBrowserDesign.SetWebBrowser(const Value: TWebBrowser);
begin
if Assigned(FWebBrowser) then
begin
ExitDesignMode;
FWebBrowser.RemoveFreeNotification(Self);
end;
FWebBrowser := Value;
if Assigned(FWebBrowser) then
FWebBrowser.FreeNotification(Self);
end;
If you put such a component in a design-time package and register it within the IDE, then you'll be able to link this component with TWebBrowser and assign OnChange event handler in the form designer. Use Active property in code to enter/exit the design mode.
type
TForm1 = class(TForm)
{ ... }
WebBrowserDesign1: TWebBrowserDesign;
{ ... }
end;
procedure WebBrowserDesign1Change(Sender: TObject);
begin
ButtonSave.Enabled := True;
end;
procedure TForm1.WebBrowser1DocumentComplete(ASender: TObject;
const pDisp: IDispatch; const URL: OleVariant);
begin
{ enter design mode }
WebBrowserDesign1.Active := True;
end;
procedure TForm1.ButtonSave(Sender: TObject);
begin
{ exit design mode }
WebBrowserDesign1.Active := False;
ButtonSave.Enabled := False;
end;
NB: Similar question has been asked regarding C#/WinForms - How do I detect when the content of a WebBrowser control has changed (in design mode)?
Final note: I'm not convinced that enabling save button after a change is the best UX design. If you think that the code above is worth to achieve your goal then go ahead. This is just a proof of concept and the code hasn't been thoroughly tested. Use it at your own risk.

Delphi: Detect when a new form has been created

I'd like to detect when a new form has been created.
Now I use the Screen.ActiveFormChange event and check for new forms in Screen.CustomForms but ActiveFormChange is fired after the OnShow event of the form.
I'd like to detect the form even before OnShow was fired. Is there any way to do this without modifying the Vcl.Forms unit?
I'd like to detect all forms (also Delphi modal messages etc.) therefore inheriting all forms from a custom class is not possible (correct me if I am wrong).
Alternatively, is it possible to detect that a new component was added to some TComponent.FComponents list?
You can use the SetWindowsHookEx function to install a WH_CBT Hook, then you must implement a CBTProc callback function and finally intercept one of the possible code values for this hook. in this case you can try with HCBT_ACTIVATE or HCBT_CREATEWND.
Check this sample for the HCBT_ACTIVATE Code.
var
hhk: HHOOK;
function CBT_FUNC(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
const
ClassNameBufferSize = 1024;
var
hWindow: HWND;
RetVal : Integer;
ClassNameBuffer: Array[0..ClassNameBufferSize-1] of Char;
begin
Result := CallNextHookEx(hhk, nCode, wParam, lParam);
if nCode<0 then exit;
case nCode of
HCBT_ACTIVATE:
begin
hWindow := HWND(wParam);
if (hWindow>0) then
begin
RetVal := GetClassName(wParam, ClassNameBuffer, SizeOf(ClassNameBuffer));
if RetVal>0 then
begin
//do something
OutputDebugString(ClassNameBuffer);
end;
end;
end;
end;
end;
Procedure InitHook();
var
dwThreadID : DWORD;
begin
dwThreadID := GetCurrentThreadId;
hhk := SetWindowsHookEx(WH_CBT, #CBT_FUNC, hInstance, dwThreadID);
if hhk=0 then RaiseLastOSError;
end;
Procedure KillHook();
begin
if (hhk <> 0) then
UnhookWindowsHookEx(hhk);
end;
initialization
InitHook();
finalization
KillHook();
end.
Note : if you uses the HCBT_CREATEWND code instead you will
intercept any window created by the system not just "forms".
Track Screen.CustomFormCount in Application.OnIdle:
private
FPrevFormCount: Integer;
end;
procedure TForm1.ApplicationEvents1Idle(Sender: TObject; var Done: Boolean);
begin
if Screen.CustomFormCount > FPrevFormCount then
Caption := Caption + ' +1';
if Screen.CustomFormCount <> FPrevFormCount then
FPrevFormCount := Screen.CustomFormCount;
end;
procedure TForm1.TestButton1Click(Sender: TObject);
begin
TForm2.Create(Self).Show;
end;
procedure TForm1.TestButton2Click(Sender: TObject);
begin
ShowMessage('Also trackable?'); // Yes!
end;
procedure TForm1.TestButton3Click(Sender: TObject);
begin
OpenDialog1.Execute; // Doesn't update Screen.CustomFormCount
end;
Native dialogs managed and shown by Windows (TOpenDialog, TFontDialog, etc...) are created apart from the VCL and to track them also, you need a hacking unit. Try this one then.
Thanks to David I found a solution: The clue is to replace Screen.AddForm method with your own. The way how to do it is described in these SO answers:
How I can patch a private method of a delphi class?
How to change the implementation (detour) of an externally declared function
Patch routine call in delphi
Thanks again!

Geting a reference to a dialog window form (ShowMessage, MsgDialog etc)

Is there any event I could use so I'd catch the moment when ShowMessage appears on the screen? I'd also need pass a reference to the TForm which has shown the Message.
So far I tried OnDeactivate, but it seems, ShowMessage is not causing it...
In.NET there is methon on Application that catches every MessageBox (Application.AddFilterMessage or smth like this), I'd need something like this in delphi
What I'm trying to achive is:
I must catch the moment while a dialog window appears (or just a modal window, but it's not that comfortable). I need to do couple of instructions then. Goal of those instructions is to give me a refference to the just recieved DialogWindow so I could for example get a number of buttons that lies on it.
In modern Delphi versions, on modern Windows versions, ShowMessage results in a Windows dialog window. You can use a WH_CBT hook to catch the activation of that dialog window.
function CBTProc(nCode: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
wnd: HWND;
ClassName: string;
begin
if nCode=HCBT_ACTIVATE then
begin
wnd := wParam;
SetLength(ClassName, 256);
SetLength(ClassName, GetClassName(wnd, PChar(ClassName), Length(ClassName)));
if (ClassName='#32770') or (ClassName='TMessageForm') then
Beep;
end;
Result := CallNextHookEx(0, nCode, wParam, lParam);
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Hook: HHOOK;
begin
Hook := SetWindowsHookEx(WH_CBT, CBTProc, HInstance, GetCurrentThreadId);
if Hook=0 then
RaiseLastOSError;
try
ShowMessage('hello');
finally
if not UnhookWindowsHookEx(Hook) then
RaiseLastOSError;
end;
end;
Note that the actual window class name varies from system to system. On XP the class name will be TMessageForm because the dialog is actually a Delphi TForm. However, on Vista and later the dialog is a standard window message box dialog with window class name #32770.
I've shown this wrapped around a single call to ShowMessage, but you could install this at startup if you want to hook all message dialogs shown in your app.
You can also use an application-wide hook installed in the main form's OnCreate event (uninstalled in OnDestroy):
procedure TMainForm.FormCreate(Sender: TObject);
begin
...
Application.HookMainWindow(ApplicationHook);
end;
procedure TMainForm.FormDestroy(Sender: TObject);
begin
...
Application.UnhookMainWindow(ApplicationHook);
end;
function TMainForm.ApplicationHook(var Message: TMessage): Boolean;
var
I: Integer;
begin
Result := False;
if (Message.Msg = WM_ENABLE) and not TWMEnable(Message).Enabled then // disabling
for I := 0 to Screen.FormCount - 1 do
with Screen.Forms[I] do
if Enabled and (ClassNameIs('TMessageForm') or // ShowMessage, MessageDlg
ClassNameIs('TForm') or // InputQuery
ClassNameIs('TMyLoginDialog')) then // your own dialogs, etc.
begin
Screen.Forms[I].Position := poScreenCenter; // for example
Result := True;
Break;
end;
end;
Why not just use OnActiveFormChange?
procedure TForm3.FormCreate(Sender: TObject);
begin
Screen.OnActiveFormChange := ScreenActiveFormChange;
end;
procedure TForm3.ScreenActiveFormChange(Sender: TObject);
begin
if Screen.ActiveForm is TOKRightDlg then
Screen.ActiveForm.Caption := 'Found';
end;
procedure TForm3.Button1Click(Sender: TObject);
begin
with TOKRightDlg.Create(nil) do
try
ShowModal;
finally
Free;
end;
end;
There are TApplication.OnModalBegin and TApplicationEvents.OnModalBegin. I haven't really tried them, so I can't comment on whether you can get the owner and/or the modal form inside these events.

Start program and dock with an existing program Delphi 2010

Is there a way in Delphi 2010 to start an application using ShellExecute then dock that application inside another?
I.e. Program A written in Delphi contains 1 form. When the form is shown, Program B, written in C# is started up and docked client into Program A's form?
Paul
Yes you can do this. You need to get hold of the window handle of the main form in the other process (call EnumWindows). Then call SetParent to make that window a child of your window.
You'll likely want to modify window style, position etc. Also call WaitForInputIdle before trying to find a window handle in the new process. You've got to give the new process a chance to get started.
What you now have is a pretty queer beast. Inside one container you have two processes. Each process has its own UI thread. You can show and interact with two modal dialogs at once. There's really no end to the fun you can have with this!
EDIT
Just for a bit of fun I had a go at writing a simple Delphi app to do this. It's rather brittle and only seems to work for very basic apps. I think you could spend a long time trying to make this work well and still end up with an unsatisfactory result. If I were you I'd look for other solutions, especially as you have the source for this C# app. Surely you could expose its functionality as an ActiveX?
Anyway, for your amusement I offer the following exceedingly un-polished piece of code:
program AppHost;
uses
Windows, Messages, SysUtils, Forms, Controls, ComCtrls;
{$R *.res}
procedure ResizePage(Page: TTabSheet);
var
hwnd: Windows.HWND;
Rect: TRect;
begin
hwnd := Page.Tag;
Rect := Page.ClientRect;
MoveWindow(hwnd, Rect.Left, Rect.Top, Rect.Right-Rect.Left, Rect.Bottom-Rect.Top, True);
end;
type
PEnumData = ^TEnumData;
TEnumData = record
ProcessID: DWORD;
hwnd: HWND;
end;
function EnumWindowsProc(hwnd: HWND; lParam: LPARAM): BOOL; stdcall;
var
ProcessId: DWORD;
EnumData: PEnumData;
begin
EnumData := PEnumData(lParam);
GetWindowThreadProcessId(hwnd, ProcessId);
if EnumData.ProcessID=ProcessID then begin
EnumData.hwnd := hwnd;
Result := False;
exit;
end;
Result := True;
end;
procedure Absorb(PageControl: TPageControl; const App: string; StartupInfo: TStartupInfo);
var
Page: TTabSheet;
ProcessInformation: TProcessInformation;
EnumData: TEnumData;
begin
Page := TTabSheet.Create(PageControl);
Page.PageControl := PageControl;
Page.Caption := ChangeFileExt(ExtractFileName(App), '');
CreateProcess(PChar(App), nil, nil, nil, False, 0, nil, nil, StartupInfo, ProcessInformation);
WaitForInputIdle(ProcessInformation.hProcess, INFINITE);
EnumData.ProcessID := ProcessInformation.dwProcessId;
EnumData.hwnd := 0;
EnumWindows(#EnumWindowsProc, LPARAM(#EnumData));
Page.Tag := Integer(EnumData.hwnd);
SetParent(HWND(Page.Tag), Page.Handle);
ResizePage(Page);
end;
type
TEventProvider = class
private
FForm: TForm;
FPageControl: TPageControl;
procedure FormResize(Sender: TObject);
public
constructor Create(Form: TForm; PageControl: TPageControl);
end;
{ TEventProvider }
constructor TEventProvider.Create(Form: TForm; PageControl: TPageControl);
begin
inherited Create;
FForm := Form;
FPageControl := PageControl;
FForm.OnResize := FormResize;
end;
procedure TEventProvider.FormResize(Sender: TObject);
var
i: Integer;
begin
for i := 0 to FPageControl.PageCount-1 do begin
ResizePage(FPageControl.Pages[i]);
end;
end;
procedure Main(Form: TForm);
var
StartupInfo: TStartupInfo;
PageControl: TPageControl;
begin
Form.ClientHeight := 600;
Form.ClientWidth := 800;
Form.Caption := 'All your processes are belong to us';
PageControl := TPageControl.Create(Form);
PageControl.Parent := Form;
PageControl.Align := alClient;
StartupInfo.cb := SizeOf(StartupInfo);
GetStartupInfo(StartupInfo);
Absorb(PageControl, 'C:\Windows\Notepad.exe', StartupInfo);
Absorb(PageControl, 'C:\Program Files\CommandLine\depends.exe', StartupInfo);
TEventProvider.Create(Form, PageControl);
end;
var
Form: TForm;
begin
Application.Initialize;
Application.MainFormOnTaskbar := True;
Application.CreateForm(TForm, Form);
Main(Form);
Application.Run;
Form.Free;
end.
Yes I have access to the C# program
I need a solution that works irrespective of the language, but any program that is loaded in this way will be one that we write
GetProcessID returns 0?
All I have done for now is to produce 2 programs in Delphi, 1 calls the other
I then want to get dockapp2 to dock inside of dockapp1 and for the user to have no idea that this is a seperate program.
Having GetProcessID returning 0 is not ideal!
procedure TForm2.BitBtn1Click(Sender: TObject);
var
n: Integer;
n2: Integer;
begin
n := ShellExecute(0, 'open', PChar('c:\temp\dockapp2\dockapp2.exe'), nil, nil, SW_SHOWNORMAL);
n2:= GetProcessId(n);
Caption := IntToStr(n2);
end;

Resources