Cutting Edge

Using Windows Hooks to Enhance MessageBox in .NET

Dino Esposito

Code download available at:CuttingEdge0211.exe(105 KB)

Contents

The New Programming Interface
The MessageBox Class
Handling MessageBox Events
The WindowCreated Event
The WindowActivated Event
Replacing the Icon
Replacing the Static Text
The WindowDestroyed Event
Putting It All Together
Conclusion

It's fairly safe to assume that all programmers working in Windows® environments are familiar with MessageBox. The MessageBox function displays a message through a popup dialog box and lets programmers add dialog buttons and icons to the message to improve user feedback. Although it's flexible and takes many parameters, in many real-world scenarios you simply need to write your own version of MessageBox. Some of the shortcomings of MessageBox are restrictions on the icon and the buttons you can use. The icon can only be one of the predefined icons and the buttons can have only one of the built-in layouts. Microsoft® Office and Visual Studio® (not to mention other third-party apps) make use of customized versions of MessageBox.

Writing a full replacement for MessageBox is not rocket science. In essence, MessageBox is merely a modal dialog box or a Windows Form in Microsoft .NET. In the .NET Framework, the MessageBox object, part of the System.Windows.Forms namespace, provides the functionalities of the Win32® MessageBox API function. The MessageBox object provides the Show method, which works like the old API function. More precisely, all the overloads of the static method Show are simply managed wrappers for the underlying Win32 API. The .NET Framework provides a more programmer-friendly, object-oriented interface, but the functionality is nearly identical.

In this column I'll build a custom .NET Framework class that fully replaces the MessageBox object, filling in some of its gaps. I won't write a new form from scratch; I'll write a piece of code that intercepts window events (creation, destruction, activation) and modifies the structure of the MessageBox dialog window on the fly. One of the advantages of this solution is that you don't have to become familiar with a new programming interface. But more importantly, an enhanced dialog box engine is created without rewriting the MessageBox function from scratch. After following my implementation of the MsdnMag.MessageBox class, you should have a good understanding of the concepts and be able to proceed with your own changes and extensions. The key technology that I'll use here is Windows hooks.

As you probably know, Windows hooks is among the few Windows-related technologies for which the .NET Framework doesn't yet provide a managed wrapper class or a full common language runtime (CLR) implementation. In my last column, I introduced a base class called WindowsHook that represents a generic Windows hook. You build your own hook classes by deriving from WindowsHook and specifying the type of hook you want (WH_CBT, WH_MOUSE, and so forth). After that, installing a managed hook is as easy as instantiating the new class and calling the parameterless Install method.

In this project, I'll use a WH_CBT hook to build an infrastructure that listens to the window events related to creating and activating the MessageBox dialog box. In the recesses of the fired events, I'll inject some code to modify layout and contents of the dialog as appropriate. My goals are to allow the programmer to use a property to indicate the icon that will be displayed and to make the displayed text selectable with the mouse cursor. Other possible enhancements are customizing text and ID of the buttons employed and making the text scrollable if it's too long for the box. (You can download the code at the link at the top of this article.)

The New Programming Interface

Let's start at the end and have a look at the code that an app would need to use the new MsdnMag.MessageBox class:

MsdnMag.MessageBox msg = new MsdnMag.MessageBox(); msg.IconFile = "shell32.dll"; msg.IconIndex = 41; DialogResult dr = msg.Show("Hello, managed world!", "Cutting Edge", MessageBoxButtons.OK); Console.WriteLine(dr.ToString());

The original Windows Forms MessageBox is a singleton object, with a single instance of the class to service all requests. The Show method is declared static and you don't need to explicitly instantiate the MessageBox object in order to show a popup. When writing the MsdnMag.MessageBox class, I opted for an instance-based approach because the overall programming interface of the class allows for properties and state. For this reason, you need an instance of the class to show dialog boxes. However, there is no need to create a new instance each time you have a message to display. A global instance created when the application first loads will fit the bill in most cases.

Figure 1 Custom Icon

Figure 1** Custom Icon **

In the code snippet shown earlier, notice the lack of an explicit icon style in the Show method's signature. The icon to display is identified by the pair of properties called IconFile and IconIndex. IconFile denotes the source file (.ico, .dll, .exe); IconIndex is a zero-based index that points to a particular icon in the file. Icon files (*.ico) normally (but not necessarily) contain only one icon; for them, you set the IconIndex property to 0. Figure 1 shows the output of the previous code snippet. As you can see, the dialog box contains a custom icon; the text is selectable and can be copied to the clipboard or passed on to the controls of other applications. The Show method has several overloads and tries to mimic the interface of the Show method on the Windows Forms MessageBox class.

DialogResult Show(string text) DialogResult Show(string text, string title) DialogResult Show(string text, string title, MessageBoxButtons buttons) DialogResult Show(string text, string title, MessageBoxButtons buttons, MessageBoxIcon icon)

The first three overloads default to the fourth for the actual implementation. By default, the OK button is displayed with no icon, as shown in Figure 1.

The MessageBox Class

The MessageBox class uses a WH_CBT hook under the hood. The hook allows the class—the Show method in particular—to get involved in the dialog box creation. The core code of the Show method is listed here:

DialogResult dr; m_cbt.Install(); dr = System.Windows.Forms.MessageBox.Show( text, title, buttons, icon); m_cbt.Uninstall(); return dr;

The method calls the standard Windows Forms MessageBox object in between two calls to the hook.

Before the message box is shown, the method installs a WH_CBT hook; the system-provided MessageBox object works under this hook's control. The hook is uninstalled as soon as the Windows Forms MessageBox call returns. The user of the MsdnMag.MessageBox object receives the dialog result that the system MessageBox has generated. Note that you must use fully qualified names in your code to distinguish between the Windows Forms MessageBox object and the custom MsdnMag.MessageBox class.

The helper WH_CBT hook class is the same class that I discussed in last month's column. Figure 2 shows the full source code.

Figure 2 MessageBox Class

using System; using System.Text; using System.Runtime.InteropServices; namespace MsdnMag { // CBT hook actions public enum CbtHookAction : int { HCBT_MOVESIZE = 0, HCBT_MINMAX = 1, HCBT_QS = 2, HCBT_CREATEWND = 3, HCBT_DESTROYWND = 4, HCBT_ACTIVATE = 5, HCBT_CLICKSKIPPED = 6, HCBT_KEYSKIPPED = 7, HCBT_SYSCOMMAND = 8, HCBT_SETFOCUS = 9 } public class CbtEventArgs : EventArgs { public IntPtr Handle; // Win32 handle of the window public string Title; // caption of the window public string ClassName; // class of the window public bool IsDialogWindow; // whether it's a popup dialog } public class LocalCbtHook : LocalWindowsHook { // ************************************************************** // Event delegate public delegate void CbtEventHandler(object sender, CbtEventArgs e); // ************************************************************** // ************************************************************** // Events public event CbtEventHandler WindowCreated; public event CbtEventHandler WindowDestroyed; public event CbtEventHandler WindowActivated; // ************************************************************** // ************************************************************** // Internal properties protected IntPtr m_hwnd = IntPtr.Zero; protected string m_title = ""; protected string m_class = ""; protected bool m_isDialog = false; // ************************************************************** // ************************************************************** // Class constructor(s) public LocalCbtHook() : base(HookType.WH_CBT) { this.HookInvoked += new HookEventHandler(CbtHookInvoked); } public LocalCbtHook(HookProc func) : base(HookType.WH_CBT, func) { this.HookInvoked += new HookEventHandler(CbtHookInvoked); } // ************************************************************** // ************************************************************** // Handles the hook event private void CbtHookInvoked(object sender, HookEventArgs e) { CbtHookAction code = (CbtHookAction) e.HookCode; IntPtr wParam = e.wParam; IntPtr lParam = e.lParam; // Handle hook events (only a few of available actions) switch (code) { case CbtHookAction.HCBT_CREATEWND: HandleCreateWndEvent(wParam, lParam); break; case CbtHookAction.HCBT_DESTROYWND: HandleDestroyWndEvent(wParam, lParam); break; case CbtHookAction.HCBT_ACTIVATE: HandleActivateEvent(wParam, lParam); break; } return; } // ************************************************************** // ************************************************************** // Handle the CREATEWND hook event private void HandleCreateWndEvent(IntPtr wParam, IntPtr lParam) { // Cache some information UpdateWindowData(wParam); // raise event OnWindowCreated(); } // ************************************************************** // ************************************************************** // Handle the DESTROYWND hook event private void HandleDestroyWndEvent(IntPtr wParam, IntPtr lParam) { // Cache some information UpdateWindowData(wParam); // raise event OnWindowDestroyed(); } // ************************************************************** // ************************************************************** // Handle the ACTIVATE hook event private void HandleActivateEvent(IntPtr wParam, IntPtr lParam) { // Cache some information UpdateWindowData(wParam); // raise event OnWindowActivated(); } // ************************************************************** // ************************************************************** // Read and store some information about the window private void UpdateWindowData(IntPtr wParam) { // Cache the window handle m_hwnd = wParam; // Cache the window's class name StringBuilder sb1 = new StringBuilder(); sb1.Capacity = 40; GetClassName(m_hwnd, sb1, 40); m_class = sb1.ToString(); // Cache the window's title bar StringBuilder sb2 = new StringBuilder(); sb2.Capacity = 256; GetWindowText(m_hwnd, sb2, 256); m_title = sb2.ToString(); // Cache the dialog flag m_isDialog = (m_class == "#32770"); } // ************************************************************** // ************************************************************** // Helper functions that fire events by executing user code protected virtual void OnWindowCreated() { if (WindowCreated != null) { CbtEventArgs e = new CbtEventArgs(); PrepareEventData(e); WindowCreated(this, e); } } protected virtual void OnWindowDestroyed() { if (WindowDestroyed != null) { CbtEventArgs e = new CbtEventArgs(); PrepareEventData(e); WindowDestroyed(this, e); } } protected virtual void OnWindowActivated() { if (WindowActivated != null) { CbtEventArgs e = new CbtEventArgs(); PrepareEventData(e); WindowActivated(this, e); } } // ************************************************************** // ************************************************************** // Prepare the event data structure private void PrepareEventData(CbtEventArgs e) { e.Handle = m_hwnd; e.Title = m_title; e.ClassName = m_class; e.IsDialogWindow = m_isDialog; } // ************************************************************** // ************************************************************** // Win32: GetClassName [DllImport("user32.dll")] protected static extern int GetClassName(IntPtr hwnd, StringBuilder lpClassName, int nMaxCount); // ************************************************************** // ************************************************************** // Win32: GetWindowText [DllImport("user32.dll")] protected static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount); // ************************************************************** } }

Handling MessageBox Events

The constructor of the MessageBox object instantiates the hook class and registers handlers for all of its events: WindowCreated, WindowDestroyed, and WindowActivated:

public MessageBox() { m_cbt = new LocalCbtHook(); m_cbt.WindowCreated += new CbtEventHandler(WndCreated); m_cbt.WindowDestroyed += new CbtEventHandler(WndDestroyed); m_cbt.WindowActivated += new CbtEventHandler(WndActivated); }

The key tasks for setting up a message box with selectable text are performed in the WindowActivated event; that is, when the dialog box is actually displayed. The WindowCreated event handler grabs and stores the window handle of the message dialog box. The WindowDestroyed event handler, in turn, resets an internal variable that prevents the dialog setup from taking place repeatedly at each dialog activation. Bear in mind that a dialog box can be activated more than once in the same session, in particular if you click on an external application and then move the focus back to the message box in the original application. The WindowActivated event is fired whenever a window receives the WM_SETFOCUS message with the status flag set to true—that is, whenever the window gets the focus. Since the MsdnMag.MessageBox class performs some one-off tasks when it gains the focus, you need to ensure that those tasks execute only once. Let's review in more detail what happens when the various window events fire.

The WindowCreated Event

The custom MessageBox class defines three protected data members to hold some worker data:

protected LocalCbtHook m_cbt; protected IntPtr m_handle = IntPtr.Zero; protected bool m_alreadySetup = false;

The m_cbt member is a property that contains the local hook object. The m_handle property represents the Win32 handle of the message window. In the .NET Framework you render a Win32 HWND type as an IntPtr. Finally, the Boolean m_alreadySetup variable is a flag that tells whether the dialog customization has been completed. Such a customization must occur only once for every display of the dialog box.

The WindowCreated event handler takes a custom event data structure called CbtEventArgs (see Figure 2 for the class source code). The structure contains a Boolean property called IsDialogWindow, which indicates whether the window being created is a dialog. This information is needed to distinguish the dialog window from all of its child control windows such as the icon, button, and static text. A dialog box is a window that has a Win32 class name of WC_DIALOG (#32770). Do not confuse Win32 class names with .NET classes. The Win32 class name simply denotes the category to which the window belongs and that determines its look and behavior. As the following code snippet shows, the handler for the WindowCreated event stores the handle of the window that it receives from the CbtEventArgs structure:

private void WndCreated(object sender, CbtEventArgs e) { if (e.IsDialogWindow) { m_alreadySetup = false; Console.WriteLine("MessageBox created"); m_handle = e.Handle; } }

The m_handle member, set only if the window being created is a dialog, will be widely used by the next events.

The WindowActivated Event

The WindowActivated event handler does something significant only if the handle of the window being activated matches the previously stored handle of the message dialog box. If the dialog box has already been activated after creation, the handler simply returns.

If the dialog box is being activated for the first time, then the event handler code modifies the layout of the message box window to meet the aforementioned requirements: using custom icons and showing selectable text.

If no predefined icon has been specified and the IconFile and IconIndex pair of properties point to a valid icon, the handler extracts the icon bits from the specified file and sets the static window representing the icon on the dialog box. Figure 3 shows the layout of any window that appears through a call to the Win32 MessageBox API.

Figure 3 MsgBox Layout

Figure 3** MsgBox Layout **

The icon window belongs to the class static and has an ID of 0x0014. This information has been obtained from Spy++. It is subject to change with different versions of Windows. The information discussed here assumes that you are running Windows 2000 SP3. If you are running a different configuration, open a MessageBox window and check its structure with Spy++. (Spy++ comes with Visual Studio 6.0 and Visual Studio .NET.)

The text of the message is displayed through another static window with an ID of 0xFFFF. This ID has been unchanged since Windows 95 and will probably stay that way. However, check it out carefully before you ship code based on this undocumented value. The event handler uses Win32 API functions to get the handles of these two windows and replaces their contents and structure. Let's see just how this is done.

Replacing the Icon

One of the key reasons that you should avoid a full replacement for MessageBox is that the API function automatically sizes the dialog window to accommodate for icons and the number of buttons that are required by the user's styles. Although it's possible to achieve, providing the same level of flexibility would require writing and testing a lot of error-prone code. The MessageBox API function makes room for the icon only if one of the predefined icons has been selected. If no icon has been chosen, the icon window—the static window with an ID of 0x0014—won't be created. For this reason, the Show method checks the length of the IconFile property and, if a custom icon has been specified, selects an arbitrary icon just to ensure that the resultant dialog box includes an icon window, as shown here:

DialogResult Show(string text, string title, MessageBoxButtons buttons, MessageBoxIcon icon) { if (IconFile.Length > 0 && icon == MessageBoxIcon.None) icon = MessageBoxIcon.Information; else IconFile = ""; ••• }

When both a custom and a predefined icon are selected, the latter takes precedence. In this case, the IconFile property—which represents the custom icon—is reset to the empty string.

When the message box is being activated, the IconFile property is nonempty only if a custom icon must be used. A few Win32 API functions do the actual job of extracting the icon and sending its bits to the static window for display:

if (m_iconFile.Length >0) { IntPtr hIcon = ExtractIcon(IntPtr.Zero, m_iconFile, m_iconIndex); IntPtr hwndIcon = GetDlgItem(m_handle, 0x0014); SendMessage(hwndIcon, STM_SETICON, hIcon, IntPtr.Zero); }

The GetDlgItem function takes a dialog window and an ID value and returns the handle of the child window that matches the ID. Such a window will receive an HICON handle that represents the bits of the icon to use. The icon is extracted from a given executable or icon file. For this particular task, I use the ExtractIcon API function, as shown here:

[DllImport("shell32.dll")] protected static extern IntPtr ExtractIcon( IntPtr hInst, // instance handle string lpszExeFileName, // file name int nIconIndex // icon index );

The function has the capability of extracting the icon with a given index in an executable. Although the .NET Framework provides several facilities to work with icons and images in general, I haven't yet found a method or a service class that lets you extract icons from an executable by index. However, you can easily extend the programming interface of the MsdnMag.MessageBox class to make it accept icons when using .NET. Notice that you have to set the icon using an HICON handle, but HICON handles can also be obtained from managed GDI+ classes such as Bitmap. The following code snippet shows an example:

Bitmap bmp = new Bitmap("app.ico"); IntPtr hIcon = bmp.GetHicon();

To set the icon, you need to use another old familiar API function: SendMessage. The message ID to use is STM_SETICON, which must be imported from the winuser.h header file:

private const int STM_SETICON = 0x00000170;

If an incorrect window ID is provided no exception will be thrown, but an empty icon will be displayed. Finally, note that ExtractIcon always returns the large icon. If you want to obtain the small icon, always import the ExtractIconEx API from shell32.dll.

Replacing the Static Text

The message of the dialog box is rendered through a window of the static class. The .NET Framework counterpart of a Win32 static window is the Label class. The text displayed through a static control window is not selectable with the mouse. To partially remedy this behavior, with Windows 2000, Microsoft introduced a little-known feature to simplify the task of copying the text displayed in a standard message box to the clipboard. Try pressing Ctrl+C when a message box window is displayed. Next, flush the clipboard's contents to a textbox control with Ctrl-V. For the sample window shown in Figure 1, you get the following text:

————————————— Cutting Edge ————————————— Hello, managed world! ————————————— OK —————————————

The text contains the message along with the window caption and the button text. The feature depends on the operating system and cannot be controlled programmatically. If you want to visually select the text to copy to the clipboard, there is no way other than by replacing the static control with a borderless read-only edit control.

The code in Figure 4 shows how to create an edit control window that shadows and replaces the original static control. As the first step, the code gets the handle to the static window and reads the text through the GetWindowText API. Note that when mapping Win32 API functions to a method on a .NET Framework class, you can use the StringBuilder class to map variable-length strings such as those pointed to by LPTSTR:

[DllImport("user32.dll")] protected static extern int GetWindowText(IntPtr hwnd, StringBuilder lpString, int nMaxCount);

Figure 4 Creating an Edit Control Window

// Get the static control with the text IntPtr hwndText = GetDlgItem(m_handle, 0xFFFF); StringBuilder sb = new StringBuilder(); sb.Capacity = 256; GetWindowText(hwndText, sb, 256); string text = sb.ToString(); // Get the position of the static window // RECT and POINT are Win32 imported structures RECT rc = new RECT(); GetWindowRect(hwndText, rc); POINT pt = new POINT(); pt.x = rc.left; pt.y = rc.top; ScreenToClient(m_handle, pt); // Create the alternate EDIT window IntPtr m_edit = CreateWindowEx(0, "edit", "", ES_READONLY|ES_MULTILINE|WS_CHILD|WS_VISIBLE, pt.x, pt.y, rc.right-rc.left, rc.bottom-rc.top, m_handle, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); // Set font and text IntPtr hFont = SendMessage(hwndText, WM_GETFONT, IntPtr.Zero, IntPtr.Zero); SendMessage(m_edit, WM_SETFONT, hFont, new IntPtr(1)); SetWindowText(m_edit, text); // Copy the displayed text to the clipboard Clipboard.SetDataObject(text); // Finalizing... DestroyWindow(hwndText);

The idea is to create an edit window with the same size and position of the static window, make the replacement, and destroy the static control. Resorting to Win32 API functions is a necessity since the only way to access windows is with a Win32 handle (HWND). The GetWindowRect function returns the screen-based rectangle that contains the static window. ScreenToClient, on the other hand, converts the screen coordinates of the top-left point in the rectangle to client coordinates relative to the dialog window. The location of the static window, and subsequently the location of the newly created edit window, are managed through a couple of Win32 data structures—RECT and POINT. Although the .NET Framework contains analogous structures (called Rectangle and Point in the System.Drawing namespace), the Win32 native structures are required here:

[StructLayout(LayoutKind.Sequential)] public class POINT { public int x; public int y; } [StructLayout(LayoutKind.Sequential)] public class RECT { public int left; public int top; public int right; public int bottom; }

Finally, CreateWindowEx utilizes this information to build a new edit window with the proper dimensions. The edit window is created without the border and with a background color identical to the parent dialog's background. The edit window is given the same font as the static control. The font must be explicitly set for the window to work properly:

IntPtr hFont; hFont = SendMessage(hwndText, WM_GETFONT, IntPtr.Zero, IntPtr.Zero); SendMessage(m_edit, WM_SETFONT, hFont, new IntPtr(1)); SetWindowText(m_edit, text);

The WM_GETFONT and WM_SETFONT constants must be explicitly defined. Their values are found in the winuser.h file.

Finally, note that you can also improve on the copy-to-clipboard automatic feature by putting the message text on the clipboard available to clients. To do this, you don't need to explicitly resort to any Win32 API calls. The .NET Framework Clipboard class is easier to use and works just fine wrapping the OleGetClipboard and OleSetClipboard API functions.

Copying the message text to the clipboard is easy:

Clipboard.SetDataObject(text);

Note that the Clipboard class belongs to the System.Windows.Forms namespace.

The WindowDestroyed Event

The WindowDestroyed event fires whenever the DestroyWindow API function gets called on a window under the control of the WH_CBT hook. To prevent infinite recursion make sure that your handler works only when the window handle is that of the MessageBox dialog:

void WndDestroyed(object sender, CbtEventArgs e) { if (e.Handle == m_handle) { m_alreadySetup = false; m_handle = IntPtr.Zero; } }

The handler doesn't do anything special except reset the internal variables. Note that due to the internal implementation of the DestroyWindow API function, the event handler is called only once for the dialog window but not at all for any of the child controls. (Child controls are iteratively destroyed by sending messages rather than calling DestroyWindow.)

Putting It All Together

You can use MsdnMag.MessageBox as a full replacement for the Windows Forms MessageBox class. The Show method supports the most frequently used combinations of buttons and options and can even be extended. In particular, you might want to support more combinations of buttons (for example, Yes-No-All-Cancel). Note that the .NET Framework version of MessageBox doesn't explicitly support the fourth button. In Win32, the fourth button is possible only if you add the MB_HELP style in the call to the MessageBox function; this style adds a fourth button named Help. The Help button doesn't dismiss the dialog, but is limited to searching for whether a callback function for help has been registered. With a little sleight of hand, you add the Help button to force MessageBox to make room for four buttons and then rename the buttons as needed. To perform the trick, you must import the MessageBox API function because the strongly typed name of the MessageBox class doesn't permit mixing integers to obtain undefined members of a given enumeration:

[DllImport("user32.dll", EntryPoint="MessageBox")] protected static extern int _MessageBox(IntPtr hwnd, string text, string caption, int options);

The MessageBox function must be imported as a member with a different name to avoid conflicts with the host class name. Define a Boolean property called YesAllNoCancel that, if set, enables the class to show a custom combination of buttons. The Show method changes as follows:

if (YesAllNoCancel) { buttons = MessageBoxButtons.YesNoCancel; m_cbt.Install(); int res = _MessageBox(GetActiveWindow(), text, title, (int) buttons + (int) icon + MB_HELP); m_cbt.Uninstall(); return (DialogResult) res; }

At this point, you get the following sequence of buttons: Yes-No-Cancel-Help. More changes are required to reach the final goal, so let's add a few more lines of code in the WindowActivated event handler. The idea is to give the four buttons new names and IDs so that they behave as expected. The All button is made equivalent to the OK button with a default text of "OK to All". You access the dialog buttons using GetDlgItem and an ID value that matches the corresponding DialogResult value for the button. For example, the Yes button has an ID equal to DialogResult.Yes and text equal to DialogResult.Yes.ToString. The Help button has an ID of 9 that I mapped to the constant IDALL. The value of 9 corresponds to the Win32 IDHELP constant (see Figure 5).

Figure 5 Mapping Button Text and ID

// Map button text SetWindowText(GetDlgItem(m_handle,(int) DialogResult.No), "OK to All"); SetWindowText(GetDlgItem(m_handle,(int) DialogResult.Cancel), DialogResult.No.ToString()); SetWindowText(GetDlgItem(m_handle, IDALL), DialogResult.Cancel.ToString()); // Map button ID SetWindowLong(GetDlgItem(m_handle, (int) DialogResult.No), GWL_ID, (int)DialogResult.OK); SetWindowLong(GetDlgItem(m_handle,(int) DialogResult.Cancel), GWL_ID, (int) DialogResult.No); SetWindowLong(GetDlgItem(m_handle, IDALL), GWL_ID, (int) DialogResult.Cancel);

Figure 6 Testing

Figure 6** Testing **

You use the SetWindowText function to modify the text of a button and SetWindowLong (with a GWL_ID selector) to modify the ID. All functions and constants must be imported from winuser.h. As a result, clicking on the All button gives you a dialog result of OK (see Figure 6).

Conclusion

The gist of the story is that the .NET Framework comes with a great deal of useful and powerful classes but, at least in version 1.0, there isn't a managed version of each and every feature that's present in Win32. This is particularly true even with a relatively simple function like MessageBox. I can't say what the future direction of the .NET Framework will be. I don't know whether it will remain tightly coupled with the Win32 platform or evolve into a more general-purpose universal platform. In the meantime, however, you can easily draw ideas and solutions from the underlying Win32 API by exploring P/Invoke.

Send questions and comments for Dino to  cutting@microsoft.com.

Dino Espositois an instructor and consultant based in Rome, Italy. He is the author of Building Web Solutions with ASP.NET and ADO.NET and Applied XML Programming for .NET, both from Microsoft Press (2002). Reach Dino at dinoe@wintellect.com.