Search code examples
delphidelphi-xe7chmhtml-help

Launch HTML Help as Separate Process


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?


Solution

  • 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.