Writing Your First Windows Application

[Previous] [Next]

Windows supports two types of applications: those based on a graphical user interface (GUI) and those based on a console user interface (CUI). A GUI-based application has a graphical front end. It can create windows, have menus, interact with the user via dialog boxes, and use all the standard "Windowsy" stuff. Almost all the accessory applications that ship with Windows (such as Notepad, Calculator, and WordPad) are GUI-based applications. Console-based applications are text-based. They don't usually create windows or process messages, and they don't require a graphical user interface. Although CUI-based applications are contained within a window on the screen, the window contains only text. The command shells—CMD.EXE (for Windows 2000) and COMMAND.COM (for Windows 98)—are typical examples of CUI-based applications.

The line between these two types of applications is very fuzzy. It is possible to create CUI-based applications that display dialog boxes. For example, the command shell could have a special command that causes it to display a graphical dialog box, in which you can select the command you want to execute, instead of having to remember the various commands supported by the shell. You can also create a GUI-based application that outputs text strings to a console window. I frequently create GUI-based applications that create a console window in which I can view debugging information as the application executes. You are certainly encouraged to use a GUI in your applications instead of the old-fashioned character interface, which is much less user-friendly.

When you use Microsoft Visual C++ to create an application project, the integrated environment sets up various linker switches so that the linker embeds the proper type of subsystem in the resulting executable. This linker switch is /SUBSYSTEM:CONSOLE for CUI applications and /SUBSYSTEM:WINDOWS for GUI applications. When the user runs an application, the operating system's loader looks inside the executable image's header and grabs this subsystem value. If the value indicates a CUI-based application, the loader automatically ensures that a text console window is created for the application. If the value indicates a GUI-based application, the loader doesn't create the console window and just loads the application. Once the application starts running, the operating system doesn't care what type of UI your application has.

Your Windows application must have an entry-point function that is called when the application starts running. There are four possible entry-point functions:

 int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow); int WINAPI wWinMain( HINSTANCE hinstExe, HINSTANCE, PWSTR pszCmdLine, int nCmdShow); int _ _cdecl main( int argc, char *argv[], char *envp[]); int _ _cdecl wmain int argc, wchar_t *argv[], wchar_t *envp[]); 

The operating system doesn't actually call the entry-point function you write. Instead, it calls a C/C++ run-time startup function. This function initializes the C/C++ run-time library so that you can call functions such as malloc and free. It also ensures that any global and static C++ objects that you have declared are constructed properly before your code executes. The following table tells you which entry point to implement in your source code and when.

Application Type Entry Point Startup Function Embedded in Your Executable
GUI application that wants ANSI characters and strings WinMain WinMainCRTStartup
GUI application that wants Unicode characters and strings wWinMain wWinMainCRTStartup
CUI application that wants ANSI characters and strings main mainCRTStartup
CUI application that wants Unicode characters and strings wmain wmainCRTStartup

The linker is responsible for choosing the proper C/C++ run-time startup function when it links your executable. If the /SUBSYSTEM:WINDOWS linker switch is specified, the linker expects to find either a WinMain or wWinMain function. If neither of these functions is present, the linker returns an "unresolved external symbol" error; otherwise, it chooses either the WinMainCRTStartup or wWinMainCRTStartup function, respectively.

Likewise, if the /SUBSYSTEM:CONSOLE linker switch is specified, the linker expects to find either a main or wmain function and chooses either the mainCRTStartup or wmainCRTStartup function, respectively. Again, if neither main nor wmain exists, the linker returns an "unresolved external symbol" error.

However, it is a little-known fact that you can remove the /SUBSYSTEM linker switch from your project altogether. When you do this, the linker automatically determines which subsystem your application should be set to. When linking, the linker checks to see which of the four functions (WinMain, wWinMain, main, or wmain) is present in your code and then infers which subsystem your executable should be and which C/C++ startup function should be embedded in your executable.

One mistake that new Windows/Visual C++ developers commonly make is to accidentally select the wrong project type when they create a new project. For example, a developer might create a new Win32 Application project but create an entry-point function of main. When building the application, the developer will get a linker error because a Win32 Application project sets the /SUBSYSTEM:WINDOWS linker switch but no WinMain or wWinMain function exists. At this point, the developer has four options:

  • Change the main function to WinMain. This is usually not the best choice because the developer probably wants to create a console application.
  • Create a new Win32 Console Application in Visual C++ and add the existing source code modules to the new project. This option is tedious because it feels like you're starting over and you have to delete the original project file.
  • Click on the Link tab of the Project Settings dialog box and change the /SUBSYSTEM:WINDOWS switch to /SUBSYSTEM :CONSOLE. This is an easy way to fix the problem; few people are aware that this is all they have to do.
  • Click on the Link tab of the Project Settings dialog box and delete the /SUBSYSTEM:WINDOWS switch entirely. This is my favorite choice because it gives you the most flexibility. Now, the linker will simply do the right thing based on which function you implement in your source code. I have no idea why this isn't the default when you create a new Win32 Application or Win32 Console Application project with Visual C++'s Developer Studio.

All of the C/C++ run-time startup functions do basically the same thing. The difference is in whether they process ANSI or Unicode strings and which entry-point function they call after they initialize the C run-time library. Visual C++ ships with the source code to the C run-time library. You can find the code for the four startup functions in the CRt0.c file; I'll summarize here what the startup functions do:

  • Retrieve a pointer to the new process's full command line.
  • Retrieve a pointer to the new process's environment variables.
  • Initialize the C/C++ run time's global variables. Your code can access these variables if you include StdLib.h. The variables are listed in Table 4-1.
  • Initialize the heap used by the C run-time memory allocation functions (malloc and calloc) and other low-level input/output routines.
  • Call constructors for all global and static C++ class objects.

After all of this initialization, the C/C++ startup function calls your application's entry-point function. If you wrote a wWinMain function, it is called as follows:

 GetStartupInfo(&StartupInfo); int nMainRetVal = wWinMain(GetModuleHandle(NULL), NULL, pszCommandLineUnicode, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT); 

If you wrote a WinMain function, it is called as follows:

 GetStartupInfo(&StartupInfo); int nMainRetVal = WinMainGetModuleHandle(NULL), NULL, pszCommandLineAnsi, (StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT); 

If you wrote a wmain function, it is called as follows:

 int nMainRetVal = wmain(_ _argc, _ _wargv, _wenviron); 

If you wrote a main function, it is called as follows:

 int nMainRetVal = main(_ _argc, _ _argv, _environ); 

When your entry-point function returns, the startup function calls the C run-time exit function, passing it your return value (nMainRetVal). The exit function does the following:

  • Calls any functions registered by calls to the _onexit function.
  • Calls destructors for all global and static C++ class objects.
  • Calls the operating system's ExitProcess function, passing it nMainRetVal. This causes the operating system to kill your process and set its exit code.

Table 4-1. The C/C++ run-time global variables that are available to your programs

Variable Name Type Description
_osver unsigned int The build version of the operating system. For example, Windows 2000 Beta 3 was build 2031. Thus, _osver has a value of 2031.
_winmajor unsigned int A major version of Windows in hexadecimal notation. For Windows 2000, the value is 5.
_winminor unsigned int A minor version of Windows in hexadecimal notation. For Windows 2000, the value is 0.
_winver unsigned int (_winmajor << 8) + _winminor
_ _argc unsigned int The number of arguments passed on the command line.
_ _argv
_ _wargv
char **
wchar_t **
An array of size _ _argc with pointers to ANSI/Unicode strings. Each array entry points to a command-line argument.
_environ
_wenviron
char **
wchar_t **
An array of pointers to ANSI/Unicode strings. Each array entry points to an environment string.
_pgmptr
_wpgmptr
char *
wchar_t *
The ANSI/Unicode full path and name of the running program.

A Process's Instance Handle

Every executable or DLL file loaded into a process's address space is assigned a unique instance handle. Your executable file's instance is passed as (w)WinMain's first parameter, hinstExe. The handle's value is typically needed for calls that load resources. For example, to load an icon resource from the executable file's image, you need to call this function:

 HICON LoadIcon( HINSTANCE hinst, PCTSTR pszIcon); 

The first parameter to LoadIcon indicates which file (executable or DLL) contains the resource you want to load. Many applications save (w)WinMain's hinstExe parameter in a global variable so that it is easily accessible to all the executable file's code.

The Platform SDK documentation states that some functions require a parameter of the type HMODULE. An example is the GetModuleFileName function, shown here:

 DWORD GetModuleFileName( HMODULE hinstModule, PTSTR pszPath, DWORD cchPath); 

NOTE
As it turns out, HMODULEs and HINSTANCEs are exactly the same thing. If the documentation for a function indicates that an HMODULE is required, you can pass an HINSTANCE, and vice versa. There are two data types because in 16-bit Windows HMODULEs and HINSTANCEs identified different things.

The actual value of (w)WinMain's hinstExe parameter is the base memory address where the system loaded the executable file's image into the process's address space. For example, if the system opens the executable file and loads its contents at address 0x00400000, (w)WinMain's hinstExe parameter has a value of 0x00400000.

The base address where an executable file's image loads is determined by the linker. Different linkers can use different default base addresses. The Visual C++ linker uses a default base address of 0x00400000 because this is the lowest address an executable file image can load to when you run Windows 98. You can change the base address that your application loads to by using the /BASE: address linker switch for Microsoft's linker.

If you attempt to load an executable that has a base address below 0x00400000 on Windows 98, the Windows 98 loader must relocate the executable to a different address. This increases the loading time of the application, but at least the application can run. If you are developing an application that will run on both Windows 98 and Windows 2000, you should make sure that the application's base address is at 0x00400000 or above.

The GetModuleHandle function, shown below, returns the handle/base address where an executable or DLL file is loaded in the process's address space:

 HMODULE GetModuleHandle(PCTSTR pszModule); 

When you call this function, you pass a zero-terminated string that specifies the name of an executable or DLL file loaded into the calling process's address space. If the system finds the specified executable or DLL name, GetModuleHandle returns the base address where that executable or DLL's file image is loaded. The system returns NULL if it cannot find the file. You can also call GetModuleHandle, passing NULL for the pszModule parameter; GetModuleHandle returns the calling executable file's base address. This is what the C run-time startup code does when it calls your (w)WinMain function, as discussed previously.

Keep in mind two important characteristics of the GetModuleHandle function. First, it examines only the calling process's address space. If the calling process does not use any common dialog functions, calling GetModuleHandle and passing it "ComDlg32" causes NULL to be returned even though ComDlg32.dll is probably loaded into other processes' address spaces. Second, calling GetModuleHandle and passing a value of NULL returns the base address of the executable file in the process's address space. So even if you call GetModuleHandle(NULL) from code that is contained inside a DLL, the value returned is the executable file's base address—not the DLL file's base address.

A Process's Previous Instance Handle

As noted earlier, the C/C++ run-time startup code always passes NULL to (w)WinMain's hinstExePrev parameter. This parameter was used in 16-bit Windows and remains a parameter to (w)WinMain solely to ease porting of 16-bit Windows applications. You should never reference this parameter inside your code. For this reason, I always write my (w)WinMain functions as follows:

 int WINAPI WinMain( HINSTANCE hinstExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow); 

Because no parameter name is given for the second parameter, the compiler does not issue a "parameter not referenced" warning.

A Process's Command Line

When a new process is created, it is passed a command line. The command line is almost never blank; at the very least, the name of the executable file used to create the new process is the first token on the command line. However, as you'll see later when we discuss the CreateProcess function, a process can receive a command line that consists of a single character: the string-terminating zero. When the C run time's startup code begins executing, it retrieves the process's command line, skips over the executable file's name, and passes a pointer to the remainder of the command line to WinMain's pszCmdLine parameter.

It's important to note that the pszCmdLine parameter always points to an ANSI string. However, if you change WinMain to wWinMain, you can access a Unicode version of your process's command line.

An application can parse and interpret the command-line string any way it chooses. You can actually write to the memory buffer pointed to by the pszCmdLine parameter—but you should not, under any circumstances, write beyond the end of the buffer. Personally, I always consider this a read-only buffer. If I want to make changes to the command line, I first copy the command-line buffer to a local buffer in my application, and then I modify my local buffer.

You can also obtain a pointer to your process's complete command line by calling the GetCommandLine function:

 PTSTR GetCommandLine(); 

This function returns a pointer to a buffer containing the full command line, including the full pathname of the executed file.

Many applications prefer to have the command line parsed into its separate tokens. An application can gain access to the command line's individual components by using the global _ _argc and _ _argv (or __wargv) variables. The following function, CommandLineToArgvW, separates any Unicode string into its separate tokens:

 PWSTR CommandLineToArgvW( PWSTR pszCmdLine, int* pNumArgs); 

As the W at the end of the function name implies, this function exists in a Unicode version only. (The W stands for wide.) The first parameter, pszCmdLine, points to a command-line string. This is usually the return value from an earlier call to GetCommandLineW. The pNumArgs parameter is the address of an integer; the integer is set to the number of arguments in the command line. CommandLineToArgvW returns the address to an array of Unicode string pointers.

CommandLineToArgvW allocates memory internally. Most applications do not free this memory—they count on the operating system to free it when the process terminates. This is totally acceptable. However, if you want to free the memory yourself, the proper way to do so is by calling HeapFree as follows:

 int nNumArgs; PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgs); // Use the arguments… if (*ppArgv[1] == L'x') {   } // Free the memory block HeapFree(GetProcessHeap(), 0, ppArgv); 

A Process's Environment Variables

Every process has an environment block associated with it. An environment block is a block of memory allocated within the process's address space. Each block contains a set of strings with the following appearance:

 VarName1=VarValue1\0 VarName2=VarValue2\0 VarName3=VarValue3\0  VarNameX=VarValueX\0 \0 

The first part of each string is the name of an environment variable. This is followed by an equal sign, which is followed by the value you want to assign to the variable. All strings in the environment block must be sorted alphabetically by environment variable name.

Because the equal sign is used to separate the name from the value, an equal sign cannot be part of the name. Also, spaces are significant. For example, if you declare the following two variables and then compare the value of XYZ with the value of ABC, the system will report that the two variables are different because any white space that appears immediately before or after the equal sign is taken into account.

 XYZ= Windows (Notice the space after the equal sign.) ABC=Windows 

For example, if you were to add the following two strings to the environment block, the environment variable XYZ with a space after it would contain Home and the environment variable XYZ without the space would contain Work.

 XYZ =Home (Notice the space before the equal sign.) XYZ=Work 

Finally, you must place an additional 0 character at the end of all the environment variables to mark the end of the block.

Windows 98

To create an initial set of environment variables for Windows 98, you must modify the system's AutoExec.bat file by placing a series of SET lines in the file. Each line must be in the following form:

 SET VarName=VarValue 

When you reboot your system, the contents of the AutoExec.bat file are parsed, and any environment variables you have set will be available to any processes you invoke during your Windows 98 session.

Windows 2000

When a user logs on to Windows 2000, the system creates the shell process and associates a set of environment strings with it. The system obtains the initial set of environment strings by examining two keys in the Registry.

The first key contains the list of all environment variables that apply to the system:

 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\ Session Manager\Environment 

The second key contains the list of all environment variables that apply to the user currently logged on:

 HKEY_CURRENT_USER\Environment 

A user can add, delete, or change any of these entries by selecting the Control Panel's System applet, clicking on the Advanced tab, and clicking on the Environment Variables button to bring up the following dialog box:

Only a user who has administrator privileges can alter the variables contained in the System Variables list.

Your application can also use the various Registry functions to modify these Registry entries. However, for the changes to take effect for all applications, the user must log off and then log back on. Some applications, such as Explorer, Task Manager, and the Control Panel, can update their environment block with the new Registry entries when their main windows receive a WM_SETTINGCHANGE message. For example, if you update the Registry entries and want to have the interested applications update their environment blocks, you can make the following call:

 SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) TEXT("Environment")); 

Normally, a child process inherits a set of environment variables that are the same as those of its parent process. However, the parent process can control what environment variables a child inherits, as you'll see later when we discuss the CreateProcess function. By inherit, I mean that the child process gets its own copy of the parent's environment block; the child and parent do not share the same block. This means that a child process can add, delete, or modify a variable in its block and the change will not be reflected in the parent's block.

An application usually uses environment variables to let the user fine-tune its behavior. The user creates an environment variable and initializes it. Then, when the user invokes the application, the application examines the environment block for the variable. If it finds the variable, it parses the value of the variable and adjusts its own behavior.

The problem with environment variables is that they are not easy for users to set or to understand. Users need to spell variable names correctly, and they must also know the exact syntax expected of the variable's value. Most (if not all) graphical applications, on the other hand, allow users to fine-tune an application's behavior using dialog boxes. This approach is far more user-friendly.

If you still want to use environment variables, there are a few functions that your applications can call. The GetEnvironmentVariable function allows you to determine the existence and value of an environment variable:

 DWORD GetEnvironmentVariable( PCTSTR pszName, PTSTR pszValue, DWORD cchValue); 

When calling GetEnvironmentVariable, pszName points to the desired variable name, pszValue points to the buffer that will hold the variable's value, and cchValue indicates the size of the buffer in characters. The function returns either the number of characters copied into the buffer or 0 if the variable name cannot be found in the environment.

Many strings contain replaceable strings within them. For example, I found this string somewhere in the registry:

 %USERPROFILE%\My Documents 

The portion in percent signs (%) indicates a replaceable string. In this case, the value of the environment variable, USERPROFILE, should be placed in the string. On my machine, the value of my USERPROFILE environment variable is:

 C:\Documents and Settings\Administrator 

So, after performing the string replacement, the resulting string becomes:

 C:\Documents and Settings\Administrator\My Documents 

Because this type of string replacement is common, Windows offers the ExpandEnvironmentStrings function:

 DWORD ExpandEnvironmentStrings( PCSTR pszSrc, PSTR pszDst, DWORD nSize); 

When you call this function, the pszSrc parameter is the address of the string that contains replaceable environment variable strings. The pszDst parameter is the address of the buffer that will receive the expanded string, and the nSize parameter is the maximum size of this buffer, in characters.

Finally, you can use the SetEnvironmentVariable function to add a variable, delete a variable, or modify a variable's value:

 BOOL SetEnvironmentVariable( PCTSTR pszName, PCTSTR pszValue); 

This function sets the variable identified by the pszName parameter to the value identified by the pszValue parameter. If a variable with the specified name already exists, SetEnvironmentVariable modifies the value. If the specified variable doesn't exist, the variable is added and, if pszValue is NULL, the variable is deleted from the environment block.

You should always use these functions for manipulating your process's environment block. As I said earlier, the strings in an environment block must be sorted alphabetically by variable name so that GetEnvironmentVariable can locate them faster. The SetEnvironmentVariable function is smart enough to keep the environment variables in sorted order.

A Process's Affinity

Normally, threads within a process can execute on any of the CPUs in the host machine. However, a process's threads can be forced to run on a subset of the available CPUs. This is called processor affinity and is discussed in detail in Chapter 7. Child processes inherit the affinity of their parent processes.

A Process's Error Mode

Associated with each process is a set of flags that tells the system how the process should respond to serious errors, which include disk media failures, unhandled exceptions, file-find failures, and data misalignment. A process can tell the system how to handle each of these errors by calling the SetErrorMode function:

 UINT SetErrorMode(UINT fuErrorMode); 

The fuErrorMode parameter is a combination of any of the flags in the following table bitwise ORed together.

Flag Description
SEM_FAILCRITICALERRORS The system does not display the critical-error-handler message box and returns the error to the calling process.
SEM_NOGPFAULTERRORBOX The system does not display the general-protection-fault message box. This flag should be set only by debugging applications that handle general protection (GP) faults themselves with an exception handler.
SEM_NOOPENFILEERRORBOX The system does not display a message box when it fails to find a file.
SEM_NOALIGNMENTFAULTEXCEPT The system automatically fixes memory alignment faults and makes them invisible to the application. This flag has no effect on x86 processors.

By default, a child process inherits the error mode flags of its parent. In other words, if a process has the SEM_NOGPFAULTERRORBOX flag turned on and then spawns a child process, the child process will also have this flag turned on. However, the child process is not notified of this, and it might not have been written to handle GP fault errors. If a GP fault occurs in one of the child's threads, the child process might terminate without notifying the user. A parent process can prevent a child process from inheriting its error mode by specifying the CREATE_DEFAULT_ERROR_MODE flag when calling CreateProcess. (We'll discuss CreateProcess later in this chapter.)

A Process's Current Drive and Directory

When full pathnames are not supplied, the various Windows functions look for files and directories in the current directory of the current drive. For example, if a thread in a process calls CreateFile to open a file (without specifying a full pathname), the system looks for the file in the current drive and directory.

The system keeps track of a process's current drive and directory internally. Because this information is maintained on a per-process basis, a thread in the process that changes the current drive or directory changes this information for all the threads in the process.

A thread can obtain and set its process's current drive and directory by calling the following two functions:

 DWORD GetCurrentDirectory( DWORD cchCurDir, PTSTR pszCurDir); BOOL SetCurrentDirectory(PCTSTR pszCurDir); 

A Process's Current Directories

The system keeps track of the process's current drive and directory, but it does not keep track of the current directory for each and every drive. However, there is some operating system support for handling current directories for multiple drives. This support is offered via the process's environment strings. For example, a process can have two environment variables, as shown here:

 =C:=C:\Utility\Bin =D:=D:\Program Files 

These variables indicate that the process's current directory for drive C is \Utility\Bin and that its current directory for drive D is \Program Files.

If you call a function, passing a drive-qualified name indicating a drive that is not the current drive, the system looks in the process's environment block for the variable associated with the specified drive letter. If the variable for the drive exists, the system uses the variable's value as the current directory. If the variable does not exist, the system assumes that the current directory for the specified drive is its root directory.

For example, if your process's current directory is C:\Utility\Bin and you call CreateFile to open D:ReadMe.Txt, the system looks up the environment variable =D:. Because the =D: variable exists, the system attempts to open the ReadMe.Txt file from the D:\Program Files directory. If the =D: variable did not exist, the system would attempt to open the ReadMe.Txt file from the root directory of drive D. The Windows file functions never add or change a drive-letter environment variable—they only read the variables.

NOTE
You can use the C run-time function _chdir instead of the Windows SetCurrentDirectory function to change the current directory. The _chdir function calls SetCurrentDirectory internally, but _chdir also adds or modifies the environment variables so that the current directory of different drives is preserved.

If a parent process creates an environment block that it wants to pass to a child process, the child's environment block does not automatically inherit the parent process's current directories. Instead, the child process's current directories default to the root directory of every drive. If you want the child process to inherit the parent's current directories, the parent process must create these drive-letter environment variables and add them to the environment block before spawning the child process. The parent process can obtain its current directories by calling GetFullPathName:

 DWORD GetFullPathName( PCTSTR pszFile, DWORD cchPath, PTSTR pszPath, PTSTR *ppszFilePart); 

For example, to get the current directory for drive C, you call GetFullPathName as follows:

 TCHAR szCurDir[MAX_PATH]; DWORD GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL); 

Keep in mind that a process's environment variables must always be kept in alphabetical order. As a result, the drive letter environment variables usually must be placed at the beginning of the environment block.

The System Version

Frequently, an application needs to determine which version of Windows the user is running. For example, an application might take advantage of security features by calling the security functions. However, these functions are fully implemented only on Windows 2000.

For as long as I can remember, the Windows API has had a GetVersion function:

 DWORD GetVersion(); 

This function has quite a history behind it. It was first designed for 16-bit Windows. The idea was simple—to return the MS-DOS version number in the high-word and return the Windows version number in the low-word. For each word, the high-byte would represent the major version number and the low-byte would represent the minor version number.

Unfortunately, the programmer who wrote this code made a small mistake, coding the function so that the Windows version numbers were reversed—the major version number was in the low-byte and the minor number was in the high-byte. Since many programmers had already started using this function, Microsoft was forced to leave the function as it was and change the documentation to reflect the mistake.

Because of all the confusion surrounding GetVersion, Microsoft added a new function, GetVersionEx:

 BOOL GetVersionEx(POSVERSIONINFO pVersionInformation); 

This function requires you to allocate an OSVERSIONINFOEX structure in your application and pass the structure's address to GetVersionEx. The OSVERSIONINFOEX structure is shown here:

 typedef struct { DWORD dwOSVersionInfoSize; DWORD dwMajorVersion; DWORD dwMinorVersion; DWORD dwBuildNumber; DWORD dwPlatformId; TCHAR szCSDVersion[128]; WORD wServicePackMajor; WORD wServicePackMinor; WORD wSuiteMask; BYTE wProductType; BYTE wReserved; } OSVERSIONINFOEX, *POSVERSIONINFOEX; 

The OSVERSIONINFOEX structure is new in Windows 2000. Other versions of Windows use the older OSVERSIONINFO structure, which does not have the service pack, suite mask, product type, and reserved members.

Notice that the structure has different members for each component of the system's version number. This was done so programmers would not have to bother with extracting low-words, high-words, low-bytes, and high-bytes, which should make it much easier for applications to compare their expected version number with the host system's version number. Table 4-2 describes the OSVERSIONINFOEX structure's members.

Table 4-2. The OSVERSIONINFOEX structure's members

Member Description
dwOSVersionInfoSize Must be set to sizeof(OSVERSIONINFO) or sizeof(OSVERSIONINFOEX) prior to calling the GetVersionEx function.
dwMajorVersion Major version number of the host system.
dwMinorVersion Minor version number of the host system.
dwBuildNumber Build number of the current system.
dwPlatformId Identifies the platform supported by the current system. This can be VER_PLATFORM_WIN32s (Win32s), VER_PLATFORM_WIN32_WINDOWS (Windows 95/Windows 98), VER_PLATFORM_WIN32_NT (Windows NT/Windows 2000), or VER_PLATFORM_WIN32_CEHH (Windows CE).
szCSDVersion This field contains additional text that provides further information about the installed operating system.
wServicePackMajor Major version number of latest installed service pack.
wServicePackMinor Minor version number of latest installed service pack.
wSuiteMask Identifies which suite(s) are available on the system
(VER_SUITE_SMALLBUSINESS,
VER_SUITE_ENTERPRISE,
VER_SUITE_BACKOFFICE,
VER_SUITE_COMMUNICATIONS,
VER_SUITE_TERMINAL,
VER_SUITE_SMALLBUSINESS_RESTRICTED,
VER_SUITE_EMBEDDEDNT,and
VER_SUITE_DATACENTER).
wProductType Identifies which one of the following operating system products is installed: VER_NT_WORKSTATION, VER_NT_SERVER, or VER_NT_DOMAIN_CONTROLLER.
wReserved Reserved for future use.

To make things even easier, Windows 2000 offers a new function, VerifyVersionInfo, which compares the host system's version with the version your application requires:

 BOOL VerifyVersionInfo( POSVERSIONINFOEX pVersionInformation, DWORD dwTypeMask, DWORDLONG dwlConditionMask); 

To use this function, you must allocate an OSVERSIONINFOEX structure, initialize its dwOSVersionInfoSize member to the size of the structure, and then initialize any other members of the structure that are important to your application. When you call VerifyVersionInfo, the dwTypeMask parameter indicates which members of the structure you have initialized. The dwTypeMask parameter is any of the following flags ORed together: VER_MINORVERSION, VER_MAJORVERSION, VER_BUILDNUMBER, VER_PLATFORMID, VER_SERVICEPACKMINOR, VER_SERVICEPACKMAJOR, VER_SUITENAME, and VER_PRODUCT_TYPE. The last parameter, dwlConditionMask, is a 64-bit value that controls how the function compares the system's version information to your desired information.

The dwlConditionMask describes the comparison using a complex set of bit combinations. To create the desired bit combination, you use the VER_SET_CONDITION macro:

 VER_SET_CONDITION( DWORDLONG dwlConditionMask, ULONG dwTypeBitMask, ULONG dwConditionMask) 

The first parameter, dwlConditionMask, identifies the variable whose bits you are manipulating. Note that you do not pass the address of this variable because VER_SET_CONDITION is a macro, not a function. The dwTypeBitMask parameter indicates a single member in the OSVERSIONINFOEX structure that you want to compare. To compare multiple members, you must call VER_SET_CONDITION multiple times, once for each member. The flags you pass to VerifyVersionInfo's dwTypeMask parameter (VER_MINORVERSION, VER_BUILDNUMBER, and so on) are the same flags that you use for VER_SET_CONDITION's dwTypeBitMask parameter.

VER_SET_CONDITION's last parameter, dwConditionMask, indicates how you want the comparison made. This can be one of the following values: VER_EQUAL, VER_GREATER, VER_GREATER_EQUAL, VER_LESS, or VER_LESS_EQUAL. Note that you can use these values when comparing VER_PRODUCT_TYPE information. For example, VER_NT_WORKSTATION is less than VER_NT_SERVER. However, for the VER_SUITENAME information, you cannot use these test values. Instead, you must use VER_AND (all suite products must be installed) or VER_OR (at least one of the suite products must be installed).

After you build up the set of conditions, you call VerifyVersionInfo and it returns nonzero if successful (if the host system meets all of your application's requirements). If VerifyVersionInfo returns 0, the host system does not meet your requirements or you called the function improperly. You can determine why the function returned 0 by calling GetLastError. If GetLastError returns ERROR_OLD_WIN_VERSION, you called the function correctly but the system doesn't meet your requirements.

Here is an example of how to test whether the host system is exactly Windows 2000:

 // Prepare the OSVERSIONINFOEX structure to indicate Windows 2000. OSVERSIONINFOEX osver = { 0 }; osver.dwOSVersionInfoSize = sizeof(osver); osver.dwMajorVersion = 5; osver.dwMinorVersion = 0; osver.dwPlatformId = VER_PLATFORM_WIN32_NT; // Prepare the condition mask. DWORDLONG dwlConditionMask = 0;     // You MUST initialize this to 0. VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL); VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL); VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL); // Perform the version test. if (VerifyVersionInfo(&osver, VER_MAJORVERSION | VER_MINORVERSION | VER_PLATFORMID, dwlConditionMask)) { // The host system is Windows 2000 exactly. } else { // The host system is NOT Windows 2000. } 



Programming Applications for Microsoft Windows
Programming Applications for Microsoft Windows (Microsoft Programming Series)
ISBN: 1572319968
EAN: 2147483647
Year: 1999
Pages: 193

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net