1

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?

TomT
  • 199
  • 1
  • 1
  • 11
  • I have no problems like that in my 64 bit delphi process. If you want to run out of process then you'll need to start the process with CreateProcess. It will make the whole experience far less slick. If I were you I'd solve the problems you have in process. – David Heffernan May 19 '15 at 21:05
  • Yes, there is a way, except you'll lose the ability to open the help file to a specific topic - it will always open stand-alone to the default topic. It's loaded in the same process because if you press `F1` on one topic with context, for example `1000` and then `F1` again for context `2000`, it needs to re-use the same help display. – Jerry Dodge May 19 '15 at 21:05
  • @David I've faced this issue as well, I don't know about 64bit though. Perhaps the HTML help viewer is 32bit only, and so it works differently? – Jerry Dodge May 19 '15 at 21:06
  • @Jerry You can open a help file in a new process at a specific project. No probs there. And no, the html viewer ocx control has 32 and 64 bit versions. – David Heffernan May 19 '15 at 21:07
  • FWIW I don't use the built in html help unit because, traditionally, it has been rubbish. I intercept the Application help events and call HtmlHelp directly. It's pretty trivial really. I've tarted it all up so that my app can remember and restore help file location from session to session but that's a just a nicety. – David Heffernan May 19 '15 at 21:10
  • This conversation got me pointed at calling HTMLHelp directly -- the difficulties enumerated in my question disappear when taking this approach (as well as another problem I did not list). What is the right way to mark this as answered -- should @David answer this so that I can mark his answer correct? – TomT May 19 '15 at 21:54
  • Either you could wait for someone to answer or you can post your own answer referencing David's comment. SO doesn't let you accept your own answer immediately though, I think it gives you 2 days or so. – Jerry Dodge May 19 '15 at 21:56

2 Answers2

2

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.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Found this post useful to understand what you did: http://stackoverflow.com/questions/21912686/most-efficient-way-for-getting-notified-on-window-open. In my case I wanted to keep it extremely simple (i.e. no saving/restoring help window positions). – boggy Nov 26 '15 at 20:08
  • David (Heffernan) please can you give us details about your TFormLayoutPrefernce class? – MimmoProf Sep 30 '20 at 16:50
  • @mimmo it just persists to user profile the position and window state of a form – David Heffernan Sep 30 '20 at 19:09
0

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);
Ken White
  • 123,280
  • 14
  • 225
  • 444
TomT
  • 199
  • 1
  • 1
  • 11