Silverlight で DropDownButton

Silverlight Toolkit の ContextMenu を使って、DropDownButton コントロールを実装してみた。下図が完成画面。一応それっぽく動いている。

f:id:griefworker:20100714200451j:image

使い方はこんな感じ。

<ctrl:DropDownButton Content="Click"
                     Canvas.Left="50"
                     Canvas.Top="50">
    <ctrl:DropDownButton.Menus>
        <input:MenuItem Header="Menu1"/>
        <input:MenuItem Header="Menu2"/>
        <input:MenuItem Header="Menu3"/>
        <input:MenuItem Header="Menu4"/>
        <input:MenuItem Header="Menu5"/>
    </ctrl:DropDownButton.Menus>
</ctrl:DropDownButton>

DropDownButton の実装コードがこちら。

using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Collections.ObjectModel;
using System.Windows.Controls.Primitives;
using System.Collections.Specialized;

namespace Nakamura.Windows.Controls
{
    /// <summary>
    /// ドロップダウンボタンを表します。
    /// </summary>
    public class DropDownButton : ToggleButton
    {
        /// <summary>
        /// ドロップダウンメニュー
        /// </summary>
        private ContextMenu _dropDownMenu;

        /// <summary>
        /// <see cref="DropDownButton"/> クラスの新しいインスタンスを初期化します。
        /// </summary>
        public DropDownButton()
        {
            this.DefaultStyleKey = typeof(ToggleButton);

            // デフォルトのコレクションにもイベントハンドラを設定したいので、
            // PropertyMetadata ではなくコンストラクタで初期化する
            this.Menus = new ObservableCollection<MenuItem>();
        }

        protected override void OnClick()
        {
            if (this._dropDownMenu == null)
            {
                this._dropDownMenu = new ContextMenu();
                this._dropDownMenu.DataContext = this;
                this._dropDownMenu.SetBinding(ContextMenu.ItemsSourceProperty, new Binding("Menus"));
                this._dropDownMenu.SetBinding(ContextMenu.IsOpenProperty, new Binding("IsChecked")
                {
                    Mode = BindingMode.TwoWay,
                });
                this._dropDownMenu.Closed += new RoutedEventHandler(DropDownMenuClosed);

                // ContextMenu の表示位置を変更
                var transform = this.TransformToVisual(Application.Current.RootVisual);
                var point = transform.Transform(new Point(0, this.ActualHeight));
                this._dropDownMenu.HorizontalOffset = point.X;
                this._dropDownMenu.VerticalOffset = point.Y;
            }
            base.OnClick();
        }

        private void DropDownMenuClosed(object sender, RoutedEventArgs e)
        {
            if (this._dropDownMenu != null)
            {
                // ContextMenu のバインディングを解除しておかないと、
                // 次表示するときに MenuItem をバインドできない
                this._dropDownMenu.DataContext = null;
                this._dropDownMenu.Closed -= DropDownMenuClosed;
            }
            this._dropDownMenu = null;
        }

        /// <summary>
        /// ドロップダウンメニューの内容
        /// </summary>
        public static readonly DependencyProperty MenusProperty = DependencyProperty.Register(
            "Menus",
            typeof(ObservableCollection<MenuItem>),
            typeof(DropDownButton),
            new PropertyMetadata(null, MenusChanged));

        /// <summary>
        /// ドロップダウンメニューの内容を取得または設定します。
        /// </summary>
        public ObservableCollection<MenuItem> Menus
        {
            get { return GetValue(MenusProperty) as ObservableCollection<MenuItem>; }
            set { SetValue(MenusProperty, value); }
        }

        private static void MenusChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            var self = (DropDownButton)sender;
            var newMenus = e.NewValue as ObservableCollection<MenuItem>;
            if (newMenus != null)
            {
                newMenus.CollectionChanged += self.Menus_CollectionChanged;
            }
            var oldMenus = e.OldValue as ObservableCollection<MenuItem>;
            if (oldMenus != null)
            {
                oldMenus.CollectionChanged -= self.Menus_CollectionChanged;
            }
        }

        private void Menus_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            if (e.NewItems != null)
            {
                foreach (MenuItem item in e.NewItems)
                {
                    item.Click += OnMenuItemClick;
                }
            }
            if (e.OldItems != null)
            {
                foreach (MenuItem item in e.OldItems)
                {
                    item.Click -= OnMenuItemClick;
                }
            }
        }

        private void OnMenuItemClick(object sender, RoutedEventArgs e)
        {
            if (this._dropDownMenu != null)
            {
                this._dropDownMenu.IsOpen = false;
            }
        }
    }
}

ToggleButto を継承していて、ToogleButton がクリックされたときに、ContextMenu を表示している。

ポイントは ContextMenu の IsOpen プロパティと、ToggleButton の IsChecked プロパティをバインドしているところ。ContextMenu が閉じられたら、自動で ToggleButton のチェックが解除されるようにしている。コントロール自身の依存プロパティの値によって、構成する要素の表示状態を変更したい場合、データバインドを使うと簡単だ。

今回作成したプロジェクト

以下のリンクからダウンロードできます。