C#定石 - MessageBox  - 親画面中央表示とＸボタン非表示

## はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

### 参考情報
下記情報を参考にさせて頂きました。

- [メッセージボックスを owner の中央に表示、ついでにディスプレイ内に収める](https://millyc.hatenadiary.org/entry/20080312/1205311545)

### テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。
- Windows Forms - .NET Framework 4.8
- Windows Forms - .NET 8
- WPF - .NET Framework 4.8
- WPF - .NET 8

::: note
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
:::

Windows Server 系での確認として、Windows Server 2025 評価版を利用しました。

## MessageBox

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.messagebox

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.messagebox

### 親画面中央表示

Windows Forms - MessageBox、および、WPF - MessageBox、双方ともに、画面中央に表示されます。このため、親画面の表示の位置を端にずらしている場合、MessageBox が、親画面から外れて表示されてしまい、みっともない状態となってしまいます。
MessageBox は、親画面の中央表示でないとダメですよね。

本件については、先駆者がいるので、ありがたく利用させて頂いています。

https://millyc.hatenadiary.org/entry/20080312/1205311545

### Xボタン非表示

MessageBoxButtons.YesNo と MessageBoxButtons.AbortRetryIgnore については、ウィンドウタイトルバー右端 X ボタンは無効化されます。

::: note
上記は Windows Forms の場合です。
WPF の場合は、MessageBoxButton.YesNo が対象です。
WPF では、AbortRetryIgnore は存在しません。

以降についても、Windows Forms としての情報を記載します。
:::

Windows 11 24H2 で、MessageBoxButtons.OK と MessageBoxButtons.YesNo それぞれの表示と、X ボタン Mouse Over の結果です。

![Windows11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/cb72e875-8403-a2e0-feb3-76c94583d086.png)


しかし、Windows Server 系では、MessageBoxButtons.YesNo と MessageBoxButtons.AbortRetryIgnore は、X ボタンは無効化されているのに、無効化されていることが視認できず、Mouse Over にも反応します。（Mouse Over に反応するが、Click はできません）
下記は、Windows 11 24H2 と同様の処理を Windows Server 2025 で実施した結果です。

![WindowsServer2025.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/073761ad-7021-0a95-87d5-bbf00e89136a.png)

あたかも X ボタンが有効に見えるこの挙動を抑止するために、MessageBoxButtons.YesNo と MessageBoxButtons.AbortRetryIgnore の場合、X ボタン（SYSMENU）非表示を追加するケースもありました。

親画面中央表示ソースコードに対する、具体的な修正内容を以下に記載します。

WIN32API 定義に SetWindowLong、GWL_STYLE、WS_SYSMENU を追加します。

```CSharp
#region defines
private static class NativeMethods
{
  // Xボタン非表示
  [DllImport("user32.dll")]
  public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, long dwLong);

＜中略＞

  public const int GWL_HINSTANCE = (-6);
  public const int WH_CBT = 5;
  public const int HCBT_ACTIVATE = 5;
  public const int SWP_NOSIZE = 0x0001;
  public const int SWP_NOZORDER = 0x0004;
  public const int SWP_NOACTIVATE = 0x0010;
  public const int GWL_STYLE = (-16);        // Xボタン非表示
  public const int WS_SYSMENU = 0x00080000;  // Xボタン非表示
}
```

内部変数として、HookButtons を追加します。

```CSharp
#region fields

private readonly IWin32Window Owner;
private IntPtr HookHandle = IntPtr.Zero;
private MessageBoxButtons HookButtons;     // Xボタン非表示

#endregion
```

Show メソッドで HookButtons に MessageBoxButtons の値を保持します。

```CSharp
#region methods

private DialogResult Show(
  string text,
  string caption,
  MessageBoxButtons buttons,
  MessageBoxIcon icon,
  MessageBoxDefaultButton defaultButton)
{
  IntPtr hInstance = NativeMethods.GetWindowLong(this.Owner.Handle, 
    NativeMethods.GWL_HINSTANCE);
  IntPtr threadId = NativeMethods.GetCurrentThreadId();
  this.HookHandle = NativeMethods.SetWindowsHookEx(NativeMethods.WH_CBT, 
    this.HookProc, hInstance, threadId);
  this.HookButtons = buttons;  // Xボタン非表示

  return MessageBox.Show(this.Owner, text, caption, buttons, icon, defaultButton);
}
```

HookProc メソッドに対して、SYSMENU 非表示するコードを追加します。

```CSharp
private IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam)
{
  if (nCode == NativeMethods.HCBT_ACTIVATE)
  {
＜中略＞
  NativeMethods.SetWindowPos(wParam, 0, x, y, 0, 0,
    NativeMethods.SWP_NOSIZE | NativeMethods.SWP_NOZORDER | NativeMethods.SWP_NOACTIVATE);

  // Xボタン非表示
  if ((this.HookButtons == MessageBoxButtons.YesNo)
   || (this.HookButtons == MessageBoxButtons.AbortRetryIgnore))
  {
    long style = (long)NativeMethods.GetWindowLong(wParam, NativeMethods.GWL_STYLE);
    NativeMethods.SetWindowLong(wParam, NativeMethods.GWL_STYLE, 
      style & ~NativeMethods.WS_SYSMENU);
  }

  try
  {
    return NativeMethods.CallNextHookEx(this.HookHandle, nCode, wParam, lParam);
  }
  finally
  {
    NativeMethods.UnhookWindowsHookEx(this.HookHandle);
    this.HookHandle = IntPtr.Zero;
  }
＜以下省略＞
```

上記対応で、下記表示となります。

![MessageBox.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3963267/ed2fba0d-5968-f580-16c1-a25ae3a615bc.png)



## サンプルコード

### Windows Forms

画面中央表示、X ボタン無効化した CenterMessageBox を GitHub で公開します。

https://github.com/chaichai0917/SampleCode

- MessageBox 親画面中央表示とＸボタン非表示
  - CenterMessageBox-WindowsForms.cs

基本的に、参考ソースは、そのままにしています。
MessageBox.Show 引数「MessageBoxOptions options」「string helpFilePath」「HelpNavigator navigator」「object param」「bool displayHelpButton」「string keyword」については、利用することもなかったので、追加していません。
必要に応じて、追加してください。



利用方法を記載します。

```CSharp
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "OK", MessageBoxButtons.OK, MessageBoxIcon.Information);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "YesNo", MessageBoxButtons.YesNo, MessageBoxIcon.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "AbortRetryIgnore", MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "OKCancel", MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "RetryCancel", MessageBoxButtons.RetryCancel, MessageBoxIcon.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "YesNoCancel", MessageBoxButtons.YesNoCancel, MessageBoxIcon.Question);

// .NET 8 の場合、下記コメントを外す
// MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
//   "CancelTryContinue", MessageBoxButtons.CancelTryContinue, MessageBoxIcon.Question);
```

### WPF

WPF の場合、UI ライブラリ利用、NuGet / GitHub などで公開されている MessageBox を利用して、標準 MessageBox をそのまま利用することは少ないと思いますが、、、
WPF 用に修正した CenterMessageBox を GitHub で公開します。

https://github.com/chaichai0917/SampleCode

- MessageBox 親画面中央表示とＸボタン非表示
  - CenterMessageBox-WPF.cs

::: note
.NET Framework の場合、「System.Drawing」参照追加が必要です。
:::

WPF 用に修正した部分について、コメント付与すると、可読性が悪くなりそうだったので、コメント付与していません。修正部分を確認したい場合は、双方のソース差分を確認してください。
MessageBox.Show 引数「MessageBoxOptions options」については、未対応です。
必要に応じて、追加してください。



利用方法を記載します。

```CSharp
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "OK", MessageBoxButton.OK, MessageBoxImage.Information);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "YesNo", MessageBoxButton.YesNo, MessageBoxImage.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "OKCancel", MessageBoxButton.OKCancel, MessageBoxImage.Question);
MyTools.CenterMessageBox.Show(this, "MessageBox 確認です",
  "YesNoCancel", MessageBoxButton.YesNoCancel, MessageBoxImage.Question);
```