Modern Style ComboBox 2

- - posted in wpf | Comments

ComboBox のスタイルとテンプレート

を参考にして、コンボボックスのスタイルをカスタマイズします。

MSDN が提供するテンプレートのサンプル ControlTemplateExamples をダウンロードし 中身を確認します。

Resources フォルダに各種の論理コンテンツ(Button.xaml、Calendar.xaml、CheckBox.xaml・・・) の ResourceDictionary が整理されています。

今回は ComboBox.xaml です。

一度にXamlコードを読み解こうよすると怯んでしまうので VisualStudio のエディタ機能を利用して、整理しながら確認します。

wpf-17-01

上から解析します。

1
2
3
  <ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="Shared.xaml" />
  </ResourceDictionary.MergedDictionaries>

Shared.xamlは、このシリーズで作成している uEN CSプロジェクト と同じで、色などの共通的なものを 一元管理しているリソースです。

ただし、DynamicResourceとして参照する場合はこのように参照を入れる必要はなく、StaticResourceで参照 する必要がある場合に利用します。

さて、二つのコントロール テンプレートと二つのStyleから構成されていることがわかります。

  • <ControlTemplate x:Key="ComboBoxToggleButton"
  • <ControlTemplate x:Key="ComboBoxTextBox"
  • <Style x:Key="{x:Type ComboBox}"
  • <Style x:Key="{x:Type ComboBoxItem}"

wpf-17-02

これらのStyleおよびテンプレートを ComboBox の Style として組み合わせています。

コントロールのコントラクト

ComboBox の Xaml は興味深く ComboBox.IsEditable プロパティを真に設定した場合に、自身のTemplateを変えて ComboBoxTextBox が表示されるようにしています。

プロパティの変更によって、Styleを書き換える方法は

1
2
3
4
5
 <ControlTemplate.Triggers>
     <Trigger Property="IsEnabled" Value="false">
         <Setter Property="Foreground" Value="{DynamicResource AppForegroundDisabled}"/>
         <Setter Property="Background" Value="{DynamicResource AppDisabled}"/>
     </Trigger>

にように適用してきました。

もちろん ComboBoxも 同じ方法で実現できますが、こちらは VisualStateManager を利用して実現しています。

MSDNの コントロール コントラクトについて に詳細が記述しています。

ControlTemplate を使用して、その視覚的な構造 (FrameworkElement オブジェクトを使用) および視覚的な動作 (VisualState オブジェクトを使用) を指定する

提供するコンテンツのルール(契約)定義のようなもので、そのルールの周知方法はクラスの属性値から確認できます。 たとえば ComboBox をみると

wpf-17-03

のように、PART_EditableTextBox という名前でテキストボックスを公開しています。 開発者は、TemplatePartAttribute属性から名称を取得してTemplateからStyleをカスタマイズすることができます。

TemplateVisualStateAttribute も同様で、コントロールの視覚的な動作の契約書みたいなもので、それを属性として宣言することで 開発者にカスタマイズ可能なことを公開します。

wpf-17-04

.Net Framework内部でも一貫して適用していればこのコントラクトも一級品として利用すると思うのですが、 TemplatePartAttributeの厳密さに比べて、TemplateVisualStateAttributeで コントラクトを示しているクラスは少ないような気がします。

今回StyleをカスタマイズするComboBox.xamlですが、この動作を実際に実装しているComboBox.csクラスでは

1
2
3
4
5
6
internal override void ChangeVisualState(bool useTransitions)
{
    if (!base.IsEnabled)
    {
        VisualStateManager.GoToState(this, "Disabled", useTransitions);
    }

のように、VisualStateManager を利用しているのですが、このコントラクトは非公開です。

ただ、ComboBox のスタイルとテンプレートの記事でComboBoxの状態の表を公開していますし VisualStateManager を利用してカスタマイズする方法を踏襲します。

wpf-17-05

ComboBoxのStyle

全体構成を担う <Style x:Key="{x:Type ComboBox}" TargetType="{x:Type ComboBox}"> と通して確認します。 ただ、MSが提供しているコントロール テンプレート サンプルは少しだけおかしな箇所があるので、見直しながら確認していきます。

wpf-17-06

まずはいつも通り、TemplateをControlTemplateを利用してカスタマイズしてくのですが

1
2
3
4
<Setter Property="Template">
    <Setter.Value>
        <ControlTemplate TargetType="{x:Type ComboBox}">
            <Grid>

気になる点として、 <Grid.ColumnDefinitions> のように、レイアウトを区切る宣言がありません。 しかし、ToggleButtonの属性にGrid.Column=“2”と宣言しています。

wpf-17-07

Templateのサンプルを初めにデザインした際に フレキシブルレイアウトとして、ドロップダウンボタンを Grid右端に表示するように配置したかったのかもしれません。

(ただし、GridをColumnDefinitionを利用して区切っていないので、この指定は無効です)

私も当初は、Templateをもとにそのようなレイアウト構成を考えていました。

ただ、ComboBox のユーザビリティは昔から踏襲してきた独特の”クセ”があり 実現する際に少し工夫が必要でした。

MSサンプルを基に工夫について確認します。

Editableモードによって、選択可能なテキストボックスとして機能するパターンと 編集不可のリストボックスとして機能するパターンで、Zオーダーを意識したフォーカス制御や 罫線描画などを考慮する必要があります。

wpf-17-10

一見すると、右側にボタンがあるだけにみえますが、編集不可の場合はどこをクリックしても ドロップダウンのポップアップが起動します。

これは右側だけではなくて、ボタン要素が全体にかかって配置されているからです。

メインコンテンツを確認します。

wpf-17-09

<ControlTemplate TargetType="{x:Type ComboBox}"> から続くコンテンツがコンボボックスの 実態になります。

VisualStateManagerの後に続いて、ToggleButton、ContentPresenter(IsHitTestVisible=“False”)、 TextBox と宣言されています。

この宣言の順序も重要で、ToggleButtonの上にContentPresenter、その上にTextBoxが重なる事になります。 ContentPresenter はヒットテストが無効になっており、ここをクリックした場合はその下のToggleButtonが 反応するようになっています。

なので、一見するとボタンに見えない箇所をクリックしてもポップアップが起動します。

Editableモードの時はもう少し複雑です。

wpf-17-08

PART_EditableTextBox というName属性の TextBox が重なるように配置されています。 TextBoxのVisibilityはHiddenで初期化されており、デフォルトでは表示されていない状態です。

Visibility は VisualStateManager によって制御され、VisualStateがEditableの状態の場合に Visibility.Visible となります。

VisualState 値は上述で記載したように、.Net Frameworkの ComboBoxクラス内部で internal override void ChangeVisualState として実装しており、IsEditable プロパティが true であるときに変化するようになっています。

これらの文字列は MSDN上で公開されています。

最後に Popup が 配置され、IsOpen="{TemplateBinding IsDropDownOpen}" としてドロップボタンを押下するタイミングで リストを表示します。

MSDNのサンプルではName属性が x:Name="Popup" となっていて、コントラクトに従っていないので、ここは PART_Popup という名にしたいと思います。

コンボボックスの罫線は、一番上に配置されているToggleButtonのTemplate上で実装されています。

しかし、.Net Frameworkが組み込みで提供するWPFのコンボボックスの外観も、 実態はとても単純な要素を組み合わせることで 表現できていることが確認でき、改めてWPFの強力かつ柔軟さを感じます。

配色などを考えてカスタマイズしたサンプルをGit上にあげました。

次回は別の要素か、テキストの制御などを考えています。


Comments