【WPF】Buttonからコンテキストメニューを表示する

【WPF】Buttonからコンテキストメニューを表示する

はじめに

本記事では、MVVM 構成 で WPF のボタン要素から以下のようなコンテキストメニューを表示する方法を解説します。

ボタンを右クリックするとコンテキストメニューが表示されます。

context-menu.gif

画面のレイアウト

MainWindow.xamlを以下のようにコーディングします。

"MainWindow.xaml"
<Window x:Class="MVVM.ButtonContextMenu.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="450"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>

        <StackPanel HorizontalAlignment="Center"
                    VerticalAlignment="Center"
                    Orientation="Horizontal">
            <Button Width="200"
                    Height="50"
                    Margin="10,10,10,10"
                    Content="コンテキストメニュー"
                    Cursor="Hand">
                <Button.ContextMenu>
                    <ContextMenu>
                        <MenuItem Command="{Binding FirstMenuItemCommand}"
                                  CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                                  Header="メニュー1" />
                        <MenuItem Command="{Binding SecondMenuItemCommand}"
                                  CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                                  Header="メニュー2" />
                    </ContextMenu>
                </Button.ContextMenu>
            </Button>
        </StackPanel>
    </Grid>
</Window>

ここでのポイントは、Button要素内にButton.ContextMenuを作成し、コンテキストメニューを表示するように記述しているところです。

各メニューのボタンを実行した際の処理と、どのボタンが押されたのかを判定する処理を記述しています。

CommandParameter

CommandParameterICommandインターフェイスを実装したコマンドに引数を渡すためのプロパティです。例えば、Button 要素がクリックされた時に、どのアクションをトリガーするかをICommandで指定するときに使います。

今回のコードでは、ICommandDelegateCommandと言うクラスに実装して使用します。

"DelegateCommand.cs"
using System;
using System.Windows.Input;

namespace Button_Sample
{
    /// <summary>
    /// 任意の型を受け取るDelegateCommandクラス
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class DelegateCommand<T> : ICommand
    {
        private readonly Action<T> _execute;
        private readonly Func<bool> _canExecute;

        public DelegateCommand(Action<T> execute) : this(execute, () => true)
        { }

        public DelegateCommand(Action<T> execute, Func<bool> canExecute)
        {
            this._execute = execute;
            this._canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            // RequerySuggested: コマンドを実行できるかどうかを変更する可能性のある条件が、
            //                    CommandManagerによって検出された場合に発生する
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }

        public bool CanExecute(object parameter)
        {
            return this._canExecute();
        }

        public void Execute(object parameter)
        {
            this._execute((T)parameter);
        }
    }


    /// <summary>
    /// 任意の型を指定する必要がないDelegateCommandクラス
    /// </summary>
    public class DelegateCommand : ICommand
    {
        private readonly Action _execute;
        private readonly Func<bool> _canExecute;

        public DelegateCommand(Action execute) : this(execute, () => true)
        { }

        public DelegateCommand(Action execute, Func<bool> canExecute)
        {
            this._execute = execute;
            this._canExecute = canExecute;
        }

        public event EventHandler CanExecuteChanged
        {
            // RequerySuggested: コマンドを実行できるかどうかを変更する可能性のある条件が、
            //                    CommandManagerによって検出された場合に発生する
            add => CommandManager.RequerySuggested += value;
            remove => CommandManager.RequerySuggested -= value;
        }

        public bool CanExecute(object parameter)
        {
            return this._canExecute();
        }

        public void Execute(object parameter)
        {
            this._execute();
        }
    }
}

MainWindow.xamlでのRelativeSource={RelativeSource Self}という記述は、Binding されたその要素自体に指定しています。つまり、CommandParameter はその要素自体(自分自身)をバインドしているので、MenuItemをクリックした際に、MenuItemというオブジェクトが渡ってきます。

Command に Binding された DelegateCommand でMenuItemを受け取るには以下のように記述します。

"MainWindowViewModel.cs"
using System.Windows;
using System.Windows.Controls;

namespace MVVM.ButtonContextMenu
{
    /// <summary>
    /// MainWindowのViewModel。ウィンドウのコンポーネントのコマンドとインタラクションを処理します。
    /// </summary>
    public sealed class MainWindowViewModel : BindableBase
    {
        #region コマンド

        /// <summary>
        /// 一番目メニューアイテムの実行に関連するコマンド。
        /// </summary>
        public DelegateCommand<object> FirstMenuItemCommand
            => _firstMenuItemCommand ?? (_firstMenuItemCommand = new DelegateCommand<object>(OnFirstMenuItemFirst));
        private DelegateCommand<object> _firstMenuItemCommand;

        /// <summary>
        /// 二番目のメニューアイテムの実行に関連するコマンド。
        /// </summary>
        public DelegateCommand<object> SecondMenuItemCommand
            => _secondMenuItemCommand ?? (_secondMenuItemCommand = new DelegateCommand<object>(OnSecondMenuItem));
        private DelegateCommand<object> _secondMenuItemCommand;

        #endregion

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindowViewModel()
        { }

        // ~ 中略 ~

    }
}

MenuItemで受け取ることも出来ますが、objectで受け取りMenuItemでキャストした際に null でなければ処理を実行するようにします。

"MainWindowViewModel.cs"
using System.Windows;
using System.Windows.Controls;

namespace MVVM.ButtonContextMenu
{
    /// <summary>
    /// MainWindowのViewModel。ウィンドウのコンポーネントのコマンドとインタラクションを処理します。
    /// </summary>
    public sealed class MainWindowViewModel : BindableBase
    {

        #region コマンド

        /// <summary>
        /// 一番目メニューアイテムの実行に関連するコマンド。
        /// </summary>
        public DelegateCommand<object> FirstMenuItemCommand
            => _firstMenuItemCommand ?? (_firstMenuItemCommand = new DelegateCommand<object>(OnFirstMenuItemFirst));
        private DelegateCommand<object> _firstMenuItemCommand;

        /// <summary>
        /// 二番目のメニューアイテムの実行に関連するコマンド。
        /// </summary>
        public DelegateCommand<object> SecondMenuItemCommand
            => _secondMenuItemCommand ?? (_secondMenuItemCommand = new DelegateCommand<object>(OnSecondMenuItem));
        private DelegateCommand<object> _secondMenuItemCommand;

        #endregion

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MainWindowViewModel()
        { }

        /// <summary>
        /// メインのボタンを押したときにContextMenuを開く方法
        /// </summary>
        /// <param name="o"></param>
        private void OnOpenContextMenu(object o)
        {
            var button = o as Button;

            if (button != null)
            {
                button.ContextMenu.PlacementTarget = button;
                button.ContextMenu.IsOpen = true;
            }
        }

        /// <summary>
        /// 最初のメニューアイテムのコマンド実行メソッド。そのチェック状態をトグルし、メッセージボックスを表示します。
        /// </summary>
        /// <param name="o">コマンドを呼び出したメニューアイテムオブジェクト。</param>
        private void OnFirstMenuItemFirst(object o)
        {

            var meuItem = o as MenuItem;

            if (meuItem != null)
            {
                if (!Status.IsMenuItemFirst)
                {
                    Status.IsMenuItemFirst = !Status.IsMenuItemFirst;
                    meuItem.IsChecked = Status.IsMenuItemFirst;

                    MessageBox.Show("メニュー1");

                    return;
                }

                Status.IsMenuItemFirst = !Status.IsMenuItemFirst;
                meuItem.IsChecked = Status.IsMenuItemFirst;

                MessageBox.Show("メニュー1");

            }

        }

        /// <summary>
        /// 二番目のメニューアイテムのコマンド実行メソッド。そのチェック状態をトグルし、メッセージボックスを表示します。
        /// </summary>
        /// <param name="o">コマンドを呼び出したメニューアイテムオブジェクト。</param>
        private void OnSecondMenuItem(object o)
        {
            var meuItem = o as MenuItem;

            if (meuItem != null)
            {

                if (!Status.IsMenuItemSecond)
                {
                    Status.IsMenuItemSecond = !Status.IsMenuItemSecond;
                    meuItem.IsChecked = Status.IsMenuItemSecond;

                    MessageBox.Show("メニュー2");

                    return;
                }

                Status.IsMenuItemSecond = !Status.IsMenuItemSecond;
                meuItem.IsChecked = Status.IsMenuItemSecond;

                MessageBox.Show("メニュー2");

            }

        }

    }

}

メソッドで受け取った object という引数がMenuItemオブジェクトであることが分かります。

MenuItemFirst

MenuItem の状態管理

MenuItemがチェックされていることを管理するために、Status.csと言うファイルを作成します。クラスはstaticにします。

"Status.cs"
namespace MVVM.ButtonContextMenu
{
    /// <summary>
    /// ContextMenuのステータスを切り替える
    /// </summary>
    public static class Status
    {
        /// <summary>
        /// メニュー1
        /// </summary>
        public static bool IsMenuItemFirst { get; set; }

        /// <summary>
        /// メニュー2
        /// </summary>
        public static bool IsMenuItemSecond { get; set; }
    }
}

MainWindowViewModel側で実装しているコードと合わせることで、チェックが入っていなければチェックを付け、付いていればチェックを外す実装をしています。

ボタンをクリックした際にコンテキストメニューを表示させたい場合

右クリックではなくボタンをクリックした場合にコンテキストメニューを表示させたい場合は以下の手順で実装します。

  1. ボタンがクリックされたときのコマンドを ViewModel に登録する
  2. CommandParameterに Button 要素自身を渡す
  3. ViewModel 側のメソッドでコンテキストメニューを開く処理を実装する
"MainWindow.xaml"
        <!-- 省略 -->
        <Button
            Margin="10,10,10,10"
+           Command="{Binding OpenContextMenuCommand}"
+           CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
            Content="コンテキスト&#x0a;メニュー"
            Cursor="Hand">
            <Button.ContextMenu>
                <ContextMenu>
                    <MenuItem
                        Command="{Binding FirstMenuItemCommand}"
                        CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                        Header="メニュー1" />
                    <MenuItem
                        Command="{Binding SecondMenuItemCommand}"
                        CommandParameter="{Binding RelativeSource={RelativeSource Self}}"
                        Header="メニュー2" />
                </ContextMenu>
            </Button.ContextMenu>
        </Button>

    </Grid>
</Window>

ViewModel 側に新しいメソッドとコマンドを登録します。

"MainWindowViewModel.cs"
// 省略

    /// <summary>
    /// ボタンをクリックした際のメニューアイテムの実行に関連するコマンド。
    /// </summary>
    public DelegateCommand<object> OpenContextMenuCommand
        => _openContextMenuCommand ?? (_openContextMenuCommand = new DelegateCommand<object>(OnOpenContextMenu));
    private DelegateCommand<object> _openContextMenuCommand;

/// <summary>
/// メインのボタンを押したときにContextMenuを開く方法
/// </summary>
/// <param name="o"></param>
private void OnOpenContextMenu(object o)
{
    var button = o as Button;

    if (button != null)
    {
        button.ContextMenu.PlacementTarget = button;
        button.ContextMenu.IsOpen = true;
    }
}

// 省略

映像だとわかりにくいですが、右クリックではなく通常の左クリックでコンテキストメニューが開くようになります。

context-menu02

おわりに

MVVM で実装すると XAML 側の記述で悩むことが多いですが、参考になれば幸いです。

今回作成したコードはこちらのリポジトリから参照できます。