Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Web Development

Help File Web Links


July 1996: Programmer's Workbench

Embedding URL links in Windows Help files

Bob is director of software development for Spry, CompuServe's Internet division. He can be contacted at [email protected].


With everyone flocking to hypertext applications on the World Wide Web, it's easy to forget that Microsoft Windows users have enjoyed a simple hypertext system for years-the venerable .HLP file. Delivered with nearly every application, the help system is an indexed collection of topics that contain text and embedded hypertext links to other topics. In this article, I'll present a method for pulling these related worlds closer together by enabling standard help files to contain embedded links to WWW Uniform Resource Locators (URLs).

What makes this technique possible is a DLL implementation of a WinHelp macro that accepts a URL parameter, causing a user's Web browser to load and display that URL. The implementation I describe here (available electronically) is Win32 specific, but the concepts also apply to Win16. Consequently, when I say "Windows," I mean Windows 95 or Windows NT. I've tested the code with Microsoft's Internet Explorer 2.0 and Netscape's Navigator Gold 2.0. The only tools you'll need to build the DLL are Visual C++ 4.0, Microsoft Help Workshop, and Developer Studio (the latter two are included with VC++ 4.0).

WinHelp Macros

WinHelp source files are .RTF documents. Example 1(a), which is text with an embedded URL link, illustrates the conventions required for creating WinHelp source files. These conventions specify that the link text be double underlined and the macro call associated with the link be introduced with an exclamation point. The exclamation point and the entire macro call should be hidden text. When a user clicks on this link, WinHelp searches for a macro named "DisplayURL." To find that macro, a declaration must be placed in the other required WinHelp source file-the .HPJ file. The declaration for the DisplayURL macro appears in the [CONFIG] section of the .HPJ file; see Example 1(b). URLHelp.dll is the DLL that will implement the DisplayURL macro. Windows will search the current working directory (among other places) to find this DLL, so there is no need to specify a full path. DisplayURL is also the name of the DLL entry point that implements the macro. Finally, the "S" indicates that this macro accepts one parameter, which must be a string.

The next step is to compile the .RTF and .HPJ files into the .HLP file required to view the help contents in WinHelp. For this, I use Microsoft Help Workshop. Simply load the .HPJ file into Help Workshop and compile it. The resulting .HLP file will attempt to call DisplayURL in URLHelp.dll.

You want the DisplayURL macro to cause the user's Web browser (as recorded in the Windows Registry) to load and display the given URL. It turns out that there is a convenient standard called SDI (short for the Spyglass Software Development Interface found at http://www.spyglass.com/techspec/sdi_spec.html) to implement this. Specific DDE commands are recorded in the registry for a client to connect to the browser and to load a URL. The specific areas of the registry used are indicated in Table 1.

Note that the SDI specification only interacts with the user's browser itself. It does not address the details of the Internet connection required to give the browser access to the Web. In most cases, this connection is provided by a dialer to deal with modem issues or by a LAN that routes to the Internet. The browser will only be successful if the user's Internet connection is active when the DisplayURL macro is invoked. Many dialers and most LANs connect to the Internet automatically when a browser is started. However, you may want to remind your user to establish an Internet connection if it won't be started automatically by the browser.

You can refine the specification for DisplayURL into several steps:

1. Retrieve the required data from the Windows Registry.

2. Attempt to establish a DDE session with the browser.

3. If unsuccessful, start the browser and then establish a DDE session.

4. Send the DDE command to cause the browser to load the URL.

To create a DLL to implement DisplayURL() using Visual C++ 4.0 and Developer Studio, select New... from the File menu (in Developer Studio). Create a DLL and name it URLHelp. Add a source file to the project with New...Text File, again in the File menu. Insert the code in Listing One, and choose the File menu's Save As... option to name it URLHelp.c.

Inside the Code

The source code in URLHelp.c adds some hidden complexity because of differences among, and bugs in, various Web browsers. The set of const char declarations in lines 10 through 17 gives you a convenient place to centralize the lengthy registry key names. For a simple DDE client implemented in C, a callback function is required. Lines 19 through 31 represent the most elementary callback function that complies with the requirements of the DDE Management Library (DDEML). URLHelp doesn't require any functionality in the callback routine, so it just returns the appropriate default values for each type of callback.

GetRegSzValue() (starting on line 33) is a convenience routine that will be used to retrieve string values from the registry. Its first three parameters specify the root key, subkey, and the name of the registry value to be retrieved. The value of that key is checked to be sure it is a simple zero-terminated character string. If so, a buffer of the necessary size is allocated and populated with a copy of the key's value. It is the responsibility of the caller to free that buffer, whose address is returned in the fourth argument to GetRegSzValue(). Any errors will result in the return of a nonzero value. A zero return value indicates successful completion.

Finally, start the definition of the DisplayURL() macro implementation, beginning on line 65. The _declspec(dllexport) modifier indicates to the linker that this routine will be called from outside the DLL. The name of the routine must match the name given in the RegisterRoutine() declaration in the .HPJ file. The first call to GetRegSzValue() retrieves the DDE Item format string from the registry. This string contains an embedded %1 that must be replaced with the URL you are trying to load. For example, Microsoft's Internet Explorer will have a DDE Item format string of "%1",,1,,,,,. If you were going to load the URL http://www.ddj.com, replacing %1 with the URL would generate "http://www.ddj.com",,-1,,,,,. This command makes some sense if you refer to the SDI specification. For our purposes, it is enough that this simply works. This substitution takes place in the code between lines 87 and 94. The assembled DDE Item string ends up in the buffer pointed to by lpszURLExec.

After retrieving the remaining DDE values from the registry, you are ready to begin establishing the DDE session with the browser using DDEML calls. The first is DdeInitialize(), which sets the value of idInst for all subsequent DDEML calls. In order to attempt an actual connection to a browser, you need to create Service and Topic string handles. The Service string is typically something like "IExplore" or "NETSCAPE." The Topic string should be "WWW_OpenURL," but you really don't need to know that. We're relying on the proper configuration of the Windows Registry by the user's browser to supply these values. The process of configuring the registry values is usually referred to as "setting the default Web browser" and is often accomplished transparently by the browser.

String handles are returned by the DDEML routine DdeCreateStringHandle() and passed to DdeConnect() to establish a DDE session with the browser. If the browser is not currently running, this attempt will fail. In this case, you WinExec a command line to start the browser and attempt to establish a DDE session with it. The last step in this process is a call to DdeClientTransaction() to submit the URL to the browser for loading.

URL loading is just that easy for Microsoft's Internet Explorer 2.0-it accepts the WWW_OpenURL with the specified URL, pops to the top of the window stack, and loads the URL. Still, Internet Explorer isn't without chinks in its armor. Netscape Navigator also accepts the WWW_OpenURL and loads the specified URL. Unfortunately, it does not pop to the top of the window stack. If the Navigator window is hidden, the user gets no indication that the URL has been loaded-not exactly what you want. Careful study of Netscape's own version of the SDI specification (at http://

www.netscape.com/newsref/std/ddeapi.html) reveals that you are expected to use the WWW_Activate DDE command to cause Navigator to pop to the top of the window stack. Unfortunately, there is a bug in Navigator that prevents WWW_Activate from working in a Win32 environment. So you have to seek the last refuge of the desperate programmer-you hack.

If you were able to get the window handle for the browser that loaded the URL, you could pop it to the top of the window stack using the SetForegroundWindow() routine from the Win32 API. The SDI specification does not provide a good way of obtaining the browser's window handle, but you can use the WWW_

GetWindowInfo command to help you do that. A well-worn cheat path is to search for that window title among all top-level windows to find the associated window handle. That search is delegated to the FindWindowByTitle() routine, located in FindWin.c (Listing Two). After getting the window handle in line 157 of URLHelp.c, you pop it to the front of the window stack, clean up after yourself, and return.

This method of searching for a window title is slow and ugly. Consequently, I don't bother to do it for Internet Explorer, which pops the window to the top of the window stack by default. There is another, less noble, reason for avoiding this step when using Explorer-it will GPF if it gets a WWW_GetWindowInfo command and there is no URL displayed. That happens often since the WWW_GetWindowInfo command gets to the browser before it has had a chance to load the URL just requested in the preceding WWW_OpenURL.

DisplayURL can be used in an application's help files for a variety of purposes. Updated information, such as release notes or current technical support contacts, might be very valuable to a customer with a problem. The URL might also point to a form on the Web for registering software or getting information about additional products, add-ons, patches, or new releases. Collecting user demographics via the help system is an interesting twist. A bug-reporting or question-and-answer system that the user can access after exhausting online help is an ideal use of URL loading. In fact, additional macro support could be developed, allowing files to be transferred via HTTP or FTP. A portion of the online help system or of the software itself (of interest to only a small percentage of the users) could be left on the Web for retrieval, saving on product-distribution costs. File-transfer commands are given in the Spyglass SDI documentation for those wishing to create such a macro. Potential applications are only limited by how far you want to integrate Web access into your software.

Into the Future

Microsoft is moving Web access into the mainstream of Windows 95 development. Don't be surprised to see support for this sort of integration reach into every corner of your application in the near future. The Web is built from HTML encoded pages and parts of your applications soon will be, too. For a taste of what's to come and how it will change the way we work, consider the following undocumented feature.

If you have the Windows 95 Plus! Pack or Internet Explorer, try selecting Run... from the Start button and typing a URL where you would normally type the name of an executable. Magic! Your currently configured browser starts and loads the URL. This does not appear to work with Navigator unless the Plus! Pack or Internet Explorer has been installed, probably due to improper or inadequate Registry configuration by Netscape. If you are sure that your application will be used only by folks with the proper configuration, you can shortcut all of the work just outlined and simply write your .RTF files to call the same function that interprets the URL in the Run... box. Try adding Load the Microsoft homepage!ExecFile("http://www .microsoft.com") to your .RTF file.

There's no need to declare the ExecFile macro-it's one of the built-in macros in WinHelp. Although this will only work if you have Plus! Pack or Internet Explorer 2.0, it's still pretty nifty. There's no need for URLHelp.dll in this case-it just works. Stay tuned for Microsoft to make similar tricks easy, especially when you and your users are using Microsoft software. The Web will be a part of your applications in the near future, if it isn't already.

Example 1: (a) Text with an embedded URL link; (b) declaration for the DisplayURL macro appears in the [CONFIG] section of the .HPJ file

(a)

Load the DDJ homepage!DisplayURL("http://www.ddj.com")

(b)

[CONFIG]
RegisterRoutine("URLHelp.dll", "DisplayURL", "S")

Table 1: Windows Registry entries for URL loading.

WindowsRegistryKey                           Purpose

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\http\    Command line to WinExec to start browser.
shell\open\command   
     
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\http\    DDE item format to load a URL.
shell\open\ddeexec 

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\http\    DDE service name.
shell\open\ddeexec\Application 

HKEY_LOCAL_MACHINE\SOFTWARE\Classes\http\    DDE topic name to load a URL.
shell\open\ddeexec\Topic]   
</PRE>
<P>

<P>

<P>

<P>

<P>

<P>

<a name="006d_0244"><B>Listing One</B><PRE>

1   // URLHelp.c -- written by Bob Lord 3/96
2   
3   #include <windows.h>
4   #include <ddeml.h>
5   #include <limits.h>
6   #include <stdio.h>
7   
8   extern HWND FindWindowByTitle(LPCSTR lpszTitle);
9   
10  const char cszDDECommandKey[]     = 
11    "SOFTWARE\\classes\\http\\shell\\open\\command";
12  const char cszDDEExecKey[]        = 
13    "SOFTWARE\\classes\\http\\shell\\open\\ddeexec";
14  const char cszDDEApplicationKey[] = 
15    "SOFTWARE\\classes\\http\\shell\\open\\ddeexec\\Application";
16  const char cszDDETopicKey[]       = 
17    "SOFTWARE\\classes\\http\\shell\\open\\ddeexec\\Topic";
18  
19  HDDEDATA CALLBACK DdeCallback(UINT uType, UINT uFmt, HCONV hconv, HSZ hsz1,
20                                HSZ hsz2, HDDEDATA hdata, DWORD dwData1,
21                                DWORD dwData2) 
22  { 
23      switch (uType)
24      { 
25          case XTYP_ADVDATA: 
26              return (HDDEDATA)DDE_FACK; 
27  
28          default: 
29              return (HDDEDATA)NULL; 
30      } 
31  } 
32   
33  LONG GetRegSzValue(HKEY hKey, LPCTSTR lpSubKey, LPCTSTR lpName,
34                     LPSTR *lplpszValue)
35  {
36      LONG result;
37      HKEY hkRegKey;
38      DWORD dwType;
39      DWORD dwSize;
40      
41      result = RegOpenKeyEx(hKey, lpSubKey, 0L, KEY_READ, &hkRegKey);
42      if (result == ERROR_SUCCESS)
43      {
44          result = RegQueryValueEx(hkRegKey, lpName, NULL, &dwType, NULL,
45                                   &dwSize);
46          if (result == ERROR_SUCCESS)
47          {
48              *lplpszValue = NULL;
49              if (dwType == REG_SZ)
50                  if (dwSize != 0 && dwSize <= UINT_MAX)
51                  {   
52                      *lplpszValue = malloc(dwSize);
53                      if (*lplpszValue != NULL)
54                          result = RegQueryValueEx(hkRegKey, lpName, NULL,
55                                                 &dwType,*lplpszValue,&dwSize);
56                  }
57          }
58          RegCloseKey(hkRegKey);
59          if (*lplpszValue == NULL)
60               result = ERROR_INVALID_FUNCTION;
61      }
62      return result;
63  }
64  
65  __declspec(dllexport) void DisplayURL(LPCSTR lpszURL)
66  {
67      LPSTR lpszCommand=NULL;
68      LPSTR lpszExec=NULL;
69      LPSTR lpszURLExec=NULL;
70      LPSTR lpszApplication=NULL;
71      LPSTR lpszTopic=NULL;
72      DWORD idInst=0;
73      HSZ hszService, hszTopic, hszItem;
74      HCONV hConv;
75      HDDEDATA hData;
76      HCURSOR hcurStarting, hcurOld;
77      
78      hcurStarting = LoadCursor(NULL, IDC_APPSTARTING);
79      hcurOld = SetCursor(hcurStarting);
80  
81      GetRegSzValue(HKEY_LOCAL_MACHINE, cszDDEExecKey, NULL, &lpszExec);
82      if (lpszExec)
83      {
84          lpszURLExec = malloc(lstrlen(lpszExec) - 2 + lstrlen(lpszURL) + 1);
85          if (lpszURLExec)
86          {
87              char *pFrom = lpszExec;
88              char *pTo = lpszURLExec;
89  
90              while (*pFrom != '%' && *(pFrom + 1) != '1')
91                  *pTo++ = *pFrom++;
92              lstrcpy(pTo, lpszURL);
93              pFrom += 2;
94              lstrcat(pTo, pFrom);
95          }
96      }
97      free(lpszExec);
98  
99      GetRegSzValue(HKEY_LOCAL_MACHINE, cszDDECommandKey, NULL, &lpszCommand);
100     GetRegSzValue(HKEY_LOCAL_MACHINE, cszDDEApplicationKey, NULL,
101                   &lpszApplication);
102     GetRegSzValue(HKEY_LOCAL_MACHINE, cszDDETopicKey, NULL, &lpszTopic);
103 
104     if (lpszCommand && lpszURLExec && lpszApplication && lpszTopic)
105     {
106         if (DdeInitialize(&idInst, DdeCallback,
107           APPCLASS_STANDARD | APPCMD_CLIENTONLY, 0L) == DMLERR_NO_ERROR)
108         {
109             hszService = DdeCreateStringHandle(idInst, lpszApplication,
110                                                CP_WINANSI);
111             hszTopic = DdeCreateStringHandle(idInst, lpszTopic, CP_WINANSI);
112             if (hszService && hszTopic)
113             {
114                 hConv = DdeConnect(idInst, hszService, hszTopic, NULL);
115                 if (!hConv)
116                     WinExec(lpszCommand, SW_SHOWNORMAL);
117                 hConv = DdeConnect(idInst, hszService, hszTopic, NULL);
118                 if (hConv)
119                 {
120                     DdeClientTransaction((LPSTR)lpszURLExec,
121                       lstrlen(lpszURLExec) + 1, hConv, 0, 0, XTYP_EXECUTE,
122                       TIMEOUT_ASYNC, NULL);
123                     DdeDisconnect(hConv);
124                 }
125             }
126 
127             if (lstrcmp(lpszApplication, "IExplore") != 0)
128             {
129                 if (hszTopic)
130                     DdeFreeStringHandle(idInst, hszTopic);
131                 hszTopic = DdeCreateStringHandle(idInst, "WWW_GetWindowInfo",
132                                                  CP_WINANSI);
133                 hszItem = DdeCreateStringHandle(idInst, "-1", CP_WINANSI);
134                 if (hszService && hszTopic && hszItem)
135                 {
136                     hConv = DdeConnect(idInst, hszService, hszTopic, NULL);
137                     if (hConv)
138                     {
139                         char *pStart, *pEnd;
140                         HWND hwnd;
141 
142                         hData = DdeClientTransaction(NULL, 0, hConv, hszItem,
143                           CF_TEXT, XTYP_REQUEST, 1000, NULL);
144                         if (hData)
145                         {
146                             pStart = DdeAccessData(hData, NULL);
147                         
148                             ++pStart;
149                             while (*pStart != '\"')
150                                 ++pStart;
151                             pStart += 3;
152                             pEnd = pStart;
153                             while (*pEnd != '\"')
154                                 ++pEnd;
155                             *pEnd = 0;
156 
157                             hwnd = FindWindowByTitle(pStart);
158                             if (hwnd)
159                             {
160                                 if (IsIconic(hwnd))
161                                     ShowWindow(hwnd, SW_RESTORE);
162                                 SetForegroundWindow(hwnd);
163                             }
164 
165                             DdeUnaccessData(hData);
166                         }
167 
168                         DdeDisconnect(hConv);
169                     }
170                 }
171                 if (hszItem)
172                     DdeFreeStringHandle(idInst, hszItem);
173             }
174             if (hszTopic)
175                 DdeFreeStringHandle(idInst, hszTopic);
176             if (hszService)
177                 DdeFreeStringHandle(idInst, hszService);
178             DdeUninitialize(idInst);
179         }
180     }
181     free(lpszURLExec);
182     free(lpszTopic);
183     free(lpszApplication);
184     free(lpszCommand);
185 
186     SetCursor(hcurOld);
187 } </PRE>
<P>

<P>
<a name="006d_0245"><B>Listing Two</B><PRE>

1  // FindWin.c -- written by Bob Lord 3/96
2  
3  #include <windows.h>
4  
5  static HWND hwndFound;
6  
7  static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam)
8  {
9      char szWindowTitle[256];
10     unsigned length;
11 
12     length = lstrlen((char *)lParam) + 1;
13     GetWindowText(hwnd, szWindowTitle, length);
14     if (lstrcmp((char *)(LPCSTR)lParam, szWindowTitle) == 0)
15     {
16         hwndFound = hwnd;
17         return FALSE;
18     }
19     return TRUE;
20 }
21 
22 HWND FindWindowByTitle(LPCSTR lpszTitle)
23 {
24     hwndFound = 0;
25     EnumWindows(EnumWindowsProc, (LPARAM)lpszTitle);
26     return hwndFound;
27 }




Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.