Working with Win32 API Data

Một phần của tài liệu .NET Framework Solution In Search of the Lost Win32 API phần 1 pptx (Trang 32 - 44)

Part I: An Overview of the Win32 API

Chapter 2: Working with Win32 API Data

Overview

The goal of most operations in a computer application is data access. The function retrieves, sets, modifies, deletes, creates, or otherwise manipulates the data. With that in mind, this chapter will discuss Win32 API data—the essential part of the Win32 API experience for most developers.

There are four levels of data that the Win32 API manipulates: variables, data structures, pointers, and enumerations. This chapter discusses all four levels of data in separate sections because each type requires a different treatment within a managed application. Even a data structure, which is essentially a collection of variables and pointers, requires special handling because of the way that managed applications work.

We’ll also discuss the all−important issue of importing resources from the unmanaged environment into the managed application environment. You need to know how to perform this task to make use of the existing resources that Windows provides instead of taking a “reinvent the wheel” approach merely because the resource is inconveniently placed in a DLL.

A Short View of Data

Microsoft wrote many of the DLLs found in Windows using C, not C++ but straight C. Some of the DLLs use C++ and a very few use other languages (and we’re talking a very few). This means that you’ll have to work with C libraries to use the Win32 API in most cases. Unlike the unmanaged environment found in Visual Studio 6, Visual Studio .NET provides little in the way of wrappers that you can simply use to access the Win32 API without the frustration of working with C.

Working with C library files means converting data from the managed environment into a form that the library functions will understand. Of course, neither Visual Basic nor C# provides support for an HRESULT or a LPTSTR, which are the standard fare of C library routines. This means that you need to know the underlying data type for the C library types that you’ll encounter. For example, you’ll find that an HRESULT converts quite easily to a System.Int32 value. The problem is that you won’t know this at first because none of the documentation provided with Visual Studio tells you about conversions between managed and unmanaged types—an issue we’ll discuss throughout the book, but especially in this chapter.

In some cases, you can’t directly convert an unmanaged type to a managed type. This is always true for structures, but you’ll also run into the problem with some variable types. When this problem occurs, the .NET Framework generally provides some way to marshal the data using the [MarshalAs] attribute. You’ll find this attribute in the System.Runtime.Interop−Services namespace. Listing 2.1 shows an example of how to use the [MarshalAs] attribute, along with a few new Win32 API techniques we haven’t yet discussed. You’ll find the source code for this example in the \Chapter 02\C#\ShowMessage and \Chapter 02\VB\ShowMessage folders of the CD.

Note The example code in Listing 2.1 shows all of the potential inputs for MessageBoxEx().

However, not all of the inputs are available in every version of Windows. In fact, many of the unique features are only available in Windows 2000 and Windows XP. Make sure you check the Platform SDK documentation for potential problems when using these features in other versions of Windows. The example was tested under both Windows 2000 and

Windows XP—it doesn’t work under most versions of Windows 9x.

Listing 2.1: MessageBoxEx() Example using the [MarshalAs] Attribute

// MessageBoxEx() provides features, including a language identifier, // not found in the .NET Framework version. This function also enables // you to add special buttons and other features to the message box.

[DllImport("user32.dll", CharSet=CharSet.Auto)]

public static extern int MessageBoxEx(

IntPtr hWnd,

[MarshalAs(UnmanagedType.LPTStr)]String Message, [MarshalAs(UnmanagedType.LPTStr)]String Header, UInt32 Type,

UInt16 LanguageID);

// Create a list of buttons.

public class MBButton {

public const UInt32 MB_OK = 0x00000000;

public const UInt32 MB_OKCANCEL = 0x00000001;

public const UInt32 MB_ABORTRETRYIGNORE = 0x00000002; public const UInt32 MB_YESNOCANCEL = 0x00000003;

public const UInt32 MB_YESNO = 0x00000004;

public const UInt32 MB_RETRYCANCEL = 0x00000005;

public const UInt32 MB_CANCELTRYCONTINUE = 0x00000006; public const UInt32 MB_HELP = 0x00004000;

} // Create a list of icon types.

public class MBIcon { public const UInt32 MB_ICONHAND = 0x00000010;

public const UInt32 MB_ICONQUESTION = 0x00000020;

public const UInt32 MB_ICONEXCLAMATION = 0x00000030; public const UInt32 MB_ICONASTERISK = 0x00000040;

public const UInt32 MB_USERICON = 0x00000080;

public const UInt32 MB_ICONWARNING = MB_ICONEXCLAMATION; public const UInt32 MB_ICONERROR = MB_ICONHAND; public const UInt32 MB_ICONINFORMATION = MB_ICONASTERISK; public const UInt32 MB_ICONSTOP = MB_ICONHAND; } // Create a list of default buttons.

public class MBDefButton { public const UInt32 MB_DEFBUTTON1 = 0x00000000; public const UInt32 MB_DEFBUTTON2 = 0x00000100; public const UInt32 MB_DEFBUTTON3 = 0x00000200; public const UInt32 MB_DEFBUTTON4 = 0x00000300; } // Create a list of message box modalities.

public class MBModal { public const UInt32 MB_APPLMODAL = 0x00000000;

public const UInt32 MB_SYSTEMMODAL = 0x00001000; public const UInt32 MB_TASKMODAL = 0x00002000;

} // Create a list of special message box attributes.

public class MBSpecial { public const UInt32 MB_SETFOREGROUND = 0x00010000;

Chapter 2: Working with Win32 API Data

public const UInt32 MB_DEFAULT_DESKTOP_ONLY = 0x00020000;

public const UInt32 MB_SERVICE_NOTIFICATION_NT3X = 0x00040000;

public const UInt32 MB_TOPMOST = 0x00040000;

public const UInt32 MB_RIGHT = 0x00080000;

public const UInt32 MB_RTLREADING = 0x00100000;

public const UInt32 MB_SERVICE_NOTIFICATION = 0x00200000;

}

// Return values can use an enum in place of a class.

public enum MBReturn {

IDOK = 1, IDCANCEL = 2, IDABORT = 3, IDRETRY = 4, IDIGNORE = 5, IDYES = 6, IDNO = 7, IDCLOSE = 8, IDHELP = 9, IDTRYAGAIN = 10, IDCONTINUE = 11, IDTIMEOUT = 32000 }

private void btnTest_Click(object sender, System.EventArgs e) {

MBReturn Result; // Result of user input.

// Display a message box.

Result = (MBReturn)MessageBoxEx(this.Handle, "This is a message box.",

"Test Message Box",

MBButton.MB_CANCELTRYCONTINUE | MBButton.MB_HELP | MBIcon.MB_ICONEXCLAMATION |

MBModal.MB_SYSTEMMODAL | MBDefButton.MB_DEFBUTTON4 | MBSpecial.MB_TOPMOST, 0);

// Determine a result.

switch (Result) {

case MBReturn.IDCANCEL:

MessageBox.Show("Returned Cancel");

break;

case MBReturn.IDTRYAGAIN:

MessageBox.Show("Returned Try Again");

break;

case MBReturn.IDCONTINUE:

MessageBox.Show("Returned Continue");

break;

default:

MessageBox.Show("Couldn’t Determine Return Value");

break;

} }

private void frmMain_HelpRequested(object sender, System.Windows.Forms.HelpEventArgs hlpevent) {

Chapter 2: Working with Win32 API Data

// Display information about the help request.

MessageBox.Show("The user requested help:\r\n" + "\r\nSender: " + sender.ToString() +

"\r\nMouse Position: " + hlpevent.MousePos, "Help Requested",

MessageBoxButtons.OK,

MessageBoxIcon.Information);

// Tell Windows that the help request was handled.

hlpevent.Handled = true;

}

Yes, this is a lot of code to display a simple message box, but the MessageBoxEx() function provides a lot of functionality that you won’t find in the MessageBox.Show() function. Like MessageBox.Show(), you can associate a MessageBoxEx() message box with the current window. In fact, you have to provide the

association to make the special features such as the Help button work correctly. If you want a working Help button, you also need to include a HelpRequested() event handler for the main form—see the

frmMain_HelpRequested() method in Listing 2.1 for details.

Tip One of the problems you’ll notice with the information provided to the frmMain_Help−Requested() method is that C# doesn’t tell you who actually called the help routine. The best way to handle this problem is to set a property or field prior to the MessageBoxEx() call, and then check that value within the frmMain_HelpRequested() method. This technique helps you determine the true source of a help request, making context−sensitive help easier to provide.

The main focus of this section is the use of the [MarshalAs] attribute in the MessageBoxEx() declaration.

Notice that we need to use this attribute for both string inputs. You might see some odd output without the attribute (or the call might simply fail). As previously mentioned, you need to use an IntPtr for handles. The Type variable can include a number of inputs as shown in the btnTest_Click() method. You use it for the buttons, icons, and special features. One special feature affects the modality of the resulting message box.

We’ll discuss the various enumerations in the "Working with Enumerations" section of the chapter. The LanguageID variable doesn’t appear to have any use within the current implementation of the

MessageBoxEx() function—at least not according to the documentation. Given the amount of work Microsoft is doing with language specific features, you should expect to see this variable implemented sometime in the future.

The btnTest_Click() shows off a few of the unique features of the MessageBoxEx() function. Figure 2.1 shows the output of this code. Notice that the message box has four buttons and that we selected the Continue button as default. The first three buttons appear because of the MBButton.MB_CANCELTRYCONTINUE enumeration member, while the help button appears because of the MBButton.MB_HELP enumeration member.

Figure 2.1: The MessageBoxEx() function provides features you won’t find in MessageBox.Show().

Chapter 2: Working with Win32 API Data

One of the special features of this message box is the result of the MBSpecial.MB_TOPMOST enumeration member. No matter what you do, this message box will remain on top—you can’t hide it. The message box opens with the Help button selected due to the inclusion of the MBDefButton.MB_DEFBUTTON4

enumeration member. In addition, notice the System menu icon in the upper left corner of the message box.

This icon is the result of the MBModal_.MB_SYSTEMMODAL enumeration member. As you can see in Figure 2.2, you have access to the normal System menu functions within this message box.

Figure 2.2: The MessageBoxEx() function enables you to add a System menu to your message box.

The btnTest_Click() method checks the return value of the test message box. Notice that you can check for those special buttons. Replacing the Cancel, Try Again, and Help buttons with Abort, Retry, and Fail resulted in a "Couldn’t Determine Return Value" return value. The return values are truly unique. Let’s get back to the [MarshalAs] attribute. The [MarshalAs] attribute tells CLR how to interact with a variable. For example, you can tell CLR that you want to use a String variable as a substitute for a LPSTR, LPWSTR, LPTSTR, or BSTR variable by specifying the correct UnmanagedType enumeration value. You can also include arguments for variable type, array and safearray size, array and safearray subtype, cookies, and a custom marshaler.

Using a custom marshaler means that you can theoretically transform any managed type into an unmanaged equivalent—in practice this task is exceptionally difficult. Not only do you have the normal concerns in writing a marshaler, but you also have to consider the transition from the managed to unmanaged environment (and back in some cases). Fortunately, the need to write a custom marshaler is rare.

One final word of caution when working with the marshaler—don’t count on all languages to implement it the same way. The marshaler tends to react differently based on language because each language has different native data types. For example, accessing the MessageBoxEx() function requires additional work in Visual Basic because of language differences. Here’s the Visual Basic declaration of the same example.

<DllImport("user32.dll", _

EntryPoint:="MessageBoxExW", _ CharSet:=CharSet.Auto)> _ Public Shared Function MessageBoxEx( _ ByVal hWnd As IntPtr, _

<MarshalAs(UnmanagedType.LPTStr)> ByVal Message As String, _ <MarshalAs(UnmanagedType.LPTStr)> ByVal Header As String, _ <MarshalAs(UnmanagedType.U4)> ByVal Type As Integer, _ <MarshalAs(UnmanagedType.U4)> ByVal LanguageID As Integer) _ As Integer

End Function

Notice that in the Visual Basic version of the declaration, you must include a specific entry point or the message text will fail to print properly (you’ll see just the first letter). The <MarshalAs> attribute now appears for all input parameters except the window handle, because we have to define the input arguments as type Integer. Unlike the examples in Chapter 1, this function must be declared as Shared—simply declaring it public won’t work. The call to the MessageBoxEx()function will fail with an ambiguous error. In short, Visual Basic tends to require more precise marshaling of variables than C# does.

Chapter 2: Working with Win32 API Data

Unmanaged Resources and the Garbage Collector

There are a number of problems that developers will face when working with Win32 API data in a managed environment—not the least of which is the Garbage Collector. It’s essential to remember that the Garbage Collector is designed to work with managed data in a managed environment that the Garbage Collector can monitor. This statement points out two potential problems when working with the Win32 API.

The first problem occurs when a developer creates unmanaged data. For example, you might need to create a pointer to an interface in a COM object or create a handle to an icon that a Win32 API function can use. The Garbage Collector doesn’t know about this resource, so it can’t automatically release the resource when it goes out of scope. In short, you need to release the resource before the application terminates. Generally, you’ll create the resource using a Win32 API function so you’ll also free the resource using a Win32 API function.

The second problem occurs when you create a managed resource that you pass to a Win32 API function. The Garbage Collector will collect any resource without a reference, and it doesn’t recognize the Win32 API function’s use of the resource. Consequently, the Garbage Collector could release the resource before the Win32 API function finishes using it. To prevent this problem, you’ll normally need to create a managed reference to the resource at the same scope level as the Win32 API function use of the resource. After you release the resource used by the Win32 API function, you can also release the managed reference to it.

The Garbage Collector can also cause other odd problems with your Win32 API calls. The big problem is that you can’t view these problems in the debugger, in many cases, because the debugger creates a reference to the variables and modifies the behavior of the application. In short, troubleshooting an application can become difficult once the Garbage Collector is involved because the problems appear as “ghosts” that you’ll have a hard time tracking.

Working with Variables

Understanding the techniques for working with variables is an essential part of gaining access to the Win32 API. For example, as we saw in the “A Short View of Data” section, you can replace the handles required by many Win32 API calls with an IntPtr. However, as with marshaled data, the transition path isn’t always clear.

In some cases, you have to make decisions on how best to move data from one environment to another.

Tip The easiest way to detect problem variables to is classify them as value or reference types. Value types tend to require little translation and you can usually move them without marshaling in C#. Reference types require some type of translation in most cases. In fact, some reference types won’t move between the managed and unmanaged environments, which means you’ll need to create a conversion routine that moves the data between object properties and structure elements.

This section of the chapter discusses techniques for working with variables. Most notably, we’ll discuss techniques for converting data from managed to unmanaged types and back. While some techniques are the same no matter which language you use, other techniques require language−specific implementations. With this in mind, the following sections discuss data conversion in light of two languages, C# and Visual Basic.

Working with Variables

Converting Variables to C# Types

The difference between reference and value types becomes critical when working with C libraries. You can pass most value types such as int directly to a C library routine without any problem. In fact, the example shown in Listing 2.1 passes several value types to the C library. Enumeration also falls into the value

category. Unless an enumeration contains both positive and negative values, use the uint type to represent it in your code.

Once you get past basic value types, it’s time to convert the C library data type into something C# can

understand (and vice versa). Generally, you’ll find that pointers convert well to the IntPtr type. We’ve already discussed handles in several places, but other pointers work well as the IntPtr type. For example, pointers to numeric values such as the LPARAM also convert to the IntPtr with relative ease. Sometimes odd−looking types like MMC_COOKIE are actually long pointers in disguise, so you can use IntPtr to represent them.

Tip It occasionally helps to create an extremely small example of a Win32 API function call in Visual C++ to determine how to handle the variables in your managed code. Once you have a small working example, you can use the IDE to help you make variable conversion decisions. Hover the mouse over a value in the header file to discover how Visual C++ defines it. In most cases, you’ll see a typedef in the balloon that makes the base type of the value clear. For example, the balloon for MMC_COOKIE contains typedef LONG_PTR MMC_COOKIE, which makes it clear that you can use an IntPtr to represent _the

MMC_COOKIE. If the balloon help isn’t as helpful as you’d like, right−click the type (not the variable) and choose Go to Definition or Go to Declaration from the context menu. Generally, you can use this technique to "drill down" into the header file and find useful information for defining the data type in the managed environment.

Variable conversion requires some level of discretion. You can’t depend on a one−to−one correspondence between pointer types in the Win32 API call and your managed code. There are some situations when there’s less of a need to use an IntPtr, even if the function indicates use of a pointer. For example, if a function only requires an integer value as input, you don’t need to use an IntPtr. Using the Int in place of the IntPtr will reduce the overhead of your application by a small amount (and those small amounts can really add up). In addition, using an int reduces the complexity of the code and makes it easier to debug.

While using an int will reduce the overhead of your code, it may leave other developers scratching their heads since the use of an int is inconsistent with the use of an IntPtr in other cases. If in doubt, always use an IntPtr, but be aware that there are some situations when an int will work just as well. When you do use an int in place of an IntPtr, be sure to document the modification and your reasoning as part of the source code.

Tip The typedefs used within C header help make the code easier to read by documenting the data type.

Needless to say, when you convert a variety of Win32 API pointers to the IntPtr type, some of that documentation is lost. Generally, this means you’ll have to provide additional comments in the code.

Because you’re replicating a documented interface, function, or enumeration, you’ll want to avoid changing the variable names. The help file provided with Visual Studio can still help the function user if you maintain the same basic function name and argument names as part of your code.

Converting Variables to Visual Basic Types

Many of the same rules that you observe when converting a variable from the unmanaged environment to C#

also apply for Visual Basic. However, the actual details of the conversion will often take a different shape in the two environments. For example, Visual Basic is more sensitive to numeric values than C# in that it uses native numeric formats easier than those supplied in CLR. This sensitivity means that it’s easier to use an

Converting Variables to C# Types

Một phần của tài liệu .NET Framework Solution In Search of the Lost Win32 API phần 1 pptx (Trang 32 - 44)

Tải bản đầy đủ (PDF)

(44 trang)