sh1’s diary

プログラミング、読んだ本、資格試験、ゲームとか私を記録するところ

WPF で Windows Explorer 風のシンプルな ComboBox を作る

Windows Explorer で使われているコンボボックスのイメージはこんな感じのものです。

f:id:shikaku_sh:20200309180929p:plain:w500
枠のないコンボボックスが標準なんですね

それを、こんな感じで作ってみました。左が作ったもの。右はなにもスタイルを与えていないデフォルトです。

f:id:shikaku_sh:20200309181040g:plain
エフェクトを絞ったので軽い動作のコンボボックスです

影表示もしないし、リストアイテムの伸びる演出もありません。ただ、色味はかなり Windows Explorer ライク。

スタイル

こんな感じで定義しました。コンボボックスの見た目の基本が ToggleButton のようになっていますが、これはデフォルトの ComboBox のスタイルがそのようになっています。「Microsoft Docs - ComboBox のスタイルとテンプレート」を確認してみるとよいと思います。

<ControlTemplate x:Key="SlimComboBoxButtonTemplate" TargetType="{x:Type ToggleButton}">
    <Border x:Name="Border" 
            BorderThickness="1" 
            BorderBrush="Transparent" Background="Transparent">
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Path x:Name="Arrow"
                  Grid.Column="1" Margin="4 0 4 0"
                  HorizontalAlignment="Center" VerticalAlignment="Center"
                  SnapsToDevicePixels="True"
                  Data="M 0 0.5 L 3 3.5 L 6 0.5 Z" Fill="#212121" />
        </Grid>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="IsMouseOver" Value="True">
            <Setter TargetName="Border" Property="BorderBrush" Value="#DD99CCFF"/>
            <Setter TargetName="Border" Property="Background" Value="#2299CCFF"/>
        </Trigger>
        <Trigger Property="IsChecked" Value="True">
            <Setter TargetName="Border" Property="BorderBrush" Value="#EE99CCFF"/>
            <Setter TargetName="Border" Property="Background" Value="#4499CCFF"/>
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

<Style x:Key="SlimComboBoxItemStyle"  TargetType="{x:Type ComboBoxItem}">

    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="OverridesDefaultStyle" Value="true" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ComboBoxItem}">
                <Border Name="Border"
                        Padding="2" Margin="0 1 0 0"
                        BorderThickness="1" BorderBrush="Transparent"
                        SnapsToDevicePixels="true">
                    <ContentPresenter />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsHighlighted" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" Value="#DD99CCFF"/>
                        <Setter TargetName="Border" Property="Background" Value="#2299CCFF"/>
                    </Trigger>
                    <Trigger Property="IsEnabled" Value="false">
                        <Setter Property="Foreground" Value="#4499CCFF"/>
                    </Trigger>
                    <Trigger Property="IsSelected" Value="true">
                        <Setter TargetName="Border" Property="BorderBrush" Value="#DD99CCFF"/>
                        <Setter TargetName="Border" Property="Background" Value="#2299CCFF"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

<Style x:Key="SlimComboBoxStyle" TargetType="{x:Type ComboBox}">
    
    <Setter Property="SnapsToDevicePixels" Value="true" />
    <Setter Property="OverridesDefaultStyle" Value="true" />
    <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />
    <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
    <Setter Property="ScrollViewer.CanContentScroll" Value="true" />
    <Setter Property="ItemContainerStyle" Value="{StaticResource SlimComboBoxItemStyle}" />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ComboBox}">
                <Grid>
                    <ToggleButton x:Name="ToggleButton"
                                  Template="{StaticResource SlimComboBoxButtonTemplate}"
                                  ClickMode="Press" Focusable="false"
                                  IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}" />
                    <ContentPresenter x:Name="ContentSite"
                                      IsHitTestVisible="False"
                                      Content="{TemplateBinding SelectionBoxItem}"
                                      ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
                                      ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"
                                      Margin="3 3 23 3"
                                      HorizontalAlignment="Left" VerticalAlignment="Stretch" />
                    <TextBox x:Name="PART_EditableTextBox"
                             Style="{x:Null}"
                             HorizontalAlignment="Left" VerticalAlignment="Bottom"
                             HorizontalContentAlignment="Left" VerticalContentAlignment="Center"
                             Margin="3 3 23 3"
                             Focusable="True" Background="Transparent" Visibility="Hidden"
                             IsReadOnly="{TemplateBinding IsReadOnly}" >
                        <TextBox.Template>
                            <ControlTemplate TargetType="TextBox" >
                                <Border Name="PART_ContentHost" Focusable="False" />
                            </ControlTemplate>
                        </TextBox.Template>
                    </TextBox>
                    <Popup Name="Popup"
                           AllowsTransparency="True" Focusable="False" PopupAnimation="None"
                           Placement="Bottom"
                           IsOpen="{TemplateBinding IsDropDownOpen}">

                        <Grid Name="DropDown"
                              SnapsToDevicePixels="True"
                              MinWidth="{TemplateBinding ActualWidth}" MaxHeight="{TemplateBinding MaxDropDownHeight}">
                            <Border x:Name="DropDownBorder"
                                    Background="White"
                                    BorderThickness="1" BorderBrush="#CCCCCC"/>
                            <ScrollViewer Margin="4 6 4 6" SnapsToDevicePixels="True">
                                <StackPanel IsItemsHost="True" KeyboardNavigation.DirectionalNavigation="Contained" />
                            </ScrollViewer>
                        </Grid>
                    </Popup>
                </Grid>
                
                <ControlTemplate.Triggers>
                    <Trigger Property="HasItems" Value="false">
                        <Setter TargetName="DropDownBorder" Property="MinHeight" Value="95" />
                    </Trigger>
                    <Trigger Property="IsGrouping" Value="true">
                        <Setter Property="ScrollViewer.CanContentScroll" Value="false" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

使う例

スタイルに指定しただけです。

<ComboBox  Width="100" Style="{DynamicResource SlimComboBoxStyle}"
            SelectedIndex="0">
    <ComboBoxItem>ランス</ComboBoxItem>
    <ComboBoxItem>シィル</ComboBoxItem>
    <ComboBoxItem>あてな2号</ComboBoxItem>
    <ComboBoxItem>魔想志津香</ComboBoxItem>
    <ComboBoxItem>上杉謙信</ComboBoxItem>
    <ComboBoxItem>山本五十六</ComboBoxItem>
</ComboBox>

変更点

通常の ComboBox コントロールは、アニメーションがたくさん用意されていますが、それらを省きました。ポップアップの表示なんかも、応答が機敏になっていると思います。

また、画像を見ればわかるとおり、縦に連続するコンボボックスはごちゃつきます。シンプルな見た目にしてあげたほうが、よい場合もあります。

f:id:shikaku_sh:20200309181303p:plain
たくさん並べるとこうなる

太さが細めなのは、いつもどおり Padding でいいです。雛形。

サンプル

GitHub の「Samples」リポジトリーにまとめて公開しています。今回のサンプルは「SimpleComboBoxSample」です。

参考