Helpfiles

.


Calling HTML-Help

WinHelp is history... HTML Help is the future. The API for HTML Help is implemented in hhctrl.ocx (don't let the filename fool you; the ocx contains both an ActiveX control and API functions). First let's take a look at the definition:

&GLOBAL-DEFINE HH_DISPLAY_TOPIC 0
&GLOBAL-DEFINE HH_KEYWORD_LOOKUP 13
&GLOBAL-DEFINE HH_DISPLAY_TEXT_POPUP 14
 
PROCEDURE HtmlHelpA EXTERNAL "hhctrl.ocx" PERSISTENT :
   DEFINE INPUT PARAMETER  hwndCaller AS LONG.
   DEFINE INPUT PARAMETER  pszFile    AS CHARACTER.
   DEFINE INPUT PARAMETER  uCommand   AS LONG.
   DEFINE INPUT PARAMETER  dwData     AS LONG.
   DEFINE RETURN PARAMETER hwndHelp   AS LONG.
END PROCEDURE.

Notice the **PERSISTENT** keyword: it is required for this function!
hwndCaller is the HWND of the calling window. The help-window stays on top of this calling window. You can specify hwndCaller=0 if you don't like that stay-on-top behaviour.
pszFile is (depending on uCommand) the name of the helpfile, combined with the name of the topic file, combined with the name of the window class in which you want to show the topic. For example "apisite.chm::/playsounda.html>mainwin".
pszFile can also be used to specify the text to show in a popup-window (if uCommand is HH_DISPLAY_TEXT_POPUP) as demonstrated on the next page, Popup windows from HTML Help

pszFile can also be NULL for some values of uCommand. That is going to be a problem in the way I defined the function using a CHAR parameter.
uCommand specifies what the function is supposed to do; examples follow.
dwData depends on uCommand, it is most often a pointer to some structure or simply NULL.
hwndHelp (the return parameter) is the HWND of the created help window. It can be used for manipulating the help window from within your application.
The following example opens helpfile apisite.chm and navigates to a specific topic file (playsounda.html).

RUN ShowHelpTopic ( FRAME {&FRAME-NAME}:HANDLE,
                    "PlaySoundA").
 
PROCEDURE ShowHelpTopic :
  DEFINE INPUT PARAMETER hParent  AS HANDLE  NO-UNDO.
  DEFINE INPUT PARAMETER cTopic   AS CHARACTER    NO-UNDO.
 
  DEFINE VARIABLE        hWndHelp AS INTEGER NO-UNDO.
 
  IF NOT VALID-HANDLE(hParent) THEN 
     hParent = CURRENT-WINDOW:HANDLE.
 
  IF cTopic NE '' THEN 
     cTopic = "::/" + cTopic + ".html".
 
  RUN HtmlHelpA( hParent:HWND , 
                 "apisite.chm" + cTopic, 
                 {&HH_DISPLAY_TOPIC},
                 0, 
                 OUTPUT hWndHelp).
END PROCEDURE.

The next example uses keyword lookup. The input parameter is a ";" separated list of keywords. Keyword lookup is case sensitive!! If exactly one matching topic is found it will immediately be shown. If more than one match is found you will get to see a menu.
What happens if no matches are found? Well, that depends on the members in the lpHH_AKLINK structure. This example is set to open the helpfile and show the "Index" tab. It could also have been set to go to a specific topic (using the pszUrl field) or to show a messagebox (using pszMsgText and pszMsgTitle)

 
RUN ShowHelpKeyword ( FRAME {&FRAME-NAME}:HANDLE,
                      "BrowseForFolder;SHBrowseForFolder").
 
PROCEDURE ShowHelpKeyword :
  DEFINE INPUT PARAMETER hParent   AS HANDLE  NO-UNDO.
  DEFINE INPUT PARAMETER cKeywords AS CHARACTER    NO-UNDO.
 
  DEFINE VARIABLE        hWndHelp    AS INTEGER NO-UNDO.
  DEFINE VARIABLE        lpKeywords  AS MEMPTR  NO-UNDO.
  DEFINE VARIABLE        lpHH_AKLINK AS MEMPTR  NO-UNDO.
 
  IF cKeywords="" THEN RETURN.
 
  IF NOT VALID-HANDLE(hParent) THEN 
     hParent = CURRENT-WINDOW:HANDLE.
 
  /* first use HH_DISPLAY_TOPIC to initialize the help window */
  RUN ShowHelpTopic (hParent, "").
  /* should really check if this succeeded.... */
 
  /* if succeeded then use HH_KEYWORD_LOOKUP */
  SET-SIZE (lpKeywords)     = LENGTH(cKeywords) + 2.
  PUT-STRING(lpKeywords, 1) = cKeywords.
 
  SET-SIZE (lpHH_AKLINK)    = 32.
  PUT-LONG (lpHH_AKLINK, 1) = GET-SIZE(lpHH_AKLINK).
  PUT-LONG (lpHH_AKLINK, 5) = INT(FALSE). /* reserved, always FALSE */
  PUT-LONG (lpHH_AKLINK, 9) = GET-POINTER-VALUE(lpKeywords).
  PUT-LONG (lpHH_AKLINK,13) = 0.          /* pszUrl      */
  PUT-LONG (lpHH_AKLINK,17) = 0.          /* pszMsgText  */
  PUT-LONG (lpHH_AKLINK,21) = 0.          /* pszMsgTitle */
  PUT-LONG (lpHH_AKLINK,25) = 0.          /* pszWindow   */
  PUT-LONG (lpHH_AKLINK,29) = INT(TRUE).  /* fIndexOnFail */
 
  RUN HtmlHelpA( hParent:HWND , 
                 "apisite.chm", 
                 {&HH_KEYWORD_LOOKUP},
                 GET-POINTER-VALUE(lpHH_AKLINK), 
                 OUTPUT hWndHelp).
 
  SET-SIZE (lpHH_AKLINK) = 0.
  SET-SIZE (lpKeywords) = 0.
 
END PROCEDURE.

Popup windows from HTML Help

The previous topic calling HTML Help described how to call HtmlHelp to show a normal HTML topic. Here is how to use HtmlHelp to show a popup or contexthelp-window.

The declaration for the HtmlHelp function is now slightly different: pszFile is now declared as a LONG parameter because we will have to pass it the NULL value. This is the new declaration:

&GLOBAL-DEFINE HH_DISPLAY_TEXT_POPUP 14
 
PROCEDURE HtmlHelpA EXTERNAL "hhctrl.ocx" PERSISTENT :
   DEFINE INPUT PARAMETER  hwndCaller AS LONG.
   DEFINE INPUT PARAMETER  pszFile    AS LONG.
   DEFINE INPUT PARAMETER  uCommand   AS LONG.
   DEFINE INPUT PARAMETER  dwData     AS LONG.
   DEFINE RETURN PARAMETER hwndHelp   AS LONG.
END PROCEDURE.

There are at least two different ways to create a popup. The first way does not need a CHM file: you can simply pass the string you want to show. The second way uses a CHM file which contains a list of strings.
Procedure HHPopupString in the following example creates a popup containing a specified string, positioned near a specified Progress widget.

{windows.i}
 
RUN HHPopupString (FILL-IN-1:HANDLE,
                   "This is the text that will be shown in the popup window").
 
PROCEDURE HHPopupString :
 
  DEFINE INPUT PARAMETER phWidget AS WIDGET-HANDLE NO-UNDO.
  DEFINE INPUT PARAMETER pText    AS CHARACTER          NO-UNDO.
 
  DEFINE VARIABLE FontSpec AS CHARACTER.
  DEFINE VARIABLE HH_POPUP AS MEMPTR.
  DEFINE VARIABLE lpText   AS MEMPTR.
  DEFINE VARIABLE lpFont   AS MEMPTR.
  DEFINE VARIABLE lpPoint  AS MEMPTR.
  DEFINE VARIABLE retval   AS INTEGER NO-UNDO.
 
  SET-SIZE (lpText)    = LENGTH(pText) + 1.
  PUT-STRING(lpText,1) = pText.
 
  /* specify a font, format "facename[, point size[, charset[ BOLD ITALIC UNDERLINE]]]" */
  FontSpec = 'MS Sans Serif,10,,BOLD'.
  FontSpec = 'MS Sans Serif,10,,'.
  SET-SIZE (lpFont) = LENGTH(FontSpec) + 1.
  PUT-STRING(lpFont,1) = FontSpec.
 
  /* screen coordinates. There is something weird about this */
  /* I currently have only one disply monitor... should test 
     this calculation with a secondary monitor using negative coords */
  SET-SIZE (lpPoint) = 8.
  PUT-LONG (lpPoint,1) = INTEGER(phWidget:WIDTH-PIXELS / 2).
  PUT-LONG (lpPoint,5) = INTEGER(phWidget:HEIGHT-PIXELS / 2).
  RUN ClientToScreen IN hpApi(phWidget:HWND,
                              GET-POINTER-VALUE(lpPoint),
                              OUTPUT retval).
 
  /* fill the HH_POPUP structure */  
  SET-SIZE (HH_POPUP)     = 52.
  PUT-LONG (HH_POPUP, 1)  = GET-SIZE(HH_POPUP).
  PUT-LONG (HH_POPUP, 5)  = 0.  /* or hInstance for a DLL that contains string resource */
  PUT-LONG (HH_POPUP, 9)  = 0.  /* or number of string resource in the DLL              */
  PUT-LONG (HH_POPUP,13)  = GET-POINTER-VALUE(lpText).
  PUT-LONG (HH_POPUP,17)  = GET-LONG(lpPoint,1). /* X-coordinate of center */
  PUT-LONG (HH_POPUP,21)  = GET-LONG(lpPoint,5). /* Y-coordinate of top    */
  PUT-LONG (HH_POPUP,25)  =  1. /* default textcolor or RGB-VALUE(Red,Green,Blue) */
  PUT-LONG (HH_POPUP,29)  = -1. /* default bgcolor   or RGB-VALUE(Red,Green,Blue) */
  PUT-LONG (HH_POPUP,33)  = -1. /* default left margin */
  PUT-LONG (HH_POPUP,37)  = -1. /* default top margin */
  PUT-LONG (HH_POPUP,41)  = -1. /* default right margin */
  PUT-LONG (HH_POPUP,45)  = -1. /* default bottom margin */
  PUT-LONG (HH_POPUP,49)  = 0.  /* or get-pointer-value(lpFont). */
 
  RUN HtmlHelpA ( phWidget:HWND, 
                  0, 
                  {&HH_DISPLAY_TEXT_POPUP}, 
                  GET-POINTER-VALUE(HH_POPUP),
                  OUTPUT RetVal).
 
  /* free memory */
  SET-SIZE (lpText)   = 0.
  SET-SIZE (lpFont)   = 0.
  SET-SIZE (lpPoint)  = 0.
  SET-SIZE (HH_POPUP) = 0.
 
END PROCEDURE.

As you can see in you can manipulate colors, fonts, margins and coordinates but I only used values that represent the system defaults.
The next example uses a CHM file to retrieve the string. A single CHM file can contain many strings, each string has a ContextId number.

{windows.i}
 
RUN HHPopupContext (FILL-IN-1:HANDLE,
                    "apisite.chm",
                    2).
 
/* or:
   RUN HHPopupContext (FILL-IN-1:HANDLE,
                       "apisite.chm::/cshelp.txt",
                       2).
*/
 
PROCEDURE HHPopupContext :
 
  DEFINE INPUT PARAMETER phWidget   AS WIDGET-HANDLE NO-UNDO.
  DEFINE INPUT PARAMETER pFilename  AS CHARACTER          NO-UNDO.
  DEFINE INPUT PARAMETER pContextId AS INTEGER       NO-UNDO.
 
  DEFINE VARIABLE FontSpec   AS CHARACTER.
  DEFINE VARIABLE HH_POPUP   AS MEMPTR.
  DEFINE VARIABLE lpFileName AS MEMPTR.
  DEFINE VARIABLE lpFont     AS MEMPTR.
  DEFINE VARIABLE lpPoint    AS MEMPTR.
  DEFINE VARIABLE retval     AS INTEGER NO-UNDO.
 
  SET-SIZE (lpFileName)    = LENGTH(pFileName) + 1.
  PUT-STRING(lpFileName,1) = pFileName.
 
  /* specify a font, format "facename[, point size[, charset[ BOLD ITALIC UNDERLINE]]]" */
  FontSpec = 'MS Sans Serif,10,,BOLD'.
  FontSpec = 'MS Sans Serif,10,,'.
  SET-SIZE (lpFont) = LENGTH(FontSpec) + 1.
  PUT-STRING(lpFont,1) = FontSpec.
 
  /* screen coordinates. There is something weird about this. */
  /* I really wish I had two video adaptors to test negative screen coordinates */
  SET-SIZE (lpPoint) = 8.
  PUT-LONG (lpPoint,1) = INTEGER(phWidget:WIDTH-PIXELS / 2).
  PUT-LONG (lpPoint,5) = INTEGER(phWidget:HEIGHT-PIXELS / 2).
  RUN ClientToScreen IN hpApi(phWidget:HWND,
                              GET-POINTER-VALUE(lpPoint),
                              OUTPUT retval).
 
  /* fill the HH_POPUP structure */  
  SET-SIZE (HH_POPUP)     = 52.
  PUT-LONG (HH_POPUP, 1)  = GET-SIZE(HH_POPUP).
  PUT-LONG (HH_POPUP, 5)  = 0.
  PUT-LONG (HH_POPUP, 9)  = pContextId.
  PUT-LONG (HH_POPUP,13)  = 0.
  PUT-LONG (HH_POPUP,17)  = GET-LONG(lpPoint,1). /* X-coordinate of center */
  PUT-LONG (HH_POPUP,21)  = GET-LONG(lpPoint,5). /* Y-coordinate of top    */
  PUT-LONG (HH_POPUP,25)  =  1. /* default textcolor or RGB-VALUE(Red,Green,Blue) */
  PUT-LONG (HH_POPUP,29)  = -1. /* default bgcolor   or RGB-VALUE(Red,Green,Blue) */
  PUT-LONG (HH_POPUP,33)  = -1. /* default left margin */
  PUT-LONG (HH_POPUP,37)  = -1. /* default top margin */
  PUT-LONG (HH_POPUP,41)  = -1. /* default right margin */
  PUT-LONG (HH_POPUP,45)  = -1. /* default bottom margin */
  PUT-LONG (HH_POPUP,49)  = 0.  /* or get-pointer-value(lpFont). */
 
  RUN HtmlHelpA ( phWidget:HWND, 
                  GET-POINTER-VALUE(lpFileName), 
                  {&HH_DISPLAY_TEXT_POPUP}, 
                  GET-POINTER-VALUE(HH_POPUP),
                  OUTPUT RetVal).
 
  /* free memory */
  SET-SIZE (lpFileName) = 0.
  SET-SIZE (lpFont)   = 0.
  SET-SIZE (lpPoint)  = 0.
  SET-SIZE (HH_POPUP) = 0.
 
END PROCEDURE.

To build a CHM that contains popup strings you have to write a TXT file. The default name for this TXT file is "cshelp.txt" and it should by default be located in the CHM root. If it has a different name or a different location you have to specify it along with the name of the CHM file, as shown in the commented RUN statement above. You can also create several text files in the same CHM project in order to organize a large amount of strings. Finally the text file has to be added to the [TEXT POPUPS] section in the project file.
This file (cshelp.txt) can contain many strings and is formatted like this little example :

.topic 1
This is the first example of a popup topic. It can be called
using HTMLHelp with the HH_DISPLAY_TEXT_POPUP option. see page 
hhpopup.html
 
.topic 2
well this is just another example. Great isn't it?

Using context help from any button

Topic using the contexthelp-button from the title-bar explained how to create the standard windows contexthelp-button on the title bar, and how to respond to its events. This standard button is commonly used with dialog boxes but is not really suitable for (top-level) windows, especially because the contexthelp-button can only be realised when the Minimize/Maximize buttons aren't visible.
On a (top-level) window, it's probably more appropriate to create a toolbar where one of the right-most buttons invokes context-help. To implement this in Progress you can simply create a button-widget and add the following ON CHOOSE-handler to it:

{windows.i}
DEFINE VARIABLE ReturnValue AS INTEGER NO-UNDO.
 
ON CHOOSE OF Btn_CTHELP IN FRAME DEFAULT-FRAME
DO:
  RUN SendMessageA IN hpApi(c-win:HWND, 
                            274,   /* = WM_SYSCOMMAND  */
                            61824, /* = SC_CONTEXTHELP */
                            0,
                            OUTPUT ReturnValue).
END.

This message will result in the mouse-pointer changing to the ?-symbol and will also make the MW_HELP message to be sent when the user clicks on a widget.
To catch the WM_HELP message and supply appropriate help, you will have to use the MsgBlaster control and follow the instructions on page Using the contecthelp-button from the title-bar

In Progress 9 you don't need to use a MsgBlaster anymore, you can just set c-win:context-help=true. However this is a little bit tedious, but Tom Bergman found the solution:

There's a trick you must do to make it work in
AppBuilder code. Progress will only respond to the click on a widget if the
context-help attribute of the window is set to true. Since the AppBuilder won't
let you set this attribute along with min and max buttons this presents a minor
problem.
Trying to set the attribute in the main block fails because the window has
already been realized.
The "trick" is to create an include file with the following content:

  {&WINDOW-NAME}:CONTEXT-HELP = TRUE.

Add this include file as a method-library and it gets included before the window
is realized.
One of the nice features of the Progress implementation of this feature is that
you don't actually need a help file to use it. If you don't reference a help
file or context-id, progress will show you the help attribute of the widget when
you click on it using what's this help.


Using the contexthelp-button from the title-bar


This topic is 32-bit only and was made in cooperation with Paul Koufalis.

This example uses the source in procedure winstyle.p, available on page WinStyle.p.
It also uses the MsgBlaster control, free available for download.

It seems common practice in Windows 95 to create a Contexthelp button in the titlebar of a dialog, rather than a 'normal' helpbutton. When the user chooses the Contexthelp button it will stay down and the mouse pointer changes into an arrow with question mark. When the user now chooses a control, Windows will send a WM_HELP message to the top-level window, releases the Contexthelp button and reloads the usual mouse-pointer.
It's up to the application to handle the WM_HELP message.

This page explains what's going on. It seems a lot of work, at least when you have to repeat all steps for every new dialog. The good news is that it must be very easy to wrap everything up into one single ActiveX control.

Added later: We actually have created this ActiveX control: cthelp.ocx and it is attached so you can download it.
The examples on this page show how to do this for a Dialog. The sources work equally well for a window but that's a bit unusual. If you do want to use this on a window please be warned that the Contexthelp-button can not be placed until the Minimize/Maximize buttons are removed from the title bar.
So on a normal window you will prefer to call context-help from a toolbar instead the title bar. In that case, see contexthelp from any button.
Let's start with adding the Help button. Create a dialog and add this source fragment to the main block:

  DEFINE VARIABLE hStyle AS HANDLE NO-UNDO.
  RUN WinStyle.p PERSISTENT SET hStyle.
  RUN AddHelpButton IN hStyle (FRAME {&frame-name}:HWND).
  DELETE PROCEDURE hStyle.
  FRAME {&frame-name}:LOAD-MOUSE-POINTER("Arrow":U).
  RUN AddContextIDs.

Load-mouse-pointer("Arrow") is necessary for a Dialog, not for a window. I don't understand why.
The button works now and sends WM_HELP messages to the dialog. Progress normally doesn't notify you when external messages occur, so we seem to need the msgblaster control.
Drop a msgblaster on the dialog and set it up as follows:

PROCEDURE MsgBlaster.Msgblst32.MESSAGE .
  DEFINE INPUT        PARAMETER p-MsgVal    AS INTEGER NO-UNDO.
  DEFINE INPUT        PARAMETER p-wParam    AS INTEGER NO-UNDO.
  DEFINE INPUT        PARAMETER p-lParam    AS INTEGER NO-UNDO.
  DEFINE INPUT-OUTPUT PARAMETER p-lplRetVal AS INTEGER NO-UNDO.
 
  IF p-MsgVal = 83 /* = WM_HELP */ THEN
     RUN HelpContextPopup(p-lParam).
 
END PROCEDURE.
 
 
PROCEDURE initialize-controls :
  DEFINE VARIABLE hparent AS INTEGER NO-UNDO.
  DEFINE VARIABLE hc AS COM-HANDLE NO-UNDO.
 
  &IF "{&window-name}" <> "" &THEN
     /* if this is a window: */
     hparent = GetParent({&window-name}:HWND).
  &ELSE
     /* if this is a dialog: */
     hParent = FRAME {&frame-name}:HWND.
  &ENDIF
 
  hc = chMsgBlaster:Msgblst32.
  hc:MsgList(0) = 83.   /* = WM_HELP */
  hc:MsgPassage(0) = 1. /* or -1 or 0, didn't notice any difference */
  hc:hWndTarget = hparent.
  RELEASE OBJECT hc.
 
END PROCEDURE.

So now we will be notified if a WM_HELP message occurs; the message event will run the procedure HelpContextPopup. HelpContextPopup is a very general procedure e.g. not specific for one dialog, so you can safely put it in a persistent library.

PROCEDURE HelpContextPopup :
/*------------------------------------------------------------------
  Purpose:     show context help in a popup window
  Parameters:  p-lParam contains a pointer to a HELPINFO structure
  Notes:       
-------------------------------------------------------------------- */
  DEFINE INPUT PARAMETER p-lParam AS INTEGER.
 
  DEFINE VARIABLE helpinfo AS MEMPTR.
  DEFINE VARIABLE ContextType AS INTEGER.
  DEFINE VARIABLE HWND AS INTEGER.
  DEFINE VARIABLE ContextID AS INTEGER.
  DEFINE VARIABLE ReturnValue AS INTEGER.
 
  SET-SIZE(helpinfo) = 28.
  SET-POINTER-VALUE(helpinfo) = p-lParam.
  ContextID = GET-LONG(helpinfo, 17).
 
  /* ContextID=0 will result in the standard text
     "No help topic is associated with this item" 
     You might want to test that and return or
     replace 0 by a different (translated) ContextID
  */
  /* Was WM_HELP called for HELPINFO_WINDOW or for HELPINFO_MENUITEM ? 
     We don't want to support help for MENUITEM right now */
 
  ContextType = GET-LONG(helpinfo, 5).
  IF ContextType<>1 /* 1=HELPINFO_WINDOW */ THEN DO:
     SET-SIZE(helpinfo)=0.
     RETURN.
  END.
 
  HWND = GET-LONG(helpinfo,13).
  RUN WinHelp{&A} IN hpApi (HWND, 
                            "myhelp.hlp", 
                            8,    /* 8 = HELP_CONTEXTPOPUP */
                            ContextID,
                            OUTPUT ReturnValue).
 
  SET-SIZE(helpinfo) = 0.
 
END PROCEDURE.

So WinHelp will be called and shows a certain ContextID from "myhelp.hlp" in a little yellow popup-window, aligned to the control where your mouse was on. Great. But what ContextID, you might wonder?

ContextID's

When you write a helpfile each topic will be assigned a unique integer identifier: the ContextID. The ContextHelp button feature requires that you write a lot of topics: at worst one for each widget, at best one topic for each group if widgets (like one for each (smart)frame).
The ContextID's supplied by your help authoring tool must be mapped to the widgets or widgetgroups in the program. From the main block we already called procedure AddContextIDs. Here's the implementation:

PROCEDURE AddContextIDs :
/*-------------------------------------------------------------------
  Purpose:     map widgets to ContextID's
  Parameters:  
--------------------------------------------------------------------- */
  DEFINE VARIABLE retval AS INTEGER NO-UNDO.
  RUN SetWindowContextHelpId IN hpApi(FRAME {&frame-name}:HWND, 101, OUTPUT retval).
  RUN SetWindowContextHelpId IN hpApi(FRAME FRAME-A:HWND, 102, OUTPUT retval).
  RUN SetWindowContextHelpId IN hpApi({&window-name}:HWND, 103, OUTPUT retval).
 
  DO WITH FRAME {&frame-name}:
     RUN SetWindowContextHelpId IN hpAPi(button-1:HWND,  143, OUTPUT retval).
     RUN SetWindowContextHelpId IN hpApi(button-2:HWND,  142, OUTPUT retval).
     RUN SetWindowContextHelpId IN hpApi(fill-in-1:HWND, 144, OUTPUT retval).
     RUN SetWindowContextHelpId IN hpApi(fill-in-2:HWND, 145, OUTPUT retval).
  END.
 
END PROCEDURE.

Of course these are all widgets your program probably don't have, it is just an example. The important part is that if you assign a ContextID to a window, all frames in that window will inherit that ContextID. If you assign a ContextID to a frame, all widgets in that frame will inherit that ContextID. And so on.
That makes it possible and convenient to assign ContextID's inside the sources of SmartObjects.

Attachments

cthelp.zip : cthelp.ocx