SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPFでFluent Design Systemを再現するライブラリを作ってみました~FluentWPF~

はじめに

少し遅れてしまいましたが、この記事はXAML Advent Calendar 2017の25日目の記事です。

今年のBuildでは、Fluent Design Systemなどの発表がありましたが、このデザインをWPFで用いる方法は提供されていません。

対応するAPIがないなら、XAMLの機能を駆使してそれっぽく作ってしまえ!!ということで、FluentDesignをXAMLで再現したライブラリを作ってみました。
XAMLの拡張性は伊達じゃない!!

f:id:minami_SC:20171226020340g:plain:w300

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クラスを作ると、アクリル効果のかかったウィンドウを表示します。

f:id:minami_SC:20171226013944p:plain

<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クラスを継承するのではなく、既存ウィンドウに添付プロパティを設定することでもアクリル化ができます。

f:id:minami_SC:20171226014000p:plain

<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や各種コントロールと組み合わせると、こんな感じになります。

f:id:minami_SC:20171226014015p:plain

<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要素をぼかして表示します。 こんな感じ!!

f:id:minami_SC:20171226020316g:plain

    <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}"

こうなります。
f:id:minami_SC:20171226014057p:plain

Reveal

FluentDesignでのRevealエフェクトを再現したスタイルを作っています。
こんな風に、マウスの動きに応じてコントロールの輪郭を光らせたり、クリックした位置から波紋状に広がる光のエフェクトなどを再現しています。

f:id:minami_SC:20171226014111g:plain

    <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の電卓風なデザインを作ってみました。

f:id:minami_SC:20171226020340g:plain

<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本家のものとだいたい同じ雰囲気にしています。

f:id:minami_SC:20171226020417g:plain

    <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のテーマで設定されたアクセントカラーを取得します。
f:id:minami_SC:20171226014202p:plain:w400

以下のようなColor/Brush定義を用意しています。

  • Colors
    • ImmersiveSystemAccent
    • ImmersiveSystemAccentLight1
    • ImmersiveSystemAccentLight2
    • ImmersiveSystemAccentLight3
    • ImmersiveSystemAccentDark1
    • ImmersiveSystemAccentDark2
    • ImmersiveSystemAccentDark3
  • Brushes
    • ImmersiveSystemAccentBrush
    • ImmersiveSystemAccentLight1Brush
    • ImmersiveSystemAccentLight2Brush
    • ImmersiveSystemAccentLight3Brush
    • ImmersiveSystemAccentDark1Brush
    • ImmersiveSystemAccentDark2Brush
    • ImmersiveSystemAccentDark3Brush

使用例
ブラシとして画面表示するとこんな感じです。
f:id:minami_SC:20171226020450p:plain:w350

        <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環境以外での動作チェック
    • まだWin7/8.xでの動作確認ができてません。おそらくAcrylicWindowなどは、ちゃんと表示できてないと思います。
    • Win7/Win8.x系では、ただの半透明ウィンドウにするなどのフォールバック処理を実装予定。
  • Connected Animation
    • これの再現は、なかなか難しいかも。。。出来たら対応します。

もうちょいライブラリを作り込んでから、サンプルコード類もたくさん用意しようと思います。