WPFでFluent Design Systemを再現するライブラリを作ってみました~FluentWPF~
はじめに
少し遅れてしまいましたが、この記事はXAML Advent Calendar 2017の25日目の記事です。
今年のBuildでは、Fluent Design Systemなどの発表がありましたが、このデザインをWPFで用いる方法は提供されていません。
対応するAPIがないなら、XAMLの機能を駆使してそれっぽく作ってしまえ!!ということで、FluentDesignをXAMLで再現したライブラリを作ってみました。
XAMLの拡張性は伊達じゃない!!
StyleやStoryboard、マークアップ拡張やコンバーターに各種添付プロパティと、XAMLの機能をフル活用して色々なエフェクトを再現しています。
ただ、ウィンドウのアクリル化はXAMLだけではどうにもならないので、↓を使ってそれっぽく再現しています。
WPFでFluent Design Systemのアクリルっぽいウィンドウを作ってみる - SourceChord
概要
WPF向けにFluent Design Systemをそれっぽく再現したテーマのライブラリです。
主に以下のような機能を実装しました。
- Acrylic
- AcrylicWindow・・・半透明でぼかしをかけて透過するスタイルのウィンドウ。
- AcrylicBrush・・・・ウィンドウ内の指定したXAML要素をぼかして表示するためのブラシ
- Reveal・・・・・・・マウスカーソルの位置に応じた照明効果が効いた、各種コントロール用スタイル。
- ParallaxView・・・UWPの同名コントロールを再現したもの。
- AccentColor・・・・OSのテーマカラーなどを用いたColor/Brush定義
準備
Nugetで「FluentWPF」と検索して、インストールしてください。
パッケージマネージャコンソールからインストールする場合は以下のコマンドで。
Install-Package FluentWPF
https://www.nuget.org/packages/FluentWPF/
準備
FluentWPFを使いたいXAMLコードで、以下の名前空間の定義を追加します。
xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF"
App.xamlに、以下のようなリソースディクショナリの参照を追加します。
<Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="pack://application:,,,/FluentWPF;component/Styles/Controls.xaml" /> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources>
ということで、使い方を順に説明していきます。
Acrylic
まずは、Fluent Design Systemで最も印象的な、アクリル効果の効いたウィンドウの作成方法から。
AcrylicWindow
AcrylicWindow派生のWindowクラスを作ると、アクリル効果のかかったウィンドウを表示します。
<fw:AcrylicWindow x:Class="FluentWPFSample.Views.AcrylicWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF" xmlns:local="clr-namespace:FluentWPFSample.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="AcrylicWindow" Width="300" Height="300" mc:Ignorable="d"> <Grid> </Grid> </fw:AcrylicWindow>
コードビハインド
コードビハインドでは、ウィンドウのクラス定義からWindowの継承をしている部分を削除しておきます。
//public partial class AcrylicWindow : Window public partial class AcrylicWindow { public AcrylicWindow() { InitializeComponent(); } }
プロパティなど
プロパティ名 | 型 | 説明 |
---|---|---|
TintColor | Color | 半透過効果に加える色味を設定 |
TintOpacity | double | TintColorの不透明度を設定 |
NoiseOpacity | double | 半透過効果に加えるノイズの不透明度を設定 |
FallbackColor | Color | ウィンドウが非アクティブになった際の、フォールバックカラーを指定 |
ShowTitleBar | bool | ウィンドウのタイトルバーを表示するか否かを設定 |
AcrylicWindowを添付プロパティとして設定する
AcrylicWindowクラスを継承するのではなく、既存ウィンドウに添付プロパティを設定することでもアクリル化ができます。
<Window x:Class="FluentWPFSample.Views.AcrylicWindow2" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:FluentWPFSample.Views" xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF" mc:Ignorable="d" Title="AcrylicWindow2" Height="300" Width="300" fw:AcrylicWindow.Enabled="True" fw:AcrylicWindow.TintColor="Violet"> <Grid> </Grid> </Window>
MahApps.Metroとの併用
添付プロパティとしてAcrylicなウィンドウを作成できるようにしているので、MahApps.Metroのような独自ウィンドウクラスを用いるライブラリとも併用できます。
MahApps.MetroのMetroWindowや各種コントロールと組み合わせると、こんな感じになります。
<controls:MetroWindow x:Class="MahAppsTest.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="http://metro.mahapps.com/winfx/xaml/controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF" xmlns:local="clr-namespace:MahAppsTest" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="525" Height="350" fw:AcrylicWindow.Enabled="True" fw:AcrylicWindow.ShowTitleBar="False" Background="Transparent" BorderThickness="1" GlowBrush="{DynamicResource AccentColorBrush}" mc:Ignorable="d"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <TextBlock Margin="5" HorizontalAlignment="Left" VerticalAlignment="Center" Text="Name:" /> <TextBox Grid.Column="1" Height="23" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Top" Text="TextBox" TextWrapping="Wrap" /> <Button Grid.Row="1" Grid.Column="1" Width="75" Margin="5" HorizontalAlignment="Right" VerticalAlignment="Top" Content="Button" /> <Slider Grid.Row="2" Grid.Column="1" Margin="5" HorizontalAlignment="Stretch" VerticalAlignment="Top" /> </Grid> </controls:MetroWindow>
そのままだと、FluentWPFのAcrylicWindowによるタイトルバーがMetroWindow内部に描画されてしまうため、AcrylicWindow.ShowTitleBar="False"
という設定を行っています。
AcrylicBrush
XAMLで書かれた背面にあるUI要素をぼかして表示します。 こんな感じ!!
<Window.Resources> <BooleanToVisibilityConverter x:Key="booleanToVisibilityConverter" /> </Window.Resources> <Grid> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <Grid x:Name="grid" Background="White"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition /> </Grid.ColumnDefinitions> <StackPanel> <Button Width="75" Margin="5" Content="Button" /> <Button Width="75" Margin="5" Content="Button" /> <Button Width="75" Margin="5" Content="Button" /> </StackPanel> <Image Grid.Column="1" Margin="5" Source="/FluentWPFSample;component/Assets/Images/1.jpg" /> </Grid> <Rectangle Grid.ColumnSpan="2" Margin="40" Fill="{fw:AcrylicBrush grid}" Stroke="Black" Visibility="{Binding IsChecked, ElementName=chkShowAcrylicLayer, Converter={StaticResource booleanToVisibilityConverter}}" /> <CheckBox x:Name="chkShowAcrylicLayer" Grid.Row="1" Margin="5" HorizontalAlignment="Left" Content="Show Acrylic Rect" IsChecked="True" /> </Grid>
このマークアップ拡張では、指定したUI要素をVisualBrushとして扱い、強烈なぼかしをかけたりTintColorで色味を加えたりしたブラシを作ります。
昔、↓のようなブラシ用マークアップ拡張を作ったことがあるのですが、この方法を応用してFluentDesign用に作り直しました。
注意点
このマークアップ拡張では、指定した要素をVisualBrushとして使用しています。
そのため、VisualTree上で親に当たるものをTargetとすると、VisualBrushのレンダリングが無限ループとなり正常に描画されません。
このマークアップ拡張は、親子関係にある要素同士では使わないように注意してください。
プロパティなど
AcrylicBrushマークアップ拡張では、以下のようなプロパティを用意しています。
プロパティ名 | 型 | 説明 |
---|---|---|
TargetName | string | (プロパティ名省略可) |
TintColor | Color | 半透過効果に加える色味を設定 |
TintOpacity | double | TintColorの不透明度を設定 |
NoiseOpacity | double | 半透過効果に加えるノイズの不透明度を設定 |
この辺のプロパティを弄ると、色々なアクリル効果を演出できます。
こんなブラシを定義すると、
Fill="{fw:AcrylicBrush grid, TintColor=Red, TintOpacity=0.3, NoiseOpacity=0.1}"
こうなります。
Reveal
FluentDesignでのRevealエフェクトを再現したスタイルを作っています。
こんな風に、マウスの動きに応じてコントロールの輪郭を光らせたり、クリックした位置から波紋状に広がる光のエフェクトなどを再現しています。
<Grid fw:PointerTracker.Enabled="True" Background="#01FFFFFF" Margin="3"> <StackPanel> <Button Content="Button" HorizontalAlignment="Left" Margin="5" Width="75" Height="32" Style="{StaticResource ButtonRevealStyle}"/> <Button Content="Button" HorizontalAlignment="Left" Margin="5" Width="75" Height="32" Background="Transparent" Style="{StaticResource ButtonRevealStyle}"/> <TextBox HorizontalAlignment="Left" Height="23" Margin="5" Text="TextBox" Width="120" Style="{StaticResource TextBoxRevealStyle}"/> </StackPanel> </Grid>
RevealEffectをかけたい領域の親要素に対しては、以下の2つの設定を行う必要があります。
PointerTracker.Enabled="True"
という添付プロパティを設定- この添付プロパティをTrueにした領域内で、マウスカーソル位置のトラッキングを開始します。
- 背景色を透明以外にする
- WPFでは、完全に透明な領域ではマウスイベントが発生しないため、マウスカーソルの移動に関するイベントが発生しなくなってしまうため。
今のところ、以下のようなスタイルを作っています。
- ButtonRevealStyle
- ButtonAccentRevealStyle
- TextBoxRevealStyle
- ListBoxRevealStyle
応用例
Windows10の電卓風なデザインを作ってみました。
<fw:AcrylicWindow x:Class="FluentWPFSample.Views.RevealStyles" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:FluentWPFSample.Views" xmlns:fw="clr-namespace:SourceChord.FluentWPF;assembly=FluentWPF" mc:Ignorable="d" Title="RevealStyles" Height="320" Width="480" fw:AcrylicWindow.TintColor="GhostWhite"> <Grid fw:PointerTracker.Enabled="True" Background="#01FFFFFF" > <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition /> </Grid.RowDefinitions> <UniformGrid Rows="1" Columns="5"> <Button Content="MC" Margin="2" Height="24" Style="{StaticResource ButtonRevealStyle}" Background="Transparent"/> <Button Content="MR" Margin="2" Height="24" Style="{StaticResource ButtonRevealStyle}" Background="Transparent"/> <Button Content="M+" Margin="2" Height="24" Style="{StaticResource ButtonRevealStyle}" Background="Transparent"/> <Button Content="M-" Margin="2" Height="24" Style="{StaticResource ButtonRevealStyle}" Background="Transparent"/> <Button Content="MS" Margin="2" Height="24" Style="{StaticResource ButtonRevealStyle}" Background="Transparent"/> </UniformGrid> <UniformGrid Grid.Row="1" Columns="4" Rows="4"> <Button Content="7" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="8" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="9" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="×" Margin="2" Style="{StaticResource ButtonAccentRevealStyle}" /> <Button Content="4" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="5" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="6" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="-" Margin="2" Style="{StaticResource ButtonAccentRevealStyle}" /> <Button Content="2" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="3" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="4" Margin="2" Style="{StaticResource ButtonRevealStyle}" /> <Button Content="+" Margin="2" Style="{StaticResource ButtonAccentRevealStyle}"/> <Button Content="±" Margin="2" Style="{StaticResource ButtonRevealStyle}"/> <Button Content="0" Margin="2" Style="{StaticResource ButtonRevealStyle}"/> <Button Content="." Margin="2" Style="{StaticResource ButtonRevealStyle}"/> <Button Content="=" Margin="2" Style="{StaticResource ButtonAccentRevealStyle}"/> </UniformGrid> </Grid> </fw:AcrylicWindow>
Parallax
Parallaxエフェクトをかけるための、ParallaxViewコントロールを作りました。
使い方は、UWP本家のものとだいたい同じ雰囲気にしています。
<Grid> <fw:ParallaxView VerticalShift="200" HorizontalShift="200" Source="{Binding ElementName=list}"> <Image Source="/FluentWPFSample;component/Assets/Images/1.jpg" Stretch="UniformToFill"/> </fw:ParallaxView> <ListBox x:Name="list" Background="#88EEEEEE" ScrollViewer.CanContentScroll="False" ItemsSource="{Binding Items}"/> </Grid>
public partial class ParallaxSample : Window { public List<string> Items { get; set; } public ParallaxSample() { InitializeComponent(); this.DataContext = this; this.Items = new List<string>(); for (var i = 0; i < 100; i++) { this.Items.Add($"item{i:D3}"); } } }
AccentColors
OSのテーマで設定されたアクセントカラーを取得します。
以下のようなColor/Brush定義を用意しています。
- Colors
- ImmersiveSystemAccent
- ImmersiveSystemAccentLight1
- ImmersiveSystemAccentLight2
- ImmersiveSystemAccentLight3
- ImmersiveSystemAccentDark1
- ImmersiveSystemAccentDark2
- ImmersiveSystemAccentDark3
- Brushes
- ImmersiveSystemAccentBrush
- ImmersiveSystemAccentLight1Brush
- ImmersiveSystemAccentLight2Brush
- ImmersiveSystemAccentLight3Brush
- ImmersiveSystemAccentDark1Brush
- ImmersiveSystemAccentDark2Brush
- ImmersiveSystemAccentDark3Brush
使用例
ブラシとして画面表示するとこんな感じです。
<StackPanel Margin="5"> <StackPanel.Resources> <Style TargetType="Border"> <Setter Property="Width" Value="120" /> <Setter Property="Height" Value="120" /> <Setter Property="Margin" Value="3" /> <Setter Property="BorderBrush" Value="Black" /> <Setter Property="BorderThickness" Value="1" /> </Style> <Style TargetType="TextBlock"> <Setter Property="TextWrapping" Value="Wrap" /> <Setter Property="VerticalAlignment" Value="Bottom" /> <Setter Property="FontSize" Value="14" /> </Style> </StackPanel.Resources> <StackPanel Orientation="Horizontal" Margin="5"> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentBrush}"> <TextBlock Text="ImmersiveSystemAccentBrush" /> </Border> </StackPanel> <StackPanel Orientation="Horizontal" Margin="5"> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentLight1Brush}"> <TextBlock Text="ImmersiveSystemAccentLight1Brush"/> </Border> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentLight2Brush}"> <TextBlock Text="ImmersiveSystemAccentLight2Brush"/> </Border> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentLight3Brush}"> <TextBlock Text="ImmersiveSystemAccentLight3Brush" /> </Border> </StackPanel> <StackPanel Orientation="Horizontal" Margin="5"> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentDark1Brush}"> <TextBlock Text="ImmersiveSystemAccentDark1Brush" Foreground="White"/> </Border> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentDark2Brush}"> <TextBlock Text="ImmersiveSystemAccentDark2Brush" Foreground="White"/> </Border> <Border Background="{x:Static fw:AccentColors.ImmersiveSystemAccentDark3Brush}"> <TextBlock Text="ImmersiveSystemAccentDark3Brush" Foreground="White"/> </Border> </StackPanel> </StackPanel>
今後の予定
まだまだ完成度は低いですが、今後もFluentDesignの再現性を高めるために、いろいろ更新しようと思ってます。
当面は、以下のような対応を予定してます。
- 各種コントロール用のスタイル定義
- Buttonなどだけではなく、CheckBox/RadioButtonや、ListViewやSliderなどの各種スタイル定義も作る。
- Windows10環境以外での動作チェック
- Connected Animation
- これの再現は、なかなか難しいかも。。。出来たら対応します。
もうちょいライブラリを作り込んでから、サンプルコード類もたくさん用意しようと思います。