您所在的位置:读书频道 > 设计开发 > 其它开发 > Writing Your First Windows Application

Writing Your First Windows Application

2008-08-29 10:57 Jeffrey Richter, Christophe Nasarre Microsoft Press 字号:T | T
一键收藏,随时查看,分享好友!

《Windows核心编程(第5版•英文版)》第四章介绍了进程的相关知识。本文主要描述写windows应用软件的步骤以及重要参数,介绍了进程实例控制块,命令行,环境变量,仿射性,以及系统版本。

AD:

Writing Your First Windows Application
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 prompt—CMD.EXE (for Windows Vista)—is a typical example 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 Studio 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 /SUB-SYSTEM: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 available for the application—such as when the application is started from a command prompt—and, if needed, another one is created—such as when the same CUI-based application is started from Windows Explorer. 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. As a C/C++ developer, there are two possible entry-point functions you can use:

int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);

Notice that the exact symbol depends on whether you are using Unicode strings or not. The operating system doesn't actually call the entry-point function you write. Instead, it calls a C/C++ run-time startup function implemented by the C/C++ run time and set at link time with the -entry: command-line option. 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. Table 4-1 tells you which entry point to implement in your source code and when.

Table 4-1: Application Types and Corresponding Entry Points 

Application Type

Entry Point

Startup Function Embedded in Your Executable

GUI application that wants ANSI characters and strings

_tWinMain (WinMain)

WinMainCRTStartup

GUI application that wants Unicode characters and strings

_tWinMain (wWinMain)

wWinMainCRTStartup

CUI application that wants ANSI characters and strings

_tmain (Main)

mainCRTStartup

CUI application that wants Unicode characters and strings

_tmain (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 to call 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 to call either the mainCRTStartup or wmainCRTStartup function, respectively. Again, if neither main nor wmain exist, 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 properties dialog box, and change the /SUBSYSTEM:WINDOWS switch to /SUBSYSTEM :CONSOLE in the Configuration Properties/Linker/System/SubSystem option, as shown in Figure 4-2. 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 properties 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 Studio.

Figure 4-2: Selecting a CUI subsystem for a project in the properties dialog box

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 crtexe.c file. Here's a summary of what the startup functions do:

Retrieve a pointer to the new process' full command line.

Retrieve a pointer to the new process' 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-2.

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.

Table 4-2: The C/C++ Run-Time Global Variables Available to Your Programs

Variable Name

Type

Description and Recommended Windows Function Replacement

_osver

unsigned int

The build version of the operating system. For example, Windows Vista RTM was build 6000. Thus, _osver has a value of 6000. Use GetVersionEx instead.

_winmajor

unsigned int

A major version of Windows in hexadecimal notation. For Windows Vista, the value is 6. Use GetVersionEx instead.

_winminor

unsigned int

A minor version of Windows in hexadecimal notation. For Windows Vista, the value is 0. Use GetVersionEx instead.

_winver

unsigned int

(_winmajor << 8) + _winminor. Use GetVersionEx instead.

__argc

unsigned int

The number of arguments passed on the command line. Use GetCommandLine instead.

__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. Notice that __argv is NULL if _UNICODE is defined and __wargv is NULL if it is not defined. Use GetCommandLine instead.

_environ

_wenviron

char

wchar_t

An array of pointers to ANSI/Unicode strings. Each array entry points to an environment string. Notice that _wenviron is NULL if _UNICODE is not defined and _environ is NULL if _UNICODE is defined. Use GetEnvironmentStrings or GetEnvironmentVariable instead.

_pgmptr

_wpgmptr

char

wchar_t

The ANSI/Unicode full path and name of the running program.

Notice that _pgmptr is NULL if _UNICODE is defined and _wpgmptr is NULL if it is not defined. Use GetModuleFileName, passing NULL as the first parameter, instead.

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

GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode,
(StartupInfo.dwFlags & STARTF_USESHOWWINDOW)
? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
And it is called as follows without _UNICODE defined:
GetStartupInfo(&StartupInfo);
int nMainRetVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineAnsi,
(StartupInfo.dwFlags & STARTF_USESHOWWINDOW)
? StartupInfo.wShowWindow : SW_SHOWDEFAULT);

Notice that _ImageBase is a linker defined pseudo-variable that shows where the executable file is mapped into the application memory. More details will be provided later in "A Process Instance Handle" on the next page.

If you wrote a _tmain function, it is called as follows when _UNICODE is defined:

int nMainRetVal = wmain(argc, argv, envp);
And it is called as follows when _UNICODE is not defined:
int nMainRetVal = main(argc, argv, envp);

Notice that when you generate your application through Visual Studio wizards, the third parameter (environment variable block) is not defined in your CUI-application entry point, as shown next:

int _tmain(int argc, TCHAR* argv[]);

If you need to access the environment variables of the process, simply replace the previous definition with the following:

int _tmain(int argc, TCHAR* argv[], TCHAR* env[])

This env parameter points to an array that contains all the environment variables followed by their value and separated by the equal sign (=) character. A detailed explanation of the environment variables is provided in "A Process' Environment Variables" on page 77.

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:

It calls any functions registered by calls to the _onexit function.

It calls destructors for all global and static C++ class objects.

In DEBUG builds, leaks in the C/C++ run-time memory management are listed by a call to the _CrtDumpMemoryLeaks function if the _CRTDBG_LEAK_CHECK_DF flag has been set.

It calls the operating system's ExitProcess function, passing it nMainRetVal. This causes the operating system to kill your process and set its exit code.

Notice that all these variables have been deprecated for security's sake because the code that is using them might be running before the C run-time library has the chance to initialize them. This is why you should rather directly call the corresponding functions of the Windows API.

A Process Instance Handle
Every executable or DLL file loaded into a process' address space is assigned a unique instance handle. Your executable file's instance is passed as (w)WinMain's first parameter, hInstanceExe. 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 hInstance,
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 hInstanceExe 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, which is 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 hInstanceExe parameter is the base memory address where the system loaded the executable file's image into the process' address space. For example, if the system opens the executable file and loads its contents at address 0x00400000, (w)WinMain's hInstanceExe 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 Studio linker uses a default base address of 0x00400000 for a historical reason: 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.

The GetModuleHandle function, shown next, returns the handle/base address where an executable or DLL file is loaded in the process' 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' 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. If your code is in a DLL, you have two ways to know in which module your code is running. First, you can take advantage of the pseudo-variable __ImageBase provided by the linker that points to the base address of the current running module. This is what the C run-time startup code does when it calls your (w)WinMain function, as discussed previously.

The other option is to call the GetModuleHandleEx function with GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS as the first parameter and the address of the current method as the second parameter. The pointer to an HMODULE passed as the last parameter will be filled in by GetModuleHandleEx, with the corresponding base address of the DLL containing the passed-in function. The following code presents both options:

extern "C" const IMAGE_DOS_HEADER __ImageBase;
void DumpModule() {
// Get the base address of the running application.
// Can be different from the running module if this code is in a DLL.
HMODULE hModule = GetModuleHandle(NULL);
_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"), hModule);
   // Use the pseudo-variable __ImageBase to get
// the address of the current module hModule/hInstance.
_tprintf(TEXT("with __ImageBase = 0x%x\r\n"), (HINSTANCE)&__ImageBase);
   // Pass the address of the current method DumpModule
// as parameter to GetModuleHandleEx to get the address
// of the current module hModule/hInstance.
hModule = NULL;
GetModuleHandleEx(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
(PCTSTR)DumpModule,
&hModule);
_tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n"), hModule);
}
int _tmain(int argc, TCHAR* argv[]) {
DumpModule();
return(0);
}

Keep in mind two important characteristics of the GetModuleHandle function. First, it examines only the calling process' 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 the address spaces of other processes. Second, calling GetModuleHandle and passing a value of NULL returns the base address of the executable file in the process' 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' Previous Instance Handle
As noted earlier, the C/C++ run-time startup code always passes NULL to (w)WinMain's hPrevInstance 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 _tWinMain(
HINSTANCE hInstanceExe,
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. Visual Studio has chosen a different solution: the wizard-generated C++ GUI projects defined take advantage of the UNREFERENCED_PARAMETER macro to remove these warnings, as shown in the following code snippet:

int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPTSTR    lpCmdLine,
int       nCmdShow) {
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
...
}

A Process' 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 a GUI application, it retrieves the process' complete command line by calling the GetCommandLine Windows function, skips over the executable file's name, and passes a pointer to the remainder of the command line to WinMain's pszCmdLine parameter.

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.

Following the example of the C run time, you can also obtain a pointer to your process' 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. Be aware that GetCommandLine always returns the address of the same buffer. This is another reason why you should not write into pszCmdLine: it points to the same buffer, and after you modify it, there is no way for you to know what the original command line was.

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 even though they have been deprecated. The following function declared in ShellAPI.h and exported by Shell32.dll, 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' Environment Variables
Every process has an environment block associated with it. An environment block is a block of memory allocated within the process' address space that 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. Notice that, in addition to the first =::=::\string, some other strings in the block might start with the = character. In that case, these strings are not used as environment variables, as you'll soon see in "A Process' Current Directories" on page 84.

The two ways of getting access to the environment block have been introduced already, but each one provides a different output with a different parsing. The first way retrieves the complete environment block by calling the GetEnvironmentStrings function. The format is exactly as described in the previous paragraph. The following code shows how to extract the environment variables and their content in that case:

void DumpEnvStrings() {
PTSTR pEnvBlock = GetEnvironmentStrings();
   // Parse the block with the following format:
//    =::=::\
//    =...
//    var=value\0
   //    ...
//    var=value\0\0
// Note that some other strings might begin with '='.
// Here is an example when the application is started from a network share.
//    [0] =::=::\
//    [1] =C:=C:\Windows\System32
//    [2] =ExitCode=00000000
//
TCHAR szName[MAX_PATH];
TCHAR szValue[MAX_PATH];
PTSTR pszCurrent = pEnvBlock;
HRESULT hr = S_OK;
PCTSTR pszPos = NULL;
int current = 0;
   while (pszCurrent != NULL) {
// Skip the meaningless strings like:
// "=::=::\"
if (*pszCurrent != TEXT('=')) {
// Look for '=' separator.
pszPos = _tcschr(pszCurrent, TEXT('='));
         // Point now to the first character of the value.
pszPos++;
         // Copy the variable name.
size_t cbNameLength = // Without the' ='
(size_t)pszPos - (size_t)pszCurrent - sizeof(TCHAR);
hr = StringCbCopyN(szName, MAX_PATH, pszCurrent, cbNameLength);
if (FAILED(hr)) {
break;
}
         // Copy the variable value with the last NULL character
// and allow truncation because this is for UI only.
hr = StringCchCopyN(szValue, MAX_PATH, pszPos, _tcslen(pszPos)+1);
if (SUCCEEDED(hr)) {
_tprintf(TEXT("[%u] %s=%s\r\n"), current, szName, szValue);
} else // something wrong happened; check for truncation.
if (hr == STRSAFE_E_INSUFFICIENT_BUFFER) {
_tprintf(TEXT("[%u] %s=%s...\r\n"), current, szName, szValue);
} else { // This should never occur.
_tprintf(
TEXT("[%u] %s=???\r\n"), current, szName
);
break;
}
} else {
_tprintf(TEXT("[%u] %s\r\n"), current, pszCurrent);
}
      // Next variable please.
current++;
      // Move to the end of the string.
while (*pszCurrent != TEXT('\0'))
pszCurrent++;
pszCurrent++;
      // Check if it was not the last string.
if (*pszCurrent == TEXT('\0'))
break;
};
   // Don't forget to free the memory.
FreeEnvironmentStrings(pEnvBlock);
}

The invalid strings starting with the = character are skipped. Each other valid string is parsed one by one—the = character is used as a separator between the name and the value. When you no longer need the block of memory returned by GetEnvironmentStrings, you should free it by calling FreeEnvironmentStrings:

BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock);

Note that safe string functions of the C run time are used in this code snippet to take advantage of the size calculation in bytes with StringCbCopyN and the truncation with StringCchCopyN when the value is too long for the copy buffer.

The second way of accessing the environment variables is available only for CUI applications through the TCHAR* env[] parameter received by your main entry point. Unlike what is returned by GetEnvironmentStrings, env is an array of string pointers, each one pointing to a different environment variable definition with the usual "name=value" format. A NULL pointer appears after the pointer to the last variable string as shown in the following listing:

void DumpEnvVariables(PTSTR pEnvBlock[]) {
int current = 0;
PTSTR* pElement = (PTSTR*)pEnvBlock;
PTSTR pCurrent = NULL;
while (pElement != NULL) {
pCurrent = (PTSTR)(*pElement);
if (pCurrent == NULL) {
// No more environment variable.
pElement = NULL;
} else {
_tprintf(TEXT("[%u] %s\r\n"), current, pCurrent);
current++;
pElement++;
}
}
}

Notice that the weird strings starting with the = character are removed before you receive env, so you don't have to process them yourself.

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 (Noticethespaceaftertheequalsign.)

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

When a user logs on to Windows, 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 System in Control Panel, clicking the Advanced System Settings link on the left, and clicking 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 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. However, because you don't know how many characters are needed to store the value of an environment variable, GetEnvironmentVariable returns the number of characters plus the final NULL character when 0 is passed to the cchValue parameter. The following code demonstrates how to safely use this function:

void PrintEnvironmentVariable(PCTSTR pszVariableName) {
PTSTR pszValue = NULL;
// Get the size of the buffer that is required to store the value
DWORD dwResult = GetEnvironmentVariable(pszVariableName, pszValue, 0);
   if (dwResult != 0) {
// Allocate the buffer to store the environment variable value
DWORD size = dwResult * sizeof(TCHAR);
pszValue = (PTSTR)malloc(size);
GetEnvironmentVariable(pszVariableName, pszValue, size);
_tprintf(TEXT("%s=%s\n"), pszVariableName, pszValue);
free(pszValue);
} else {
_tprintf(TEXT("'%s'=<unknown value>\n"), pszVariableName);
}
}

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

%USERPROFILE%\Documents

The portion that appears in between 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:\Users\jrichter

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

C:\Users\jrichter\Documents

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

DWORD ExpandEnvironmentStrings(
PTCSTR pszSrc,
PTSTR pszDst,
DWORD chSize);

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 chSize parameter is the maximum size of this buffer, in characters. The returned value is the size in characters of the buffer needed to store the expanded string. If the chSize parameter is less than this value, the %% variables are not expanded but replaced by empty strings. So you usually call ExpandEnvironmentStrings twice as shown in the following code snippet:

DWORD chValue =
ExpandEnvironmentStrings(TEXT("PATH='%PATH%'"), NULL, 0);
PTSTR pszBuffer = new TCHAR[chValue];
chValue = ExpandEnvironmentStrings(TEXT("PATH='%PATH%'"), pszBuffer, chValue);
_tprintf(TEXT("%s\r\n"), pszBuffer);
delete[] pszBuffer;

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' environment block.

A Process' Affinity
Normally, threads within a process can execute on any of the CPUs in the host machine. However, a process' 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, "Thread Scheduling, Priorities, and Affinities." Child processes inherit the affinity of their parent processes.

A Process' 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 shown in Table 4-3 bitwise ORed together.

Table 4-3: Flags for SetErrorMode 

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/x64 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' 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' 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' current drive and directory by calling the following two functions:

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

If the buffer you provide is not large enough, GetCurrentDirectory returns the number of characters required to store this folder, including the final '\0' character, and copies nothing into the provided buffer, which can be set to NULL in that case. When the call is successful, the length of the string in characters is returned, without counting the terminating '\0' character.

 Note  The MAX_PATH constant defined in WinDef.h as 260 is the maximum number of characters for a directory name or a filename. So it is safe to pass a buffer of MAX_PATH elements of the TCHAR type when you call GetCurrentDirectory.

A Process' Current Directories
The system keeps track of the process' current drive and directory, but it does not keep track of the current directory for every drive. However, there is some operating system support for handling current directories for multiple drives. This support is offered via the process' 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' 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' 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' 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 by calling SetEnvironmentVariable 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' current directories. Instead, the child process' 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 cchLength = GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL);

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 the Windows transacted file system feature by calling the special functions such as CreateFileTransacted. However, these functions are fully implemented only on Windows Vista.

For as long as I can remember, the Windows application programming interface (API) has had a GetVersion function:

DWORD GetVersion();

This function has quite a history behind it. It was first one 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. Because 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(POSVERSIONINFOEX 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 has been available since 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 that 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-4 describes the OSVERSIONINFOEX structure's members.

Table 4-4: 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), or VER_PLATFORM_WIN32_NT (Windows NT/Windows 2000, Windows XP, Windows Server 2003, and Windows Vista).

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 or suites 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, VER_SUITE_DATACENTER, VER_SUITE_ SINGLEUSERTS (for single terminal services session per user), VER_SUITE_PERSONAL (to make the difference between Home and Professional editions of Vista), VER_SUITE_BLADE, VER_SUITE_ EMBEDDED_RESTRICTED, VER_SUITE_SECURITY_APPLIANCE, VER_SUITE_STORAGE_SERVER, and VER_SUITE_COMPUTE_SERVER).

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.

The "Getting the System Version" page on the MSDN Web site (http://msdn2.microsoft.com/en-gb/library/ms724429.aspx) provides a very detailed code sample based on OSVERSIONINFOEX that shows you how to decipher each field of this structure.

To make things even easier, Windows Vista offers the 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 a nonzero value 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 Vista:

// Prepare the OSVERSIONINFOEX structure to indicate Windows Vista.
OSVERSIONINFOEX osver = { 0 };
osver.dwOSVersionInfoSize = sizeof(osver);
osver.dwMajorVersion = 6;
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 Vista exactly.
} else {
// The host system is NOT Windows Vista.
}
【责任编辑:阚书 TEL:(010)68476606】

回书目   上一节   

分享到:

关于windows  应用软件  GUI  CUI  函数的更多文章

  1. Linux服务器配置全程实录
  2. 揭秘--优秀PPT这样制作

热点职位

更多>>

热点专题

更多>>

读书

数据库系统工程师考试全程指导
为了满足广大考生的需要,我们组织了参与过多年资格考试命题或辅导的教师,以新的考试大纲为依据,编写了《数据库系统工程师考试

51CTO旗下网站

领先的IT技术网站 51CTO 领先的中文存储媒体 WatchStor 中国首个CIO网站 CIOage 中国首家数字医疗网站 HC3i 51CTO学院