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本構成。

目次

忙しい人向けに最初にまとめ

  • 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仲間いたら、どんどんシェア待ってるぜ!!

執筆者

バイブス父さん — 業務 SE 7 年 (正社員 2 / フリーランス 5)。 現職は SEO 直轄部の AI アドバイザー兼 PL、 副業で中小 SIer の CTO。 SES 複数社・フリーランスエージェント複数経由の経験ベースで「業務 SE 視点」 の技術 + キャリア記事を書いています。

🐦 X: @hiro_progra0524 (日々の現場メモ更新中)
📝 About Me で経歴詳細を見る

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

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

コメント

コメントする

CAPTCHA


目次