使用 C# MessageBoxIndirect Wrapper 进行高级消息框






4.61/5 (12投票s)
2004年10月5日
11分钟阅读

111740

1212
本文介绍了一个友好的 C# 包装器类,用于 MessageBoxIndirect API。MessageBoxIndirect 类允许您为消息框添加帮助按钮、自定义图标、支持本地化语言的按钮以及不同的模态。
引言
.NET Framework 中进行 Windows Forms 编程的任何人都熟悉 MessageBox
类。然而,托管的 MessageBox
缺少 Win32 API 中提供的一些功能,包括添加帮助按钮、指定对话框按钮所使用的语言以及添加自定义图标。在本文中,我将介绍 MessageBoxIndirect
Win32 API 函数的托管包装器,它提供了这些功能。
背景
Win32 API 包含三个用于向用户显示消息框的函数。它们是 MessageBox
、MessageBoxEx
和 MessageBoxIndirect
。在这三个函数中,MessageBoxIndirect
功能最强大,允许您添加帮助按钮、自定义图标以及按钮文本的特定语言。它对程序员来说也最不友好,这通常导致人们围绕它创建一个友好的包装器类。您可以在这篇 Code Project 文章中找到一个 C++ 中的示例。
要从 .NET Framework 的托管世界访问 MessageBoxIndirect
函数,需要使用 Platform Invoke (PInvoke
) 编写一些互操作代码。由于 PInvoke
代码可能有点难读写,所以通常应该只写一次,确保它能正常工作,然后就不用管它了。这意味着您应该将其包装在一个可重用的类中。这就是 MessageBoxIndirect
类,一个 C# 包装器类,它公开了 MessageBoxIndirect
API 的全部功能。
设计说明
MessageBoxIndirect
类公开了底层 MessageBoxIndirect
API 的四个关键功能:选择不同的对话框模态、指定按钮所使用的语言标识符、添加可用的帮助按钮以及添加自定义图标。我们将在下面详细讨论每一个,但首先我将提及一些设计决策。
MessageBoxIndirect
支持标准的 MessageBox
行为,例如从标准按钮和图标集中进行选择的能力。因此,为了简化编程模型,MessageBoxIndirect
类使用了现有 MessageBox
类中定义的枚举,例如 MessageBoxButtons
、MessageBoxDefaultButton
、MessageBoxOptions
和 DialogResult
。
在我看来,.NET Framework 中的 MessageBox
类是“不太好的”面向对象设计的绝佳范例。它是一个静态类。也就是说,它只为几个静态 Show
方法提供了一个命名空间。确切地说,是十二个静态 Show
方法。为什么这是个糟糕的设计?想象一下,如果微软想添加,比如说,为消息框指定自定义图标的功能,他们会不得不添加多少个 Show
重载?面对所有这些选项,为每种场景支持 Show
方法的组合对于维护来说非常困难。在这种情况下,您真正应该做的是允许用户创建一个实例,即“MessageBox mb = new MessageBox()
”,然后设置它的属性。为了加快速度,可以添加一些仅用于最常见选项的构造函数重载,可能包括文本、标题和按钮。
我为 MessageBoxIndirect
类采用了上面建议的面向对象模型。为了让您更容易地将代码转换为使用这个类,我添加了与内置 MessageBox
的静态 Show
方法的签名匹配的构造函数重载。在创建了 MessageBoxIndirect
实例并设置了所有期望的选项后,您只需要调用实例方法 Show
(它返回一个 DialogResult
)即可向用户显示消息框。
演示项目
包含的解决方案 MessageBoxIndirect.sln 包含一个 Windows 应用程序,其中的 DemoForm
窗体演示了使用 MessageBoxIndirect
类的多种方法。现在我们将更详细地介绍这些方法。在演示项目中,SetResult
方法会在窗口的状态栏中显示 Show
方法的输出(即用户选择的内容)。
我包含了一个名为 Win32Resources 的 Visual C++ 解决方案,它构建了一个包含自定义图标的资源 DLL。如果您想尝试加载来自单独 DLL 的自定义图标的演示部分,请构建此解决方案并将输出 DLL 复制到演示项目可执行文件相同的目录(\MessageBoxIndirect\bin\Debug 或 \MessageBoxIndirect\bin\Release)。
模态
消息框在用户与消息框交互时可以具有三种基本行为,这取决于用户在消息框显示期间还可以执行什么操作。这些是应用程序模态、任务模态和系统模态。应用程序模态消息框接受一个所有者窗口作为参数,并阻止与给定窗口的任何交互,直到消息框关闭。任务模态消息框的工作方式相同,只是生成的消息框窗口是顶部的。这旨在指示一个相对严重的情况。最后,系统模态消息框会阻止与调用应用程序的所有顶级窗口进行任何交互,而无需您传递所有者窗口。
以下是使用 MessageBoxIndirect
类指定模态的示例
MessageBoxIndirect mb = new MessageBoxIndirect( this, "App Modal", "Test" );
mb.Modality = MessageBoxIndirect.MessageBoxExModality.AppModal;
DialogResult dr = mb.Show();
传递 LangID
MessageBoxIndirect
类允许您指定一个语言标识符 (LangID
),指示在默认消息框按钮中使用的语言。在下面的示例中,我实际上传递了一个已转换的区域设置标识符 (LCID
),而不是 LangID
,但 LCID
的低 16 位实际上就是 LangID
,所以可以这样处理。关于 LangID
和 LCID
的更深入讨论超出了本文的范围,但如果您有兴趣,可以查看MSDN 关于 Windows national language support 的主题。请注意,如果您确实选择传递不同的 LangID
,则需要在系统中安装相应的语言才能看到效果。
MessageBoxIndirect mb = new MessageBoxIndirect( "Pass a LangID: " +
Thread.CurrentThread.CurrentUICulture.LCID.ToString(), "Test" );
mb.LanguageID = (uint) Thread.CurrentThread.CurrentUICulture.LCID;
DialogResult dr = mb.Show();
添加帮助按钮
您可以通过将 ShowHelp
属性设置为 true
来为消息框添加帮助按钮。您可以通过两种不同的方式处理帮助按钮。第一种方式是提供一个 MsgBoxCallback
类型的委托,当单击帮助按钮时会调用该委托,如下面的示例所示
MessageBoxIndirect mb = new MessageBoxIndirect( "Help Button",
"Test", MessageBoxButtons.YesNoCancel );
mb.ShowHelp = true;
mb.ContextHelpID = 555;
mb.Callback = new MessageBoxIndirect.MsgBoxCallback( this.ShowHelp );
DialogResult result = mb.Show();
ShowHelp
函数返回 void,并接受一个 HELPINFO
实例。最重要的是,HELPINFO
实例的 dwContextId
成员包含您在调用 Show
之前(上面示例中的 555)设置到 MessageBoxIndirect
类中的上下文 ID。
处理帮助的第二种方法是请求将 WM_HELP
消息发送到父窗口。以下代码演示了这一点。请注意,我们在构造函数中为 MessageBoxIndirect
类提供了一个所有者窗口(“this
”,可能是父窗体),以作为 WM_HELP
消息的目标
MessageBoxIndirect mb = new MessageBoxIndirect( this,
"Help Button", "Test", MessageBoxButtons.YesNoCancel );
mb.ShowHelp = true;
mb.ContextHelpID = 444;
DialogResult result = mb.Show();
要在窗体的重写的 WndProc
中处理 WM_HELP
消息,请注意 Message.LParam
指向一个 HELPINFO
实例。在使用 HELPINFO
之前,您必须对其进行解组。我在 HELPINFO
类中添加了一个名为 UnmarshalFrom
的静态辅助方法来帮助您完成此过程。只需将 LParam
传递给它,它就会返回适合您用于调用帮助的 HELPINFO
。
添加自定义图标
这绝对是我包装 MessageBoxIndirect
API 工作中最困难但也是在我看来最有趣的部分。如果您设置了 MB_USERICON
标志,MessageBoxIndirect
会尝试从 MSGBOXPARAMS
的 lpszIcon
成员中给出的 ID 加载一个资源,该资源来自 hInstance
成员中提供的实例句柄。问题在于这必须是传统的 Win32 资源。.NET 使用完全不同的技术来存储和管理资源,而且 Visual Studio.NET(截至 2003 版)似乎无法将 Win32 资源添加到您的编译程序集(更不用说在 C# 或 VB.NET 项目中将 .rc 脚本编译为 .res 文件了),除了应用程序图标。关于 Win32 和 .NET 资源之间差异的讨论超出了本文的范围,但可以说,为了让 MessageBoxIndirect
显示自定义图标,您必须费一番周折。我看到三种选择可以让您的图标生效
1.您可以在构建程序集后使用资源编辑器工具将图标作为 Win32 资源嵌入。虽然 .NET 使用的资源格式不同,但它确实会创建标准的 Win32 二进制文件,并且没有什么可以阻止您向其中添加资源。我个人没有推荐的工具来完成此操作,因此我将不再讨论此选项。
2.您可以使用 csc.exe 或 vbc.exe 命令行编译器进行构建,它们支持 /win32res 标志,允许您指定一个已编译的资源文件(.res)链接为 Win32 资源。当然,此选项意味着您必须放弃 Visual Studio 作为您的构建环境,这可能会成为一个问题,尤其是在大型项目中。此外,/win32res 标志与用于指定应用程序图标的 /win32icon 标志不兼容,因此如果您选择此路径,则必须确保您期望的应用程序图标是 Win32 .res 文件中编号最小的图标资源。
在示例代码中,有一个 build.bat 脚本,它演示了使用我提供的名为 Win32Resources.res 的 .res 文件来完成此技术。在演示窗体上,单击“自定义图标(本exe)”按钮进行尝试。请注意,除非您使用命令行和 /win32res 标志进行编译,否则此按钮将不会为您提供自定义图标。
3.您可以动态加载一个资源 DLL,并将生成的实例句柄传递给 MessageBoxIndirect
。这是我的首选选项。在示例项目中,我提供了一个仅包含图标资源的 DLL,名为 Win32Resources.dll。在实践中,您需要使用 Visual C++ 等工具创建您的资源 DLL。演示窗体使用 LoadLibraryEx
的 PInvoke
调用加载此模块,并指示 MessageBoxIndirect
使用它作为自定义图标的来源,如下面的代码所示
if( hWin32Resources == IntPtr.Zero )
{
hWin32Resources = LoadLibraryEx( Application.StartupPath +
"\\Win32Resources.dll", IntPtr.Zero, 0 );
Debug.Assert( hWin32Resources != IntPtr.Zero );
}
// Win32 Resource ID of the icon we want to put in the message box.
const int Smiley = 102;
MessageBoxIndirect mb = new MessageBoxIndirect( "Custom Icon", "Test" );
// Load the icon from the resource DLL that we loaded.
mb.Instance = hWin32Resources;
mb.UserIcon = new IntPtr(Smiley);
DialogResult result = mb.Show();
如果这不起作用,请确保 Win32Resources DLL 位于正确的位置(应用程序可执行文件旁边)。
实现
关于使 MessageBoxIndirect
类工作的 .NET Interop
代码的详细讨论本身就可以写一篇文章。幸运的是,MSDN 和网络上有很多关于 Interop
和 PInvoke
的优秀教程,因此我在这里将只关注调用 MessageBoxIndirect
API 的特定亮点。
MessageBoxIndirect
API 函数接受一个参数,该参数是一个结构,其中包含结果消息框对话框所需的所有选项。声明是
[DllImport("user32", EntryPoint="MessageBoxIndirect")]
private static extern int _MessageBoxIndirect(
ref MSGBOXPARAMS msgboxParams );
请注意,“ref”修饰符表示底层 API 接受结构的指针。
传递给此函数的结构具有以下托管声明
[StructLayout(LayoutKind.Sequential)]
public struct MSGBOXPARAMS
{
public uint cbSize;
public IntPtr hwndOwner;
public IntPtr hInstance;
public String lpszText;
public String lpszCaption;
public uint dwStyle;
public IntPtr lpszIcon;
public IntPtr dwContextHelpId;
public MsgBoxCallback lpfnMsgBoxCallback;
public uint dwLanguageId;
};
此结构(以及后续的其他声明)源自 winuser.h Platform SDK 头文件。请注意,我们按照推荐的做法为每个 HANDLE 使用 IntPtr
。此外,lpszIcon
(稍后将详细讨论)被定义为 IntPtr
,尽管在 API 中它是 LPCTSTR
。这是因为传递给 lpszIcon
的典型值是调用 MAKEINTRESOURCE
宏的结果,该宏只是进行一些类型转换,使传递给它的数字看起来像一个地址。
此结构包含一个成员 lpfnMsgBoxCallback
,这是一个回调,在用户按下消息框对话框上可选的帮助按钮时会调用它。在 .NET 中,回调自然地实现为委托,因此我们将此回调包装在以下兼容的委托声明中
public delegate void MsgBoxCallback( HELPINFO lpHelpInfo );
HELPINFO
结构提供了一些关于特定帮助请求的有用信息。其托管声明是
[StructLayout(LayoutKind.Sequential)]
public struct HELPINFO
{
public uint cbSize;
public int iContextType;
public int iCtrlId;
public IntPtr hItemHandle;
public IntPtr dwContextId;
public POINT MousePos;
};
如上所述,处理消息框上帮助按钮的一种方法是处理 WM_HELP
消息。要在处理 WM_HELP
时检索 HELPINFO
,您需要进行一些解组,如下面 HELPINFO
类中定义的辅助方法
public static HELPINFO UnmarshalFrom( IntPtr lParam )
{
return (HELPINFO) Marshal.PtrToStructure(
lParam, typeof( HELPINFO ) );
}
最后,我们定义了 winuser.h 中的一些消息框相关常量供我们的实现使用,以及以下枚举来表示消息框可以采取的各种模态行为
public enum MessageBoxExModality : uint
{
AppModal = MB_APPLMODAL,
SystemModal = MB_SYSTEMMODAL,
TaskModal = MB_TASKMODAL
}
让自定义系统图标生效有点棘手。理论上,所要做的就是向 MessageBox
窗口发送一个 WM_SETICON
消息。实际问题是获取发送消息的窗口句柄。为了做到这一点,我使用 SetWindowsHookEx 安装了一个钩子,用于执行线程上的 WH_CBT
消息。
DialogResult retval = DialogResult.Cancel;
try
{
// Only hook if we have a reason to, namely, to set the custom icon.
if( _sysSmallIcon != IntPtr.Zero )
{
HookProc CbtHookProcedure = new HookProc(CbtHookProc);
hHook = SetWindowsHookEx(WH_CBT, CbtHookProcedure, (IntPtr) 0,
AppDomain.GetCurrentThreadId());
}
retval = (DialogResult) _MessageBoxIndirect( ref parms );
}
finally
{
if( hHook > 0 )
{
UnhookWindowsHookEx(hHook);
hHook = 0;
}
}
WH_CBT
消息可能很有用,尤其是在自动化系统方面。在这种情况下,我关心的是 HCBT_CREATEWND
消息,它在窗口创建后但在 WM_CREATE
广播之前出现。当我看到它时,我会验证它实际上是针对 MessageBox
对话框的,然后加载图标,并发送 WM_SETICON
消息
private int CbtHookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
if( nCode == HCBT_CREATEWND )
{
// Make sure this is really a dialog.
StringBuilder sb = new StringBuilder();
sb.Capacity = 100;
GetClassName( wParam, sb, sb.Capacity );
string className = sb.ToString();
if( className == "#32770" )
{
// Found it, look to set the icon if necessary.
if( _sysSmallIcon != IntPtr.Zero )
{
EnsureInstance();
IntPtr hSmallSysIcon = LoadIcon( Instance, _sysSmallIcon );
if( hSmallSysIcon != IntPtr.Zero )
{
SendMessage( wParam, WM_SETICON, new IntPtr(ICON_SMALL),
hSmallSysIcon );
}
}
}
}
return CallNextHookEx(hHook, nCode, wParam, lParam);
}
在设置钩子时,最后一定要调用 CallNextHookEx
。在我显示 MessageBox
并收集结果后,我会用 UnhookWindowsHookEx
清理钩子。
MessageBoxIndirect
类中的其余代码大部分是为了支持所有不同的构造函数,以及根据类的较高级属性构建 dwStyle
值。
历史
- 初始发布:2004 年 10 月 1 日
- 更新自定义系统图标:2007 年 3 月 3 日