Firefox の SideBar もどきを作る

はじめに

ネタが思いつかないので苦し紛れに始めた「○○もどきを作る」シリーズも今回が3回目。私の中で WPF 熱が再燃しました。というか WCF ちょっと飽きた。

今回のターゲットは FirefoxSideBar。お気に入りをツリー表示したりするアレです。FirefoxSideBar は拡張さえ入れなければ非常にシンプルなのでマネするのは難しくなさそう。VisualStudio の DockingWindow とか作るなら死ねますが…。

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

f:id:griefworker:20090424110404p:image

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

まずは XAML を記述して SideBar の外観を作成します

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:SideBarSample">

    <!--つまみ部分の Thumb 用スタイル-->
    <Style x:Key="ThumbStyle" TargetType="{x:Type Thumb}">
        <Setter Property="Width" Value="6"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Thumb}">
                    <!--境界線だけ表示-->
                    <Border BorderBrush="{TemplateBinding BorderBrush}"
                            Background="{TemplateBinding Background}"
                            BorderThickness="{TemplateBinding BorderThickness}"/>
                    <ControlTemplate.Triggers>
                        <!--マウスカーソルが上にあるときはカーソルを変更--> 
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Cursor" Value="{x:Static Cursors.SizeWE}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!--ヘッダ部分に表示する TextBlock 用スタイル-->
    <Style x:Key="TextBlockStyle" TargetType="{x:Type TextBlock}">
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="Margin" Value="5,1,1,1"/>
    </Style>

    <!--CloseButton の上にマウスカーソルがあるときに表示するイメージ-->
    <BitmapImage x:Key="CloseMouseOverImage" UriSource="Images/close_mouseover.png"/>
   
    <!--SideBar を閉じるボタン用のスタイル-->
    <Style x:Key="CloseButtonStyle" TargetType="{x:Type Button}">
        <Setter Property="VerticalAlignment" Value="Center"/>
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Margin" Value="2,1,5,1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type Button}">
                    <Image x:Name="PART_CloseButtonImage"
                           Source="Images/close_normal.png"
                           Width="13"
                           Height="13"
                           Stretch="Uniform"/>
                    <ControlTemplate.Triggers>
                        <!--マウスカーソルが上にあるときは画像を変更-->
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Source"
                                    TargetName="PART_CloseButtonImage"
                                    Value="{StaticResource CloseMouseOverImage}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!--SideBar のスタイル-->
    <Style TargetType="{x:Type local:SideBar}">
        <Setter Property="Background" Value="{x:Static SystemColors.ControlBrush}"/>
        <Setter Property="BorderBrush" Value="{x:Static SystemColors.ControlDarkBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:SideBar}">
                    <Border Background="{x:Static SystemColors.WindowBrush}"
                            BorderBrush="{TemplateBinding BorderBrush}">
                        <DockPanel LastChildFill="True">
                            <!--サイズを変更するつまみになる Thumb-->
                            <Thumb x:Name="PART_Thumb"
                                   Style="{StaticResource ThumbStyle}"
                                   Background="{TemplateBinding Background}"
                                   BorderBrush="{TemplateBinding BorderBrush}"
                                   BorderThickness="1,0,1,0"
                                   DockPanel.Dock="Right"/>
                            <!--ヘッダ部分-->
                            <Border BorderThickness="0,0,0,1"
                                    BorderBrush="{TemplateBinding BorderBrush}"
                                    Background="{TemplateBinding Background}"
                                    Height="25"
                                    DockPanel.Dock="Top">
                                <DockPanel LastChildFill="True">
                                    <Button Style="{StaticResource CloseButtonStyle}"
                                            Command="{x:Static local:SideBar.CloseCommand}"
                                            DockPanel.Dock="Right"/>
                                    <TextBlock Style="{StaticResource TextBlockStyle}"
                                               Text="{TemplateBinding Header}"
                                               Foreground="{TemplateBinding Foreground}"
                                               DockPanel.Dock="Left"/>
                                </DockPanel>
                            </Border>
                            <!--コンテンツ部分-->
                            <ContentPresenter/>
                        </DockPanel>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="DockPanel.Dock" Value="Right">
                            <!--右側にドッキングされているときは Thumb を左側に表示-->
                            <Setter Property="DockPanel.Dock" TargetName="PART_Thumb" Value="Left"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

SideBar をドラッグして幅を調節するツマミには Thumb を使っています。サイズ変更可能な事が利用者に分かるように、マウスが上にある時はカーソルを変更するようにしています。

GridSplitter を使えばこの手間は不要なんですが、そもそも Grid 使ってないし。SideBar の幅を変更するコードはどうせ書かないといけないし。

SideBar の動作も一気に記述します

いつもなら記事数を稼ぐために「次回へ続く」ところですが、今回作成する SideBar は大したコードじゃないので、もう記述してしまいます。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Controls.Primitives;

namespace SideBarSample
{
    [TemplatePart(Name = SideBar.PART_THUMB, Type = typeof(Thumb))]
    public class SideBar : HeaderedContentControl
    {
        internal const string PART_THUMB = "PART_Thumb";
        private Thumb _thumb;

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

            // CloseButton を押した時に実行するコマンドをクラスに登録
            CloseCommand = new RoutedCommand("CloseCommand", typeof(SideBar));
            CommandManager.RegisterClassCommandBinding(typeof(SideBar), new CommandBinding(CloseCommand, OnCloseCommand));
        }

        // CloseButton を押した時に実行するコマンド
        public static RoutedCommand CloseCommand { get; private set; }

        private static void OnCloseCommand(object sender, ExecutedRoutedEventArgs e)
        {
            SideBar sideBar = sender as SideBar;
            if (sideBar != null)
            {
                // SideBar を非表示にする
                sideBar.Visibility = Visibility.Collapsed;
            }
        }

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

            _thumb = GetTemplateChild(PART_THUMB) as Thumb;
            if (_thumb != null)
            {
                _thumb.DragDelta += new DragDeltaEventHandler(OnThumbDragDelta);
            }
        }

        // Thumb をドラッグされたら SideBar の幅を変更する
        private void OnThumbDragDelta(object sender, DragDeltaEventArgs e)
        {
            double ajustX = Width;
            switch (DockPanel.GetDock(this))
            {
                case Dock.Left:
                    ajustX += e.HorizontalChange;
                    break;
                case Dock.Right:
                    ajustX -= e.HorizontalChange;
                    break;
            }

            if (0 < ajustX)
            {
                Width = ajustX;
            }
        }
    }
}

説明が不要なほど簡単ですね。すみません。

まとめ

今回はほとんど HeaderedContentControl の Template を差し替えたようなものです。
逆に考えれば、「Firefox のシンプルな SideBar 程度なら、 WPF 標準コントロールの Template を変更して、ちょっと動作をカスタマイズすれば実装できる」という事ですね!

作成したプロジェクト

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