Material Design In XAML Toolkitでお手軽にWPFアプリを美しく

なんとブログ書くのは3ヶ月ぶり近い!えー、うーん、そんな経っちゃってるのか、こりゃいかん。と、いうわけかでWPFアプリを入り用で作ったんですが、見た目がショボくてゲッソリしてました。WPFでアプリ書いても別に綺麗な見た目にならんのですよね、むしろショボいというか。自分でデザイン作りこんだりなんて出来ないし、でもWPFのテーマ集なんかを適用してもクソダサいテーマしかなかったりして一層ダサくなるだけで全く意味ないとかそんなこんなんで、まぁ割とげっそりだったのですが、Material Design In XAML Toolkitは相当良い!良かった、のでちょうど手元に作り中のWPFアプリがあって適用してみたんで紹介してきます。

最終的に↑のような感じになりました。サクサクッとテーマ適用してくだけでこの程度に整えられるならば、上等すぎるかな、と。私的にはマテリアルデザイン、相当気に入りました。WindowsのModern UI風のフラットテーマは普通に適用しただけだと超絶ダサくなるという、センスが要求されすぎてキツかったんですが、マテリアルデザインはそれなりに質感が乗っかってるのでまぁまぁ見れる感じになる。また、画像からは分かりませんが結構細かくアニメーションが設定されていて感触が良い(マテリアルデザインの重要な要素だそうで)のも嬉しい。

Before

Beforeはこんな感じです。

TextBoxとボタンの羅列、実にギョーミーな雰囲気。機能的には私の要件はこれで満たしてるんですが(ちなみにコレが何かは後日紹介するしGitHubで公開もするつもりですが今は本題ではないのでスルーします)、いかんせん見た目が悲しいかな、と。そこで現れたMaterial Design In XAML Toolkit!NuGetからのインストールとコピペ一発で素敵な見た目に……。 なるほど世の中はさすがに甘くなかったですね:)

適用は簡単で、NuGetからMaterialDesignThemesをダウンロード、そしてApp.xaml.csにこのApp.xamlのApplication.Resourcesをコピペ。そしてMainWindowに以下の4項目を貼っつけてあげればできあがり。

<MainWindow
    xmlns:wpf="clr-namespace:MaterialDesignThemes.Wpf;assembly=MaterialDesignThemes.Wpf"
    TextElement.Foreground="{DynamicResource MaterialDesignBody}"
    Background="{DynamicResource MaterialDesignPaper}"
    FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto">

簡単簡単。これで美しくなるなら素晴らしいですね?そしてその結果がこれ。

うん、ダメ、理想とは程遠いダサさに溢れてます。Bootstrapを適用しただけじゃ普通にダサいままってのと同じ。ボーダーが吹っ飛んだので境目がわからず使いにくくなったし、やっぱダサ……、なんか一部のボタンは文字埋まっちゃってるし。で、引き返そうと思ったんですが、なんとなく良さそうな気配は感じたのでもう少し粘って作業することにしました。

デモアプリを見ながら細工

まずMaterialDesignInXamlToolkitのプロジェクトを落としましょう。CloneしてもいいしDownload Zipでもいいので。で、MainDemo.Wpfをビルドして実行しましょう、特に躓くことなくビルドできるはずですので。このデモアプリが非常によく出来ていて、出来ること全ての解説になってますし、当然それをやりたければそのxamlを開いてコピペすればなんとかなります!

というわけでデモアプリを眺めつつ自分のクソダサアプリのどこから手を入れようか。まず画面の構成要素のうち、上の部分のテキストボックスとボタンが並んでるところはコンフィグに近いので色分けしようかな、と。ヘッダ部分の色分け例はマテリアルデザインでよく見るパターンですしね。よく見るパターンということは、専用のパーツがしっかり用意されています。ColorZoneで囲むことで色がガラッと変わります。

<wpf:ColorZone Mode="Inverted" Padding="0">
    ...
</wpf:ColorZone>

ModeのInvertedは逆転した色、というわけで、これだけでまぁまぁ引き締まった雰囲気が出てきました、これはやって正解。また、ボタンの文字が埋まっているのはMargin入れて小さくしてたせいだったので、Heightを設定する形で小さくすることにしました。この状態でちょっとだけ問題があって、コンボボックスの選択時のフォントが通常カラーのままなので色が薄く見えなくなってしまうことに……。

これはテーマから外れたItemContainerStyleを設定して回避。

<ComboBox ItemsSource="{Binding UseConnectionType}">
    <ComboBox.ItemContainerStyle>
        <Style TargetType="ComboBoxItem">
            <Setter Property="Foreground" Value="Gray" />
        </Style>
    </ComboBox.ItemContainerStyle>
</ComboBox>

よくわからんけどこんなんでいいでしょふ、よくわからんけど。真面目にXAML書くの5年ぶりぐらいなんで正直もう全然覚えてないんですよね。

そういえば、オマケコントロール(?)としてTextBoxにウォーターマークがつけれるのが入ってます。使い方はwpf:TextFieldAssist.Hintを入れるだけ。

<TextBox wpf:TextFieldAssist.Hint="うぉーたーまーく" />

かなり綺麗に出て素敵なので最高だと思いました、まる。

MahAppsの導入

タイトルウィンドウが乖離しててダサいというか気になってきた。ので、ここを手軽に改変できるMahAppsを入れましょう。MahAppsだけだと、Metro風ということでこれ単体では別に素敵な見た目に出来ないんですが(ほんとメトロ風はムズカスぃ!)、Material Design In XAML Toolkitと合わせるとお互いの領域をカバーできる。ちゃんとMaterial Design In XAML Toolkit側で統合のための設定が用意されているので組み合わせるのは簡単です。MahAppsの基本的な導入はQuick Startに従う通り、まずWindowをMetroWindowに差し替えて

// Xaml 
<Controls:MetroWindow
    xmlns:Controls="clr-namespace:MahApps.Metro.Controls;assembly=MahApps.Metro">

// CodeBehind
public partial class MainWindow : MahApps.Metro.Controls.MetroWindow

App.xamlにリソースを投下、なのですが、MaterialDesignInXamlToolkitと統合するためのサンプルがMaterialDesignInXamlToolkit側に用意されているので、リソースはMahMaterialDragablzMashUp/App.xamlからコピペってきましょう。DragablzというChromeみたいなドラッグアンドドロップで切り離せるタブのためのライブラリを使わない場合(今回は使いませんでした)は、Dragablzに対する行は削除しておk(というか削除しないと動きません)。これで

となりました。うーん、よくなってきた!タイトルバーのところにテキストでいい感じなレイアウトで手軽にコマンドを突っ込めるのも嬉しかった。というわけでBeforeではステータスバーのところにやけくそにダサい感じで置いてたDuplicate Windowボタン(ウィンドウを複製する)をタイトルバーに移動。ついでにAlign Window(複数ウィンドウを整列させる)コマンドも追加。ちなみにこのアプリは複数ウィンドウを並べて使うのが前提なので、並べた時に重なって鬱陶しいためウィンドウ枠を光らせるのはあえて切ってるんですが、単体アプリなら光らせたほうが見栄え良いかもですね。入れるの自体は簡単で

<!-- 光らせるところ、GlowBrushを削れば光らない -->
<Controls:MetroWindow
    GlowBrush="{DynamicResource AccentColorBrush}">    

    <!-- コマンド入れるところ -->
    <Controls:MetroWindow.RightWindowCommands>
        <Controls:WindowCommands>
            <Button Content="Align Window" Click="AlignWindow_Click" />
            <Button Content="Duplicate Window" Click="DuplicateWindow_Click" />
        </Controls:WindowCommands>
    </Controls:MetroWindow.RightWindowCommands>

をMainWindows.xamlに突っ込むだけです。お手軽素敵。

最終調整

Purpleじゃない色調にしたかったのでテーマをデモアプリのパレットから眺めてBlueGrayに決定。テーマはApp.xamlを弄ればヨイデス。MaterialDesignColor.xxx.xamlの部分ですね、他の色とかはデモアプリのPaletteで確認できます。その他Light/Darkの切り替えやSecondaryColourの設定なんかも、xxx.xamlのそれっぽい部分をなんとなく書き換えれば書き換わります。

<!-- include your primary palette -->
<ResourceDictionary.MergedDictionaries>
    <ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/MaterialDesignColor.BlueGrey.xaml" />
</ResourceDictionary.MergedDictionaries>

これで全体の色が変わったので、最後に、中央部分がMarginが消えてて区切りめがわからず使いづらいのは変わらずだったので、ここは枠をいれて明確な分離を。最初はボーダー入れて調整とかしてみたんですがイマイチしっくりこなかったんで、まぁ枠かな、と。マテリアルデザイン風のシャドウのある枠はヘッダーで色分けした時と同じく ColorZone で囲むだけです、ModeはStandardを選択。モードがどんなのがあるかもデモアプリを見れば一発で分かります。

<wpf:ColorZone Mode="Standard" Padding="5" CornerRadius="3" Effect="{DynamicResource MaterialDesignShadowDepth1}" Margin="2">
    <local:OperationItem />
</wpf:ColorZone>

影の出方はMaterialDesignShadowDepthの1~5で調整可能で、今回は1にしてます。その他の調整として、ログを表示しているテキストボックスのボーダーを上にも出すようにしたり、中身によって拡縮するようになっちゃたのでVerticalContentAlignmentを設定したりとちょっとした調整を少し入れて、最初に出した画像のものになりました。もっかい同じのを載せますけれど。

アプリの見た目が良くなるってのは純粋にテンション上がるんでいいものですねぇ、機能的には何も変わっちゃいないですが、気分は随分と良いです。まぁギョームコウリツとは関係ないとこなんであんまり手を入れまくってもアレですが、ちょっとテーマ適用して調整するだけで必要最低限整ってくれるのは実に良いです。

+アイコン

あとパラメータのコピペが欲しくなりました、複数ウィンドウ間で貼って回ったりするので。というわけでボタンにアイコンを用意したくて、それもマテリアルデザインなら簡単!

<Button Background="{StaticResource PrimaryHueLightBrush}"
        HorizontalAlignment="Left"
        Width="24" Height="24" Padding="0" Margin="5"
        Command="{Binding PasteCommand}"
        ToolTip="Paste">
    <Viewbox Width="16" Height="16">
        <Canvas Width="24" Height="24">
            <Path Data="M19,20H5V4H7V7H17V4H19M12,2A1,1 0 0,1 13,3A1,1 0 0,1 12,4A1,1 0 0,1 11,3A1,1 0 0,1 12,2M19,2H14.82C14.4,0.84 13.3,0 12,0C10.7,0 9.6,0.84 9.18,2H5A2,2 0 0,0 3,4V20A2,2 0 0,0 5,22H19A2,2 0 0,0 21,20V4A2,2 0 0,0 19,2Z"
                     Fill="{DynamicResource MaterialDesignBody}" />
        </Canvas>
    </Viewbox>
</Button>

これはMaterial Design Iconsにあるアイコンから取ってきてます。そこにはXAMLのPath Dataも載ってるので、タグをそのまま貼り付けるだけでアイコンとして使えます。これは楽ちんでめっちゃ良い!アイコンは揃えるのどうしても面倒ですからねー、このお手軽さは嬉しすぎます。色とかを用意されてるMaterialDesignのスタイルを入れ込んでやればそれだけで中々見栄えのするアイコンの出来上がり。

ReactiveCommand

えむぶいぶいえむ的なのはReactivePropertyで実装してます。で、ReactivePropertyもいーんですが、私的には昔から結構ReactiveCommand押しなんですよ、ReactiveCommandいいんだけどなー。例えば実際こんなコードになってます。

// peer = ReactiveProperty<Connection>
// ObserveStatusChangedで状態の変化の監視 + コネクションは切り替わることがあるので前のを破棄するSwitch
// Disconnectが押せるのはStatusがConnectの時だけ
Disconnect = peer.Select(x => x.ObserveStatusChanged())
    .Switch()
    .Select(x => x == StatusCode.Connect)
    .ToReactiveCommand();

// Disconnectの逆、だけどConnectが押せるのはそれに加えて接続先アドレス入力欄が空でない場合
Connect = peer.Select(x => x.ObserveStatusChanged())
    .Switch()
    .CombineLatest(Address, (x, y) => x != StatusCode.Connect && !string.IsNullOrEmpty(y))
    .ToReactiveCommand();

とか。若干込み入って面倒くさいのがスッキリ + ボタンのCanExecuteとぴったり来る。あとはプロセスを監視してて、存在してれば止めるボタンが押せるというのは、一秒毎のチェックにしていて、Observable.Intervalで繋ぎあわせてます。

// PhotonSocketServerが存在すれば押せるコマンド、1秒毎のポーリングで監視
KillPhotonProcess = Observable.Interval(TimeSpan.FromSeconds(1))
    .Select(x => Process.GetProcessesByName("PhotonSocketServer").Any()); 
    .ToReactiveCommand();

こういうの悩まずサクサク書けるのは幸せ度高い。

で、これ何なの?

なんなんでしょーねぇ。ということの一端はMetro.cs #1という勉強会で「IL から Roslyn まで - Metaprogramming Universe in C#」というタイトルでお話しますよ!2015-09-16(水)19:30 - 22:00に渋谷でやりますので、気になる人は是非是非参加くだしあ。内容はRoslyn 20%, C#全般 60%, WPF 10%, Unity 10%ぐらいなイメージですかしらん。このWPFのどこにメタプログラミング要素があるかというと、中身はMono.Cecil使ってアセンブリ解析してるからです。へー。とかそういうことを話します。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive