MetroWindow のプロパティに連動する Behavior【case: 1-2 WPF UI Gallery】

前回記事「MahApps.Metro の HamburgerMenu から Prism の RequestNavigate で画面を切り替える【case: 1-1 WPF UI Gallery】」

 

前回は MahApps.Metro の HamburgerMenu を紹介しましたが、今回は MahApps.Metro の MetroWindow と MetroWindow のプロパティ変更に対応する Behaviorを紹介します。

尚、この連載は Visual Studio 2019 Community Edition で .NET Core 3.1 以上 と C# + Prism 7.2 以降 + ReactiveProperty を使用して 、WPF アプリケーションを MVVM パターンで作成するのが目的で、C# での基本的なコーディング知識を持っている人が対象です。

MahApps.Metro の MetroWindow

MahApps.Metro と Material Design In XAML Toolkit のインストールや初期設定は『WPF Prism episode: 19 ~ MahApps.Metro と Material Design In XAML Toolkit たちは Prism でも余裕で生き抜くようです! ~』を見てください。

MahApps.Metro.IconPacks については前回の case: 1-1 を見てください。

このエントリのサンプルコードは MahApps.Metro と MahApps.Metro.IconPacks、Material Design In XAML Toolkit がインストールされている前提で書いています。

MetroWindow で追加されるプロパティ

MahApps.Metro.MetroWindow のプロパティは WPF Prism episode: 19 でもいくつか紹介済みですが、紹介していないプロパティもまだ多くあります。

MetroWindow に追加されたプロパティは全てバインド可能(依存関係プロパティ)なプロパティとして定義されているためバインド先の VM から実行時でも変更が可能ですが、GlowBrush 等は値の反映に Window の再 Load が必要です。(つまりアプリの再起動が必要)

GlowBrush

WPF Prism episode: 19 でも紹介した GlowBrush は設定すると Window の外枠に Visual Studio のような光る効果が追加されます

fig.1 GlowBrush を設定した MetroWindow

GlowBrush を設定した fig. 1 は前回の case: 1-1 で紹介したサンプルアプリをそのまま流用しています。ハンバーガーメニュー等については case: 1-1 を見てください。

GlowBrush は MainWindow の XAML へ src. 1 のように記述すると fig. 1 のような外枠が光る Window になります。(BorderThickness には 1 以上の値が必要です)

効果が分かりにくい場合や、もう少し目立たせたいような場合は、BorderThickness に少し大きめの値を設定します。

又、src. 1 を『GlowBrush=”Black”、BorderThickness=”0″』のように変更すると fig. 2 のように『ドロップシャドウ付きの Window』になります。

fig.2 ドロップシャドウ付き Window

MetroWindow には EnableDWMDropShadow プロパティもありますが、Obsolete でマークされているのでドロップシャドウを付けたい場合は、上で紹介した『GlowBrush=”Black”、BorderThickness=”0″』を使用してください。

その他 MetroWindow で追加されたプロパティ

GlowBrush 以外のプロパティは実行時でも値の変更が可能なので、前回の case: 1-1 で紹介したサンプルアプリを流用して実行時に MetroWindow のプロパティを変更するサンプルアプリを作成しました。

サンプルアプリ起動後、MahApps.Metro HamburgerMenu の一番上のメニューボタン(ToolTip:MetroWindowプロパティのデモ)を Click して MetroWindow プロパティ操作 View を開くと」プロパティが変更できます。

fig.3 MetroWindow Properties GroupBox 左端プロパティデモ

fig. 3 は【MetroWindow Properties GroupBox】左端のプロパティを変更した場合の動作で、確認できるのは以下のプロパティです。

プロパティ内容
IgnoreTaskbarOnMaximzeWindow を最大化した際、タスクバーを無視して最大化します。
動画再生アプリのような全画面表示が可能です。
Window が最大化の状態で変更しても反映されます。
fig. 3 では分かりにくいので操作していません。
IsWindowDraggableWindow のドラッグ可 / 不可を設定します。
WindowTransitionEnabledWindow Load 時のトランジションを無効にします。
タイトル文字の位置を変更した場合に Load 時と同じトランジションが実行されるので fig. 3 ではタイトル文字位置の変更で動作を確認しています。
false に設定するとタイトル文字位置を変更してもトランジションが実行されないことが確認できます。

続いて fig. 4 は【MetroWindow Properties GroupBox】中央のプロパティを変更した場合の動作です。

fig.4 MetroWindow Properties GroupBox 中央プロパティデモ

主にタイトルバーに関連する項目の表示 / 非表示を切り替えるプロパティです。

プロパティ内容
ShowTitleBarタイトルバーの表示 / 非表示を切り替えます。
fig. 4 の通りタイトルバーを非表示にしても Window 自体の高さは変わらず、クライアント領域の高さが変わります。
タイトルバーを非表示にすると『閉じるボタン』、『最大化ボタン』等は操作できないように制御していますが、値の設定は可能です。
ShowCloseButton閉じるボタンの表示 / 非表示を切り替えます。
非表示にすると『IsCloseButtonEnabled トグルボタン』が操作不可になるようにしています。
ShowMaxRestoreButton最大化 / 元に戻すボタンの表示 / 非表示を切り替えます。
非表示にすると『IsMaxRestoreButtonEnabled トグルボタン』が操作不可になるようにしています。
ShowMinButton最小化ボタンの表示 / 非表示を切り替えます。
非表示にすると『IsMinButtonEnabled トグルボタン』が操作不可になるようにしています。
ShowSystemMenuOnRightClickシステムメニューの表示 / 非表示を切り替えます。

Show~Button プロパティは false に設定すると非表示になり、右上の閉じるボタン等の位置が右詰めされます。

fig. 5 は残りのプロパティの動作です。

fig.5 残りのプロパティのデモ

fig. 5 ではタイトルバーの色に紛れて分かりにくいと思いますが、Is~ButtonEnabled プロパティを false に設定するとボタンはグレーアウトされます

プロパティ内容
IsCloseButtonEnabled閉じるボタンの有効 / 無効を切り替えます。
IsMaxRestoreButtonEnabled最大化 / 元に戻すボタンの有効 / 無効を切り替えます。
IsMinButtonEnabled最小化ボタンの有効 / 無効を切り替えます。
TitleAlignmentタイトルバー文字の表示位置を設定します。
Left:左寄せ(デフォルト値)
Center:中央寄せ
Right:右寄せ
Stretch:左寄せと同じ
TitleCharacterCasingタイトルバー文字の大文字・小文字を設定します。
Upper:全て大文字(デフォルト値)
Lower:全て小文字
Normal:設定通り大文字・小文字混在
ShowIconOnTitleBarタイトルバーのアプリアイコン表示 / 非表示を切り替えます。
SaveWindowPositionfig. 5 では出てきていませんが、True を設定すると Window の位置とサイズを保存するようになり次回表示する際に復元されます。

Prism Module ⇔ Shell 間でバインドしたプロパティの値を同期する方法等は前回の case: 1-1 で紹介した内容と同じなので、このエントリでは紹介しません。詳しくは case: 1-1 を見てください。

ここで紹介したプロパティを fig. 3 ~ 5 のように実行時に変更したいと思うことはあまり無さそうな気はしますが、閉じるボタンは非同期処理実行中等に無効にしたいことはありそうなので一通り動かして動作を確認していると…

『閉じるボタンを無効(非表示)にしていても Alt + F4 で Window が閉じる…だ…と?』



MetroWindow プロパティと Window 標準動作

どうやら上で紹介したプロパティはあくまでも MetroWindow の見た目の制御に限定されているようで、Windows 標準の動作は fig. 6 のように MetroWindow のプロパティを無効に設定していても全て普通にできてしまいます。

fig.6 ボタンを無効にしても Windows の標準操作が可能

ざっと確認した所、全プロパティを無効にしても以下の操作は可能でした。

  • Alt + F4 で Window が閉じる
  • タスクバーからシステムメニューは表示できる
  • タスクバーから表示したシステムメニューの項目は全て操作可能
  • タスクバーのアイコンを Click すると Window が最小化する
  • タイトルバーをダブルクリックすると Window が最大化する

『Window を継承してんだからその辺りも対応しておいてくれよ punker76 さん…』と思いましたが、Issue や プルリクを出すのはハードルが高そうなので、何とか自前で制御する方法を探してみると【Behavior(ビヘイビア)】を作成すればできそうなので作ってみることにしました。

MetroWindow のプロパティと連動する Behavior

Behavior は今までも WPF Prism episode: 18 等で Livet に含まれる Behavior を使うことはありましたが、1 から作成したことは無かったのでこれをチャンスに作ってみました。

WPF の Behavior とは『WPFのビヘイビア – Qiita』や『ビヘイビア(Behavior)の作り方 – かずきのBlog@hatena』等にも書かれているように、言葉自体は振る舞い、態度、作用作用、反応等を意味しますが、WPF では関連付けられた依存オブジェクトに新しいプロパティや動作を追加したい場合に利用されます

継承コントロールと違って種類の違うコントロールに適用できたり、後付け可能なので他画面や他プロジェクトへも簡単に流用できると言う特徴があります。

Behavior は MVVM パターンで紹介されることも多いので、コードビハインドからコードを追い出すことが目的と感じる人が居るかもしれません(管理人もその中の 1 人でした)が誤解です。
Behavior の目的とは処理を委譲して流用し易くすることであって、コードビハインドから処理を追い出せるのは副作用の 1 つでしかありません。その画面だけに必要な処理であればコードビハインドに書いても問題ありません。と言うよりコードビハインドに書く方が良い場合も多いと思います。(あくまでも実現したい内容によりますが…)



作成した Behavior について

Behavior は本来流用し易くするため、単機能で作成する方が良いと思いますが、ここで紹介する Behavior には以下の機能を全部乗せました。

  • 閉じる、最大化、最小化ボタンの状態に対応する OS 標準操作の有効 / 無効
  • タスクバーアイコンの右クリックからシステムメニューを表示する / しない
  • Window Closing イベントに対応したコールバックコマンド
  • Window Close 時に VM を Dispose する
  • VM から Window を閉じる

まず Behavior の基本構造は、じんぐるさん主宰の『システムメニューを操作するビヘイビア – xin9le.net』で紹介されている Behavior を流用させてもらいました。

加えてかずきさんがメンテナーをされている Livet から DataContextDisposeActionWindowCloseCancelBehavior の機能を取り込んでいます。

更に『VMからウィンドウを閉じる添付ビヘイビア – SourceChord』で紹介されている VM から Window を閉じる機能も取り込みました。

WPFのビヘイビア – Qiita』に依ると Behavior にはそもそも添付 Behavior と Blend SDK Behavior の 2 種類があるようですが違いが今一ピンと来ないので、じんぐるさんのサンプルのまま Blend SDK ベースの Behavior として作成しています。

Blend SDK ベースの Behavior は元々 System.Windows.Interactivity.dll に含まれる Behavior<T> を継承しますが、現時点(2020 年 4 月現在)は .NET Core、.NET Framework のどちらの場合も XamlBehaviors for WPF を参照するように変わっています

.NET Framework の場合は System.Windows.Interactivity.dll でも使用できると思いますが、.NET Core の場合は XamlBehaviors for WPF を使用しないとコンパイル時に警告が出ます

XamlBehaviors for WPF は System.Windows.Interactivity.dll から名前空間の変更と言う破壊的変更がされているため互換性が無くなっています。そのためタイミングを計る必要はあるでしょうが、できるだけ早めに変更しておいた方が良いと思います。
XamlBehaviors for WPF は NuGet からインストールできるので、詳しくは WPF Prism episode: 18 を見てください。

WindowControlBehavior

src. 2 はかなり長いですが、今回作成した全部乗せ Behavior です。

Behavior は添付対象の依存オブジェクトを型パラメータに指定する必要があるので、この Behavior では Window を依存オブジェクトに指定していますが、基本的に MetroWindow を対象に作成したので WPF 標準の Window に添付した場合の動作までは確かめていません。

Behavior のインタフェース

src. 2 では『~ 略 ~』で省略していますが、以下のようなインタフェースを用意しています。

インタフェース内容
CanClosefalse を設定している間は Alt + F4 等でも Window の Close を中止するプロパティ。又、システムメニューの『閉じる』項目もグレーアウトします。
true を設定している間に Window Close 操作を行うと Closeされますが、false のままでは Window が Close できません
CanMaxmizefalse を設定している間はタイトルバーをダブルクリックしても最大化を中止するプロパティ。又、システムメニューの『最大化』項目もグレーアウトします。
true を設定している間は最大化されますし、システムメニューも有効になります。
CanMinimizefalse を設定している間はタスクバーのアイコンをクリックしても最小化を中止するプロパティ。又、システムメニューの『最小化』項目もグレーアウトします。
true を設定している間は最小化されますし、システムメニューも有効になります。
CanMoveシステムメニューの『移動』項目の有効 / 無効を切り替えるプロパティ
SystemMenuVisibleタスクバーアイコンの右クリックからシステムメニューを表示する / しないを切り替えるプロパティ
RequestClosetrue を設定すると Window を閉じるプロパティ
このプロパティに true を設定する前に CanClose プロパティが true に設定されている必要があります。
CloseCanceledCallbackWindow を Close しようとした場合に呼び出されるこの Behavior 唯一のコマンド。
このコマンドにバインドした処理の中で CanClose プロパティに false をセットすると Window の Close を中止することができます。

src. 2 では一応見本を兼ねて CanClose の定義のみ残してありますが、省略したプロパティの定義はほとんど同じです。依存プロパティの定義自体はかなり長いですが、『propdp』スニペットを使用すれば、fig. 7 のように簡単に入力できます

fig.7 『propdp』スニペットと『Ctrl + .』でクイックアクション

『propdp』スニペットから依存プロパティを入力すると 4 か所変更するだけで済むので大した手間なく入力できますが、スニペットのテンプレートはプロパティ名が文字列で埋め込まれるので fig. 7 では『nameof』に手動で書き換えています。又、横に長いので『Ctrl + .』ショートカットでパラメータリストを折り返しています

fig. 7 で使用している『Ctrl + .』ショートカットはパラメータを折り返すだけでなく不足している using を追加してくれたり、インタフェースの実装を追加してくれたり…と書ききれない程いろんな事ができるので詳しくは『Visual Studio で一番費用対効果の高いショートカット「Ctrl + .」 – かずきのBlog@hatena』を見てください。

管理人は上記のかずきさんの記事を見るまでこのショートカットキーを知りませんでしたが、記事を読んで使ってみるとコーディングがかなり快適になりましたし生産性も上がったような気がします。
今では困ったらすぐに『Ctrl + .』を押してみるくらいの常用キーなので、知らなかった人はとりえず上記のかずきさんブログ を読んでみてください。

Behavior でハンドルするイベント

Behavior は添付した依存オブジェクトの Load 時に OnAttached、Unload 時に OnDetaching メソッドが呼び出されるので、それぞれで初期化・終了処理を行うことができます。
この Behavior では OnAttached メソッド内で以下の Window イベントをハンドルして動作に必要な処理を実装しています。

Closing イベント

Livet の WindowCloseCancelBehavior の実装をほぼそのまま取り込んでいるので、Window の Closing イベントで CloseCanceledCallback コマンドを呼び出すようにしています。
CloseCanceledCallback コマンドのバインド先で CanClose プロパティに false をセットすると Window の Close を中止できます。

サンプルでは CloseCanceledCallback にバインドしたメソッド内で fig. 7 のようにメッセージボックスを表示して Close の可否を問い合わせています。

fig.7 Window 終了の確認

Alt + F4 キーで Window Close を禁止する方法を検索するとウィンドウプロシージャをフックして… 的な記事も見かけますが、この Behavior のように Closing イベントをキャンセルすることでも可能です。
但し、Alt + F4 のキー操作だけ Window の Close を禁止したい的な事はできません。

Closed イベント

これも Livet の DataContextDisposeAction の実装をそのまま取り込んでいます。

Livet の DataContextDisposeAction は TriggerAction なので、XAML へ設定する際は Closed イベントを指定した EventTrigger が必要ですが、この Behavior には EventTrigger の設定は不要です。
必要なのは Window の DataContext に設定される VM が IDisposable を継承することだけです。

PreviewMouseDoubleClick イベント

MetroWindow タイトルバーのダブルクリックで最大化を中止するためにハンドルしています。
WPF 標準の Window であればウィンドウプロシージャで WM_NCLBUTTONDBLCLK メッセージを処理する事で可能なようですが、MetroWindow のタイトルバーは標準 Window のタイトルバーと違ってクライアント領域と同じく MouseDoubleClick が発火してマウス座標がタイトルバー内であれば最大化する実装なので、MouseDoubleClick の発火を中止するためにハンドルしています。

MetroWindow ではタイトルバーもクライアント領域である事に気付かず WM_NCLBUTTONDBLCLK メッセージが飛んで来ない理由が分からずかなり悩みました…

onSourceInitialized イベント

じんぐるさんの『システムメニューを操作するビヘイビア – xin9le.net』でも紹介されていますが、onSourceInitialized イベントでウィンドウハンドルが取得できるようになるので、ウィンドウプロシージャをフックしたい場合はこのイベント内でフック先のメソッドを設定するようです。

.NET Core からの Win32API 呼び出し

onSourceInitialized イベントでフックしたウィンドウプロシージャでシステムメニュー Open メッセージを受け取るとメニュー項目の有効 / 無効を設定するような実装にしています。
EnableMenuItem API は検索してもあまり使用事例が出てこなかったので多少の不安はありますが、現状では特に問題なく動作しています。

src. 2 の WndProc フックプロシージャ依存プロパティ変更時に呼び出されるコールバックメソッドから Win32API を呼び出して Window を制御しています。
管理人の場合、Win32API は呼び出し方が多少分かる程度なのでここでは詳しい紹介等はしませんが、簡単に言うと Windows OS が備えている C 言語の関数を呼び出して WPF 標準ではできない処理を実行しています。

Win32API と言っても単なる外部関数呼び出しなので、.NET Core でアプリを作成する場合でもパッケージの追加等は不要で、.NET Framework と全く同じように呼び出せます

このサンプルアプリでは Win32API を実際に呼び出す部分を別プロジェクトに分けて再利用できるようにしていて、src. 3 が Win32API 呼び出し用 static クラスです。

Window の最大化・最小化の可 / 不可と、システムメニューの表示 / 非表示は WindowStyle を変更することで可能なので、ChangeWindowStyle メソッドで GetWindowLong API で Window の Style を取得して、SetWindowLong API で Style を設定しています。

又、システムメニュー項目の有効 / 無効は GetSystemMenu API でシステムメニューハンドルを取得して、EnableMenuItem API で有効 / 無効を設定しています。

そしてこの Behavior を MainWindow に添付して実行すると fig. 8 のようになります。

fig.8 Behavior 設定後の動作

fig. 6 では操作できていたシステム関連の動作が fig. 8 では無効になっているのが分かると思います。
補足 fig. 8 では Window Closing イベントでのメッセージボックス表示を一時的にコメントアウトしています。

この WindowControlBehavior を使用するには MainWindow.xaml へ src. 4 のハイライト部分を追加します。

VM 側はプロパティを単純にバインドしているだけなので、GitHub リポジトリ で見てください。

このサンプルアプリは Prism Module 内の View(UserControl)上の各コントロールの値と Prism Shell(MainWindow)のプロパティを連動するようにしていて、Shell ⇔ Module 間は Model 層に位置する MainWindowService が仲介する典型的な MVVM パターンのサンプルになっていると思います。
基本的には前回 case: 1-1 で紹介した内容と同じなので詳しくは前回の case: 1-1 を見てください。

又、MainWindowService を仲介せず src. 5 のように WindowControlBehavior のプロパティを MetroWindow のプロパティに直接バインドすることもできます

src. 5 のように ElementName を指定すると同一 XAML 上の別コントロールのプロパティと直接バインディングすることもできるので作成するアプリの仕様に合った方を選択すれば良いと思います。

かなり長文の紹介になりましたが全ソースコードをこのエントリでは紹介できないので、必要があれば GitHub リポジトリ を見てください。

タイトルバーにコントロールを追加する WindowCommands

MetroWindow には LeftWindowCommands、RightWindowCommands と言う WindowCommands 型のプロパティがあり、fig. 9 のようにそれぞれタイトルバーの左側と右側にボタン等のコントロールを追加することができます。

fig.9 WindowCommands でタイトルバーにコントロールを追加

WindowCommands は fig. 9 の通りボタンやテキストボックス(⁉)等も含める事ができ、XAML は src. 6 のように記述します。

WindowCommands は ToolBar を継承しているので、基本的な使用法は ToolBar と同じで Panel 系コントロール無しで複数のコントロールが配置できます。又、fig. 9 のようにボタンだけではなく TextBox や ComboBox、CheckBox 等も配置できます。(配置ができるだけであまりお勧めはしません)

機能を目立たせたいような場合には効果があると思うので試してみると良いんじゃないでしょうか。

又、WindowCommands とは直接関係ありませんが、LeftWindowCommands に配置したボタンから GitHub リポジトリを開くようにしていますが、.NET Framework ではできていた『Process.Start(https://github.com/…”);』のような呼び出しを実行すると .NET Core では『System.ComponentModel.Win32Exception: ‘指定されたファイルが見つかりません。’ 例外』が Throw されるように変わっています。

詳しくは『.NET Core Process.Start(URL) でWEBブラウザを表示できない – OITA: Oika’s Information Technological Activities』に書かれていますが、プラットフォームに依存する動作を止めたことが原因のようです。

上記ブログにも書かれている通り、.NET Core では src. 7 のように記述しないとデフォルトブラウザで URL が開けないようになっています

自分のサイトへ誘導する機能を組み込みたい場合もあると思うので .NET Core で開発する場合は注意が必要です。

 

元々は MetroWindow 以外に TextBox 等のコントロールも紹介する予定でしたが、Behavior と Win32API まで紹介することになるのは想定外で、今回もかなり長文のエントリになってしまいましたが、MetroWindow のプロパティ紹介は一旦ここまでにします。

MetroWindow の全プロパティを紹介できた訳ではなくグラフィック系やダイアログ・ Flyout 関連のプロパティが残っていますが、ダイアログや Flyout を紹介するエントリまで持ち越すことにします。
(グラフィック系のプロパティは管理人が理解できていないので今の所紹介する予定はありません)

 

ここで紹介したサンプルコードはいつものように GitHub リポジトリ に上げています。

 

次回記事「TextBox と Material Design In XAML Toolkit【case: 1-3 WPF UI Gallery】」

 

 

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください