お前はまだ MahApps.Metro を知らない【#1 WPF MVVM L@bo】
Prism 7.2 で追加された新機能については WPF Prism episode: 20 までで紹介し終えたので新ネタを開始することにしました。
ただ、新ネタとは言っても WPF Prism episode シリーズから緩くつながる新シリーズにする予定なので、Prism や ReactvieProperty、MahApps.Metro、Material Design In XAML Toolkit 等を利用するのは変わりません。
基本的な流れとしては、管理人が裏でひっそり作っている WPF アプリの制作過程紹介をメインストリームとして、MVVM の中級向けエントリになればと考えています。
まあ、何となく思い付きで書き始めたので着地点等は何も決めていませんがお付き合い頂けると有り難いです。
尚、この記事は Visual Studio 2019 Community Edition で .NET Core 3.1 以上 と C# + Prism 7.2 以降 + ReactiveProperty + Livet + MahApps.Metro + Material Design In XAML Toolkit を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。
サンプルアプリについて
このシリーズで紹介するサンプルプロジェクト用に WPF Prism episode シリーズとは別の GitHub リポジトリ を作成しました。
アプリの仕様は GitHub リポジトリの ReadMe.md で公開していて、随時更新する予定です。
このサンプルプロジェクトは管理人が公開を予定しているアプリのプロトタイプ兼実験用なので、実装途中で放置している箇所あり、間違えたまま放置している箇所あり、クラス名や名前空間は大した設計もせず思い付きで実装を進めているので途中で変わる可能性もある的なふわっとしたサンプルです。
アプリを作ろうと思った経緯
管理人は自炊したマンガをスマホ(iPhone)に転送して読んでいて、現時点でも結構な数のファイルを持っています。
ただ、手持ちのファイルは 20年以上前に弟と手分けしてスキャンしたものも多く、1 巻ごとに圧縮しているタイトルもあれば複数巻をまとめて圧縮しているタイトルもあるような状況で、圧縮形式も zip あり、rar ありでバラバラの状態です。
管理人がマンガ読む際に使っているのは iPhone 用の SideBooks と言うアプリで、fig. 1 のような zip ファイル(1 ファイル 2GB までの制限アリ)も読むことができるので、iPhone 転送前には毎回 fig. 1 のような zip ファイルを作成しています。
1 つの zip ファイルに圧縮し直すのは面倒と言えば面倒ですが、そこまで手間がかかる作業と言う訳ではありません。実際に 1 番手間がかかっているのは見開きページの対応です。
管理人的には単一ページごとに zip ファイルへ圧縮されていれば、スマホを回転させる必要も拡大する必要もなく読めるので理想ですが、手持ちの自炊ファイルは元々 PC で読むためにスキャンしていたので、2 ページ単位でスキャンしているファイルもあれば、1 ページごとでスキャンしているファイルもあるような状況です。
そのため、2 ページ単位でスキャンしたファイルは 1 ページごとに分割してから zip ファイルへ圧縮していて、このサイトで公開している ImageSplitter もこの分割作業を楽にするために作りました。
イメージファイルの分割自体は ImageSplitter を使えば楽にできますが、フォルダ内に 2 ページ単位のスキャンファイルがあるかを確認するために、フォルダを 1 つずつ開いて確認する必要があり、巻数が多い場合は結構な手間ですし見落とす事も多いので困っています。
そんな訳で上記のような作業を自動でやってくれるアプリが欲しくなったのと、ブログのネタになりそうなので作る気になりましたw
ただ、こんなアプリを作っても管理人以外に必要な人が居るかは分かりませんが、完成後は一応オープンソースとして公開する予定です。
現時点のプロトタイプについて
今回、プロトタイプを作ろうと思った 1 番の理由は UI が決まらないことです。
WPF も MVVM パターンも MahApps.Metro も Material Design In XAML Toolkit もアプリの作成に使うのは初めてで慣れてないから UI を決める想像が追い付かないと言う要因はありますが、使いやすいと思える UI が思い付かなかったので、とりあえず作って使いながら考えることにしました。
そしてまず最初に作る機能は【新規 zip ファイルの作成機能】で、以下のようなフローを想定しています。
- 作成元になるアーカイブファイルを選択。(ファイルを開くコモンダイアログでファイルを選択)
- 選択したアーカイブファイルを展開。(SharpCompress を使って展開)
- 展開した画像ファイルを指定フォルダへ配置。
- 配置する際、見開きページは単一ページに分割してファイル名、フォルダ名も揃える。
- 配置したフォルダを圧縮して指定フォルダへ出力する。(SharpCompress で zip 形式に圧縮)
そして上記 No.1 で表示する作成元ファイル選択画面と No.2 以降の処理を実行するファイルの展開・配置画面は現状、別 exe で作成しようと考えています。(別 exe は Process.Start で起動)
そして fig. 2 が現時点のプロトタイプの動作です。
まずは zip ファイルを出力するまでの処理を作成したいので、細かい処理や動作、エラー処理などは飛ばして実装を進めています。
緑のタイトルバーの画面が上記フロー No.1 の作成元ファイル選択画面、紫のタイトルバーの画面が上記フロー No.2 以降の処理を担当するファイルの展開・配置画面で、呼び出し元のファイル選択画面から Process.Start で起動しています。
又、今回から Material Design In XAML Toolkit を .NET Core 3.0 対応版の Ver.3.0.0 にアップグレードしていますが、MahApps.Metro の .NET Core 3.0 対応版(Ver.2.0.0)が未だリリースされていないので WPF Prism episode: 19 の通りにインストールしている場合は Material Design In XAML Toolkit のアップグレード後に『NU1608:依存関係の制約外で検出されたパッケージのバージョン: MaterialDesignThemes.MahApps 0.1.0 では MaterialDesignThemes (> 2.5.1 && < 3.0.0) が必要ですが、バージョン MaterialDesignThemes 3.0.0 は解決されました。』と言う警告が出るようになりますが、特に問題は出ていないのでそのまま実装を進めています。
警告を避けたい場合は MahApps.Metro の .NET Core 3.0 対応版か、MaterialDesignThemes.MahApps の新バージョンリリースまで待つ必要があります。
加えて、Material Design In XAML Toolkit は Ver.3.0.0 で破壊的な変更が入っているようなので、Ver.2.6.0 以前の Material Design In XAML Toolkit を使用して作成していたプロジェクトをアップグレードする場合は Material Design In XAML Toolkit の GitHub Issue 『Breaking 3.0.0 changes #1301』 で影響範囲を確認してからアップグレードしてください。
今回のサンプルプロジェクトの場合、TextBlock 用の StaticResource 名を修正をする程度で済みました。
次章からは fig. 2 のサンプルアプリへ実装した機能やライブラリの紹介をしていきます。
(WPF Prism episode シリーズでは紹介していない内容を紹介します)
アプリ起動時の画面
fig. 3 はアプリの MainWindow(View)で、現時点ではこの画面でどんな情報を表示するかを決めかねているのでとりあえず View なしの状態です。
MahApps.Metro の MetroWindow や Material Design In XAML Toolkit の導入等の基本的な部分については WPF Prism episode:19 辺りを見てもらうとして、次画面への遷移には Material Design In XAML Toolkit の PopupBox を使用しています。
Material Design In XAML Toolkit の PopupBox
Material Design In XAML Toolkit の PopupBox は fig. 3 の左上に配置した丸いボタンで、XAML を src. 1 のように記述します。
(スクリーンショットを撮る都合上、ToolTip の表示位置を指定していますが必須ではありません)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | <metro:MetroWindow x:Class="HalationGhost.WinApps.ImaZip.MainWindow" ~ 略 ~ xmlns:prism="http://prismlibrary.com/" xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" ~ 略 ~ BorderThickness="2"> ~ 略 ~ <Grid> ~ 略 ~ <md:ColorZone Grid.Row="0" Mode="Dark" Padding="20" md:ShadowAssist.ShadowDepth="Depth2"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="80"/> <ColumnDefinition/> <ColumnDefinition Width="80"/> </Grid.ColumnDefinitions> <md:PopupBox Grid.Column="0" PlacementMode="BottomAndAlignCentres" Style="{StaticResource MaterialDesignMultiFloatingActionAccentPopupBox}" IsEnabled="{Binding MenuSelectButtonEnabled.Value}" ToolTip="処理を選択" ToolTipService.Placement="Right"> <md:PopupBox.ToggleContent> <md:PackIcon Kind="ArrowRightBoldBox" Width="28" Height="28" /> </md:PopupBox.ToggleContent> <StackPanel> <Button Style="{StaticResource MaterialDesignFloatingActionMiniLightButton}" Margin="0, 0, 0, 5" Command="{Binding LoadSettingViews}" CommandParameter="AppendToZip"> <md:PackIcon Kind="AnimationPlay" Width="24" Height="24" ToolTip="既存のZipに追加" ToolTipService.Placement="Right" /> </Button> <Button Style="{StaticResource MaterialDesignFloatingActionMiniButton}" Margin="0, 0, 0, 5" Command="{Binding LoadSettingViews}" CommandParameter="AppendToZipWithSave"> <md:PackIcon Kind="AnimationPlus" Width="24" Height="24" ToolTip="既存のZipに追加(新規登録)" ToolTipService.Placement="Right" /> </Button> <Button Style="{StaticResource MaterialDesignFloatingActionMiniAccentButton}" Command="{Binding LoadSettingViews}" CommandParameter="CreateNewZip"> <md:PackIcon Kind="BookOpenPageVariant" Width="24" Height="24" ToolTip="新規Zipを作成して登録" ToolTipService.Placement="Right" /> </Button> </StackPanel> </md:PopupBox> ~ 略 ~ </Grid> </md:ColorZone> ~ 略 ~ </Grid> </metro:MetroWindow> |
PopupBox は XAML へ配置するだけで Popup する効果が自動的に追加されますが、ポップアップする方向や位置を指定したい場合は PlacementMode を設定します。(src. 1 では下方向へポップアップするよう指定しています)
又、ポップアップするボタンは Material Design In XAML Toolkit のカラー区分ごとに Style が用意されていて 3 パターンの中から任意で選択できます。src. 1 ではとりあえず 3 パターン全てを指定していますが全て同じ色でも構わないと思います。
CommandParameter を指定して Command を共有する
そしてポップアップするボタンに設定した Command は VM に定義した 1 つの Command を共有して CommandParameter で切り替えるようにしていて、受け取り側の VM は src. 2 のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | using System.Reflection; using HalationGhost.WinApps.ImaZip.AppSettings; using Prism.Regions; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace HalationGhost.WinApps.ImaZip { /// <summary>MainWindowのViewModelを表します。</summary> public class MainWindowViewModel : HalationGhostViewModelBase { ~ 略 ~ /// <summary>メニュー選択ボタンをドロップダウンするボタンのEnabledを取得します。</summary> public ReactivePropertySlim<bool> MenuSelectButtonEnabled { get; } /// <summary>設定ViewをLoadするコマンド。</summary> public ReactiveCommand<string> LoadSettingViews { get; } /// <summary>設定ViewをLoadします。</summary> /// <param name="param">LoadするViewの種類を表す文字列。</param> private void onLoadSettingViews(string param) { var viewName = string.Empty; switch (param) { case "AppendToZip": break; case "AppendToZipWithSave": break; case "CreateNewZip": viewName = "ZipFileListPanel"; break; } this.regionManager.RequestNavigate("FileListArea", viewName); this.MenuSelectButtonEnabled.Value = false; } ~ 略 ~ /// <summary>コンストラクタ。</summary> /// <param name="regMan">Prismのリージョンマネージャを表すIRegionManager。</param> public MainWindowViewModel(IRegionManager regMan, IImaZipCoreProto01Settings imaZipSettings) { this.regionManager = regMan; this.appSettings = imaZipSettings; this.Title = new ReactivePropertySlim<string>(this.getApplicationTitle()) .AddTo(this.disposable); this.MenuSelectButtonEnabled = new ReactivePropertySlim<bool>(true) .AddTo(this.disposable); this.LoadSettingViews = this.MenuSelectButtonEnabled .ToReactiveCommand<string>() .WithSubscribe(p => this.onLoadSettingViews(p)) .AddTo(this.disposable); } ~ 略 ~ } } |
XAML の CommandParameter に設定した値を受け取るには ReactiveCommand の Generic 型に string を指定して受け取ることができます。
現時点ではここでの挙動を決めていませんが、おそらく Load する View を切り替えるだけで済みそうな気がしているので Command を共有することにしました。
Command の共有を使う機会はあまり無いかもしれませんが、似たような処理が分散しなくて済みます。
View の切り替えは Prism の RequestNavigate を呼び出しているので詳細が知りたい場合は WPF Prism episode: 6.5 を見てください。
MahApps.Metro の Control と Helper
fig. 4 は PopupBox から遷移した zip ファイル作成用の設定値を入力するための画面です。
この画面(View)は以下のような特徴を持っています。
- View 遷移時に効果を追加(左から右へスライド)
- View 遷移後、PopupBox を無効に設定
- ファイルを開くコモンダイアログから複数ファイルを選択して ListBox に追加(重複したファイルは除く)
- フォルダを開くコモンダイアログから複数フォルダを選択して ListBox に追加(現状未実装)
- ListBox の項目を選択すると項目の削除ボタンが有効になる(項目の削除機能は未実装)
- ListBox の先頭項目を選択すると上へ移動ボタンが無効になる(項目の移動機能は未実装)
- ListBox の末尾項目を選択すると下へ移動ボタンが無効になる(項目の移動機能は未実装)
- ComboBox で数値型の選択肢を表示
- TextBox に全クリアボタンを表示
- TextBox がフォーカスを受け取った際にテキストを全選択
MahApps.Metro の TransitioningContentControl
fig. 4 では分かりにくいかもしれませんが、zip ファイル作成情報設定画面が表示された際に View が 左から右へスライドする効果を追加しています。
この効果を追加するために、MainWindow に配置していた ContentControl を MahApps.Metro に含まれる TransitioningContentControl に置き換えていて、XAML は src. 3 のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <metro:MetroWindow x:Class="HalationGhost.WinApps.ImaZip.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls" xmlns:bh="http://schemas.microsoft.com/xaml/behaviors" xmlns:prism="http://prismlibrary.com/" ~ 略 ~ BorderThickness="2"> ~ 略 ~ <Grid> ~ 略 ~ <Grid Grid.Row="1" Margin="15, 10, 15, 0"> <metro:TransitioningContentControl IsTabStop="False" Transition="Right" prism:RegionManager.RegionName="FileListArea" /> </Grid> </Grid> </metro:MetroWindow> |
配置した TransitioningContentControl は Transition プロパティを設定するだけで効果が追加される非常にお手軽なコントロールです。TransitioningContentControl に設定できる Transition はスライドのようなシンプルな効果ばかりなので管理人的には派手過ぎないので丁度良いと思っています。
又、置き換え前に配置していた ContentControl は Prism の RequestNavigate で表示する View の土台になるコントロールでしたが、置き換えても問題なく動作します。
これは TransitioningContentControl が ContentControl を継承しているからで、現時点では不都合は起きていません。何か問題が見つかれば追記します。
Transition に設定できる効果は左から右へスライドする効果を含めて 8 種類の中から選択できますが、管理人は実装を進めたい都合上、『Right』以外の効果はちゃんと確認していません。
『Right』以外の効果は RequestNavigate で複数のページを切り替えられるようにしてそれぞれで確認してみてください。
このような Transition は MahApps.Metro だけでなく Material Design In XAML Toolkit にも含まれていますが、Material Design In XAML Toolkit の Transition は結構派手で TransitioningContentControl.Transition 程お手軽に扱える訳ではなさそうなので、又調べてどこかで使えそうならエントリを起こしたいと思います。
MahApps.Metro の TextBoxHelper
MahApps.Metro に含まれる TransitioningContentControl も簡単に使えそうなのに日本語での情報はひょっとしてここでの紹介が初めて?と言うくらい見かけませんが、TextBoxHelper も便利な割に日本語での情報は見かけません。
まず、TextBoxHelper を設定しない場合の動作を見てください。
Tab キーでアクティブなコントロールを移動しているだけですが、fig. 5 が WPF の 標準コントロールの動作です。
では、src. 4 のように XAML へ MahApps.Metro の TextBoxHelper を設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <UserControl x:Class="HalationGhost.WinApps.ImaZip.ImageFileSettings.ZipFileListPanel" ~ 略 ~ xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls" ~ 略 ~ xmlns:md="http://materialdesigninxaml.net/winfx/xaml/themes" ~ 略 ~ prism:ViewModelLocator.AutoWireViewModel="True"> ~ 略 ~ <Grid> ~ 略 ~ <Grid Grid.Row="1"> ~ 略 ~ <Grid Grid.Column="1" Margin="15, 10, 5, 0"> ~ 略 ~ <Grid Grid.Row="1"> ~ 略 ~ <TextBox Grid.Column="1" Margin="30, 10, 0, 10" md:HintAssist.Hint="フォルダ名のテンプレート" md:HintAssist.FloatingScale="0.9" md:TextFieldAssist.HasClearButton="True" metro:TextBoxHelper.IsMonitoring="True" metro:TextBoxHelper.SelectAllOnFocus="True" Style="{StaticResource MaterialDesignFloatingHintTextBox}" Text="{Binding FolderNameTemplate.Value, Mode=TwoWay}"/> </Grid> <Grid Grid.Row="2"> ~ 略 ~ <TextBox Grid.Row="0" Margin="0, 10, 0, 10" md:HintAssist.Hint="ファイル名のテンプレート" md:HintAssist.FloatingScale="0.9" md:TextFieldAssist.HasClearButton="True" metro:TextBoxHelper.IsMonitoring="True" metro:TextBoxHelper.SelectAllOnFocus="True" Style="{StaticResource MaterialDesignFloatingHintTextBox}" Text="{Binding FileNameTemplate.Value, Mode=TwoWay}"/> <TextBlock Grid.Row="1" Style="{StaticResource MaterialDesignCaptionTextBlock}" Text="フォルダ名連番:【?】 ファイル名連番:【*】" /> </Grid> </Grid> ~ 略 ~ </Grid> </Grid> </UserControl> |
src. 3 の設定を追加すると fig. 6 のように動作するようになります。
fig. 5 と同じように Tab キーでアクティブなコントロールを移動すると入力済みのテキストが全て選択されるようになりました。
マウスでコントロールを Click した時も同様に全選択されています。
MahApps.Metro の TextBoxHelper は、この WPF MVVM L@bo シリーズとは別の WPF UI Gallery case: 1-3 でも詳しく紹介しているので良ければそちらも見てください。
但し、マウスでテキストの上を Click してしまうと選択がクリアされてしまうのは少し残念な部分です。
ですが、添付プロパティを設定するだけでテキストが全選択されるようになるのは魅力だと思います。
その上、fig. 5、6 で選択している TextBox は MahApps.Metro に含まれるコントロールではなく WPF 標準の TextBox で、Material Design In XAML Toolkit の Style が適用されている TextBox です。
つまり TextBoxHelper 添付プロパティを使用すれば、わざわざ Extended WPF Toolkit™ の AutoSelectTextBox 等を使う必要が無いと言う事です。
但し、1 つ注意があって SelectAllOnFocus = true にする場合は、IsMonitoring も併せて true に設定しないと SelectAllOnFocus の機能が有効になりません。
(管理人も最初動作しないので使えないのかと思いましたが、ソースコードを読むと IsMonitoring が必要だと気付きました)
加えて、MahApps.Metro の TextBoxHelper は以下のような設定をコントロールに付加できます。
- コントロールがアクティブになると入力済みテキストを全選択
- コントロールにウォーターマーク(フローティングウォーターマーク含む)を追加
- 入力コントロールにテキスト全クリアボタンを追加(Enable 等の制御も VM から可能)
- 入力コントロールに任意の機能を実行可能なボタンを追加
- HasText や TextLength プロパティをバインド可能にする
そして、TextBoxHelper は以下のようなコントロールに添付可能です。
- TextBoxBase を継承したコントロール
- ComboBox コントロール
- DatePicker コントロール
このように TextBoxHelper は結構有用な機能が含まれていますが、日本語の解説記事はほぼ存在しない状況です。
そして、MahApps.Metro には TextBoxHelper 以外にも多数の Helper 添付プロパティが含まれているので興味があれば MahApps.Metro の GitHub リポジトリ を探索してみてください。
又、MahApps.Metro には 各種 Helper 以外に NumericUpDown コントロールも含まれているので使用してみようと思いましたが、コントロールの見た目を Material Design In XAML Toolkit の Style で上書きする方法が分からず現時点では使用を断念しました。
ただ、ControlTemplate を上書きすればイケそうな気はするので、方法が分かれば別のエントリで紹介したいと思っています。
今回は 2019 年最後の記事として新ネタの第 1 回目を公開しました。
初回と言う事で前置きも長く小ネタばかりの記事になりましたが、次回以降はそれなりの内容のエントリを書こうと思っています。
いつもの通り、紹介したソースコードは GitHub リポジトリ に上げていますが、WPF Prism episode シリーズとは違い、この『WPF MVVM L@bo シリーズ』はエントリごとにプロジェクトは分けず、実装中のソリューションをそのまま公開しています。
そのため、記事として紹介していない部分や紹介したソースコードから変わっている箇所もあるかもしれませんが、対応については追々考えていきたいと思っています。
それでは、2019 年もこんな拙いエントリを読んで頂いてありがとうございました。
引き続き 2020 年もよろしくお願いします。
:: halation ghost :: 管理人 妖精作戦
次回記事「MVVM さえあればいい。【#2 WPF MVVM L@bo】」