Extending Off-the-Shelf Windows Applications with Macros and DLLs

Bill Eisner

A new generation of software designed for sophisticated computing environments with graphical user interfaces offers a remarkable degree of customization. Products such as Microsoft Excel, Samna Ami ProfessionalÔ, and Microsoft Word for Windows allow you to develop personalized shortcuts, alter menus, and change default characteristics.

In-house developers and consultants can often satisfy the needs of end-users by tailoring and extending off-the-shelf products such as these, rather than developing a software solution from scratch. Even when custom software is needed, a high degree of integration between it and off-the-shelf products is often required. A new generation of highly alterable and extendable software simplifies

this task.

Although the techniques discussed here are illustrated using Microsoft Word for Windows (hereafter "Word"), its BASIC macro language, and DLL support, you can use similar facilities in a number of off-the-shelf Windows and OS/2 Presentation Manager environment-based applications. If you have a copy of Word, you can type the examples here or download them from any MSJ bulletin board.

Word supports alteration of its entire menu structure, modification of the behavior of its keys, and "hot spots" that result in programmed actions when clicked with a mouse. A variety of tools support these modifications, including a document template facility; a dialog description language; a generalized field capability; table structures; dynamic data exchange; WordBASIC, a powerful macro language; and the ability to call dynamic-link libraries (DLLs).

This article addresses the latter two features, WordBASIC and DLLs. WordBASIC is a fairly comprehensive programming facility embedded in Word. The development process for WordBASIC macros is facilitated by a macro recorder, which creates a WordBASIC program directly from keyboard actions, and an integrated debugging system that permits step-wise execution.

DLLs in particular offer an extraordinary degree of flexibility. A simple DLL can be used to extend Word seamlessly or even replace existing capabilities of software packages supporting DLL extensions.

DLLs

DLLs are familiar to Windows1 and OS/2 programmers. The applications programming interface (API) and, in fact, most of the system code of these environments is implemented as a collection of DLLs accessed by systems and application software.

Conventional C programs contain statements in one source program that call functions in other source programs or libraries of compiled programs. Usually, a linker connects external calls from compiled objects to entry points in other compiled objects or library routines. In a traditional "static" environment, linking a compiled object to a library routine results in compiled code from the library being added to the execution module by the linker.

The Microsoft Windows Software Development Kit (SDK) introduced a new form of linking that delays the resolution of some library linkages until run time. This dynamic technique allows these library execution routines to be shared, since the object code is not bound with individual software packages.

By inventing DLLs, Windows developers created a new form of execution object that resembles a conventionally linked program (so closely that DLLs used to be identified with the EXE extension), but that has additional entry point information so that individual functions in the library can be found.

DLLs are supported by a new type of library called an import library (IMPLIB). An IMPLIB is used like a conventional library, except that when working with an import library, the linker merely adds a reference to an entry point in a DLL, instead of adding compiled code and data to the executable.

The final linkage between the calling and the called objects within DLLs typically takes place as the program is loaded. However, it is also possible for a running

program to load a DLL and call one of its functions. Word exploits this to allow WordBASIC programs to invoke DLL routines.

Application of DLLs

There are three types of DLLs that can be used to extend the capabilities of a package such as Word: native operating environment DLLs (used via the environment's standard API), commercial DLLs, and custom-built DLLs. Use of the first type of DLL is illustrated in Figure 1, a simple WordBASIC program. To execute this program, edit a new macro using the Macro Edit command. Enter the short program in Figure 1, then press the Start button. The Word title bar will be modified, demonstrating the power of the word processor and Windows itself. A large majority of the API functions in the Windows operating environment DLLs can be called directly from WordBASIC.

A more complex use of the Windows API functions is shown in Figure 2. This macro invokes the Windows sound library to play music. If the character string "cdeccdecef+

g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c" is highlighted when the macro is invoked, the Windows sound functions play "Frere Jacques."

You can call functions other than the Windows functions directly from WordBASIC as well. Other APIs, such as the NETAPI.DLL functions that provide the interface to Microsoft LAN Manager, can be called directly except when the calls require data types not supported by

WordBASIC. The highly interactive nature of WordBASIC makes it useful for exploring even complex API functionality.

Commercial DLLs, the second type of DLLs, are more "promise" than "practice." Just as the wide acceptance of the C language has resulted in the proliferation of a variety of static C libraries, it is highly likely that the popularity of Windows will spawn commercial DLLs to expand its capabilities. Although a few vendors use DLL facilities to support expanded graphics, spell-checking, and other functionality, this commercial opportunity has yet to be significantly exploited.

The opportunities for the third type, user-built DLLs, lie primarily in environments where a large number of users are provided with an integrated set of facilities to perform predefined tasks. Computing environments for corporate PC users are often enhanced with company-built templates and macros to simplify operation or to enhance functionality. While WordBASIC provides a great deal of flexibility for customization, a custom-written DLL provides even more, since all the features of the Windows environment can be exploited from inside it.

Macros and DLLs

Since DLL coding requires greater programming expertise and represents a substantial increase in programming investment over WordBASIC, it is important to balance the use of these two tools. Because programs can be developed and debugged more quickly in a macro language such as WordBASIC, it is often the tool of choice. Another reason is WordBASIC's comprehensive string handling, which can greatly simplify many typical operations even though it is limited to 32Kb strings. Also, WordBASIC functions can access the internal properties of Word and the documents in it directly. Until call-back functions exist that let DLLs retrieve information from the internals of Word, WordBASIC is the only reasonable means for accessing selected data, bookmarks, glossary information, and other word-processing-specific information. Therefore, even when DLLs are used, they must cooperate with WordBASIC calling routines that can move data between Word and the DLL.

DLLs are superior to macros in three areas: speed, storage, and control. WordBASIC is interpreted, and executes even simple loops slowly. DLLs written in C and compiled in machine language speed up routines that scan and manipulate characters or perform complex mathematical calculations. The performance enhancement is offset somewhat by the extra time required to initiate a DLL routine, but long routines are substantially improved by substituting a compiled language.

As for storage, the implementation of many programs' macro language limits both program and data space. DLLs relieve these constraints through the availability of medium model (multiple code segment) programming and access to the global allocation routines of Windows.

Direct access to storage provides a great deal of flexibility for DLL routines. For example, while WordBASIC supports only two fundamental data types, real numbers and strings, DLLs coded in C can take advantage of a variety of types, structures, and unions. Furthermore, storage allocated by DLLs can be retained beyond the life of a single WordBASIC routine. One simple way to maintain storage after exiting a DLL relies on the fact that Windows will not release the storage of a DLL in use. If an initialization routine within an AutoNew or AutoOpen WordBASIC macro invokes the Windows LoadModule function with the name of the library, the data storage for the library will be kept. A termination routine can clean up storage by calling the FreeLibrary function.

Perhaps the greatest benefit from writing DLLs is the degree of control available to the programmer. Dialog boxes are a good illustration. The WordBASIC dialog facility is quite flexible, but it supports only a limited user interface. It is not possible to change the contents of a combo box or list box based on the selection of a radio button or a list item. The NovaNew library routine (see Figure 3) demonstrates the adaptability of DLLs. The

main function, NovaNew, replaces the standard FileNew

function of Word, which confronts the user with a

cryptic list of up-to-eight-character template names (see Figure 4). NovaNew's dialog box groups the template names into logical categories such as correspondence, legal,

labels, and so on (see Figure 5). If you select a category in the left list box, NovaNew describes the document templates in another list.

The NovaNew function relies on description files with an LB extension that are used as data for the list boxes (see Figure 6). This approach is quite appropriate to the common corporate use of Word in which standardized templates are provided for different types of documents by a central staff. The creation of a new template requires the updating of these lists, although you could write a simple WordBASIC macro to simplify that operation.

The NovaNew Files

The NovaNew DLL consists of the usual Windows files: make, source, header, link, module definition, resource, and dialog files. An additional file needed for libraries, LIBENTRY.ASM, can be found in the Windows SDK. After you install the Windows SDK, a C compiler, and a macro assembler, create the library NOVANEW.DLL with the command:

nmake novanew

The library should be placed with the other DLLs in the Windows SYSTEM directory.

Replacing FileNew with NovaNew

The FileNew routine is used to call the DLL routine. It relies on the fact that each Word command is initiated by a WordBASIC program. Often, these very simple programs merely call Word's internal functionality and can be replaced by a user-built function. The FileNew program is installed in Word by editing and saving the global macro "FileNew" resulting in a modification to the file NORMAL.DOT. In this manner, any of Word's high-level functions can be seamlessly replaced by WordBASIC routines, which can call DLLs.

To add the new FileNew, use the Macro Edit command to specify "FileNew" in the Edit Macro Name dialog box. Replace the entire contents with the text shown in Figure 7. Then use the File SaveAll command to save the new macro. (Note that the substituted WordBASIC FileNew routine maintains the original contents in a subroutine named OLD, so that you can regain the original function while debugging. To revert back, simply change MAIN to NEW and OLD to MAIN.)

The substituted FileNew routine declares the library's main function with the following statement.

Declare Function NovaNew Lib "NovaNew.DLL"(Name$) As Integer

This statement identifies the function name, NovaNew; the library name, NOVANEW.DLL; the function's argument type (in WordBASIC, a variable ending in $ signifies a string); and the return type, Integer. Once declared, DLL functions can be called using normal WordBASIC syntax, as shown below.

If(NovaNew(A$) = 1) Then ...

The NovaNew library function returns a 1 if successful and a 2 if the user has pressed Cancel. Conventional Windows dialog boxes indicate success by returning IDOK or IDCANCEL, which are defined in the Windows SDK as 1 and 2, respectively. If the routine is successful, the string variable, A$, has been filled by NovaNew with the name of the selected document template. Before being used as an argument, the length of A$ must be established using a BASIC statement such as the following.

A$ = String$(50," ")

Failure to specify a large enough string as an argument to a DLL will typically result in a general protection fault.

NOVANEW.C

The main entry point in NOVANEW.DLL is the function NovaNew. NovaNew uses the Windows function DialogBox to invoke the GET_TEMPLATE dialog and return its results to WordBASIC. NovaNew relies on a small amount of trickery in the use of the GetActiveWindow function. Typically, library routines that manipulate windows will have a calling routine pass a window handle. Since WordBASIC does not have such a window handle, you must assume that GetActiveWindow returns the window handle of Word itself.

The heart of NOVANEW.C is the dialog function, fnNew. This function responds to calls from the Windows dialog manager that indicate user interaction with the dialog box. During initialization, a generalized function, FillListBox, is called to fill the left list box with the contents of TEMPLATE.LB. Whenever a left list box item is selected, the right list box is filled with the contents of a file with the LB extension. The names of these files are actually in the list boxes, hidden beyond the right edge of the window frame: there are simply plenty of blanks between the visible entry and the filename. The LastWord function extracts a filename from the list box entry text. The ExtractTemplate function creates a template filename from the list box entry and verifies that the template file exists whenever a right list box item is double-clicked or the OK button is pressed.

The first two functions in NOVANEW.C, LibMain and WEP, come with the Windows SDK. Both are specific to the creation of DLLs because they use a unique initialization mechanism. LibMain is called by LIBENTRY.ASM when the library is first invoked by the WordBASIC Declare statement. In NOVANEW.C, LibMain retains the library's instance handle in a global variable so that it can be used for Windows functions that require an hInstance. WEP is called when the library is being removed from memory. Although it serves no useful function in this library, it could be used to perform exit processing, such as the freeing of allocated storage. Both functions return TRUE to indicate normal execution.

SS != DS

Although NOVANEW.C looks like a normal Windows routine, it uses a unique memory convention that requires special care when building a library or converting existing Windows routines. In normal Windows programs, the stack segment (SS) and the data segment (DS) are the same. When a DLL is called, the DLL's data segment is used, but the stack is within the caller's data segment. The implication of this convention is that you cannot legitimately pass a short pointer to an object on the stack to a function. You must either use functions that use long pointers or you must place the objects of pointers in static or global storage. Usually, this convention interferes only with the conventional use of C string functions such as strcpy. Microsoft provides special versions of string functions in the SDK such as lstrlen, lstrcpy, and lstrcat that use long pointers. The Microsoft C compiler also has a switch, -Aw, that is used when compiling a DLL to verify that the SS != DS convention has been followed.

All of the short pointer C functions can be used against static or global objects, since they are located in the library's data segment. Consequently, NOVANEW.C's string buffers are defined as global, allowing functions such as strcpy and strcat to be used without concern. Using global arrays for large objects is actually preferable when writing Word DLLs. Although Word supports DLLs, it does not guarantee any specific amount of stack space for library routine execution. Thus, it is possible to overflow the stack with large automatic arrays or highly recursive routines. Allocating large arrays in the data segment avoids this problem and has few adverse consequences in the Windows environment Version 3.0.

NOVANEW.LNK and NOVANEW.DEF

The link control and module definition files used in constructing DLLs are very similar to those used in making Windows programs with some notable exceptions. The Windows SDK includes a set of libraries, such as SDLLCEW.LIB and MDLLCEW.LIB, that are specific to the creation of DLLs. The LIBRARY entry in the module definition file is also necessary to identify the type of module properly. The EXPORTS section of the definition file identifies entry points in the library. Any exported function in a DLL can be called from WordBASIC.

Debugging a Word DLL

WordBASIC and Microsoft CodeView debugger for Windows make a very strong debugging environment for DLLs. It is relatively simple to generate and quickly modify a range of test cases for a DLL using WordBASIC statements. Computed results can be presented using the Print statement, the MsgBox WordBASIC function, or even using Insert text$, which will place the results in the current file being edited. A bug in a DLL routine usually causes Word (if not Windows itself) to fail, so it is essential that WordBASIC test programs be saved frequently.

A misbehaving DLL routine can be best diagnosed using CodeView2. To use CodeView requires that the DLL be compiled with the -Zi switch and linked with the /CO option. CodeView also requires a machine with two monitors, such as a VGA monitor combined with a monochrome monitor or an IBM 8514/A. With that setup, it is possible to watch the source code and variables on one monitor while running the program on another.

When debugging DLLs with Word, rather than specifying WINWORD.EXE as the debugged object, it is usually more convenient to specify a simple program, such as NOTEPAD.EXE, along with your DLL name. This can be done using the Program Manager's Properties dialog box and an entry such as the following, which specifies running CodeView on Notepad with NOVANEW as a DLL.

c:\windev\dbgtools\cvw.EXE /l e:system\novanew c:\windows\notepad.exe

With a setup such as this, CodeView can be started to debug the DLL at any time, even after the DLL has been invoked.

Conclusion

Through the use of embedded macro languages and DLLs, systems developers and integrators can add value to off-the-shelf software. A total end-user solution can be constructed by combining canned applications and custom programming. Often, a quality hybrid system can be implemented with less effort than coding entirely new applications.

1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. Windows refers only to this Microsoft product and is not intended to refer to such products generally.

2For ease of reading, "CodeView' refers to the Microsoft CodeView debugger for Windows. CodeView refers only to this Microsoft product and is not intended to refer to such products generally.

Figure 2. A Program Using the Windows Sound DLL

Declare Sub SetVoiceQueueSize Lib "sound"(voice As Integer, n As Integer)

Declare Sub OpenSound Lib "sound"(foo As Integer)

Declare Sub CloseSound Lib "sound"(foo As Integer)

Declare Sub StartSound Lib "sound"(foo As Integer)

Declare Sub StopSound Lib "sound"(foo As Integer)

Declare Sub SetVoiceNote Lib "sound"(nVoice As Integer, nValue As Integer,

nlength As Integer, nCdots As Integer)

Declare Sub SetVoiceSound Lib "sound"(nVoice As Integer, nFreq As Integer,

nDuration As Integer)

Declare Sub SetVoiceAccent Lib "sound"(nVoice As Integer, nTempo As Integer,

nVolume As Integer, nMode As Integer, nPitch As Integer)

Sub MAIN

Notes$ = "G1a2bc3d4ef5g6A7BC8D9EF0"

Song$ = Selection$()

voice = 1

OpenSound(1)

StopSound(1)

SetVoiceQueueSize(voice, 10 +(Len(song$) * 6))

Duration = 8

For i = 1 To Len(song$)

If Mid$(Song$, i, 1) = "+" Then

Duration = 4

ElseIf(Mid$(song$, i, 1)) = "-" Then

Duration = 8

ElseIf(Mid$(song$, i, 1)) = "/" Then

duration = 16

ElseIf InStr(Notes$, Mid$(Song$, i, 1)) <> 0 Then

SetVoiceNote(voice, InStr(Notes$, Mid$(Song$, i, 1)) + 19,

Duration, 0)

End If

Next i

StartSound(1)

REM song1 -- cdeccdecef+g-ef+g/gAgf-ec/gAgf-ec-cG+c-cG+c

REM song 2 - +cc-cd+e-edef+gg/CCCgggeee+c-gfed+c

REM song 3 -

End Sub

Figure 6. Template Description Files TEMPLATE.LB CORRESPONDENCE CORRESPM

LEGAL LEGAL

LABELS LABELS

MISCELLANEOUS MISCELLA

EXAMPLES EXAMPLES

OTHER WINWORD WINWORD CORRESPM.LB Normal Document NORMAL

Plain Letter PLNLETER

Letter LETTER

Memorandum MEMO

Envelope ENVELOPE LEGAL.LB Legal Contract CONTRACT

Legal Brief BRIEF LABELS.LB 1-inch labels in one column, continuous LBL1COLT

1-inch labels in two columns, sheet feed LBL2COL

1-inch labels in two columns, continuous LBL2COLT

1-inch labels in three columns, sheet feed LBL3COL

1-inch labels in three columns, continuous LBL3COLT EXAMPLES.LB Article for a Periodical ARTICLE

Basic Template BASIC

Technical Paper PAPER

Report REPORT

Brochure BROCHURE

Data for a Brochure BROCHDAT

Brochure Merge BROCHMRG

Form Letter FRMLTR

Template for Data Entry DATADOC

Example Document EXAMPLES MISCELLA.LB Independent Systems Vendor ISV

Display of Keycaps KEYCAPS WINWORD.LB PCWORD PCWORD

SETUP SETUP

PRICMEMO PRICMEMO

Figure 7. The FileNew WordBASIC Routine

Declare Function NovaNew

Lib "NOVANEW.DLL"(Name$) As IntegerSub MAIN A$ = String$(50," ")

If(NovaNew(A$) = 1) Then A$ = Left$(A$, InStr(A$, ".") - 1) Super

FileNew .NewTemplate = 0, .Template = A$ End IfEnd SubSub OLDDim dlg As

FileNewGetCurValues dlgDialog dlgSuper FileNew dlgEnd Sub