Writing a Setup Application for Windows in C Herman RodentMicrosoft Developer Network Technology Group Created: February 10, 1993 Abstract This article describes how to create a Microsoft® Windows-based setup utility for a Windows-based application in C, rather than Microsoft Test TestBasic, using the graphical user interface (GUI) setup tools and libraries shipped with the Microsoft Windows version 3.1 Software Development Kit (SDK). The sample code included with this technical article contains everything you need to create a setup application in C. The following points are covered: n Creating a simple setup application n Adding support for functions not included in the sample code Introduction The Microsoft® Windows version 3.1 Software Development Kit (SDK) includes a set of tools for creating a graphical user interface (GUI) setup application. The tools include a special version of Microsoft Test, which executes a setup script written in TestBasic. Most of the functionality is actually provided by a set of dynamic-link libraries (DLLs) that has a set of function definitions provided in SETUPAPI.INC. Because an application written in C can call these DLLs, you can create an entire setup application without having to write any of it in Basic. So, if you've just finished creating your project in C and need to create a setup tool for it, but don't want to have to learn Basic, this article is what you need. The Setup Application Structure The set of DLLs that come with the Windows version 3.1 SDK includes functions to create the main setup window, do hardware detection, modify .INI files, copy files from diskette to destination disk, and create and manipulate Program Manager groups. The most interesting feature is that they create the main window and provide a message processing loop for it, so an application that uses these DLLs simply consists of a single WinMain function. Close inspection of SETUPAPI.INC, which is the TestBasic include file for GUI setup, shows that it consists of a set of global definitions followed by a set of functions and subroutines, as well as a short piece of code that calls the initialization functions. I took the essential stuff from this file and created SETUPAPI.H and SETUPAPI.C. These two files contain a subset of the GUI setup function set. SETUPAPI.H has most of the global definitions, some macros to map function names from their TestBasic equivalents to their names in the DLLs, and a few functions that validate parameters before calling a DLL function. The essential structure of WinMain becomes something like this: WinMain() { // Perform initialization. // Execute setup commands. Install(); // Perform cleanup. } As you can see, the structure is more like an MS-DOS®–based application than a Windows-based one. In order to implement some of the functions in the same way the Basic code does, I had to use a lot of goto statements and (worse still) had to use Catch and Throw to implement the equivalent of ON ERROR GOTO. The net result is code that, while it is written in C and looks like C, flows a lot like Basic. A few global variables at the start of the code determine the names of directories and other major variables. Most of the work is done by the Install function, which is where you put most of your code to copy files, set up Program Manager groups, and so on. The global variables and the steps in the Install function are described in the following sections. Global Variables The sample code uses these global variables: char *szAppName = "CSetup"; // App name char *szCaption = "'C' Setup"; // Caption char szDest[_MAX_PATH] = "C:\\BOGUS"; // Destination path char *szProgmanGroup = "C Setup"; // Progman group caption szAppName is used in the caption to message boxes that report error conditions. szCaption is the text that will show in the caption bar of the main setup window. szDest is the default destination directory that will be shown in the dialog box used to get the destination directory from the user. szProgmanGroup is the caption to be used in the Program Manager group that will be created and used to contain the application's Program Manager items. The Program Manager group is not mandatory—you may choose to install your items in some other group. Initialization Before the code calls the Install function, it presents a standard Welcome dialog to the user, followed by a dialog requesting the destination path. Control is then passed to the Install function. Here's the code that shows the Welcome dialog: Welcome: switch(UIStartDlg(szCUIDLL, WELCOME, "FInfoDlgProc", APPHELP, "FHelpDlgProc")) { case CONTINUE: UIPop(1); break; default: AskQuit(); goto Welcome; } GetPath: SetSymbolValue("EditTextIn", szDest); SetSymbolValue("EditFocus", "END"); GetPathL1: ui = UIStartDlg(szCUIDLL, DESTPATH, "FEditDlgProc", APPHELP, "FHelpDlgProc"); GetSymbolValue("EditTextOut", szDest, sizeof(szDest)); if (ui == CONTINUE) { if (IsDirWritable(szDest) == 0) { BadPath(); goto GetPathL1; } UIPop(1); } else if (ui == REACTIVATE) { goto GetPathL1; } else if ( ui == BACK) { UIPop(1); goto Welcome; } else { AskQuit(); goto GetPath; } Creating the Destination Directory First, the Install function finds out where the source files are and creates the destination directory: GetSymbolValue("STF_SRCDIR", szSrcDir, sizeof(szSrcDir)); CreateDir(szDest, cmoNone); The STF_SRCDIR variable is set by default to the disk from which setup is executed. The CreateDir call will validate the path and create the directory if it doesn't exist. Opening the Log File The main steps of the setup procedure can be recorded in a log file, which is very useful to read if the installation is aborted or fails for some reason. The Install function opens a log file and writes some initial text to it: MakePath(szDest, "LOGFILE.OUT", szTemp); OpenLogFile(szTemp, 0); WriteToLogFile(""); WriteToLogFile(" Destination directory: %s", (LPSTR)szDest); WriteToLogFile(""); Adding Files to the Copy List and Copying Files If your application has various installation options, the user needs to choose one or more before the file copy list is created. If you have separated files into various sections of SETUP.INF, it will make creating the file copy list easier. Once the options have been chosen, all sections that need to be copied are added to the copy list with calls like: AddSectionFilesToCopyList("Files", szSrcDir, szDest); This code adds all the files in the [Files] section of SETUP.INF to the copy list. Once all the relevant sections have been added to the list, the files can be copied to their destinations by calling: CopyFilesInCopyList(); Creating or Modifying .INI Files Your application may wish to add sections or individual items to WIN.INI, SYSTEM.INI, or an .INI file of its own. The sample code creates its own .INI file and makes a number of entries. Note that you could do this with the calls shown here or just use the regular SDK application programming interface (API) calls. The advantage of the calls that the setup tool kit uses is that they will handle all the file creation details. MakePath(szDest, "DEMO.INI", szIni); CreateIniKeyValue(szIni, "Section 1", "Key 1", "Value 1" , cmoNone); CreateIniKeyValue(szIni, "Section 2", "Key 2", "Value 2" , cmoNone); CreateIniKeyValue(szIni, "Section 3", "Key 3", "Value 3" , cmoNone); Creating Program Manager Groups and Items As the final stage, the code creates Program Manager groups and the items added to them. This sample code creates one group and adds two items: CreateProgmanGroup(szProgmanGroup, "", cmoNone); ShowProgmanGroup(szProgmanGroup, 1, cmoNone); MakePath(szDest, "file1.txt", szTemp); wsprintf(buf, "notepad.exe %s", (LPSTR)szTemp); CreateProgmanItem(szProgmanGroup, "File 1", buf, "", cmoOverwrite); MakePath(szDest, "file2.txt", szTemp); wsprintf(buf, "notepad.exe %s", (LPSTR)szTemp); CreateProgmanItem(szProgmanGroup, "File 2", buf, "", cmoOverwrite); This code creates two items that will open Notepad to view text files. Using More of the Basic Functions The sample code included here only implements a small subset of the APIs presented in the setup script procedure list. Adding more of them is relatively simple. The APIs fall into two categories: n Those that can be implemented simply as a macro that maps the call to a function in one of the DLLs n Those that need a small amount of code written to support them In the latter case, the code goes in SETUPAPI.C, and in both cases, function definitions go in SETUPAPI.H. The information used to create the function prototypes and macros are contained in the SETUPAPI.INC file. The following sections describe an example of a macro case and a code routine case. Using a macro depends on two things: The function in the DLL must have the same number and type of arguments as the Basic function, and none of the parameters can require validation. Many of the original Basic functions consist of a short section of code that validates the parameters, followed by a call to the DLL routine. Functions Implemented as Macros As an example of a function that can be implemented as a macro, we'll look at SetBitmap. The SetBitmap subroutine is declared in SETUPAPI.INC as: DECLARE SUB SetBitmap(szDll$, Bitmap%) If we look further down SETUPAPI.INC, we can find the implementation of the function: SUB SetBitmap(szDll$, Bitmap%) STATIC IF FSetBitmap(szDll$, Bitmap%) = 0 THEN '$ifdef DEBUG StfApiErr saeFail, "SetBitmap", szDll$+","+STR$(Bitmap%) '$endif ''DEBUG ERROR STFERR END IF END SUB As you can see, all this does is call FSetBitmap and test the result so that it can put up an error dialog if the call fails. You can see that the arguments to FSetBitmap are the same as those to SetBitmap, and this can be verified by looking up the definition of FSetBitmap in SETUPAPI.INC: DECLARE FUNCTION FSetBitmap LIB "msshlstf.dll" (szDll$, Bitmap%) AS INTEGER If we don't care to test the return value for errors, we can implement SetBitmap as a macro in SETUPAPI.H: #define SetBitmap(a,b) FSetBitmap((a),(b)) and make another entry in SETUPAPI.H for the function prototype for the FSetBitmap function: extern int FAR PASCAL FSetBitmap(LPSTR lpszDll, UINT uiBitmap); And that's all there is to it. Functions Implemented as Code Routines Some routines do parameter validation and, if you want to preserve that or if the Basic and DLL functions have different arguments, you will have to include a small routine to do the mapping. Here's a Basic routine that does parameter validation: SUB ShowProgmanGroup (szGroup$, Cmd%, cmo%) STATIC '$ifdef DEBUG if szGroup$ = "" or len(szGroup$) > 24 then BadArgErr 1, "ShowProgmanGroup", szGroup$+", " +STR$(Cmd%)+", "+STR$(cmo%) end if '$endif ''DEBUG IF FShowProgManGroup(szGroup$, STR$(Cmd%), cmo%) = 0 THEN '$ifdef DEBUG StfApiErr saeFail, "ShowProgmanGroup", szGroup$ +", "+STR$(Cmd%)+", "+STR$(cmo%) '$endif ''DEBUG ERROR STFERR END IF END SUB To implement this, the code makes an entry in SETUPAPI.H for the ShowProgmanGroup function and another entry for the FShowProgManGroup function based on their Basic definitions in SETUPAI.INC: DECLARE SUB ShowProgmanGroup (szGroup$, Cmd%, cmo%) DECLARE FUNCTION FShowProgManGroup LIB "msinsstf.dll" (szGroup$, szCmd$, cmo%) AS INTEGER Here's what the C definitions for these functions look like: extern BOOL ShowProgmanGroup(LPSTR szGroup, UINT uiCmd, UINT cmo); extern BOOL FAR PASCAL FShowProgManGroup(LPSTR szGroup, LPSTR szCmd, UINT cmo); Then, the code of the original Basic subroutine is rewritten in C as a part of SETUPAPI.C: BOOL ShowProgmanGroup(LPSTR szGroup, UINT uiCmd, UINT cmo) { char buf[256]; if (!lstrlen(szGroup) || lstrlen(szGroup) > 24) { BadArgErr(1, "ShowProgmanGroup", szGroup); } wsprintf(buf, "%u%", uiCmd); if (FShowProgManGroup(szGroup, buf, cmo) == 0) { wsprintf(buf, "%s, %u, %u", szGroup, uiCmd, cmo); StfApiErr(saeFail, "ShowProgmanGroup", buf); ERROR(STFERR); } return TRUE; } Routines Returning Strings Some Basic routines return strings, and because this is not easy to implement directly in C, I chose to modify the API slightly either to take a pointer to the return buffer or to return something else. For example, the UIStartDlg Basic function returns a string. Examining how this Basic function is used shows that there are a small fixed set of possible return strings being used effectively as constants, so I changed the function to return an integer and created a set of defines for the constant values it could return. Import Libraries The GUI setup kit includes the set of DLLs in Table 1. Table 1. DLLs in the GUI Setup Kit Name Description MSCOMSTF.DLL Common functions that support the other DLLs. MSCUISTF.DLL Custom user interface dialogs. This DLL is built by the sample code. MSDETSTF.DLL Procedures for doing hardware detection and for finding out system information. MSINSSTF.DLL Procedures for installing files to the destination disk. MSUILSTF.DLL User interface procedures. MSSHLSTF.DLL Window management functions. MSCOMSTF, MSSHLSTF, and MSUILSTF each have an import library (.LIB file) that is supplied with the SDK. MSCUISTF is built by the sample code and doesn't require an import library. The remaining two, MSINSSTF and MSDETSTF, have plenty of functions we need to use but no import library. I got around the lack of import libraries by running the EXEHDR tool on the DLLs and taking the import lists to create an IMPORTS section in CSETUP.DEF, which lists each of the entry points with its ordinal value. Sample Code The sample code includes everything you need to create a DLL for your own custom dialogs and a setup application. The make file will build the dialog DLL first and then the setup application. The sample will run as-is, creating a directory called BOGUS and copying some text files to it. The sample then creates a Program Manager group and installs two items into it. If all you need to do is copy some files, the sample has most of what you need. If the user needs to select options during setup, you will need to add some more dialogs and possibly create mappings for more of the Basic functions, such as those used for hardware detection and system information extraction.