Installing Windows 95 Programs
Your own installation program for Windows 95, Windows 3.1, and Windows NT
Al Williams
Al is the author of several books, including OLE 2.0 and DDE Distilled and Commando Windows Programming (both from Addison-Wesley). Look for his latest book, Steal This Code! in bookstores soon. You can contact Al on CompuServe at 72010,3574.
Gone are the days when applications could get by with simple batch scripts for installing programs. Today, you must have a full-blown installation program to keep Windows users happy. While there are a number of commercially available installation packages--Stirling's InstallShield, Jetstream's InstallWizard, Knowledge Dynamics' Winstall, Sax's Setup Wizard, and Microsoft's SetWizard (included with Visual C++ and Visual Basic) come to mind--sometimes you'll need to write your own installation programs. When it comes to this, however, there's good and bad news.
The good news is that Windows provides reasonable support for installation, including version checking and decompression. The bad news is these functions are quirky and poorly documented. In this article, I'll present a Windows 95 toolkit you can use to write high-quality installation programs in C, C++, and other languages. You won't need to learn a new scripting language--this installer uses C or C++. In the process, I'll examine tabbed dialogs, Microsoft's new property sheets.
Windows Support
The VER.DLL library (a standard part of Windows) has three ways to directly support installation programs:
- The VERSIONINFO resource statement, which records information about EXE-format files (including DLLs).
- VerFindFile(), which locates appropriate places to install files.
- VerInstallFile(), which decompresses and copies files to their destinations.
Listing One shows a typical VERSIONINFO entry. Notice that the information is in a simple, text-based format. There is a fixed portion of data and a varying portion enclosed in a BLOCK keyword. Each block has its own keywords and syntax; see Table 1.
VerFindFile() recommends a location for a file you want to install. You supply several parameters, and the function returns the recommended path and a status word; see Table 2. The status word is 0 on success; otherwise, one or more bits may be set:
- VFF_CURNEDEST--a previous version of the file resides in a nonrecommended directory.
- VFF_FILEINUSE--a previous version of the file is in use; you won't be able to remove it.
- VFF_BUFFTOOSMALL--an input buffer was too small.
The Ver... functions examine the version resource in your files, if it exists. Any file that has resources can contain a version resource (DLL, EXE, FON, and so on). Listing One shows a typical version resource containing copyright information, version numbers, language identifiers, and other information. To add a version resource to your files, simply place a version-resource block in your RC file and compile as usual. The version resource contains version information, other fixed values, and a variable part that begins with the BLOCK keyword. These blocks contain string information that varies from language to language.
Install-Program Characteristics
An install program must perform these basic steps:
- Determine the directory where you will install and the location of the installation disk, and collect any options. Typically, you'll present a dialog (with default values) where the user can enter this information. However, some programs might install to a fixed location (the Windows directory, for example).
- Verify that there is enough disk space available.
- Create any necessary directories and subdirectories (this may vary depending on the options the user selects in step 1).
- Make the first file to install the current file.
- Call VerFindFile() for the current file. If many applications share the file (for example, a common DLL), use the VFFF_ISSHAREDFILE flag. Set the source filename to the complete path of the source file and the application-directory parameter to your program's install directory. This call will return the directory that currently holds the file (if applicable) and the directory that VerFindFile() recommends for installation.
- Call VerInstallFile() for the current file. Set the source to the complete path of the file on the install disk and the destination path to the one recommended in step 5. You also pass the current file's location from step 5 to VerInstallFile() so it can remove the old file. This function will decompress the file (if required) and copy it to the proper location.
- Check the return value from VerInstallFile(). If the VIF_TEMPFILE bit is set, you must delete the temporary file that VerInstallFile() uses. You may report other errors to the user or force the installation by repeating step 6 with VIFF_FORCEINSTALL.
- If there are more files, make the next one current and go to step 5.
- Make any entries required in INI files or the system registry (optional).
- Create icons for your program in the user's shell (optional).
The main problem with the VerFindFile() and VerInstallFile() documentation is the description of the lpszFileName field. The documentation states that you should not include the path for the file in this parameter--only the filename and extension--but this is wrong. The culprit is LzOpenFile(), the underlying function that opens the file (which may be compressed).
COMPRESS.EXE (the Windows SDK compression utility) changes a file's extension by appending an underscore to it (or replacing the last extension character with an underscore). For example, READ.ME becomes READ.ME_ and README.TXT becomes README.TX_. When the Ver... functions use LzOpenFile() to open the file README.TXT, it looks for the filename in the current directory. Since the file is now README.TX_, it doesn't find it. It then looks for the file in the Windows directory, Windows system directory, all the directories on the path, and any mapped network drives. If it doesn't find the file, it then looks for README.TX_, which it will find.
Unfortunately, it is a good bet that the file README.TXT does exist in one of the myriad places LzOpenFile() searches. Then, the Ver... functions will cheerfully copy the alien file to your install directory and remove it from its current location. This is an endless source of confusion for users when they open your README.TXT file and it talks about a Borland product (or whatever LzOpenFile() found). The solution to this problem is simple: Ignore the documentation. Always specify a complete path to the install file.
Another documentation bug is the behavior of VerFindFile() when VFFF_ISSHAREDFILE is set. This flag allegedly causes the function to select the Windows (or Windows system) directory as the file's destination. This appears to not work under any current version of the VER.DLL library. It is safer to manually select the Windows directory when necessary. You can use the GetWindowsDirectory() and GetSystemDirectory() calls to find the names. (Win16 programs must use the GetWindowsDir() and GetSystemDir() calls instead.)
Using Multiple Disks
If your entire product fits on one disk, the install program can load from the disk and run. If you need more than one disk, however, you may have problems running the install program from the floppy. When Windows runs any program, it may not keep all of it in memory at one time. It can go back to the disk any time to load code segments, resources, or whatever. If your install program is on disk 1 and the user has disk 2 in the drive, havoc will ensue.
You can manipulate your resources and your DEF file to cause Windows to load the entire program and lock it in memory. However, this doesn't work with certain networking software (including software from Novell). The network detects that you are running a program from the floppy and keeps the file locked. When you change disks, DOS will report an invalid disk change error and not allow you to continue. This happens even if Windows no longer needs the file.
The only realistic alternative is to copy the install program to the hard disk as the first step of installation. Then the installer can run from the hard disk. If the install program can copy itself, the performance penalty is minimal since Windows will use the same in-memory copy of the program. However, the network will know that the floppy need not remain in the drive.
Problems with Compressed Hard Disks
Calculating the available disk space is always difficult. You can't simply compute the free bytes on the disk and compare that number to the amount of space your software requires because DOS stores files in clusters. If a hard disk's cluster size is 2K, for example, each file stores in multiples of 2K. A 1-byte file takes 2K; so does a 1K file. A 3900-byte file requires 4K. Therefore, you should compute space requirements for each file based on the target hard disk's cluster size and compare the number of clusters.
However, this usually isn't worth the trouble. If the user employs a disk-compression program (Stacker, for example), the numbers are meaningless. Compression programs report an estimated free space that may be way off, depending on the nature of the files you store. There is no way to know how much actual space you have until you use it.
As a compromise, I usually compute an estimated free space and compare it to the space available on the disk. If the free space is less than the estimate, I'll warn users, but allow them to continue. Although not an optimal solution, this technique works in practice.
If the user reinstalls your software, calculating free space is even more difficult. You should take into account files that you will overwrite when computing free space. With shared files, this can be difficult. Again, simply warning the user if disk space appears low is safe and easy.
Encapsulating Installation
It isn't difficult to write an installation library that does most of the hard work. You don't want a DLL for an installation program since you could have problems loading a DLL at install time. A static link library works well and allows you to produce a single install program with no external parts.
The installation library can easily incorporate all the usual Windows trappings, including a WinMain() function and the main window class. You have to supply a list of files to install, directories to create, and the information required in step 1 of the installation program. The installation library will provide support for adding program-manager groups, but you'll have to make the calls yourself. You also can make calls to set up any INI files (or the registry).
Using the Install Library
Figure 1 is an install program written with my install toolkit. The toolkit provides a window (filled with your logo) that serves as a workspace. You can provide multiple logos, and the installer will select the one
that best fits the current display. Select a logo slightly smaller than the screen size so you can leave room for the window title. For example, a logo for a 640x480 screen might be 600x400.
Your program supplies a global string (TITLE) that the installer uses as the main-window title. You can also provide an icon by using the APPICON ID in your RC file. If you use multiple disks and want the installer to copy itself to the user's hard disk, you should declare the hdcopy variable to True. If you omit this declaration, the installer will run from the floppy.
The only other required item is the install() function; see Figure 2. From here you can bring up a dialog, read configuration information, and so on. When you are ready, call cw_Install() to initiate the installation; see Figure 3. The cw_Install() function returns either SUCCESS, CANCEL, or RETRY. If the return value is SUCCESS, you are free to continue with the remainder of the installation. If it is RETRY, you should get new installation values (for example, show your dialog again) and call cw_Install() with the new parameters. When cw_Install() returns CANCEL, you should display any error or help messages you want and return.
You can also provide a cw_InstallInit() function. The toolkit executes this function before creating a window. To cancel the installation, return False from this routine. Usually you don't need this function, but it is available for special initialization.
The parameters to cw_Install() are straightforward. First, you pass the parent-window handle (usually the same one the toolkit sends you). Next, you supply the application directory (which need not exist) and the option bit mask. Each file and directory can have an option bit mask. If the mask is 0, the toolkit always installs the file. If the mask is not 0, the toolkit installs the file only when the option bit mask you pass to cw_Install() has the same bit set. For example, if you use a bit mask of 3, the installer will process files marked with 0, 1, 2, or 3.
Following the option bits, you supply cw_Install() with a pointer to an array of subdirectories (using the install_dirs structure; see Table 4) and the length of the array. If you don't need subdirectories, you can use NULL and 0 for these parameters. Next is a pointer to an array of _inst_files structures (and the length of the array). This structure contains six fields: an option bit mask, source filename, destination filename, destination directory, and flags for the VerFindFile() and VerInstallFile() flags.
The last two parameters are the estimated size (in bytes) and a Boolean that controls what happens when cw_Install() successfully completes. If you set this variable to True, cw_Install() will display a message box when the installation is successful. However, if you have more work to do (for example, setting up INI files) you may want to display your own message box at the end. To eliminate the message, set this parameter to False.
Some fields in the _inst_files structure can take special values. If the destination filename is NULL, the filename remains unchanged. The destination directory is usually "." to signify the current directory. You can also specify a subdirectory name, WINDIR for the Windows directory, or SYSDIR for the Windows system directory.
If the first flag field is -1, the installer copies the file without using the Ver... functions. This is useful for storing compressed files on the user's hard disk. Also, if the source filename is NULL, the installer uses the flag field as a disk number. It searches the install disk for a file named DISKn.ID (where n is the number in the flag field). If it can't find the file, it prompts the user to insert the disk. You can use this feature to prompt the user for multiple disks. If the disk is already present, no prompt occurs. Therefore, you will often start the list with a check for DISK1.ID.
The install toolkit offers several helper functions for use in your main routine. The cw_VersionCheck() call checks which versions of DOS and Windows are present. You can also send Program Manager DDE commands using cw_ProgManCmd(). You use these commands to set up icons in Program Manager for your application. (For more details about Program Manager's DDE interface, see the accompanying text box entitled, "Adding Icons.") To modify INI files or the registry, call the standard Windows API functions.
An Example Install Program
Listing Two is an example install program that copies a program called "CoolWorx32" and two 32-bit example editors. (CoolWorx is a C/C++ toolkit I wrote that uses the object-oriented nature of Windows programming to simplify application development; see my article "Simplifying Windows Development," Dr. Dobb's Sourcebook, March/April 1995.) The program uses two option bits: Option 1 installs the single document interface (SDI) editor, while option 2 installs the multiple document interface (MDI) editor. The installer always installs the CoolWorx support files that the editors use.
The _inst_files structure contains a list of all required files. The installer always copies files marked with option 0. Otherwise, the installer only copies files that have at least one option bit set that is also set in the selected option word.
To select options and set the install directory, this installer uses a tabbed dialog (Microsoft calls these "property sheets"), but an ordinary dialog box would serve just as well. A single call to PropertySheet() works like the ordinary DialogBox() call except that it manages multiple dialog templates. Since the tabbed dialog has two pages, the installer has two dialog templates, PG1 and PG2. Once the user enters the options and the install directory information, the installer calls cw_install(), which does all the work required to assign file locations, decompress files, and copy them to the hard disk.
If the cw_install() function returns SUCCESS, and the user selected one or both of the editor examples, the installer creates a program-manager group and adds icons for the editors. The cw_ProgManCmd() function makes this easy.
Finally, the installer opens the README.TXT file using WORDPAD.EXE, the Windows 95 replacement for NOTEPAD.EXE. Once it launches WORDPAD (using WinExec()), the installer exits. You could easily use calls like SetPrivateProfileString() or RegSetValue() to install INI files or registry entries.
Using Tabbed Dialogs
Tabbed dialogs are simple to create under Windows 95. In Figure 1 (a typical tabbed dialog), each tab represents a different dialog page. Clicking on the tab makes the specified page active.
Each page in a tabbed dialog is an individual dialog. It has its own template and can have a separate callback. Of course, you can route the callbacks for each page to the same routine, if you prefer.
The three property-sheet calls are in PRSHT.H (although Microsoft may move these to COMMCTRL.H later), but you will usually only use one--PropertySheet(). This call is analogous to the standard DialogBox() call. You supply a pointer to a PROPSHEETHEADER structure (see Table 5), which has a pointer to an array of PROPSHEETPAGE structures (Table 6). These two structures specify how the tabbed dialog behaves.
The PROPSHEETHEADER structure defines properties for the entire tabbed dialog. You need to set the dwFlags field to indicate which fields you will use; see Table 7. For example, if you want each tab to use an icon, you set the PSH_USEICON or PSH_USEICONID flag and fill in the hIcon or pszIcon field to select an icon.
If you don't set the PSH_PROPSHEETPAGE bit in the dwFlags field, you must separately create each page using CreatePropertySheetPage(). This call returns a handle that you can store in an array to use in the phpage field of the PROPSHEETHEADER structure. Usually, you simply set the PSH_PROPSHEET flag and use an array of PROPSHEETPAGE structures (in the ppsp field) instead of handles.
Each PROPSHEETPAGE structure also has a dwFlags field; see Table 8 for a list of values. You can provide a resource template name in the pszTemplate field or a dynamic-dialog template in the pResource field. If you use a dynamic-dialog template, you must set the PSP_DLGINDIRECT flag in the dwFlags field.
If you set the PSP_USETITLE flag, you can also set a title for the tab in the pszTitle field. If you don't set the flag, the dialog's caption becomes the tab title. The dialog callback is exactly like an ordinary dialog function and is set in the pfnDlgProc field. The other fields allow you to set a function to run before Windows destroys the page.
Messages
The property sheet accepts several messages (by way of macros) and can send you several WM_NOTIFY messages; see Table 9 and Table 10. You usually won't use most of these. The PSM_SETCURSEL message allows you to make a page active, and PSM_PRESSBUTTON lets you programmatically push any of the tabbed dialog buttons (not the buttons in your dialog template).
If you haven't used any of Microsoft's common controls (see "Windows 95 Common Controls," by Vinod Anantharaman, DDJ, May 1995) you may not be familiar with WM_NOTIFY. The new controls send WM_NOTIFY to alert you of noninput events. In the past, notifications came with WM_COMMAND messages (for example, the EN_CHANGED notification). WM_NOTIFY messages pass a pointer to a structure in lParam. The first part of this structure corresponds to a NMHDR structure (see Table 11). By examining the code field in this structure, you can determine the type of notification. For property sheets, these codes begin with PSN_ (see PRSHT.H). Then you cast the structure pointer to a more-specific structure. For property sheets, you don't need a special structure; just cast lParam to an LPNMHDR and examine the code field.
You can also control the state of the buttons with the PSM_CHANGED, PSM_UNCHANGED, and PSM_CANCELTOCLOSE messages. You should use these to inform the user about the state of the dialog.
You can catch notifications that tell you when Windows activates or deactivates a page (PSN_SETACTIVE and PSN_KILLACTIVE). Other notifications inform you when the user presses certain buttons; see Table 9.
There are a few tricks to using tabbed dialogs:
- Use the WS_CHILD style for each dialog template.
- Don't place OK and Cancel buttons in the template--the property sheet will add these.
- Make certain each dialog template is the same size.
- If you have a common item on multiple pages, be sure it lines up exactly in each dialog template.
Inside the Install Library
Most of the install library is straightforward. It is detailed in Listing Three; listings are available electronically, see "Availability," page 3. The WinMain() function creates a window that contains the logo bitmap and calls your install() routine. Later, you call cw_Install() to do all the messy work. When your install() routine returns, the toolkit cleans up and exits.
If you set the hdcopy flag, the installer checks its command-line arguments. If there are none, it copies itself to the temporary directory and runs the new copy with two arguments: the source directory and the name of the temporary executable. When it detects these arguments, the installer continues with normal processing. When processing completes, the installer removes itself from the temporary directory. This prevents problems with network locking.
The only special part of the install toolkit is the cw_Install() function. The other portions are straightforward Windows programming. The cw_Install() call walks through the directory array creating directories, then walks through the file list calling VerFindFile() and VerInstallFile() repeatedly. Of course, the calls only occur if the correct option bits are set.
Adding Icons
Program Manager (and Program Manager replacements) provides a DDE interface that allows install programs to manage groups and icons. The Windows 95 default shell supports this interface by adding pseudogroups and icons to the Start-button menu.
Commands are passed to Program Manager via DDE and are enclosed in square brackets. The most common commands are as follows:
- CreateGroup(name,[path]) creates a new group with the specified name and optional group filename.
- ShowGroup(name,cmd) is the display group. cmd can range from 1 to 8: 1 activates and displays the group window, 2 activates the group as an icon, 3 activates and maximizes the group window, 4 restores the window, 5 activates the window in place, 6 minimizes the window, 7 displays the group as an icon without activation, and 8 restores the group without activation.
- DeleteGroup(name) deletes a group and its contents.
- Reload([group]) reloads a group from its group file. If you specify no group, PROGMAN reloads all groups.
- AddItem(cmd,[name],[icon_file],[icon_index],[x] [y],[start_dir],[hotkey],[minimize]) adds a new item to the current group. The parameters are: cmd, the command line; name, the item name; icon_file, the file that contains the item's icon; icon_index, the icon to use from the icon_file; x, the new item's x-coordinate (if this parameter is present, the y parameter is not optional); y, the new item's y-coordinate; start_dir, the working directory; hotkey, the item's shortcut key; and minimize, the item's run state.
- DeleteItem(item) removes the item from the current group.
- ReplaceItem(item) removes the item from the current group and marks its position for use by the next AddItem command.
- ExitProgMan(save) exits the program manager. The save parameter specifies whether PROGMAN should save its current state. If PROGMAN is your default shell, this command won't work.
--A.W.
Table 1: Fixed VERSIONINFO resource.
Figure 1: The installer in action.
Figure 2: The install() function.
Copyright © 1995, Dr. Dobb's Journal
Field Description
FILEVERSION Version of this file.
PRODUCTVERSION Version of entire product.
FILEFLAGSMASK Contains a 1 for valid bits in the FILEFLAGS field.
FILEFLAGS File attributes (for example, VS_FF_DEBUG).
FILEOS Operating system (for example VOS_WINDOWS32).
FILETYPE File type (for example, VFT_APP).
FILESUBTYPE Type of driver, font, or VxD (if applicable).
Table 2: VerFindFile() parameters.
Parameter Description
<I>dwFlags</I> 0 for normal file; VFFF_ISSHAREDFILE for shared files.
<I>szFileName</I> Filename.
<I>szWinDir</I> Windows directory.
<I>szAppDir</I> Application's directory (destination).
<I>szCurDir</I> <I>VerFindFile</I> places the file's current location in this variable.
<I>lpuCurDirLen</I> Length of <I>szCurDir</I> array.
<I>szDestDir</I> <I>VerFindFile</I> places recommended install directory in this variable.
<I>lpuDestDirLen</I> Length of <I>szDestDir</I> array.
Table 3: VerInstallFile() parameters.
Parameter Description
<I>dwFlags</I> Control flags (0, VIFF_FORCEINSTALL, or VIFF_DONTDELETEOLD).
<I>szSrcFileName</I> Source filename.
<I>szDestFileName</I> Destination name.
<I>szSrcDir</I> Source directory.
<I>szDestDir</I> Destination directory.
<I>szCurDir</I> Directory where file currently resides.
<I>szTmpFile</I> Temporary filename possibly returned by <I>VerInstallFile()</I>.
<I>lpuTmpFileLen</I> Length of above array.
Table 4: The _inst_files structure.
Field Description
<I>bitmask</I> Option bits that apply to this file.
<I>srcfile</I> Source filename.*
<I>dstfile</I> Destination filename.*
<I>dstdir</I> Destination directory.*
<I>flags</I> Flags set to <I>VerFindFile()</I>.*
<I>cflags</I> Flags set to <I>VerInstallFile()</I>.
<I>* May take special values.</I>
Table 5: PROPSHEETHEADER structure.
Field Description
<I>dwSize</I> Size of structure.
<I>dwFlags</I> PSH flags (see <a href="#0109_0056">Table 6</A>).
<I>hwndParent</I> Parent window.
<I>hInstance</I> <I>hInstance</I> that contains resources.
<I>hIcon</I> Icon handle (if PSH_USEHICON is set).
<I>pszIcon</I> Icon name (if PSH_USEICONID is set).
<I>pszCaption</I> Title to use when PSH_PROPTITLE is set.
<I>nPages</I> Number of tabs.
<I>nStartPage</I> Beginning tab number (if PSH_USEPSTARTPAGE is not set).
<I>pStartPage</I> Name of beginning tab (if PSH_USEPSTARTPAGE is set).
<I>ppsp</I> Pointer to array of property-sheet structures (if PSH_PROPSHEETPAGE is set).
<I>phpage</I> Pointer to array of property-sheet handles (if PSH_PROPSHEETPAGE is not set).
<I>pfnCallback</I> Global property-sheet callback.
Table 6: PROPSHEETPAGE structure.
Field Description
<I>dwSize</I> Size of structure.
<I>dwFlags</I> PSP_ flags (see <a href="#0109_0057">Table 7</A>).
<I>hInstance</I> Instance handle for resources.
<I>pszTemplate</I> Name of dialog template (if PSP_DLGINDIRECT is not set).
<I>pResource</I> Pointer to resource (if PSP_DLGINDIRECT is set).
<I>hIcon</I> Icon handle (if PSP_USEICON is set).
<I>pszIcon</I> Icon name (if PSP_USEICONID is set).
<I>pszTitle</I> Name to override template's title.
<I>pfnDlgProc</I> Dialog callback.
<I>lParam</I> 32 bits of user-defined data.
<I>pfnCallback</I> Called before destruction if PSP_USERELEASEFUNC is set.
<I>pcRefParent</I> Pointer to reference count (used with PSP_USERREFPARENT flag).
Table 7: Flag bits for PROPSHEETHEADER.
Bit Description
PSH_DEFAULT 0-no bits set.
PSH_PROPTITLE Prepend "Properties for" ahead of title.
PSH_USEHICON Use icon handle.
PSH_USEICONID Use icon name or ID.
PSH_PROPSHEETPAGE Use <I>ppsp</I> field instead of <I>phpage</I> field.
PSH_MULTILINETABS Use multiline tabs.
PSH_WIZARD Suppress tabs and treat dialog as a wizard.
PSH_USEPSTARTPAGE Use <I>pStartPage</I> field.
PSH_NOAPPLYNOW Suppress the Apply Now button.
PSH_USECALLBACK Enable global callback.
PSH_HASHELP Support help.
Table 8: Flag bits for PROPSHEETPAGE.
Bit Description
PSP_DEFAULT 0-no bits set.
PSP_DLGINDIRECT Use indirect dialog resources.
PSP_USEHICON Use icon handle.
PSP_USEICONID Use resource ID for icon.
PSP_USETITLE Use override title.
PSP_USEREFPARENT Use reference-count variable.
PSP_USECALLBACK Use release callback.
PSH_HASHELP Support help.
Table 9: Property-sheet notifications.
Code Description
PSN_SETACTIVE Page receiving focus.
PSN_KILLACTIVE Current page is losing focus.
PSN_APPLY OK or Apply button pressed.
PSN_RESET Cancel button pressed (too late to stop).
PSN_HASHELP Query page to see if it supports help.
PSN_QUERYCANCEL Cancel button pressed (possible to abort).
PSN_WIZBACK Back button pressed (wizard only).
PSN_WIZNEXT Next button pressed (wizard only).
PSN_WIZFINISH Finish button pressed (wizard only).
Table 10: Commonly used property-sheet messages.
Message Pseudocall Description
PSM_SETCURSEL PropSheet_SetCurSel Sets active page by
handle.
PSM_SETCURSELID PropSheet_SetCurSelByID Sets active page by
ID.
PSM_CHANGED PropSheet_Changed Enable Apply Now
button.
PSM_RESTARTWINDOWS PropSheet_RestartWindows Ask Windows to
restart when property
sheet closes.
PSM_REBOOTSYSTEM PropSheet_RebootSystem Ask Windows to reboot
when property sheet
closes.
PSM_CANCELTOCLOSE PropSheet_CancelToClose Change "Cancel" button
to "Close."
PSM_QUERYSIBLINGS PropSheet_QuerySiblings Forward message to all
initialized pages
until one returns
nonzero; return the
value.
PSM_UNCHANGED PropSheet_UnChanged Disable Apply Now
button.
PSM_APPLY PropSheet_Apply Do the same processing
as if the Apply Now
button were depressed.
PSM_SETTITLE PropSheet_SetTitle Sets dialog title.
PSM_SETWIZBUTTONS PropSheet_SetWizButtons Enable specific wizard
or button (wizards
PropSheet_SetWizButtonsNow only); PropSheet_____LINEEND____
SetWizButtonsNow()
uses SendMessage()
instead of
PostMessage().
PSM_PRESSBUTTON PropSheet_PressButton Programatically press
a button.
PSM_SETFINISHTEXT PropSheet_SetFinishText Set text on "Finish"
button (wizards only).
PSM_GETTABCONTROL PropSheet_GetTabControl Get handle to tab
control.
Table 11: Notification header structure.
Field Description
<I>hwndFrom</I> Window handle of originating control.
<I>idFrom</I> Window ID of originating control.
<I>code</I> Specific notification.
int install(HWND mainwin, HANDLE hInst, LPSTR src);
where: mainwin=window handle for main window
hInst=install program's instance handle
src=string containing the install source directory
The install program ignores the return value from <I>install()</I>.
Figure 3: The cw_Install() function.
int cw_Install(HWND w, LPSTR appdir, DWORD bitmask,
struct install_dirs *subdir, int nrdirs,
struct _inst_files *inst_files, int nrfiles,
unsigned long space, BOOL mbflag);
where: w=install window
appdir=destination directory
bitmask=option bits (see text)
subdir=list of subdirectories to create
nrdirs=number of elements in subdir
inst_files=list of files
nrfiles=number of elements in inst_files
space=projected size of installed components
mbflag=FALSE to disable success message box
return values: SUCCESS=installation complete
CANCEL=installation canceled
RETRY=installation failed
Listing One
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1, 0, 0, 0
PRODUCTVERSION 1, 0, 0, 0
FILEOS VOS_DOS_WINDOWS32
FILETYPE VFT_DLL
{
BLOCK "StringFileInfo"
{
BLOCK "040904E4"
{
VALUE "CompanyName",
"Al Williams Computing\000\000"
VALUE "FileDescription",
"CoolWorx Application Framework\000"
VALUE "FileVersion", "1.00\000\000"
VALUE "InternalName",
"CoolWorx\000"
VALUE "LegalCopyright",
"Copyright ) 1994 by Al Williams\
Computing\000\000"
VALUE "OriginalFilename",
"COOLWORX.DLL\000"
}
}
}
Listing Two
/* CoolWorx Install program */
#include <windows.h>
#include <prsht.h> /* could change */
#include "cwinstal.h"
#include "install.h"
#include <stdlib.h>
/* You must declare the title string */
char TITLE[]="CoolWorx Install"; // title
/* The code in this source file uses this string as
a default */
#define DEFDIR "C:\\COOLWORX"
/* Subdirectories */
struct install_dirs subdirs[]=
{
{0,"BIN"},
{1,"SDI"},
{2,"MDI"}
};
/* Number of subdirectories */
#define NRDIRS sizeof(subdirs)/sizeof(subdirs[0])
/* File list. If srcfile is NULL install will prompt
for disk change using flags as disk number --
verify that diskN.id file exists on it.
If dstdir is WINDIR, then use Windows directory
if dstdir is SYSDIR, then use System directory
If dstdir is ".", then use default install directory
If flags are -1 then force straight copy
(no decompress, etc.) */
struct _inst_files inst_files[]=
{
{ 0,NULL,NULL,NULL,1,0 },
{ 0,"README.TXT",NULL,".",0,0 },
{ 0,"COOLWORX.DLL",NULL,WINDIR,VFFF_ISSHAREDFILE,0},
{ 0,"CBUTTON.DLL",NULL,WINDIR,VFFF_ISSHAREDFILE,0},
{ 0,"CWMISC.DLL",NULL,WINDIR,VFFF_ISSHAREDFILE,0},
{ 2,"COOLEDIT.EXE",NULL,"MDI",0,0},
{ 1,"SDIEDIT.EXE",NULL,"SDI",0,0}
};
/* Number of files */
#define NRFILES (sizeof(inst_files)/sizeof(inst_files[0]))
char appdir[_MAX_PATH];
char opts[2];
/* 1st tab dialog proc */
BOOL pg1proc(HWND dlg,UINT cmd,WPARAM wParam,LPARAM lParam)
{
switch (cmd)
{
case WM_INITDIALOG:
SetDlgItemText(dlg,DIR,DEFDIR);
break;
case WM_NOTIFY:
{
LPNMHDR nh=(LPNMHDR)lParam;
switch (nh->code)
{
/* Cancel */
case PSN_RESET:
EndDialog(dlg,FALSE);
return TRUE;
/* OK */
case PSN_APPLY:
/* Return TRUE */
SetWindowLong(dlg,DWL_MSGRESULT,TRUE);
GetDlgItemText(dlg,DIR,appdir,sizeof(appdir));
EndDialog(dlg,TRUE);
return TRUE;
}
break;
}
}
return FALSE;
}
/* 2nd tab dialog proc */
BOOL pg2proc(HWND dlg,UINT cmd,WPARAM wParam,LPARAM lParam)
{
switch (cmd)
{
case WM_INITDIALOG:
/* Init state */
SendDlgItemMessage(dlg,SDI,BM_SETCHECK,
opts[0]=='X',0);
SendDlgItemMessage(dlg,MDI,BM_SETCHECK,
opts[1]=='X',0);
break;
case WM_NOTIFY:
{
LPNMHDR nh=(LPNMHDR)lParam;
switch (nh->code)
{
case PSN_RESET:
EndDialog(dlg,FALSE);
return TRUE;
/* OK */
case PSN_APPLY:
SetWindowLong(dlg,DWL_MSGRESULT,TRUE);
opts[0]=SendDlgItemMessage(dlg,SDI,
BM_GETCHECK,0,0)?'X':' ';
opts[1]=SendDlgItemMessage(dlg,MDI,
BM_GETCHECK,0,0)?'X':' ';
EndDialog(dlg,TRUE);
return TRUE;
}
break;
}
}
return FALSE;
}
/* Your main install routine must be named install!
w=main window handle
hInst=instance handle of install program
srcdir=source directory for install (e.g., a:\) */
install(HWND w,HANDLE hInst,LPSTR srcdir)
{
int i;
DWORD bitmask; /* option bitmask */
char tmpfile[_MAX_PATH];
/* Tabbed dialog body */
PROPSHEETPAGE pages[2]=
{
{sizeof(PROPSHEETPAGE),0,0,"PG1",
NULL,NULL,(DLGPROC)pg1proc,0,NULL,NULL},
{sizeof(PROPSHEETPAGE),0,0,"PG2",
NULL,NULL,(DLGPROC)pg2proc,0,NULL,NULL}
};
/* Tabbed dialog header */
PROPSHEETHEADER psh={sizeof(PROPSHEETHEADER),
PSH_PROPSHEETPAGE,NULL,NULL,NULL,
"CoolWorx32 Install",
2,0,pages };
/* Set defaults */
lstrcpy(appdir,DEFDIR);
opts[0]='X';
opts[1]='X';
psh.hInstance=pages[0].hInstance=
pages[1].hInstance=hInst;
psh.hwndParent=w;
/* Come here if install returns RETRY */
retry:
if (!PropertySheet(&psh))
{
/* Come here if install is cancelled */
cancelinst:
MessageBox(w,"Installation Cancelled",
NULL,MB_OK|MB_ICONSTOP);
return 1;
}
UpdateWindow(w); /* make sure window updates */
bitmask=0;
/* decode options to bitmask */
for (i=0;i<sizeof(opts);i++)
if (opts[i]=='X') bitmask|=1<<i;
/* Call installer -- pass main window, app directory and options */
switch (cw_Install(w,appdir,bitmask,subdirs,
NRDIRS,inst_files,NRFILES,0,FALSE))
{
case RETRY:
goto retry; // show options again
case CANCEL:
goto cancelinst; // forget it
}
/* success */
/* Set up INI file, groups, etc.*/
if (opts[0]=='X'||opts[1]=='X'
{
cw_ProgManCmd("[CreateGroup(CoolWorx32 Alpha,)]");
cw_ProgManCmd("[ShowGroup(CoolWorx32 Alpha,1)]");
if (opts[0]=='X')
{
cw_ProgManCmd("ReplaceItem(SDI Editor)]";
wsprintf(tmpfile,
"[AddItem(%s\\SDI\\SDIEDIT,SDI Editor,,,,,)]",
appdir);
cw_ProgManCmd(tmpfile);
}
if (opts[1]=='X')
{
cw_ProgManCmd("ReplaceItem(CoolEdit)]";
wsprintf(tmpfile,
"[AddItem(%s\\MDI\\COOLEDIT,CoolEdit,,,,,)]",
appdir);
cw_ProgManCmd(tmpfile);
}
WinExec("WORDPAD README.TXT",SW_SHOW);
MessageBox(w,"Installation Complete","Notice",
MB_OK|MB_ICONEXCLAMATION);
}
}
Listing Three
int WINAPI cw_Install(HWND w,LPSTR appdir,DWORD bitmask,
struct install_dirs *subdirs,int NRDIRS,
struct _inst_files *inst_files,int NRFILES,unsigned long space, BOOL mbflag)
{
int i;
unsigned cdlen;
unsigned inslen;
char tmpfile[_MAX_PATH];
char curdir[_MAX_PATH];
char instdir[_MAX_PATH];
char srcf[_MAX_PATH];
unsigned tmplen;
if (i=cw_ChdirEx(appdir))
{
.
.
.
}
if (space)
{
unsigned long freesp;
#ifdef _WIN32
DWORD secper,bps,freec,tclust;
#else
struct diskfree_t df;
#endif
/* Compute free space */
#ifdef _WIN32
GetDiskFreeSpace(NULL,&secper,&bps,&freec,&tclust);
freesp=(unsigned long)secper*bps*freec;
#else
_dos_getdiskfree(0,&df);
freesp=
(unsigned long)df.avail_clusters*
df.sectors_per_cluster*df.bytes_per_sector;
#endif
if (freesp/1024<space)
{
int id=
MessageBox(w,"You may not have enough free disk space.\n"
.
.
.
}
}
/* Set up progress bar */
if (!prog)
prog=cw_ProgressDlg(w,"Installing...","",
NRDIRS+NRFILES,TRUE);
if (prog) // position progress bar
{
RECT r,dr;
int x,y,h,xw;
GetClientRect(w,&r);
ClientToScreen(w,(LPPOINT)&r);
ClientToScreen(w,((LPPOINT)&r)+1);
GetWindowRect(prog,&dr);
x=((r.right-r.left)-(xw=dr.right-dr.left))/2;
y=((r.bottom-r.top)-(h=dr.bottom-dr.top))/2;
MoveWindow(prog,x+r.left,y+r.top,xw,h,TRUE);
}
/* Need to make directory tree here */
for (i=0;i<NRDIRS;i++)
{
if (subdirs[i].bitmask)
if (!(subdirs[i].bitmask&bitmask)) continue;
if (prog)
{
char ptitle[_MAX_PATH+33];
wsprintf(ptitle,"Creating subdirectory %s",
(LPSTR)subdirs[i].dir);
if (cw_ProgressSet(prog,i,ptitle)) return -1;
UpdateWindow(w);
}
if (access(subdirs[i].dir,0)&&mkdir(subdirs[i].dir))
{
MessageBox(w,"Can't create subdirectory.",
subdirs[i].dir,MB_OK|MB_ICONSTOP);
return -2;
}
}
/* Install files */
for (i=0;i<NRFILES;i++)
{
UINT vrv;
DWORD vrvi;
char *dst;
/* Skip file if install bits don't match */
if (inst_files[i].bitmask)
if (!(inst_files[i].bitmask&bitmask)) continue;
if (!inst_files[i].srcfile)
{
/* Special... prompt for new disk */
static char msg[66],idfile[_MAX_PATH];
if (srcdir[lstrlen(srcdir)-1]!='\\') lstrcat(srcdir,"\\");
wsprintf(msg,"Please insert disk #%d",inst_files[i].flags);
if (srcdir[lstrlen(srcdir)-1]=='\\')
srcdir[lstrlen(srcdir)-1]='\0';
wsprintf(idfile,"%s\\DISK%d.ID",(LPSTR)srcdir,
inst_files[i].flags);
while (access(idfile,0))
{
struct dlgboxparam pblk;
pblk.msg=msg;
pblk.srcdir=srcdir;
pblk.sizsrc=sizeof(srcdir);
if (DialogBoxParam(hInst,MAKEINTRESOURCE(DISKDLG),
w,diskdlg,(DWORD)&pblk))
return -1;
UpdateWindow(w);
if (srcdir[lstrlen(srcdir)-1]=='\\')
srcdir[lstrlen(srcdir)-1]='\0';
wsprintf(idfile,"%s\\DISK%d.ID",
(LPSTR)srcdir,inst_files[i].flags);
}
continue;
}
/* Get on with it */
if (inst_files[i].dstfile)
dst=inst_files[i].dstfile;
else
{
dst=strrchr(inst_files[i].srcfile,'\\');
if (dst) dst++; else dst=inst_files[i].srcfile;
}
if (prog)
{
char ptitle[_MAX_PATH+33];
wsprintf(ptitle,"Installing %s",(LPSTR)dst);
if (cw_ProgressSet(prog,i+NRDIRS,ptitle)) return -1;
UpdateWindow(w);
}
fretry:
cdlen=sizeof(curdir);
inslen=sizeof(instdir);
if (inst_files[i].flags==0xFFFF)
{
/* copy unconditionally w/o decompress or checking */
if (!copyfile(inst_files[i].dstdir,dst,
srcdir,inst_files[i].srcfile))
{
int id=
MessageBox(w,"Could not copy this file.\n"
"You may be able to close other applications\n"
"and then successfuly install.\n"
"Retry?",inst_files[i].srcfile,
MB_RETRYCANCEL|MB_ICONSTOP);
if (id==IDRETRY) goto fretry;
return -1;
}
}
else
{
vrv=VerFindFile(inst_files[i].flags,dst,
NULL,inst_files[i].dstdir?inst_files[i].dstdir:
appdir,curdir,&cdlen,instdir,&inslen);
if (vrv&VFF_FILEINUSE)
{
int id=
MessageBox(w,"This file is in use and can't be"
" installed.\n"
"You may be able to close other applications\n"
"and then successfuly install.\n"
"Retry?",inst_files[i].srcfile,
MB_RETRYCANCEL|MB_ICONSTOP);
if (id==IDRETRY) goto fretry;
return -1;
}
tmplen=sizeof(tmpfile);
if (!lstrcmpi(curdir,srcdir)) *curdir='\0';
if ((vrv&VFF_CURNEDEST)&&*curdir
&&!(inst_files[i].flags&VFFF_ISSHAREDFILE))
*curdir='\0';
if (inst_files[i].dstdir)
{
if (inst_files[i].dstdir==(char *)1)
getboot(instdir); /* not supported for WIN32 */
else if (inst_files[i].dstdir==(char *)2)
GetWindowsDirectory(instdir,sizeof(instdir));
else
lstrcpy(instdir,inst_files[i].dstdir);
}
*tmpfile='\0';
lstrcpy(srcf,srcdir);
if (srcf[lstrlen(srcf)-1]!='\\') lstrcat(srcf,"\\");
lstrcat(srcf,inst_files[i].srcfile);
vrvi=VerInstallFile(inst_files[i].cflags,srcf,
dst,"",
instdir,
curdir,tmpfile,&tmplen);
if (vrvi&VIF_TEMPFILE)
{
char dfile[_MAX_PATH];
lstrcpy(dfile,instdir);
if (dfile[lstrlen(dfile)-1]!='\\')
lstrcat(dfile,"\\");
lstrcat(dfile,tmpfile);
unlink(dfile);
}
if (vrvi&(VIF_WRITEPROT|VIF_FILEINUSE|
VIF_OUTOFSPACE|VIF_ACCESSVIOLATION|
VIF_SHARINGVIOLATION|VIF_CANNOTCREATE|
VIF_CANNOTDELETE|VIF_CANNOTRENAME|
VIF_OUTOFMEMORY|VIF_CANNOTREADSRC|
VIF_CANNOTREADDST))
{
int m=0;
int id;
if (vrvi&VIF_WRITEPROT) m=1;
if (vrvi&VIF_FILEINUSE) m=2;
if (vrvi&VIF_OUTOFSPACE) m=3;
if (vrvi&VIF_ACCESSVIOLATION) m=4;
if (vrvi&VIF_SHARINGVIOLATION) m=5;
if (vrvi&VIF_CANNOTCREATE) m=6;
if (vrvi&VIF_CANNOTDELETE) m=6;
if (vrvi&VIF_CANNOTRENAME) m=6;
if (vrvi&VIF_OUTOFMEMORY) m=7;
if (vrvi&VIF_CANNOTREADSRC) m=8;
if (vrvi&VIF_CANNOTREADDST) m=6;
id=MessageBox(w,vermessage[m]
,inst_files[i].srcfile,MB_RETRYCANCEL|MB_ICONSTOP);
if (id==IDRETRY) goto fretry;
return -1;
}
} /* end of else */
}
cw_ProgressSet(prog,0xFFFF,"Copying Complete");
if (mbflag)
MessageBox(w,"Installation complete",
"Success",MB_OK|MB_ICONEXCLAMATION);
return 0;
}