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

はじめに

Sleipnir 3 Alpha のロケーションバーがカッコいいので、WPF で挑戦する題材に決定。
WPF で ○○ を作る」シリーズ第4段です。

今回で作成した画面がこちら

f:id:griefworker:20090521144902p:image

背景が白でちょっと分かりにくいかな。ボタンの背景色が2段階のグラデーションになっています。

まずコントロール内部のレイアウトを決めます

Sleipnir 3 Alpha の LocationBar を Spy++ で調べてみたところ、ComboBox と Button がくっついてるみたいでした。作成するコントロールの内部レイアウトはこれに倣います。

ファビコンを表示する部分も ComboBox の一部なので、通常の ComboBox の Template を差し替える方法ではなく、カスタム ComboBox を作成する方法をとります。

コントロールで使うブラシをまとめて定義します

Vista のときに適用されるスキンと、できるだけ近い外観になるように色を調整。

Brush.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <!--LocationBarの境界線-->
    <SolidColorBrush x:Key="LocationBarBorderBrush" Color="#A9ACB3"/>

    <!--標準時のボタンの背景色-->
    <LinearGradientBrush x:Key="ButtonDefaultBrush" StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#F5F8FF" Offset="0"/>
        <GradientStop Color="#EBEDF9" Offset="0.5"/>
        <GradientStop Color="#DCE0EF" Offset="0.5"/>
        <GradientStop Color="#EEF3F7" Offset="1"/>
    </LinearGradientBrush>

    <!--マウスオーバー時のボタンの背景色-->
    <LinearGradientBrush x:Key="ButtonActiveBrush" StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#F0F2FB" Offset="0"/>
        <GradientStop Color="#D8DEF3" Offset="0.5"/>
        <GradientStop Color="#B2BFE8" Offset="0.5"/>
        <GradientStop Color="#E4E9F7" Offset="1"/>
    </LinearGradientBrush>

    <!--ボタン押下時の背景色-->
    <LinearGradientBrush x:Key="ButtonPressedBrush" StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#D6DBE9" Offset="0"/>
        <GradientStop Color="#D4DBF2" Offset="0.5"/>
        <GradientStop Color="#ABB9E6" Offset="0.5"/>
        <GradientStop Color="#D7DEF3" Offset="1"/>
    </LinearGradientBrush>

    <!--移動ボタンの矢印の背景色-->
    <LinearGradientBrush x:Key="GoButtonForegroundBrush" StartPoint="0,0" EndPoint="0,1">
        <GradientStop Color="#57575C" Offset="0"/>
        <GradientStop Color="#525258" Offset="1"/>
    </LinearGradientBrush>

</ResourceDictionary>

色は Web に張り付けてあるスクリーンショットをもとに、ColorZilla を使って調べました。

カスタム ComboBox のスタイルを記述します

CustomComboBox.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:LocationBarSample">
    
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/LocationBarSample;component/Common/Brush.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <!--ファビコンを表示するボタンのスタイル-->
    <Style x:Key="FaviconButtonStyle" TargetType="Button">
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Width" Value="32"/>
        <Setter Property="Background" Value="{StaticResource ButtonDefaultBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource LocationBarBorderBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border x:Name="Border"
                            Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="0,0,1,0"
                            CornerRadius="2,0,0,2">
                        <ContentPresenter VerticalAlignment="Center"
                                          HorizontalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="{StaticResource ButtonActiveBrush}"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background" Value="{StaticResource ButtonPressedBrush}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

    <!--ComboBox 内の TextBox に適用するスタイル-->
    <Style x:Key="TextBoxStyle" TargetType="TextBox">
        <Setter Property="VerticalContentAlignment" Value="Center"/>
        <Setter Property="BorderBrush" Value="Transparent"/>
        <Setter Property="BorderThickness" Value="0"/>
    </Style>

    <!--▼ボタンのスタイル-->
    <Style x:Key="DropDownButtonStyle" TargetType="ToggleButton">
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Width" Value="16"/>
        <Setter Property="BorderBrush" Value="{StaticResource LocationBarBorderBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="ToggleButton">
                    <Border x:Name="Border"
                            Background="Transparent"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="0,0,0,0">
                        <ContentPresenter VerticalAlignment="Center"
                                          HorizontalAlignment="Center"/>
                    </Border>
                    <ControlTemplate.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter TargetName="Border" Property="BorderThickness" Value="1,0,0,0"/>
                            <Setter TargetName="Border" Property="Background" Value="{StaticResource ButtonActiveBrush}"/>
                        </Trigger>
                        <Trigger Property="IsChecked" Value="True">
                            <Setter TargetName="Border" Property="BorderThickness" Value="1,0,0,0"/>
                            <Setter TargetName="Border" Property="Background" Value="{StaticResource ButtonPressedBrush}"/>
                        </Trigger>
                    </ControlTemplate.Triggers>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

    <!--カスタム ComboBox のスタイル-->
    <Style TargetType="{x:Type local:CustomComboBox}">
        <Setter Property="IsEditable" Value="True"/>
        <Setter Property="BorderBrush" Value="{StaticResource LocationBarBorderBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:CustomComboBox}">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            CornerRadius="2,0,0,2">
                        <Grid>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="Auto"/>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="Auto"/>
                            </Grid.ColumnDefinitions>
                            <Button x:Name="PART_FaviconButton"
                                    Style="{StaticResource FaviconButtonStyle}"
                                    Grid.Column="0">
                                <!--アイコンは今回は固定-->
                                <Image Source="../Images/page.png" Stretch="None"/>
                            </Button>
                            <TextBox x:Name="PART_TextBox"
                                     Style="{StaticResource TextBoxStyle}"
                                     Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedValue}"
                                     Grid.Column="1"/>
                            <ToggleButton x:Name="PART_DropDownButton"
                                          Style="{StaticResource DropDownButtonStyle}"
                                          IsChecked="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsDropDownOpen}"
                                          ClickMode="Press"
                                          Grid.Column="2">
                                <!--▼は Path を使って自力描画-->
                                <Path VerticalAlignment="Center"
                                      HorizontalAlignment="Center"
                                      Data="M 0 0 L 4 4 L 8 0 Z">
                                    <Path.Fill>
                                        <SolidColorBrush Color="#FF333333"/>
                                    </Path.Fill>
                                </Path>
                            </ToggleButton>
                            <Popup x:Name="PART_Popup"
                                   Height="160"
                                   Focusable="False"
                                   PopupAnimation="Slide"
                                   Placement="Bottom"
                                   PlacementTarget="{Binding ElementName=PART_FaviconButton}"
                                   IsOpen="{TemplateBinding IsDropDownOpen}">
                                <Border BorderThickness="1"
                                        BorderBrush="{TemplateBinding BorderBrush}">
                                    <ScrollViewer VerticalScrollBarVisibility="Visible">
                                        <StackPanel Background="{x:Static SystemColors.WindowBrush}"
                                                    IsItemsHost="True"/>
                                    </ScrollViewer>
                                </Border>
                            </Popup>
                        </Grid>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

ファビコンを表示するボタンは、今回、画像を固定しています。
▼ボタンのチェック状態と Popup の表示/非表示が連動するようにしています。
ComboBox のテンプレートは MSDN のサンプルを参考にしました。

LocationBar のスタイルを記述します

<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:LocationBarSample">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/LocationBarSample;component/Common/Brush.xaml"/>
    </ResourceDictionary.MergedDictionaries>

    <!--移動ボタンのスタイル-->
    <Style x:Key="GoButtonStyle" TargetType="Button">
        <Setter Property="IsTabStop" Value="False"/>
        <Setter Property="Focusable" Value="False"/>
        <Setter Property="Width" Value="32"/>
        <Setter Property="Background" Value="{StaticResource ButtonDefaultBrush}"/>
        <Setter Property="BorderBrush" Value="{StaticResource LocationBarBorderBrush}"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="Button">
                    <Border Background="{TemplateBinding Background}"
                            BorderBrush="{TemplateBinding BorderBrush}"
                            BorderThickness="0,1,1,1"
                            CornerRadius="0,2,2,0">
                        <ContentPresenter VerticalAlignment="Center"
                                          HorizontalAlignment="Center"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
        <Style.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter Property="Background" Value="{StaticResource ButtonActiveBrush}"/>
            </Trigger>
            <Trigger Property="IsPressed" Value="True">
                <Setter Property="Background" Value="{StaticResource ButtonPressedBrush}"/>
            </Trigger>
        </Style.Triggers>
    </Style>

    <!-- LocationBar のスタイル -->
    <Style TargetType="{x:Type local:LocationBar}">
        <Setter Property="BorderBrush" Value="{StaticResource LocationBarBorderBrush}"/>
        <Setter Property="BorderThickness" Value="1"/>
        <Setter Property="MinHeight" Value="24"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:LocationBar}">
                    <Grid>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="*"/>
                            <ColumnDefinition Width="Auto"/>
                        </Grid.ColumnDefinitions>
                        <local:CustomComboBox x:Name="PART_ComboBox"
                                              Grid.Column="0"/>
                        <Button x:Name="PART_GoButton"
                                Style="{StaticResource GoButtonStyle}"
                                Grid.Column="1">
                            <!--矢印は Path を使って自力描画-->
                            <Path HorizontalAlignment="Stretch"
                                  VerticalAlignment="Stretch"
                                  Stretch="Uniform"
                                  Fill="{StaticResource GoButtonForegroundBrush}">
                                <Path.Data>
                                    <PathGeometry>
                                        <PathFigure StartPoint="0,10"
                                                    IsClosed="True"
                                                    IsFilled="True">
                                            <PathFigure.Segments>
                                                <LineSegment Point="0,5"/>
                                                <LineSegment Point="5,5"/>
                                                <LineSegment Point="5,0"/>
                                                <LineSegment Point="12.5,7.5"/>
                                                <LineSegment Point="5,15"/>
                                                <LineSegment Point="5,10"/>
                                                <LineSegment Point="0,10"/>
                                            </PathFigure.Segments>
                                        </PathFigure>
                                    </PathGeometry>
                                </Path.Data>
                            </Path>
                        </Button>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

</ResourceDictionary>

カスタム ComboBox と Button を並べています。2つを Border で囲ってはいません。
移動ボタンに表示する画像は、良いものがなかったので、自力で描画しました。

Generic.xaml に上記の XAML をマージします

Generic.xaml
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:LocationBarSample">
    <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="/LocationBarSample;component/CustomComboBox/CustomComboBox.xaml"/>
        <ResourceDictionary Source="/LocationBarSample;component/LocationBar/LocationBar.xaml"/>
    </ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

MergedDictionaryies を使えば、XAML を分割できるので、管理しやすくなります。

次回に続く

結構な量の XAML を記述したので疲れました。今回はここまで。
次回はコントロールの内部動作を実装します。作成したプロジェクトも公開予定です。