Wednesday, January 20, 2010

My First HTML Help Builder Add-In

Although I’ve used West Wind HTML Help Builder to create HTML Help (CHM) files for my applications for more than 10 years, and have done lots of advanced things such as supporting dynamic text and generating help projects programmatically, I haven’t created an add-in for it. HTML Help Builder supports add-ins using a simple mechanism: once you’ve registered an add-in, it appears in the Tools, Add-Ins menu, ready to run whenever you need it.

The add-in I created does two things:

  • Fixes the icon for the INDEX topic. Every HTML Help Builder project has one topic of type INDEX, and I use that as the “welcome to this help file” topic. Unfortunately, when it builds the CHM, HTML Help Builder uses the wrong icon for that topic. I always had to manually run HTML Help Workshop, edit the icon for that topic (from “11” to “auto”), and rebuild the CHM file. My add-in does that automatically.
  • Turns on searching for the HTML content. In addition to providing CHM files, we post our help files as HTML on our Web site (for example, the Stonefield Query SDK help file), both so Google can index it and so we can provide links to help topics in support messages without having to say “open the help file, open the “How To” heading, then navigate to the “Whatever” topic”. HTML Help Builder recently added a search function to the HTML files it generates, but the generation of that function is turned off by default and I often forget to turn it on. My add-in automatically changes one of the generated files to enable search.

Add-ins can be VFP code (PRG or APP/EXE), a .Net assembly, or a COM object. You register an add-in using the Tools, Add-In Manager function. This is the only cumbersome part of the process: because the add-ins registry table (AddIns.DBF) is stored in the program folder, under Vista or Windows 7 it’s read-only, so you have to launch HTML Help Builder as administrator. Perhaps in a future release, author Rick Strahl will move this file to a writable folder so this isn’t necessary.

image

In my case, I created a class named FixHelp in FixHelp.PRG, and put the code for the add-in into the Activate method. Here’s the code for the class (thanks to Chris Wolf for handling the 64-bit stuff). It should be easy enough to follow.

#define CSIDL_PROGRAM_FILES 0x0026
#define HKEY_LOCAL_MACHINE -2147483646

define class FixHelp as custom
function Activate(toHelpForm)
local loHelp, ;
lcProjectFile, ;
lcPath, ;
lcFile, ;
lcText, ;
lcProgramFiles, ;
lcRegVCX, ;
loRegistry, ;
lcKey, ;
llGotPath, ;
lcLogFile

* Get a reference to the help object, then figure out the path for the
* current project.

loHelp = toHelpForm.oHelp
lcProjectFile = loHelp.cFileName
lcPath = addbs(justpath(lcProjectFile))

* Turn on searching in case it wasn't turned on when the files were
* generated.

lcFile = lcPath + 'index2.htm'
lcText = filetostr(lcFile)
lcText = strtran(lcText, 'var AllowSearch = false;', ;
'var AllowSearch = true;')
strtofile(lcText, lcFile)

* Remove the image number for the root node so it defaults to "auto".

lcFile = forceext(lcProjectFile, 'hhc')
lcText = filetostr(lcFile)
lcText = strtran(lcText, '' + ;
chr(13) + chr(10), '', 1, 1)
strtofile(lcText, lcFile)

* Find the location of HTML Help Workshop. Try the 32-bit registry key
* first. If that doesn't work, try the 64-bit key.

lcProgramFiles = This.GetSpecialFolder(CSIDL_PROGRAM_FILES)
lcRegVCX = addbs(lcProgramFiles) + ;
'Microsoft Visual FoxPro 9\FFC\Registry.vcx'
loRegistry = newobject('Registry', lcRegVCX)
lcPath = ''
lcKey = '\Microsoft\Windows\CurrentVersion\App Paths\hhw.exe'
llGotPath = loRegistry.GetRegKey('Path', @lcPath, ;
'SOFTWARE' + lcKey, HKEY_LOCAL_MACHINE) = 0
if not llGotPath
llGotPath = loRegistry.GetRegKey('Path', @lcPath, ;
'\SOFTWARE\Wow6432Node' + lcKey, HKEY_LOCAL_MACHINE) = 0
endif not llGotPath

* Compile the CHM file if we found it. Log the results.

if llGotPath
lcPath = This.ShortPath(forcepath('hhc.exe', lcPath))
lcProjectFile = '"' + forceext(lcProjectFile, 'hhp') + '"'
lcLogFile = '"' + addbs(justpath(lcProjectFile)) + 'log.txt"'
erase (lcLogFile)
run &lcPath &lcProjectFile > &lcLogFile
if file(lcLogFile)
declare integer ShellExecute in SHELL32.DLL ;
integer nWinHandle, string cOperation, string cFileName, ;
string cParameters, string cDirectory, integer nShowWindow
ShellExecute(0, 'Open', lcLogFile, '', '', 1)
else
erase (lcLogFile)
endif file(lcLogFile)
else
messagebox('Cannot locate HTML Help Workshop')
endif llGotPath
return .T.
endfunc

* Get the short (8.3) path for the specified path.

function ShortPath(tcPath)
local lcPath, ;
lnLength, ;
lcBuffer, ;
lnResult
declare integer GetShortPathName in Win32API ;
string @lpszLongPath, string @lpszShortPath, integer cchBuffer
lcPath = tcPath
lnLength = 260
lcBuffer = space(lnLength)
lnResult = GetShortPathName(@lcPath, @lcBuffer, lnLength)
return iif(lnResult = 0, '', left(lcBuffer, lnResult))
endfunc

* Get the location of the specified "special" folder.

function GetSpecialFolder(tnFolder)
local lcSpecialFolderPath, ;
lcPath
lcSpecialFolderPath = space(255)
declare SHGetSpecialFolderPath in shell32.dll ;
long hwndOwner, string @cSpecialFolderPath, long nWhichFolder
SHGetSpecialFolderPath(0, @lcSpecialFolderPath, tnFolder)
lcPath = alltrim(lcSpecialFolderPath)
return lcPath
endfunc
enddefine


Now when I want to generate a help file, I click Build Help, select the “Don’t build Help file” option (since my add-in will generate the CHM, there’s no need to do it twice), and let it run. I then choose Tools, Add-Ins, Generate Help File (my add-in) to tweak the generated files and build the CHM. There’s a couple of less manual tasks for my build process.