Launch HTML Help as Separate Process

893 views Asked by At

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?

2

There are 2 answers

3
David Heffernan On BEST ANSWER

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:

  1. 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.
  2. 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.
  3. ReportError and ReportErrorFmt are my helper functions to show error dialogs. You can replace those with calls to MessageBox or similar.
0
TomT On

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);