アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

WPF で DropDown メニューボタン

Google Chrome の GUI ではメイン メニューの代りにメニューボタンを提供している。

Chrome

このボタンを押すとメニューが表示される。これを WPF で実現する場合、ぱっと思いつく方法は以下が挙げられる。

  1. ボタンにコンテキストメニューを付ける
  2. ボタンが押された時に自前でコンテキストメニューを表示する

方法 1 の場合、ボタンの左クリックではメニューが表示されずメニュー位置がマウス カーソル座標となる。方法 2 だとメニュー設定にコード ビハインドが必要となり面倒。可能な限り外観に関するものは XAML に集約したい。

というわけで方法 2 を発展させたカスタム コントロールを実装して XAML からのメニュー設定を実現してたい。以降、このボタンを DropDownMenuButton と呼ぶ。

サンプル プロジェクト

今回のコントロールを含めた、サンプル プロジェクト一式を以下に公開する。ライセンスは放棄しているので改変・再配布はご自由に。

DropDownMenuButton

DropDownMenuButton の簡単な仕様は以下のようになる。

  1. ボタンが押された時にメニューをボタンの下部に表示する
  2. XAML からメニューを設定できる

仕様 1 の挙動はクリック イベント時に処理すれば良いだろう。ボタンが押されている状態とメニュー開閉の連動 (ボタンに表示している絵を変更するとか) を考えたら ToggleButton が適任なのでこれを継承する。

仕様 2 は依存プロパティを使用。通常のコンテキスト メニュー設定もこの方法で実現されているから、設計としては演算子オーバーロードみたいなものか。

これらを実装したものの実装は、以下のようになる。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;

namespace WpfDwopDownMenuButton
{
    /// <summary>
    /// ドロップ ダウン メニューを表示する為のボタン コントロール クラスです。
    /// </summary>
    public sealed class DropDownMenuButton : ToggleButton
    {
        /// <summary>
        /// インスタンスを初期化します。
        /// </summary>
        public DropDownMenuButton()
        {
            var binding = new Binding("DropDownContextMenu.IsOpen") { Source = this };
            this.SetBinding(DropDownMenuButton.IsCheckedProperty, binding);
        }

        /// <summary>
        /// ドロップ ダウンとして表示するコンテキスト メニューを取得または設定します。
        /// </summary>
        public ContextMenu DropDownContextMenu
        {
            get
            {
                return this.GetValue(DropDownContextMenuProperty) as ContextMenu;
            }
            set
            {
                this.SetValue(DropDownContextMenuProperty, value);
            }
        }

        /// <summary>
        /// コントロールがクリックされた時のイベントです。
        /// </summary>
        protected override void OnClick()
        {
            if (this.DropDownContextMenu == null) { return; }

            this.DropDownContextMenu.PlacementTarget = this;
            this.DropDownContextMenu.Placement = PlacementMode.Bottom;
            this.DropDownContextMenu.IsOpen = !DropDownContextMenu.IsOpen;
        }

        /// <summary>
        /// ドロップ ダウンとして表示するメニューを表す依存プロパティです。
        /// </summary>
        public static readonly DependencyProperty DropDownContextMenuProperty = DependencyProperty.Register("DropDownContextMenu", typeof(ContextMenu), typeof(DropDownMenuButton), new UIPropertyMetadata(null));
    }
}

コンストラクタで ToggleButtonIsCheckedProperty とメニューの IsOpen を関連付けている。この処理によりメニュー開閉とボタン状態を連動させられる。

依存プロパティはコントロールに元から存在する ContextMenu との衝突を避けるために DropDownContextMenu と命名。コード ビハインドからのメニュー設定用に通常のプロパティも作成しておく。

OnClick イベントでメニュー開閉を実行する。メニューのオーナーを this、位置を下部、開閉状態を ToggleButton 自身の押下状態と連動させている。

今回は ToggleButton を継承しているが表示状態の連動などが不要なら Button 派生でもよい。その場合は上記コードからコンストラクタを消して OnClick を以下のように変更する。

protected override void OnClick()
{
    if (this.DropDownContextMenu == null) { return; }

    this.DropDownContextMenu.PlacementTarget = this;
    this.DropDownContextMenu.Placement = PlacementMode.Bottom;
    this.DropDownContextMenu.IsOpen = !this.DropDownContextMenu.IsOpen;
}

このコントロールを使用する XAML 記述は以下のようになる。

<Window
  x:Class="WpfDwopDownMenuButton.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:l="clr-namespace:WpfDwopDownMenuButton"
  Title="DwopDownMenuButton Test" Height="100" Width="200">
    <Grid>
        <l:DropDownMenuButton Width="120" Height="32">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Show menu" />
                <Path Width="8" Height="6" Margin="8,0,0,0" Stretch="Fill" Fill="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type l:DropDownMenuButton}},Path=Foreground}" Data="F1 M 57.5692,88L 99.1384,16L 16,16L 57.5692,88 Z "/>
            </StackPanel>
            <l:DropDownMenuButton.DropDownContextMenu>
                <ContextMenu>
                    <MenuItem Header="Item one" />
                    <Separator />
                    <MenuItem Header="Item two" />
                </ContextMenu>
            </l:DropDownMenuButton.DropDownContextMenu>
        </l:DropDownMenuButton>
    </Grid>
</Window>

l:DropDownMenuButton.DropDownContextMenu が依存プロパティ。通常の ContextMenu と同じく要素の配下にメニューを定義する。

ボタン内にテキストと下向き三角アイコンを表示している。三角アイコンの色をボタンの前景色 (文字色などと同じ) と連動させたいので Fill の内容は親の Foreground プロパティを取得している。色の指定は直に Black とかを指定してもよい。この辺はお好みで。

実際にサンプル UI を表示すると以下のようになる。ボタンを押すことで直下にメニューが表示される。

実行例