Firefox の検索バーもどきを作る(2)

はじめに

WPFFirefox の検索バーみたいなコントロールを作ろうという連載。前回XAML でコントロールの外観を記述しました。今回は動作の方を記述します。

今回作成したサンプルの実行画面がこちら

f:id:griefworker:20090410112600p:image
検索エンジンに Google が選択されているので、Google のアイコンが表示されています。
Yahoo! を選択すると…
f:id:griefworker:20090410112613p:image
アイコンが切り替わります

以下、解説です。

検索エンジンを表すクラスを作成

public class SearchEngine
{
    public string Name { get; set; }
    public string Query { get; set; }
    public string Icon { get; set; }
}

Icon プロパティには検索エンジン選択ボタンに表示するアイコンのパスを格納します。コントロールは Icon プロパティを見て表示するアイコンを決定します。

検索エンジン切り替え時イベント用のクラスを作成

public class SelectedEngineChangedEventArgs : EventArgs
{
    public SelectedEngineChangedEventArgs(SearchEngine before, SearchEngine after)
    {
        Before = before;
        After = after;
    }

    public SearchEngine Before { get; private set; }
    public SearchEngine After { get; private set; }
}

切り替え前と後の検索エンジンの情報を格納します。

以下、解説です。

検索ボタン押下イベント用のクラスを作成

public class SearchEventArgs : EventArgs
{
    public SearchEventArgs(string searchString, SearchEngine engine)
    {
        SearchString = searchString;
        SearchEngine = engine;
    }

    public string SearchString { get; private set; }
    public SearchEngine SearchEngine { get; private set; }
}

検索バーに入力された文字列と、実行する検索エンジンの情報を格納します。

検索バー本体の動作を記述

using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media.Imaging;

namespace SearchBarSample
{
    [TemplatePart(Name = TextBoxPart, Type = typeof(TextBox))]
    [TemplatePart(Name = SelectEngineButtonPart, Type = typeof(Button))]
    [TemplatePart(Name = SearchButtonPart, Type = typeof(Button))]
    public class SearchBar : Control
    {
        internal const string TextBoxPart = "TextBoxPart";
        internal const string SelectEngineButtonPart = "SelectEngineButtonPart";
        internal const string SearchButtonPart = "SearchButtonPart";

        private TextBox _textBox;
        private Button _searchEngineButton;
        private SearchEngine _selectedEngine;
        private ContextMenu _contextMenu;

        public static RoutedCommand SearchCommand { get; private set; }

        public event EventHandler<SearchEventArgs> Search;
        public event EventHandler<SelectedEngineChangedEventArgs> SelectedEngineChanged;

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

            SearchCommand = new RoutedCommand("SearchCommand", typeof(SearchBar));
            CommandManager.RegisterClassCommandBinding(typeof(SearchBar), new CommandBinding(SearchCommand, OnSearchCommand));
            CommandManager.RegisterClassInputBinding(typeof(SearchBar), new InputBinding(SearchCommand, new KeyGesture(Key.Enter)));
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SearchBar()
        {
            SearchEngines = new ObservableCollection<SearchEngine>();
        }

        /// <summary>
        /// 選択中の検索エンジンを取得します。
        /// </summary>
        public SearchEngine SelectedEngine
        {
            get { return _selectedEngine; }
            set
            {
                if (_selectedEngine == value)
                {
                    return;
                }
                var oldEngine = _selectedEngine;
                _selectedEngine = value;
                OnSelectedEngineChanged(new SelectedEngineChangedEventArgs(oldEngine, value));
            }
        }

        /// <summary>
        /// 利用可能な検索エンジンの一覧を取得します。
        /// </summary>
        public ObservableCollection<SearchEngine> SearchEngines { get; private set; }

        private static void OnSearchCommand(object sender, ExecutedRoutedEventArgs e)
        {
            SearchBar searchBar = sender as SearchBar;
            if (searchBar != null)
            {
                searchBar.OnSearch(new SearchEventArgs(
                    searchBar._textBox.Text,
                    searchBar.SelectedEngine));
            }
        }

        /// <summary>
        /// Search イベントを発生させます。
        /// </summary>
        /// <param name="e">イベントデータを格納する SearchEventArgs。</param>
        protected virtual void OnSearch(SearchEventArgs e)
        {
            var handler = Search;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        /// <summary>
        /// SelectedEngineChanged イベントを発生させます。
        /// </summary>
        /// <param name="e">イベントデータを格納する SelectedEngineChangedEventArgs。</param>
        protected virtual void OnSelectedEngineChanged(SelectedEngineChangedEventArgs e)
        {
            var handler = SelectedEngineChanged;
            if (handler != null)
            {
                handler(this, e);
            }

            UpdateSearchEngineButtonIcon();
        }

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

            _textBox = GetTemplateChild(TextBoxPart) as TextBox;

            _searchEngineButton = GetTemplateChild(SelectEngineButtonPart) as Button;
            if (_searchEngineButton != null)
            {
                _searchEngineButton.Click += new RoutedEventHandler(OnEngineButtonClick);
                if ((SelectedEngine == null) && (0 < SearchEngines.Count))
                {
                    SelectedEngine = SearchEngines.First();
                }
                UpdateSearchEngineButtonIcon();
            }
        }

        /// <summary>
        /// 検索エンジン選択ボタンに表示するアイコンを変更します。
        /// </summary>
        private void UpdateSearchEngineButtonIcon()
        {
            if (File.Exists(SelectedEngine.Icon) && (_searchEngineButton != null))
            {
                MemoryStream stream = new MemoryStream();
                var icon = new System.Drawing.Icon(SelectedEngine.Icon);
                icon.Save(stream);
                IconBitmapDecoder decoder = new IconBitmapDecoder(stream, BitmapCreateOptions.None, BitmapCacheOption.Default);

                var image = new Image();
                image.Source = decoder.Frames[0];
                _searchEngineButton.Content = image;
            }
        }

        /// <summary>
        /// 検索エンジン選択メニューのチェックを更新します。
        /// </summary>
        private void UpdateSearchEngineMenu()
        {
            foreach (var item in _contextMenu.Items)
            {
                MenuItem menuItem = item as MenuItem;
                if ((menuItem != null) && (menuItem.Tag as SearchEngine != null) && (((SearchEngine)menuItem.Tag) == SelectedEngine))
                {
                    menuItem.IsChecked = true;
                }
                else
                {
                    menuItem.IsChecked = false;
                }
            }
        }

        /// <summary>
        /// 検索エンジン選択メニューを作成します。
        /// </summary>
        private void CreateSearchEngineMenu()
        {
            _contextMenu = new ContextMenu();
            foreach (var engine in SearchEngines)
            {
                var item = new MenuItem { Header = engine.Name, Tag = engine };
                item.Click += new RoutedEventHandler(OnContextMenuItemClick);
                _contextMenu.Items.Add(item);
            }
            _contextMenu.Placement = PlacementMode.Bottom;
            _contextMenu.PlacementTarget = _searchEngineButton;
        }

        /// <summary>
        /// 検索エンジン選択メニューを表示します。
        /// </summary>
        private void OnEngineButtonClick(object sender, RoutedEventArgs e)
        {
            if (_contextMenu == null)
            {
                CreateSearchEngineMenu();
            }
            UpdateSearchEngineMenu();
            _contextMenu.IsOpen = !_contextMenu.IsOpen;
        }

        /// <summary>
        /// 選択された検索エンジンに切り替えます。
        /// </summary>
        private void OnContextMenuItemClick(object sender, RoutedEventArgs e)
        {
            MenuItem item = (MenuItem)sender;
            SelectedEngine = item.Tag as SearchEngine;
        }
    }
}

見ての通り、大したことはやっていません。
WPF の ContextMenu は Popup を継承しているので、Placement プロパティを使って簡単にボタンの下に表示できます。
アイコンを読み込んで表示するのは、一度メモリに書き込んでから変換するという、ちょっと面倒な方法を使っています。コードで書く場合はこうするしか無いみたい。

まとめ

WPF ならちょっと見た目に凝ったコントロールも簡単に作れますね!
ExpressionBlend を使えばもっと簡単になると思います。

作成したプロジェクト

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