WinForms メニュー3兄弟 — MenuStrip / ToolStrip / ContextMenuStrip の使い分けと DataGridView 連携

みなさんこんにちは!ヒロポンです!

WinForms で業務系アプリ書いてる時、こんな場面ないですか??

  • DataGridView の行を右クリックして「編集」「削除」メニュー出したいけど、ContextMenuContextMenuStrip どっち使うか迷う
  • 保存 / 印刷ボタンをフォーム上部のツールバーに置きたいけど、ToolStrip のアイコンが透過しない
  • File / Edit メニューの Ctrl+S ショートカット重複でハマった

俺も最初の WinForms 業務系プロジェクトで DataGridView 右クリック編集メニュー作ろうとした時に、ContextMenu(旧式)と ContextMenuStrip(新式)を混同して、デザイナで配置できなくて30分溶かしたんですよね。.NET Framework 4.7.2 / VS2019 / C# 7.3 の構成だったんですけど、当時は「どっち使うのが正解なんや??」って状態でした。

ん?普通にメニュー置くだけやろ??って思いますよね。

結論先出すと、WinForms のメニュー UI は 3兄弟 で整理できます。MenuStrip(上メイン)/ ToolStrip(上ツールバー)/ ContextMenuStrip(右クリック)。今回はこの3兄弟の使い分けと、業務系で頻出する DataGridView 連携をコード付きで解説。コード5本構成。

目次

TL;DR

  • WinForms メニュー UI は3クラス: MenuStrip(フォーム上部のメインメニュー)/ ToolStrip(アイコン付きツールバー)/ ContextMenuStrip(右クリックメニュー)
  • 新規実装は全部 Strip 系を使う。旧式の MainMenu / ToolBar / ContextMenu は使わない
  • DataGridView の右クリック編集メニューは ContextMenuStrip + CellMouseDown イベント で実装
  • アイコン透過は ImageTransparentColor プロパティで指定(マゼンタ背景画像なら Color.Magenta

なぜ3兄弟(Strip 系)を使うのか — 旧式との違い

WinForms には旧式(.NET Framework 1.x 時代)と新式(2.0 以降)のメニュー UI があります。

旧式 新式(Strip 系) 違い
MainMenu MenuStrip サブメニュー / 画像 / レンダラ対応
ToolBar ToolStrip ボタン以外(テキストボックス / コンボボックス)も配置可能
ContextMenu ContextMenuStrip デザイナサポート / Items の動的制御が容易

業務系で新規実装するなら 全部 Strip 系一択。旧式は VB6 から移植したコードで残ってる場合があるくらいで、新規にあえて使う場面は皆無です。

ハマりやすいのが「旧プロジェクトの拡張」で、ContextMenu が残ってる画面に新規メニューを追加したい時。無理に Strip に置換せず、新規追加分だけ別 ContextMenuStrip で隣に置く のが業務SE 鉄則。旧式と新式は混在できる設計なので、置換コストを払う前にこんな感じで隣置きで凌げます。

パターン1: MenuStrip — フォーム上部のメインメニュー

File / Edit / Help みたいなメインメニューを置くやつ。デザイナで Form の上部に貼り付けて使います。

// デザイナで MenuStrip を追加し、Items に「ファイル」「編集」「ヘルプ」を登録
// 「ファイル」配下に「開く」「保存」「終了」サブメニュー

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        // 「保存」メニューの Click イベント
        saveToolStripMenuItem.Click += (sender, e) =>
        {
            SaveData();
        };

        // 「終了」メニューの Click イベント
        exitToolStripMenuItem.Click += (sender, e) =>
        {
            this.Close();
        };

        // ShortcutKeys 設定(デザイナでも可)
        saveToolStripMenuItem.ShortcutKeys = Keys.Control | Keys.S;
        exitToolStripMenuItem.ShortcutKeys = Keys.Alt | Keys.F4;
    }

    private void SaveData()
    {
        // 業務系の保存処理
        MessageBox.Show("保存しました");
    }
}

ShortcutKeys プロパティで Ctrl+S 等のキーボードショートカットを設定。ここでハマりやすいのが、同一 MenuStrip 内で同じ ShortcutKeys を設定するとコンパイル時には通って実行時に最初の項目のみ動く という挙動。業務系で「Ctrl+S が保存と保存ダイアログの両方に割り当たっちゃってる」事故が発生しがちなので、デザイナの Document Outline ペインで全 ShortcutKeys を一覧して重複チェックするのがいい感じです。

パターン2: ToolStrip — アイコン付きツールバー

フォーム上部に保存 / 印刷 / 検索のアイコンボタンを並べるやつ。MenuStrip の下に貼って使うパターンが業務系で頻出。

public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();

        // ToolStripButton の Click イベント
        saveButton.Click += (sender, e) =>
        {
            SaveData();
        };

        printButton.Click += (sender, e) =>
        {
            PrintData();
        };

        // アイコン画像の透過設定(マゼンタ背景の ICO の場合)
        saveButton.ImageTransparentColor = Color.Magenta;
        printButton.ImageTransparentColor = Color.Magenta;

        // ToolStrip に動的にアイテム追加(実行時拡張)
        var refreshButton = new ToolStripButton("更新");
        refreshButton.Click += (s, e) => RefreshData();
        mainToolStrip.Items.Add(refreshButton);
    }

    private void PrintData() { /* ... */ }
    private void RefreshData() { /* ... */ }
}

ハマりポイント。ImageTransparentColor を設定しないと透過しません。デフォルトは Color.Empty なので、マゼンタ背景の社内アイコンライブラリを貼ると四角い背景が残ったまま表示されて「なんで透過しないんや??」状態になります。

業務系の社内アイコン規約は大体「マゼンタ背景の 16×16 ICO / BMP」なので、saveButton.ImageTransparentColor = Color.Magenta; をテンプレ化しておくと事故防げます。PNG 形式なら透過情報を持つので ImageTransparentColor 設定不要、っていう逃げ道もあります。

パターン3: ContextMenuStrip + DataGridView — 右クリック編集メニュー

業務系で頻出のパターン。DataGridView の行を右クリックして「編集」「削除」メニューを表示するやつ。

public partial class MasterForm : Form
{
    public MasterForm()
    {
        InitializeComponent();

        // デザイナで ContextMenuStrip (rowContextMenu) を追加し、
        // Items に「編集」「削除」を登録しておく

        // DataGridView に ContextMenuStrip を紐付け
        masterGridView.ContextMenuStrip = rowContextMenu;

        // 右クリックされた行を検出するためのイベント
        masterGridView.CellMouseDown += MasterGridView_CellMouseDown;

        // 「編集」メニューの Click イベント
        editToolStripMenuItem.Click += (sender, e) =>
        {
            EditCurrentRow();
        };

        // 「削除」メニューの Click イベント
        deleteToolStripMenuItem.Click += (sender, e) =>
        {
            DeleteCurrentRow();
        };
    }

    private void MasterGridView_CellMouseDown(object sender, DataGridViewCellMouseEventArgs e)
    {
        // 右クリックの時のみ処理
        if (e.Button != MouseButtons.Right) return;
        if (e.RowIndex < 0) return;  // ヘッダ行は対象外

        // 右クリックした行をカレント行に切り替え
        masterGridView.CurrentCell = masterGridView.Rows[e.RowIndex].Cells[0];
    }

    private void EditCurrentRow()
    {
        if (masterGridView.CurrentRow == null) return;
        var id = (int)masterGridView.CurrentRow.Cells["id"].Value;
        // 編集画面を開く
    }

    private void DeleteCurrentRow()
    {
        if (masterGridView.CurrentRow == null) return;
        var id = (int)masterGridView.CurrentRow.Cells["id"].Value;
        // 削除確認 → 削除実行
    }
}

ここで業務SE がハマるポイント2つ。

1つ目: CellMouseDownmasterGridView.CurrentCell を更新しないと、右クリックした行と編集対象の行が ズレる 事故が発生します。右クリックは「カレント行を変更しない」のがデフォルト挙動なので、CellMouseDown で明示的に CurrentCell を切り替える必要があります。

2つ目: e.RowIndex < 0 のガードを忘れると、ヘッダ行右クリックで例外が出ます。ヘッダ行は RowIndex = -1 なので、ガードしないと Rows[-1] で IndexOutOfRangeException 発火。これも業務系の WinForms あるあるです。

俺も最初の DataGridView 右クリックメニュー実装で CurrentCell 切り替えを忘れて、編集ボタン押したら 別の行が編集される 事故やらかしました。テストでお客さん側のレビュアーが「この行を編集したのに別の行が変わった!!」って報告上げてきて、調査に半日かかった経験あります。

パターン4: ContextMenuStrip.Opening — 動的な表示制御

「権限のないユーザーには削除メニューを表示しない」みたいな動的制御は Opening イベントで書きます。

private void rowContextMenu_Opening(object sender, CancelEventArgs e)
{
    // メニュー全体をキャンセル(表示しない)
    if (masterGridView.CurrentRow == null)
    {
        e.Cancel = true;  // メニュー表示自体を抑制
        return;
    }

    // 個別アイテムの表示制御
    var currentUserCanEdit = CheckEditPermission();
    var currentUserCanDelete = CheckDeletePermission();

    editToolStripMenuItem.Visible = currentUserCanEdit;
    deleteToolStripMenuItem.Visible = currentUserCanDelete;

    // ステータスによる表示制御
    var status = (string)masterGridView.CurrentRow.Cells["status"].Value;
    deleteToolStripMenuItem.Enabled = (status != "確定済");  // 確定済は削除不可
}

private bool CheckEditPermission() { return true; /* 実際は権限チェック */ }
private bool CheckDeletePermission() { return false; /* 実際は権限チェック */ }

ハマりポイント。e.Cancel = trueメニュー表示自体の抑制 で、すでに表示中のメニューを閉じる用途には使えません。表示中のメニューを閉じたい場合は Closing イベントで e.Cancel = true を使う、というのが正解(実際の業務系ではほぼ Opening 一択ですが、念のため)。

Visible / Enabled の使い分けも業務系で迷いがち。Visible = false は項目自体を消す(メニュー自体の縦サイズが縮む)、Enabled = false はグレーアウト表示(ユーザーに「この操作は今できない」と伝わる)。業務系で「権限がない場合」は Visible = false、「条件付きで一時的に使えない場合」は Enabled = false で使い分けるのがいい感じです。

ハマりポイント整理 — 旧式と新式の混在トラブル

業務系で一番ハマるのが、旧プロジェクトに新メニューを追加する時の ContextMenuContextMenuStrip 混在 トラブル。

// ❌ NG: 同一コントロールに ContextMenu と ContextMenuStrip 両方設定
// myControl.ContextMenu = oldMenu;          // 旧式
// myControl.ContextMenuStrip = newStrip;    // 新式
// → 動作は新式 (ContextMenuStrip) が優先されるが、旧式の Items は無視される

// ✅ OK: どちらか一方に揃える
myControl.ContextMenuStrip = newStrip;
// または
myControl.ContextMenu = oldMenu;  // 旧プロジェクトの保守でのみ

両方設定すると 新式が優先 されますが、旧式に登録した Items が突然消える挙動に見えるので、新規追加時は 旧式の ContextMenu を null にしてから新式に切り替える のが安全。

// 安全な切り替え
myControl.ContextMenu = null;
myControl.ContextMenuStrip = newStrip;

俺の現場でも VB6 → C# 移行プロジェクトで ContextMenu が大量に残ってて、新機能追加の度に「どっち使うんやっけ??」状態になってました。プロジェクト規約として「新規追加は全部 Strip 系」と最初に決めておくのが業務SE 鉄則です。

まとめ

WinForms メニュー3兄弟の使い分け:

クラス 用途 業務系での頻度
MenuStrip フォーム上部のメインメニュー(File / Edit / Help) ★★★
ToolStrip アイコン付きツールバー(保存 / 印刷 / 検索) ★★★
ContextMenuStrip 右クリックメニュー(DataGridView 行操作) ★★★
StatusStrip フォーム下部のステータスバー(接続中DB / ログイン名) ★★

業務SE 必修パターン3つ:

  1. MenuStrip + ShortcutKeys 重複チェック — Document Outline ペインで一覧確認
  2. ToolStrip + ImageTransparentColor = Color.Magenta — マゼンタ背景アイコンの透過
  3. ContextMenuStrip + DataGridView + CellMouseDown で CurrentCell 切り替え — 右クリック編集メニュー

新規実装は 全部 Strip 系一択、旧式(MainMenu / ToolBar / ContextMenu)は保守時のみ触る、という方針で運用するとプロジェクトのメニュー UI 周りが揃って、いい感じに保守性が上がります。

よくある質問

Q1. ContextMenu と ContextMenuStrip の違いは?

A. ContextMenu は .NET Framework 1.x からある旧式クラス、ContextMenuStrip は 2.0 以降の新式クラスです。業務系で新規実装するなら ContextMenuStrip 一択。デザイナサポート / 画像・チェックマーク・サブメニュー対応 / ToolStrip との内部実装共通、の3点で新式が優れています。旧プロジェクトで ContextMenu が残ってる場合は無理に置換せず、新規追加分は ContextMenuStrip にする現実解です。

Q2. DataGridView の右クリックで「編集」「削除」メニューを出すには?

A. DataGridView.ContextMenuStrip プロパティに ContextMenuStrip を設定し、CellMouseDown イベントで右クリック行を検出します。実装3ステップ: (1)デザイナで ContextMenuStrip を追加し Items に「編集」「削除」を登録、(2)DataGridView.ContextMenuStrip = ctxMenuStrip; を設定、(3)CellMouseDown で e.Button == MouseButtons.Right の時に DataGridView.CurrentCell を更新。

Q3. ToolStrip にアイコンを設定したら透過しないのは?

A. ToolStripItem.ImageTransparentColor プロパティで透過色を指定する必要があります。デフォルトは Color.Empty なので透過しません。マゼンタ背景の社内 ICO / BMP なら Color.Magenta を指定。PNG 形式の画像を使えば透過は自動です。

Q4. ContextMenuStrip.Opening で e.Cancel = true しても閉じない時は?

A. Opening の e.Cancel = true は「メニュー表示自体をキャンセル」する設定で、すでに表示中のメニューを閉じる用途には使えません。表示中のメニューを閉じたい場合は Closing イベントで e.Cancel = true を扱います。業務系で「条件付きでメニューを表示制御」したい時は、Opening で Items.Visible を動的に切り替える方が現実的です。

Q5. MenuStrip のキーボードショートカット重複の対処は?

A. 同一 MenuStrip 内で同じ ShortcutKeys を複数項目に設定するとコンパイル時には通っても実行時に最初の項目のみ動く挙動になります。対処は ShortcutKeys を一意に設計し直す(Ctrl+S = 保存、Ctrl+Shift+S = 名前を付けて保存、のように差別化)か、デザイナの Document Outline ペインで全 ShortcutKeys を一覧して重複チェックするのが業務SE 鉄則です。

関連記事

以上!

同じ罠でハマってる業務SE仲間いたら、どんどんシェア待ってるぜ!!


この記事が気に入ったら
いいねしてね!

どんどんシェア待ってるぜ!!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次