65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使 MessageBox 居中在其父窗体上

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (32投票s)

2010 年 2 月 17 日

CPOL

4分钟阅读

viewsIcon

149277

downloadIcon

3250

使 MessageBox 居中在其父窗体上

引言

每个人都会使用 MessageBox - 它一直是 Windows 的一个组成部分,自第一天起,其格式和调用方式几乎没有变化。

对我来说,MessageBox 最大的缺点就是它在屏幕上居中显示,而不是在其父窗体上居中显示,而且没有办法告诉它在其父窗体上居中显示。

MessageBoxCenterOnParent/regular.PNG

这就是我想要的

MessageBoxCenterOnParent/centered.PNG

你可能会认为至少有一个 MessageBox.Show() 的 21 个重载会有一些方法可以做到这一点,或者可能有一个 MessageBoxOptions.CenterOnParent 标志,但没有这样的好运。

本文介绍了一种简单的机制,用于实现显示在其父窗体上居中的 MessageBox

背景

我的技术使用了一个自定义类 - 我称之为 MsgBox - 它用 Windows 钩子包装了标准的 MessageBox.Show 调用。 在弹出 MessageBox 之前设置钩子,hookproc 在 MessageBox 首次显示之前找到并将其居中,然后释放钩子。

为了简单起见,我的示例仅适用于单个线程。 如果你有多个线程以不协调的方式弹出 MessageBox,你需要添加一些代码来处理这种情况。

对于其他参考资料/文章,CodeProject 上有许多关于钩子的文章。 搜索 "SetWindowsHookEx"。

Microsoft 的文档可在此链接中找到。

Using the Code

如上所述,核心机制是 Windows 钩子。 对于新手来说,这是一种 Windows 机制,允许你的代码访问一些低级 Windows 功能; 你本质上是将你的应用程序代码注入到 Windows 的内部运作中。

有多种类型的钩子; 我的代码使用 WH_CBT 钩子并作用于 HCBT_ACTIVATE 事件。 (阅读上面链接的 Microsoft 页面以了解 WH_CBT 的详细信息。)

钩子不是 .NET 的一部分。 要使用它们,你必须使用 PInvoke 访问 Win32 钩子 API SetWindowsHookEx()CallNextHookEx()UnhookWindowsHookEx()。 这些设置本地钩子,这意味着它们仅在我们的进程中的窗口上运行。 这正是我们想要的 - 我们想处理其他应用程序显示的消息框,仅处理我们自己的。

当你设置 WH_CBT 钩子时,你的回调将收到窗口事件的通知,例如创建、激活、移动/调整大小和销毁。 我们对激活感兴趣:当消息框首次激活时(但在它最初可见之前),我们将重新定位它,然后我们就完成了。

下面的代码片段是从附加的示例中提取的。 在其中,你将看到我使用 Win32.* 语法 - 在示例中,我已将所有 P/Invoke 方法和定义收集在一个名为 Win32 的单独类中,这是我为我的项目采用的常见做法。

准备工作

首先,你需要导入钩子 API

using System.Runtime.InteropServices;

public class Win32
{
   public const int WH_CBT = 5;
   public const int HCBT_ACTIVATE = 5;

   public delegate int WindowsHookProc(int nCode, IntPtr wParam, 
                                       IntPtr lParam);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern int SetWindowsHookEx(int idHook, 
          WindowsHookProc lpfn, IntPtr hInstance, int threadId);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern bool UnhookWindowsHookEx(int idHook);

   [DllImport("user32.dll", CharSet = CharSet.Auto, 
              CallingConvention = CallingConvention.StdCall)]
   public static extern int CallNextHookEx(int idHook, int nCode, 
                            IntPtr wParam, IntPtr lParam);
}

并定义一些变量来管理钩子

private int _hHook = 0;
private Win32.WindowsHookProc _hookProcDelegate;
private static string _title = null;
private static string _msg = null;

设置、处理和释放钩子

创建一个回调委托,然后调用 SetWindowsHookEx() 来设置钩子。 它返回一个钩子 ID,你将在你的回调中使用它,并在你释放钩子时使用它。

// Remember the title & message that we'll look for.
// The hook sees *all* windows, so we need
// to make sure we operate on the right one.
_msg = msg;
_title = title;

Win32.WindowsHookProc hookProcDelegate = 
                      new Win32.WindowsHookProc(HookCallback);
_hHook = Win32.SetWindowsHookEx(Win32.WH_CBT, hookProcDelegate, 
         IntPtr.Zero, AppDomain.GetCurrentThreadId()); 

你的钩子回调看起来像这样。 完成处理通知后,你必须通过 CallNextHookEx() 将事件传递给下一个钩子。 (请注意此处使用你的钩子 ID _hHook。)

private static int HookCallback(int code, IntPtr wParam, IntPtr lParam)
{
   if (code == Win32.HCBT_ACTIVATE)
   {
      // wParam is the handle to the Window being activated.
      if(TestForMessageBox(wParam))
      {
         CenterWindowOnParent(wParam);
         Unhook();   // Release hook - we've done what we needed
      }
   }
   return Win32.CallNextHookEx(_hHook, code, wParam, lParam); 
}

然后,最后,当你完成查找消息框后,你释放钩子

private static void Unhook()
{
   Win32.UnhookWindowsHookEx(_hHook);
   _hHook = 0;
   _hookProcDelegate = null;
   _title = null;
   _msg = null;
}

查找你的消息框

只需注意具有正确标题和消息的对话框

private static bool TestForMessageBox(IntPtr hWnd)
{
   string cls = Win32.GetClassName(hWnd);
   if (cls == "#32770") // MessageBoxes are Dialog boxes
   {
      string title = Win32.GetWindowText(hWnd);
      string msg = Win32.GetDlgItemText(hWnd, 0xFFFF); // -1 aka IDC_STATIC
      {
         if ((title == _title) && (msg == _msg))
         {
            return true;
         }
      }
   }
   return false;
}

在父窗体上居中显示消息框

在一个窗口上居中显示另一个窗口 - 这里没有什么特别的

private static void CenterWindowOnParent(IntPtr hChildWnd)
{
   // Get child (MessageBox) size
   Win32.RECT rcChild = new Win32.RECT();
   Win32.GetWindowRect(hChildWnd, ref rcChild);
   int cxChild = rcChild.right - rcChild.left;
   int cyChild = rcChild.bottom - rcChild.top;

   // Get parent (Form) size & location 
   IntPtr hParent = Win32.GetParent(hChildWnd);
   Win32.RECT rcParent = new Win32.RECT();
   Win32.GetWindowRect(hParent, ref rcParent);
   int cxParent = rcParent.right - rcParent.left;
   int cyParent = rcParent.bottom - rcParent.top;

   // Center the MessageBox on the Form 
   int x = rcParent.left + (cxParent - cxChild) / 2;
   int y = rcParent.top + (cyParent - cyChild) / 2;
   uint uFlags = 0x15; // SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE;

   Win32.SetWindowPos(hChildWnd, IntPtr.Zero, x, y, 0, 0, uFlags);
}

就是这样!

在你的调用代码中,只需调用 MsgBox.Show() 而不是 MessageBox.Show(),你将获得父级居中的弹出窗口。

关注点

我经常想知道为什么消息框显示在屏幕上居中,而不是在其窗体上居中。 我认为这一定有某种方法 - 微软不会让像这样的错误持续 20 年! (好吧,也许他们会的... :) )

在开发此代码时,我找到了一个合理的解释:例如,你的窗口位于显示边界之外,并且发生错误弹出消息框。 在这种情况下,如果消息框在其父级上居中,你将永远看不到它。 我认为这是一个有效的担忧,你在使用居中的消息框时应该考虑它,也许选择对某些错误消息进行父级居中,以防万一。

历史

  • 2010 年 2 月 17 日:初始版本
© . All rights reserved.