コントロールのスタイルが突然消えた!?【exhibition #3 WPF UI Gallery】
Material Design In XAML Toolkit や MahApps.Metro のような UI デザインフレームワークを使用したり、自作したコントロールを使用して画面を作成していると突然スタイルがクリアされて焦ることがあります。このエントリではそんな場合の対処法を紹介します。
この連載は Visual Studio 2019 Community Edition で .NET 5 以上 と C# + Prism 8 以降 + MahApps.Metro + Material Design In XAML Toolkit を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
目次
前提
このエントリは WPF の UI デザインフレームワークである Material Design In XAML Toolkit や MahApps.Metro が既に導入済みで画面へコントロールの配置ができる方が対象です。
MVVM フレームワークの Prism を利用して WPF アプリを作成する方法は連載記事 .NET5 WPF Prism MVVM 入門 2020 等で紹介しているので、以下の連載を見てください。(本エントリで紹介しているサンプルアプリは Prism を利用していますが内容に Prism は関係ありません)
又、Material Design In XAML Toolkit のインストールについては『.NET Core 正式対応版 MahApps.Metro Ver. 2【exhibition #1 WPF UI Gallery】』で紹介しているので、そちらを見てインストールしてください。
上記エントリの通りにインストールを進めると MahApps.Metro も同時にインストールされます。
スタイルが消えるとは
Material Design In XAML Toolkit や MahApps.Metro 等を利用して画面を作成している場合や、独自にデザインしたコントロールを使用する場合等に、設定していたスタイルが突然消える(クリアされる)事があります。
ここでは ListBox を例に紹介しますが、ListBox のようなリスト系コントロールだけで起こる現象ではなく、どのコントロールでも起こる可能性はあります。管理人の経験上、リスト系コントロールでは起こる可能性が高いと思っています。
まず、スタイルが消えるとはどのような状態を指すのかを紹介します。
スタイルが消えた ListBox のサンプル
fig. 1 は Material Design In XAML Toolkit 標準の ListBox です。
Material Design In XAML Toolkit の ListBox は外枠線無しのデザインなので、目立たせるために GroupBox の中に配置していますが、Material Design In XAML Toolkit インストール後に Window や UserControl へ ListBox を配置すると ListBoxItem 上のマウスホバーやクリック時には fig. 1 のようなエフェクトが追加されます。
続いて fig. 2 は Material Design In XAML Toolkit のエフェクトが削除(クリア)された ListBox のサンプルです。
fig. 1、2 は同一の XAML ですが、fig. 1 にある設定を追加すると fig. 2 のようなエフェクトに変わります。どこが変わったのか少し分かりにくいかもしれませんが、具体的には以下の個所が変わっています。
- ListBoxItem のマウスホバー時の項目選択色がグレー ⇒ ブルーに
- ListBoxItem を選択した際に描画される Material Design In XAML Toolkit のリップルエフェクトが無くなっている
ListBox の場合はあまり大きく変わっていないので fig. 2 でも良いんじゃない?と思う人も居るかもしれませんが、ここではスタイルの良し悪しではなく、元に戻したいと思った時にどうやって戻すかを紹介するエントリです。
戻す方法が分かった上で、fig. 2 の方が好みだと言うならそれで構わないと思います。
リスト系コントロールとは
ここまでは特に説明していませんが、リスト系コントロールとは、このサイトで連載中(2021/3 現在)の『.NET 5 WPF MVVM 入門 2020 step: 9』でも紹介した通り、ItemsControl を継承した以下のようなコントロールを指します。
- ListBox
- ComboBox
- TreeView
- ListView
- DataGrid
このエントリでは ListBox を例に紹介しますが、他のコントロールでも以降で紹介する設定を追加すれば同じ状況が発生します。
スタイルが消える原因の Style – Setter
fig. 1 ⇒ 2 のようにスタイルが消える(クリアされる)原因は『.NET 5 WPF ReactiveCollection MVVM 入門 2020 step: 9』でも紹介した src. 1 の Style – Setter が原因です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <UserControl x:Class="PrismSample.ReactiveMvvm.ReactiveSample2" ~ 略 ~ prism:ViewModelLocator.AutoWireViewModel="True"> <Grid Margin="10 0 10 0"> ~ 略 ~ <GroupBox Grid.Row="2" Header="コレクションとバインドするListBox" Margin="0 0 10 10"> <ListBox VirtualizingPanel.IsVirtualizing="True" ~ 略 ~ SelectionMode="Extended"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="IsSelected" Value="{Binding IsSelected.Value, Mode=TwoWay}" /> </Style> </ListBox.ItemContainerStyle> ~ 略 ~ </ListBox> </GroupBox> </Grid> </UserControl> |
src. 1 は複数項目選択可能な ListBox で選択項目を取得するために ListBoxItem 用の ViewModel と ListBoxItem.IsSelected をバインドするための設定ですが、src. 1 の 12 ~ 17 行目を追加すると Style が消える…と言うより Style 要素内の TargetType に指定した型の標準スタイルに上書きされます。通常、Style 要素内には変更が必要なプロパティしか指定しないので、未指定のプロパティは Style.TargetType に指定したクラスのスタイルで上書きされます。そのため Style – Setter を指定した~Item は fig. 2 のように標準のスタイルに戻ってしまいます。
Style の上書きを防ぐための BasedOn
ListBoxItem.IsSelected に代表される ~Item のプロパティをバインドするための記述は .NET 5 WPF ReactiveCollection MVVM 入門 2020 step: 9 でも紹介した通り、WPF アプリを MVVM パターンで作成する場合は重要な手法ですし、使う機会も多いので当然回避手段も用意されています。
Style – Setter を指定しても元のスタイルを維持する場合は src. 2 のように Style へ BasedOn 属性を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <UserControl x:Class="PrismSample.ReactiveMvvm.ReactiveSample2" ~ 略 ~ prism:ViewModelLocator.AutoWireViewModel="True"> <Grid Margin="10 0 10 0"> ~ 略 ~ <GroupBox Grid.Row="2" Header="コレクションとバインドするListBox" Margin="0 0 10 10"> <ListBox VirtualizingPanel.IsVirtualizing="True" ~ 略 ~ SelectionMode="Extended"> <ListBox.ItemContainerStyle> <Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource MaterialDesignListBoxItem}"> <Setter Property="IsSelected" Value="{Binding IsSelected.Value, Mode=TwoWay}" /> </Style> </ListBox.ItemContainerStyle> ~ 略 ~ </ListBox> </GroupBox> </Grid> </UserControl> |
src. 2 のように BasedOn を指定すると Setter を指定しないプロパティやスタイルは全て BasedOn で指定したリソースから読み込まれるようになります。ここでは Material Design In XAML Toolkit のスタイルにしたいので、定義済みの MaterialDesignListBoxItem を指定します。
ListBox 以外のコントロールの場合は BasedOn に何を指定してするの?と言う方も居るかもしれませんが、Material Design In XAML Toolkit には【MaterialDesign】から始まるリソースが定義されていますし、IntelliSence で候補も表示されるのでそこまで困ることは少ないと思います。
src. 2 のように BasedOn を指定して実行すると fig. 1 の Material Design In XAML Toolkit のスタイルが戻ってきますし、Style – Setter で指定した ListBoxItem.IsSelected のバインドもちゃんと動作します。
まとめ的な
src. 2 のように BasedOn を指定するとスタイルが消える(クリアされる)問題は解決できますが、Style – Setter を指定した全てのコントロールに指定する必要があるので非常に面倒な気はしますが、これ以外の方法は無さそうです…。基本的には App.xaml で指定したスタイルを引き継いでくれれば良いのにわざわざ記述しないといけないのは互換性の問題なのか WPF の普及が鈍く、開発リソースをあまり割り当てられなかったからなのかは分かりませんが、正に『WPF のそう言うトコやぞ!』と言いたくなります。
とは言え、BasedOn を指定する以外は回避しようがなさそうなので、Material Design In XAML Toolkit やMahApps.Metro のような UI デザインフレームワークを使用する場合は【BasedOn】もセットで覚えておく必要があります。
今回はこのエントリ専用のプロジェクトは作成していないので、サンプルを試したい場合は GitHub リポジトリに上げている .NET 5 WPF ReactiveCollection MVVM 入門 2020 step: 9 用のソリューション で確認してください。