Please support our sponsor:
VB3, VB4 16/32, VB5, VB6
Level: Intermediate
Practical VB Help Connection Solutions
With a nod to C++ programmers
David E. Liske, 3910 Paragon Road, Tipton, Michigan, USA 48439
(517) 431-3813
delmar@tc3net.com
http://users.tc3net.com/delmar/
One of the biggest problems I've encountered in developing
Visual Basic applications is finding complete, valid information on the basics of
connecting to Help files. The information I've found has been quite confusing, and is
conflicting in most instances. After quite a bit of research, and a whole lot of some seriously
frustrating trial and error, I've settled on a group of functions which work quite well.
I use the HelpBreeze version 2.0 Help Compiler. Why? Well, it's one of
those things that since it's the first one I ever used, it's the one I always go back to.
(This is the same reason why I still use WordPerfect 6.0 for a lot of things.) HelpBreeze
also supports Windows 95/98 emulation when running under Windows 3.x, which is a big plus
when working on client/server systems where installed copies of Win16 and Win32 are about
50/50.
The method I use doesn't use the CommonDialog control found in Visual
Basic. Rather, it's in the form of a single module, which contains the most commonly-used
WinHelp DLL calling conventions for both 16 and 32-bit applications. A portion of this
module also contains the path strings for any number of help files an application may
need. When the module is used as a template, this particular function is the only area
where changes will need to be made. The end result is the ability to call WinHelp using
plain language, with a minimum of arguments to be passed, using identically-named
subroutines for both 16 and 32 bit applications.
Getting Started - The Declarations Section
The syntax for calling WinHelp is the same for either
the 16 or 32-bit DLL's:
BOOL WinHelp(HWND hwnd, LPCTSTR lpszHelpFile, UINT fuCommand, DWORD
dwData)
This is actually the C++ version, copied from the Microsoft
Help Authoring Guide. Let's boil it down a little so we can use it in Visual Basic. We
know the function will return True if it succeeds and False if it doesn't. VB knows this
as well, so we drop the BOOL keyword. Next, hwnd is the handle of the window
requesting Help. Type this into a VB module window and it will change to hWnd
automatically. The string lpszHelpFile is the path of the help file we're trying to
use, fuCommand is the type of help we're requesting, and dwData can be any
number of things depending on which fuCommand we're using.
WinHelp is contained in the file user.dll for win16 and user32.dll
for win32. The calling conventions differ if you're developing for 16-bit Windows vs.
32-bit, so we'll use conditional compilation to cover both possibilities.
#If Win16 Then
Declare Function WinHelp Lib "user" _
(ByVal hWnd As Integer, _
ByVal lpHelpFile As String, _
ByVal wCommand As Integer, _
ByVal dwData As Long) As Integer
Declare Function WinHelpTopic Lib "user" Alias "WinHelp" _
(ByVal hWnd As Integer, _
ByVal lpHelpFile As String, _
ByVal wCommand As Integer, _
ByVal dwData As String) As Integer
#Else
Declare Function WinHelp Lib "user32" Alias "WinHelpA" _
(ByVal hWnd As Long, _
ByVal lpHelpFile As String, _
ByVal wCommand As Long, _
ByVal dwData As Long) As Long
Declare Function WinHelpTopic Lib "user32" Alias "WinHelpA" _
(ByVal hWnd As Long, _
ByVal lpHelpFile As String, _
ByVal wCommand As Long, _
ByVal dwData As String) As Long
#End If
Note that there are a couple of differences between the
16-bit and 32-bit functions. The 16-bit version uses data type integer for a couple
of things while the 32-bit uses long. This is a minor difference compared to the
'A' suffix for WnHelp used in the 32-bit version.
Actually there are two possibilities for this suffix. The 'A' suffix
specifies the ANSI coding for Windows 95/98 while a 'W' suffix is for the unicode of
Windows NT. I found that Windows 95/98 could not access help without the 'A' suffix at
all, which differs from the first DECLARE statement for 16-bit. The same may also be true
for NT and its 'W' suffix but I haven't had the opportunity to find out.
Note there two different functions for using data vs. strings in the dwData
section. In some instances we'll need strings to reference a keyword or topic so the
WinHelpTopic function is necessary. I wouldn't have done this if the Variant or Any
data types would have worked. Since they didn't, this was the next best thing.
This is all the functionality we need to get going. We could actually
leave the rest of the module blank and call the DLL from anywhere using the WinHelp
statement itself. But then we would have to deal with the syntax of the WinHelp statement
every time, along with declaring the Help file path in every instance. Getting rid of this
excess code is quite simple. Using it is even simpler.
Declaring The Help Files
We can use the Select
Case
End Select
statement to return the paths of multiple Help files from anywhere in our application:
Public Function SetHelpStrings(ByVal intSelHelpFile As Integer) As String
' Set the string variable to
' include the application path
Select Case intSelHelpFile
Case 1
SetHelpStrings = App.Path & "\Help\MyHelp.hlp"
Case 2
' Place other Help file paths in successive Case statements
End Select
End Function
All we need to do is specify which Case statement we
want to go to and the correct Help file path comes back. We'll call this function from the
subroutines we're about to discuss.
Displaying The Help Contents Tab
Displaying the Help Contents tab in the Help dialog box
would appear to be a simple task. However, the documentation is laced with all kinds of
"gotchas" that can cause you to wonder why you decided to attempt the procedure
in the first place! For instance, the on-line help for VB5 under HelpCommand states that
"cdlHelpContents (&H3)
displays the Help contents topic as defined by the
Contents option in the [OPTION] section of the *.hpj file". This would lead one to
believe that using HelpCommand.cdlHelpContents is all that is necessary to instantly
display the *.cnt file in the Help dialog box. Unfortunately, this doesn't work as easily
as it sounds. Even if you have a *.cnt file specified in the [OPTION] section of the *.hpj
file, when you use cdlHelpContents the contents topic from the help file (generally the
first topic in the file) is displayed in the Help window, and the Help dialog isn't
displayed at all. The Contents tab can then be reached by clicking the Contents button.
This is can be rather annoying.
There is a way to display the Contents tab of the Help dialog from a
Visual Basic application the way it occurs in shrink-wrapped Windows 95/98/NT
applications. We'll directly access the WinHelp API, avoiding the use of the CommonDialog
control altogether.
The HELP_CONTENTS constant "&H3&", with which some
programmers are familiar, is intended for use in C++. This function must also be declared
prior to use in that language, which will confuse some C++ programmers making the
transition to Visual Basic. In Visual Basic we can use a constant directly, without having
to declare it, to accomplish the desired task.
Something I fought with for some time was a basic problem with the Help
dialog. Using the HELP_PARTIALKEY command I would open the Index tab of the Help dialog,
find a specific topic, close help, then attempt to open the Contents tab with
HELP_CONTENTS only to see the Index tab instead. Something, somewhere, was remembering
which tab of the Help dialog had been selected most recently.
Directly related to the *.hlp file is a hidden *.gid file. This is a
configuration file which is created the first time a help file is opened or updated. I had
noticed that recently compiled help files don't have a *.gid file, but it's there after
the first time the help file is opened. Also, I disovered that if the user deletes a *.gid
file, WinHelp creates a new one the next time the user opens the Help file. (Yes, I did
this on purpose. Sometimes taking that kind of risk is the only way to find out how
something works.) According to the Microsoft Help Authoring Guide the following
information is contained in the *.gid file:
- Binary representation of the contents (*.cnt) file, including jumps and
commands, after it has been processed. Only topics that were found during processing are
stored.
- The filenames and titles of all Help files included in the contents file.
- Keywords from other Help files (if :Index statements were used in the contents file).
- List of which files have full-text search index (*.fts) files.
- The size and location of Help windows and dialog boxes.
Notice the last statement. This should probably read "The size and
location of Help windows and dialog boxes, including which tab was last selected in the
Help dialog box".
WinHelp creates a *.gid file for each Help file that
does not have a contents file, or for each contents file even if that contents file
is for a family of Help files.
This presents a whole series of questions. With all of Microsoft's talk
of how the registry was to replace *.ini and other configuration files, why does the *.gid
file even exist in Windows 95/98? Would it make the size of the system any larger to write
this information to the registry? Sure, the *.gid file is binary which may make it a bit
smaller. It also makes it much more difficult to change, which may be desirable from a
security standpoint. But you can open a *.cnt (contents) file in Microsoft Word and edit
it directly, which susequently changes the contents of the *.gid file. So at first glance
the existence of the *gid file in Windows 95/98 doesn't seem to make sense.
For an indirect answer, The Microsoft Help Authoring Guide makes
these statements: "If multiple users try to build a .gid file on a network server at
the same time, only the first attempt will actually create the .gid file. Other users will
see an animation window until the first attempt to create a .gid file succeeds." In a
client/server application, where the Help file is located on the server, we want this type
of information to be in a common location, not on everyone's machine, just like the Help
file itself. Writing this kind of configuration to the registry just isn't practical when
you look at the bigger picture. There are numerous other examples similar to this one for
other network-based applications that don't use the registry to store configuration
settings. A client-accessed, server-based registry may not be too far in the future
Fortunately there's an easy way to open directly to the Contents tab.
Undocumented in the VB Help files is the HELP_TAB constant of 15 (&HF). In fact, the Microsoft
Help Authoring Guide states that this constant must be included in a C++ header file
since it's not included in the C++ programmer's windows.h file. In Visual Basic this
constant doesn't need to be declared, we can just use it. Used with the value of 0 in the dwData
position, the Contents tab pops right up in Win32:
Public Sub ShowHelpContents(ByVal intHelpFile As Integer)
#If Win16 Then
' Open the help file using the
' 16-bit HELP_CONTENTS constant (3)
WinHelp hWnd, SetHelpStrings(intHelpFile), 3, 0
#Else
' Open the help file using the
' 32-bit HELP_TAB constant (15)
WinHelp hWnd, SetHelpStrings(intHelpFile), 15, 0
#End If
End Sub
Notice that we still use the value of 3 for fuCommand
in the 16-bit section. Unfortunately, there is no equivalent for the Contents tab in a
system that doesn't know what a Contents tab is. We just have to put up with the Help
file's Contents topic being displayed (normally the first topic in the file). One method
I've used occasionally in a system that emulates Win32 help is to provide a graphic on the
contents topic for a 16-bit Help file. I then have the Contents() macro start when this
topic is displayed (this must be specified in the [CONFIG] section of the Help file's
*.hpj project file). The result is a kind of "splash screen", where the graphic
is displayed first (I generally design it so it duplicates the image on the application's
own splash screen) and then the emulated Help dialog appears, with the Contents tab
showing.
The code under the Help|Contents menu selection will look like
this:
Private Sub mnuContents_Click()
ShowHelpContents 1
End Sub
The argument specifies the Help file to be called. This
value is then passed to the SetHelpStrings function to return the Help file path to the
ShowHelpContents subroutine. This is all it takes to display the Contents tab from
anywhere in your program.
Why not use the ShowHelp method of the CommonDialog control? It's
definitely not as clean. The CommonDialog control translates the HelpCommand syntax and
then accesses the WinHelp API as required. Also, since we can call the same function in
one line, why have the overhead of using the control? Besides, in using the WinHelp API
ourselves we can have a single module to do all the work for us instead of placing the
CommonDialog control on every form we want to access our Help files from.
Displaying The Help Index Tab
Displaying the Index tab of the Help dialog box is even
simpler. WinHelp gives us this function through the HELP_PARTIALKEY constant (261):
Public Sub ShowHelpIndex(ByVal intHelpFile As Integer)
' Open the help file using the
' HELP_PARTIALKEY constant (261)
WinHelpTopic hWnd, SetHelpStrings(intHelpFile), 261, ""
End Sub
Here we're using the WinHelpTopic command, passing an empty string to
the subroutine as our search criteria . If we were to pass a topic title as a string,
WinHelp would open the topic. Passing a word or two without giving a complete topic title
will open the Index tab of the Help dialog with the word(s) in the text box.
Some 32-bit programmers may be familiar with the HELP_FINDER constant
of 11 (&HB). While this command does open the Help dialog to the Index tab, almost
every time I tried it a different single Swedish character would appear in the text box!
Also, using the HELP_TAB command with a different number in the dwData spot always
opened to the Contents tab, so this isn't a solution either. HELP_PARTIALKEY,with an empty
string, works quite well.
The code under the Help|Search for help on
menu selection
will look like this:
Private Sub mnuSearch_Click()
ShowHelpIndex 1
End Sub
Displaying A Particular Help Topic
There are two other methods besides HELP_PARTIALKEY for
displaying any particular help topic from a command button, highlighted text, or a dialog
box. One is to use a keyword, the other is to use a Context Integer. We'll first look at a
situation using a command button.
You've developed a tabbed dialog box for the computerized cash register
application you're writing for your uncle's deli, complete with the touch screen. (He saw
one at the bar he goes to when your aunt's out of town at the monthly quilting bee
conference and he wants one just like it.) Your uncle's not what they'd call
"computer literate" so the help file you've written covers every possible
question.
The dialog box you've developed for the financial portion has three
tabs on an SSTab control. Also, on the background of the form, between the SSTab control
and the bottom of the form itself, are four buttons; Help, Cancel, Apply,
and OK.
The best way to treat the Help file portion of this is to write three
separate topics, one for each of the tabs on the SSTab control. The Help button then goes
to the correct topic by checking which tab is selected through the Select
Case
command.
The Select...Case statement for the Help button on the tabbed dialog
box passes the desired Help Topic keyword to a subroutine in the Help module:
Private Sub btnHelp_Click()
Select Case tabCheckThisOut.Tab
Case 0
ShowHelpTopic 1, "DailyTotals"
Case 1
ShowHelpTopic 1, "Register Back-up"
Case 2
ShowHelpTopic 1, "Inventory"
End Select
End Sub
It wouldn't be all that difficult to have the same
button call different topics from entirely separate Help files depending on which tab on
the SSTab control was selected.
The subroutine in the Help module accepts the arguments from the above
subroutine and then uses the arguments to open the desired topic using the HELP_KEY
constant "&H101":
Public Sub ShowHelpTopic(ByVal intHelpFile As Integer, strTopic As String)
' Open the help file using the
' HELP_KEY constant (&H101)
WinHelpTopic hWnd, SetHelpStrings(intHelpFile), &H101, strTopic
End Sub
I used the hex constant &H101 in this
subroutine since its decimal equivalent apparently did nothing. Double-checking the
equivalent was no help at all.
This type of function can also be helpful for the very kind of keyword
sensitive help we see in Visual Basic. For instance, you can allow the user to access a
help topic for a highlighted keyword in a RichTextBox by passing the string you get from
the SelText property of the control to the module that calls the Help file.
One situation that doesn't use the WinHelp declaration at all is that
of displaying a help topic while a message box is being displayed. Take a look at the
following code for a message box:
Msg = "Please rescan the barcode."
Style = vbOKOnly + vbExclamation
Title = "Barcode Scan Error"
Help = SetHelpStrings(1)
CTxt = 1009
Response = MsgBox(Msg, Style, Title, Help, CTxt)
Providing the path for the Help file from the
SetHelpStrings function, and a specific Context Integer for a given topic, enables the F1
key while the message box is displayed. This way the user can get extra help on the
situation that caused the message box to be displayed.
Going directly to a topic via its Context Integer takes very little
code:
Public Sub ShowHelpContextID(ByVal intHelpFile As Integer, lngContextID As Long)
' Open the help file using the
' HELP_CONTEXT constant (1)
WinHelp hWnd, SetHelpStrings(intHelpFile), 1, lngContextID
End Sub
Calling this subroutine involves specifying the Help
file to be called and the Context Integer of the topic to be displayed:
Private Sub btnHelp_Click()
ShowHelpTopic 1, 1000
End Sub
There's one "gotcha" when dealing with Context
Integers. In order for an application to find a Context Integer, there must be an
"include" file generated for your Help file. Include files come from the C/C++
language, so the default name for this file is cntxmap.h. For Visual Basic, the default
name is cntxmap.txt. If you so desire, the Help compiler can generate both types, but you
only need one or the other. You can change the label to whatever you want in the Help
compiler. Strangely enough, the option to generate this file in HelpBreeze version 2.0 is
turned off by default! Other Help compilers may work the same way, so be aware of this.
If you do have the compiler generate the cntxmap.txt file, make sure
you distribute the file with your application. The compiler may also allow you to change
the name of this file prior to compiling (don't change it afterwards or WinHelp won't be
able to find it!) Keep track of what you've renamed it to so you don't accidentally delete
it some time later.
Displaying A Particular Help Topic In A Secondary
Window
Another use for the Context Integer is in displaying
Help topics in a secondary window. Note that this subroutine also has the ContextID passed
to it:
Public Sub ShowHelpSecondary(ByVal intHelpFile As Integer, lngContextID As Long)
' Open the help file in a secondary
' window named "subMain" using the
' HELP_CONTEXT constant (1)
strHelpWindow = SetHelpStrings(intHelpFile) & ">subMain"
WinHelp hWnd, strHelpWindow, 1, lngContextID
End Sub
The secondary windows are windows you design in the Help compiler. The
"Example" windows in the VB help file are examples of secondary windows. The
above subroutine allows you to call such a window from within your application. Calling
this procedure is the same as calling ShowHelpContextID.
Conclusion
These are just some of the things you can accomplish through the WinHelp
API. It's much cleaner than using the common dialog control, and runs faster as well.
Change older common dialog code to the API, and you'll be surprised at the performance
difference.
|