WPF で DropDown メニューボタン
Google Chrome の GUI ではメイン メニューの代りにメニューボタンを提供している。
このボタンを押すとメニューが表示される。これを WPF で実現する場合、ぱっと思いつく方法は以下が挙げられる。
- ボタンにコンテキストメニューを付ける
- ボタンが押された時に自前でコンテキストメニューを表示する
方法 1 の場合、ボタンの左クリックではメニューが表示されずメニュー位置がマウス カーソル座標となる。方法 2 だとメニュー設定にコード ビハインドが必要となり面倒。可能な限り外観に関するものは XAML に集約したい。
というわけで方法 2 を発展させたカスタム コントロールを実装して XAML からのメニュー設定を実現してたい。以降、このボタンを DropDownMenuButton
と呼ぶ。
サンプル プロジェクト
今回のコントロールを含めた、サンプル プロジェクト一式を以下に公開する。ライセンスは放棄しているので改変・再配布はご自由に。
DropDownMenuButton
DropDownMenuButton の簡単な仕様は以下のようになる。
- ボタンが押された時にメニューをボタンの下部に表示する
- 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));
}
}
コンストラクタで ToggleButton
の IsCheckedProperty
とメニューの 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 を表示すると以下のようになる。ボタンを押すことで直下にメニューが表示される。