オーナーウィンドウの中央にメッセージボックスを表示する

オーナーウィンドウの中央(フォームの中央)にメッセージボックスを表示するコードを紹介します。

概要

こちらの記事では、C#でメッセージボックスの表示をするコードを紹介しました。メッセージボックスを簡単に表示でき便利ですが、PCのスクリーンが大きい場合や、マルチウィンドウ環境の場合はちょっと動作が不便な場合があります。

先の記事で作成したプログラムを起動し、ボタンをクリックしてメッセージボックスを表示します。


メッセージボックスが表示されますが、ウィンドウの中央にメッセージボックスが表示されます。解像度が低い場合は問題ないですが、解像度が高くマルチディスプレイ環境の場合、ウィンドウの位置とメッセージボックスの位置が離れてしまいアプリケーションが使いにくくなってしまいます。


この記事では、メッセージボックスをオーナーウィンドウの中央に表示するコードを紹介します。

プログラム例

Windows フォームアプリケーションを作成します。

UI

下図のUIを作成します。フォームにボタンを一つ配置します。

コード

下記のコードを記述します。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;


namespace MessageBoxDemo
{
  public partial class FormMessageBoxHook : Form
  {
    private IntPtr HHook;

    public FormMessageBoxHook()
    {
      InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
      SetHook(this);
      MessageBox.Show(this, "確認メッセージです", "確認");
    }

    private void SetHook(IWin32Window owner)
    {
      // フック設定
      IntPtr hInstance = WindowsAPI.GetWindowLong(this.Handle, (int)WindowsAPI.WindowLongParam.GWLP_HINSTANCE);
      IntPtr threadId = WindowsAPI.GetCurrentThreadId();
      HHook = WindowsAPI.SetWindowsHookEx((int)WindowsAPI.HookType.WH_CBT, new WindowsAPI.HOOKPROC(CBTProc), hInstance, threadId);
    }

    private IntPtr CBTProc(int nCode, IntPtr wParam, IntPtr lParam)
    {
      if (nCode == (int)WindowsAPI.HCBT.HCBT_ACTIVATE) {
        WindowsAPI.RECT rectOwner;
        WindowsAPI.RECT rectMsgBox;
        int x, y;

        // オーナーウィンドウの位置と大きさを取得
        WindowsAPI.GetWindowRect(this.Handle, out rectOwner);
        // MessageBoxの位置と大きさを取得
        WindowsAPI.GetWindowRect(wParam, out rectMsgBox);

        // MessageBoxの表示位置を計算
        x = rectOwner.Left + (rectOwner.Width - rectMsgBox.Width) / 2;
        y = rectOwner.Top + (rectOwner.Height - rectMsgBox.Height) / 2;

        //MessageBoxの位置を設定
        WindowsAPI.SetWindowPos(wParam, 0, x, y, 0, 0, 
          (uint)(WindowsAPI.SetWindowPosFlags.SWP_NOSIZE | WindowsAPI.SetWindowPosFlags.SWP_NOZORDER | WindowsAPI.SetWindowPosFlags.SWP_NOACTIVATE));

        // フック解除
        WindowsAPI.UnhookWindowsHookEx(HHook);
      }
      // 次のプロシージャへのポインタ
      return WindowsAPI.CallNextHookEx(HHook, nCode, wParam, lParam);
    }
  }
}
WindowsAPI.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;

namespace MessageBoxDemo
{
  public static class WindowsAPI
  {
    [DllImport("user32.dll")]
    public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("kernel32.dll")]
    public static extern IntPtr GetCurrentThreadId();

    [DllImport("user32.dll")]
    public static extern IntPtr SetWindowsHookEx(int idHook, HOOKPROC lpfn, IntPtr hMod, IntPtr dwThreadId);

    [DllImport("user32.dll")]
    public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

    [DllImport("user32.dll")]
    public static extern bool SetWindowPos(IntPtr hWnd, int hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

    [DllImport("user32.dll")]
    public static extern bool UnhookWindowsHookEx(IntPtr hHook);

    [DllImport("user32.dll")]
    public static extern IntPtr CallNextHookEx(IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);

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

    [StructLayout(LayoutKind.Sequential)]
    public struct RECT
    {
      public int Left, Top, Right, Bottom;

      public RECT(int left, int top, int right, int bottom)
      {
        Left = left;
        Top = top;
        Right = right;
        Bottom = bottom;
      }

      public RECT(System.Drawing.Rectangle r) : this(r.Left, r.Top, r.Right, r.Bottom) { }

      public int X
      {
        get { return Left; }
        set { Right -= (Left - value); Left = value; }
      }

      public int Y
      {
        get { return Top; }
        set { Bottom -= (Top - value); Top = value; }
      }

      public int Height
      {
        get { return Bottom - Top; }
        set { Bottom = value + Top; }
      }

      public int Width
      {
        get { return Right - Left; }
        set { Right = value + Left; }
      }

      public System.Drawing.Point Location
      {
        get { return new System.Drawing.Point(Left, Top); }
        set { X = value.X; Y = value.Y; }
      }

      public System.Drawing.Size Size
      {
        get { return new System.Drawing.Size(Width, Height); }
        set { Width = value.Width; Height = value.Height; }
      }

      public static implicit operator System.Drawing.Rectangle(RECT r)
      {
        return new System.Drawing.Rectangle(r.Left, r.Top, r.Width, r.Height);
      }

      public static implicit operator RECT(System.Drawing.Rectangle r)
      {
        return new RECT(r);
      }

      public static bool operator ==(RECT r1, RECT r2)
      {
        return r1.Equals(r2);
      }

      public static bool operator !=(RECT r1, RECT r2)
      {
        return !r1.Equals(r2);
      }

      public bool Equals(RECT r)
      {
        return r.Left == Left && r.Top == Top && r.Right == Right && r.Bottom == Bottom;
      }

      public override bool Equals(object obj)
      {
        if (obj is RECT)
          return Equals((RECT)obj);
        else if (obj is System.Drawing.Rectangle)
          return Equals(new RECT((System.Drawing.Rectangle)obj));
        return false;
      }

      public override int GetHashCode()
      {
        return ((System.Drawing.Rectangle)this).GetHashCode();
      }

      public override string ToString()
      {
        return string.Format(System.Globalization.CultureInfo.CurrentCulture, "{{Left={0},Top={1},Right={2},Bottom={3}}}", Left, Top, Right, Bottom);
      }
    }

    public enum WindowLongParam
    {
      GWL_WNDPROC = -4,
      GWLP_HINSTANCE = -6,
      GWLP_HWNDPARENT = -8,
      GWL_ID = -12,
      GWL_STYLE = -16,
      GWL_EXSTYLE = -20,
      GWL_USERDATA = -21,
      DWLP_MSGRESULT = 0,
      DWLP_USER = 8,
      DWLP_DLGPROC = 4
    }


    [Flags()]
    public enum SetWindowPosFlags : uint
    {
      SWP_ASYNCWINDOWPOS = 0x4000,
      SWP_DEFERERASE = 0x2000,
      SWP_DRAWFRAME = 0x0020,
      SWP_FRAMECHANGED = 0x0020,
      SWP_HIDEWINDOW = 0x0080,
      SWP_NOACTIVATE = 0x0010,
      SWP_NOCOPYBITS = 0x0100,
      SWP_NOMOVE = 0x0002,
      SWP_NOOWNERZORDER = 0x0200,
      SWP_NOREDRAW = 0x0008,
      SWP_NOREPOSITION = 0x0200,
      SWP_NOSENDCHANGING = 0x0400,
      SWP_NOSIZE = 0x0001,
      SWP_NOZORDER = 0x0004,
      SWP_SHOWWINDOW = 0x0040,
    }
    
    public enum HCBT : 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 enum HookType : int
    {
      WH_MSGFILTER = -1,
      WH_JOURNALRECORD = 0,
      WH_JOURNALPLAYBACK = 1,
      WH_KEYBOARD = 2,
      WH_GETMESSAGE = 3,
      WH_CALLWNDPROC = 4,
      WH_CBT = 5,
      WH_SYSMSGFILTER = 6,
      WH_MOUSE = 7,
      WH_HARDWARE = 8,
      WH_DEBUG = 9,
      WH_SHELL = 10,
      WH_FOREGROUNDIDLE = 11,
      WH_CALLWNDPROCRET = 12,
      WH_KEYBOARD_LL = 13,
      WH_MOUSE_LL = 14,
    }
  }
}

解説

ウィンドウをフックすることでメッセージボックスが表示されるタイミングでメッセージボックスの座標値を変更し、オーナーウィンドウの中央に表示させる動作とします。
MessageBoxを表示するコードは通常と同じコードを記述しますが、MessageBoxを表示するコードの前にウィンドウのフックを設定します。
  private void button1_Click(object sender, EventArgs e)
  {
    SetHook(this);
    MessageBox.Show(this, "確認メッセージです", "確認");
  }

GetWindowLong() WindowsAPIを呼び出し、呼び出し元のウィンドウのインスタンスハンドルを取得します。また、GetCurrentThreadId() WindowsAPIを呼び出しスレッド識別子を取得します。
SetWindowsHookEx() WindowsAPIを呼び出し、フックを設定します。第一引数は WH_CBT を与えCBTProc のフックを設定します。第二引数はフックにより呼び出されるコールバック関数を与えます。今回はCBTProcメソッドを与えます。(WH_CBT により呼び出されるCBTProcと同じ名称のため混同に注意)第三引数にはアプリケーションのインスタンスハンドル、第四引数にスレッドの識別子を与えます。
  private void SetHook(IWin32Window owner)
  {
    // フック設定
    IntPtr hInstance = WindowsAPI.GetWindowLong(this.Handle, (int)WindowsAPI.WindowLongParam.GWLP_HINSTANCE);
    IntPtr threadId = WindowsAPI.GetCurrentThreadId();
    HHook = WindowsAPI.SetWindowsHookEx((int)WindowsAPI.HookType.WH_CBT, new WindowsAPI.HOOKPROC(CBTProc), hInstance, threadId);
  }

フックにより呼び出される関数が下記になります。WH_CBTを与えてフックを設定したため、呼び出されるフックは CBTProc になります。CBTProcは、ウィンドウのアクティブ化、作成、破棄、最小化、最大化、移動、サイズ変更のいずれかを行う前、システムコマンドを完了する前、システムメッセージキューからマウスイベントやキーボードイベントを削除する前、キーボードフォーカスを設定する前、またはシステムメッセージキューを同期させる前にフックの関数を呼び出します。
今回はメッセージボックスが表示されるタイミングで処理をするため、第一引数の nCodeが HCBT_ACTIVATE すなわち、システムがウィンドウをアクティブ化しようとするタイミングで処理をします。ウィンドウのサイズと大きさを取得しメッセージボックスの表示位置を計算します。求めた表示位置をSetWindowPos() WindowsAPI を呼び出しメッセージボックスに設定します。アクティブ化しようとするウィンドウのハンドルが wParam に設定されるため、この値を利用することでメッセージボックスにアクセスできます。
処理が終わったら、UnhookWindowsHookEx() WindowsAPIを呼び出し、フックを解除します。最後にCallNextHookEx() WindowsAPIを呼び出し、次のフックを処理します。(現在のフックチェーン内の次のフックプロシージャに、フック情報を渡すことで次のフックの処理を実行します。)
  private IntPtr CBTProc(int nCode, IntPtr wParam, IntPtr lParam)
  {
    if (nCode == (int)WindowsAPI.HCBT.HCBT_ACTIVATE) {
      WindowsAPI.RECT rectOwner;
      WindowsAPI.RECT rectMsgBox;
      int x, y;

      // オーナーウィンドウの位置と大きさを取得
      WindowsAPI.GetWindowRect(this.Handle, out rectOwner);
      // MessageBoxの位置と大きさを取得
      WindowsAPI.GetWindowRect(wParam, out rectMsgBox);

      // MessageBoxの表示位置を計算
      x = rectOwner.Left + (rectOwner.Width - rectMsgBox.Width) / 2;
      y = rectOwner.Top + (rectOwner.Height - rectMsgBox.Height) / 2;

      //MessageBoxの位置を設定
      WindowsAPI.SetWindowPos(wParam, 0, x, y, 0, 0, 
        (uint)(WindowsAPI.SetWindowPosFlags.SWP_NOSIZE | WindowsAPI.SetWindowPosFlags.SWP_NOZORDER | WindowsAPI.SetWindowPosFlags.SWP_NOACTIVATE));

      // フック解除
      WindowsAPI.UnhookWindowsHookEx(HHook);
    }
    // 次のプロシージャへのポインタ
    return WindowsAPI.CallNextHookEx(HHook, nCode, wParam, lParam);
  }

実行結果

プロジェクトを実行します。下図のウィンドウが表示されます。ウィンドウのボタンをクリックします。


メッセージボックスがウィンドウの中央に表示されます。


ウィンドウが画面の隅にある場合でも、メッセージボックスはオーナーウィンドウの中央に表示されます。


著者
iPentecのメインプログラマー
C#, ASP.NET の開発がメイン、少し前まではDelphiを愛用
掲載日: 2018-07-09
iPentec all rights reserverd.