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.
The VER.DLL library (a standard part of Windows) has three ways to directly support installation programs:
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:
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.
An install program must perform these basic steps:
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.)
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.
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.
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).
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.
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.
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.
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:
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.
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:
--A.W.
Table 1: Fixed VERSIONINFO resource.
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
dwFlags 0 for normal file; VFFF_ISSHAREDFILE for shared files.
szFileName Filename.
szWinDir Windows directory.
szAppDir Application's directory (destination).
szCurDir VerFindFile places the file's current location in this variable.
lpuCurDirLen Length of szCurDir array.
szDestDir VerFindFile places recommended install directory in this variable.
lpuDestDirLen Length of szDestDir array.
Table 3: VerInstallFile() parameters.
Parameter Description
dwFlags Control flags (0, VIFF_FORCEINSTALL, or VIFF_DONTDELETEOLD).
szSrcFileName Source filename.
szDestFileName Destination name.
szSrcDir Source directory.
szDestDir Destination directory.
szCurDir Directory where file currently resides.
szTmpFile Temporary filename possibly returned by VerInstallFile().
lpuTmpFileLen Length of above array.
Table 4: The _inst_files structure.
Field Description
bitmask Option bits that apply to this file.
srcfile Source filename.*
dstfile Destination filename.*
dstdir Destination directory.*
flags Flags set to VerFindFile().*
cflags Flags set to VerInstallFile().
* May take special values.
Table 5: PROPSHEETHEADER structure.
Field Description
dwSize Size of structure.
dwFlags PSH flags (see Table 6).
hwndParent Parent window.
hInstance hInstance that contains resources.
hIcon Icon handle (if PSH_USEHICON is set).
pszIcon Icon name (if PSH_USEICONID is set).
pszCaption Title to use when PSH_PROPTITLE is set.
nPages Number of tabs.
nStartPage Beginning tab number (if PSH_USEPSTARTPAGE is not set).
pStartPage Name of beginning tab (if PSH_USEPSTARTPAGE is set).
ppsp Pointer to array of property-sheet structures (if PSH_PROPSHEETPAGE is set).
phpage Pointer to array of property-sheet handles (if PSH_PROPSHEETPAGE is not set).
pfnCallback Global property-sheet callback.
Table 6: PROPSHEETPAGE structure.
Field Description
dwSize Size of structure.
dwFlags PSP_ flags (see Table 7).
hInstance Instance handle for resources.
pszTemplate Name of dialog template (if PSP_DLGINDIRECT is not set).
pResource Pointer to resource (if PSP_DLGINDIRECT is set).
hIcon Icon handle (if PSP_USEICON is set).
pszIcon Icon name (if PSP_USEICONID is set).
pszTitle Name to override template's title.
pfnDlgProc Dialog callback.
lParam 32 bits of user-defined data.
pfnCallback Called before destruction if PSP_USERELEASEFUNC is set.
pcRefParent 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 ppsp field instead of phpage field.
PSH_MULTILINETABS Use multiline tabs.
PSH_WIZARD Suppress tabs and treat dialog as a wizard.
PSH_USEPSTARTPAGE Use pStartPage 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
hwndFrom Window handle of originating control.
idFrom Window ID of originating control.
code Specific notification.
Figure 1: The installer in action.
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 install().
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;
}