Sleipnir のようなロケーションバーを作る(2)

はじめに

Sleipnir 3 Alpha みたいなロケーションバーを作る本連載。前回XAML をゴリゴリ記述して、外観を作成しました。今回は動作を記述して、コントロールとして仕上げます。

完成画面

実行画面はこちら。

f:id:griefworker:20090522155445p:image

入力した URI がリストに追加されているんですけど…動いているところを見せないと伝わりませんね。

以下、ソースコードを解説します。

カスタム ComboBox の動作を記述

[TemplatePart(Name = CustomComboBox.PART_FAVICONBUTTON, Type = typeof(Button))]
[TemplatePart(Name = CustomComboBox.PART_DROPDOWNBUTTON, Type = typeof(ToggleButton))]
[TemplatePart(Name = CustomComboBox.PART_TEXTBOX, Type = typeof(TextBox))]
[TemplatePart(Name = CustomComboBox.PART_POPUP, Type = typeof(Popup))]
[TemplatePart(Name = CustomComboBox.PART_LISTBOX, Type = typeof(ListBox))]
public class CustomComboBox : ComboBox 
{
    internal const string PART_TEXTBOX = "PART_TextBox";
    internal const string PART_DROPDOWNBUTTON = "PART_DropDownButton";
    internal const string PART_FAVICONBUTTON = "PART_FaviconButton";
    internal const string PART_POPUP = "PART_Popup";
    internal const string PART_LISTBOX = "PART_ListBox";

    private Popup _popup;
    private Button _faviconButton;
    public TextBox EditBox { get; private set; }
    public event RoutedEventHandler FaviconButtonClick;

    static CustomComboBox()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomComboBox), new FrameworkPropertyMetadata(typeof(CustomComboBox)));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        _popup = GetTemplateChild(PART_POPUP) as Popup;
        if (_popup != null)
        {
            _popup.Opened += (sender, e) =>
                {
                    // Popup の幅を揃える
                    _popup.Width = ActualWidth;
                };
        }

        _faviconButton = GetTemplateChild(PART_FAVICONBUTTON) as Button;
        if (_faviconButton != null)
        {
            _faviconButton.Click += (sender, e) =>
                {
                    var handler = FaviconButtonClick; 
                    if (handler != null)
                    {
                        handler(this, new RoutedEventArgs());
                    }
                };
        }

        EditBox = GetTemplateChild(PART_TEXTBOX) as TextBox;
    }
}

外部から ComboBox 内の TextBox にアクセスするためのプロパティを追加しています。ComboBox の Text プロパティから入力された文字列を取得できなかった為、苦肉の策です。

LocationBar の動作を記述

[TemplatePart(Name=LocationBar.PART_COMBOBOX,Type=typeof(CustomComboBox))]
[TemplatePart(Name = LocationBar.PART_GOBUTTON, Type = typeof(Button))]
public class LocationBar : Control
{
    internal const string PART_GOBUTTON = "PART_GoButton";
    internal const string PART_COMBOBOX = "PART_ComboBox";

    private CustomComboBox _comboBox;
    private Button _goButton;

    public event EventHandler<RequestNavigateEventArgs> RequestNavigate;
    public event EventHandler<RequestBookmarkEventArgs> RequestBookmark;

    static LocationBar()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(LocationBar), new FrameworkPropertyMetadata(typeof(LocationBar)));
    }

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        _comboBox = GetTemplateChild(PART_COMBOBOX) as CustomComboBox;
        if (_comboBox != null)
        {
            _comboBox.PreviewKeyDown += new KeyEventHandler(_comboBox_PreviewKeyDown);
            _comboBox.FaviconButtonClick += new RoutedEventHandler(_comboBox_FaviconButtonClick);
        }

        _goButton = GetTemplateChild(PART_GOBUTTON) as Button;
        if (_goButton != null)
        {
            _goButton.Click += new RoutedEventHandler(_goButton_Click);
        }
    }

    // ファビコンボタンをクリックされたとき
    private void _comboBox_FaviconButtonClick(object sender, RoutedEventArgs e)
    {
        string uri = _comboBox.EditBox.Text;
        if (!string.IsNullOrEmpty(uri))
        {
            OnRequestBookmark(new RequestBookmarkEventArgs(uri));
        }
    }

    // 移動ボタンをクリックされたとき
    private void _goButton_Click(object sender, RoutedEventArgs e)
    {
        string uri = _comboBox.EditBox.Text;
        if (!string.IsNullOrEmpty(uri))
        {
            OnRequestNavigate(new RequestNavigateEventArgs(uri));
        }
    }

    private void _comboBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        // 入力エリアで Enter を押されたら ComboBox に URL 履歴を追加して
        // イベントを発生させる
        if (e.Key == Key.Enter)
        {
            string uri = _comboBox.EditBox.Text;
            if (!string.IsNullOrEmpty(uri))
            {
                // ComboBox に履歴を残す
                if (_comboBox.Items.Contains(uri))
                {
                    _comboBox.Items.Remove(uri);
                }
                _comboBox.Items.Insert(0, uri);
                _comboBox.SelectedIndex = 0;

                OnRequestNavigate(new RequestNavigateEventArgs(uri));
            }
        }
    }

    protected virtual void OnRequestNavigate(RequestNavigateEventArgs e)
    {
        var handler = RequestNavigate;
        if (handler != null)
        {
            handler(this, e);
        }
    }

    protected virtual void OnRequestBookmark(RequestBookmarkEventArgs e)
    {
        var handler = RequestBookmark;
        if (handler != null)
        {
            handler(this, e);
        }
    }
}

public class RequestNavigateEventArgs : EventArgs
{
    public string Uri { get; private set; }

    public RequestNavigateEventArgs(string uri)
    {
        Uri = uri;
    }
}

public class RequestBookmarkEventArgs : EventArgs
{
    public string Uri { get; private set; }

    public RequestBookmarkEventArgs(string uri)
    {
        Uri = uri;
    }
}

まとめ

今回作成したローケーションバーは、以前作成した検索バーと、作成手順はほとんど変わっていません。入力系のコントロールを作る場合、たいていは同じ手順になると思います。今回の作成手順は次の通りです。

  1. Border の中に Grid や StackPanel などを使ってコントロール要素を配置。
  2. 各要素に適用するスタイルを記述。必要ならテンプレートも記述する。
  3. コントロール本体のスタイル(テンプレート含む)を記述する。
  4. コントロールの動作を記述する。イベントやプロパティを追加したり、コントロール間のプロパティやイベントを結びつけたりする。

あと、「WPF でコイツを再現してほしい」っていうリクエストがあれば受け付けます。というか是非。ネタが切れそうなので(;^_^A

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

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