MetroWindow のプロパティに連動する Behavior【case: 1-2 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 のような光る効果が追加されます。
GlowBrush を設定した fig. 1 は前回の case: 1-1 で紹介したサンプルアプリをそのまま流用しています。ハンバーガーメニュー等については case: 1-1 を見てください。
GlowBrush は MainWindow の XAML へ src. 1 のように記述すると fig. 1 のような外枠が光る Window になります。(BorderThickness には 1 以上の値が必要です)
1 2 3 4 5 6 7 8 9 10 | <metro:MetroWindow x:Class="MetroWindowAndControls.MainWindow" ~ 略 ~ xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls" ~ 略 ~ GlowBrush="{DynamicResource PrimaryHueMidBrush}" BorderThickness="1" ~ 略 ~ WindowTransitionsEnabled="{Binding WindowTransitionsEnabled.Value, Mode=OneWay}"> ~ 略 ~ </metro:MetroWindow> |
効果が分かりにくい場合や、もう少し目立たせたいような場合は、BorderThickness に少し大きめの値を設定します。
又、src. 1 を『GlowBrush=”Black”、BorderThickness=”0″』のように変更すると 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】左端のプロパティを変更した場合の動作で、確認できるのは以下のプロパティです。
プロパティ | 内容 |
---|---|
IgnoreTaskbarOnMaximze | Window を最大化した際、タスクバーを無視して最大化します。 動画再生アプリのような全画面表示が可能です。 Window が最大化の状態で変更しても反映されます。 fig. 3 では分かりにくいので操作していません。 |
IsWindowDraggable | Window のドラッグ可 / 不可を設定します。 |
WindowTransitionEnabled | Window Load 時のトランジションを無効にします。 タイトル文字の位置を変更した場合に Load 時と同じトランジションが実行されるので fig. 3 ではタイトル文字位置の変更で動作を確認しています。 false に設定するとタイトル文字位置を変更してもトランジションが実行されないことが確認できます。 |
続いて fig. 4 は【MetroWindow Properties GroupBox】中央のプロパティを変更した場合の動作です。
主にタイトルバーに関連する項目の表示 / 非表示を切り替えるプロパティです。
プロパティ | 内容 |
---|---|
ShowTitleBar | タイトルバーの表示 / 非表示を切り替えます。 fig. 4 の通りタイトルバーを非表示にしても Window 自体の高さは変わらず、クライアント領域の高さが変わります。 タイトルバーを非表示にすると『閉じるボタン』、『最大化ボタン』等は操作できないように制御していますが、値の設定は可能です。 |
ShowCloseButton | 閉じるボタンの表示 / 非表示を切り替えます。 非表示にすると『IsCloseButtonEnabled トグルボタン』が操作不可になるようにしています。 |
ShowMaxRestoreButton | 最大化 / 元に戻すボタンの表示 / 非表示を切り替えます。 非表示にすると『IsMaxRestoreButtonEnabled トグルボタン』が操作不可になるようにしています。 |
ShowMinButton | 最小化ボタンの表示 / 非表示を切り替えます。 非表示にすると『IsMinButtonEnabled トグルボタン』が操作不可になるようにしています。 |
ShowSystemMenuOnRightClick | システムメニューの表示 / 非表示を切り替えます。 |
Show~Button プロパティは false に設定すると非表示になり、右上の閉じるボタン等の位置が右詰めされます。
fig. 5 は残りのプロパティの動作です。
fig. 5 ではタイトルバーの色に紛れて分かりにくいと思いますが、Is~ButtonEnabled プロパティを false に設定するとボタンはグレーアウトされます。
プロパティ | 内容 |
---|---|
IsCloseButtonEnabled | 閉じるボタンの有効 / 無効を切り替えます。 |
IsMaxRestoreButtonEnabled | 最大化 / 元に戻すボタンの有効 / 無効を切り替えます。 |
IsMinButtonEnabled | 最小化ボタンの有効 / 無効を切り替えます。 |
TitleAlignment | タイトルバー文字の表示位置を設定します。 Left:左寄せ(デフォルト値) Center:中央寄せ Right:右寄せ Stretch:左寄せと同じ |
TitleCharacterCasing | タイトルバー文字の大文字・小文字を設定します。 Upper:全て大文字(デフォルト値) Lower:全て小文字 Normal:設定通り大文字・小文字混在 |
ShowIconOnTitleBar | タイトルバーのアプリアイコン表示 / 非表示を切り替えます。 |
SaveWindowPosition | fig. 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 のプロパティを無効に設定していても全て普通にできてしまいます。
ざっと確認した所、全プロパティを無効にしても以下の操作は可能でした。
- 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 から DataContextDisposeAction と WindowCloseCancelBehavior の機能を取り込んでいます。
更に『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 です。
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | using System; using System.Windows; using System.Windows.Input; using System.Windows.Interop; using HalationGhost.Win32Api; using MahApps.Metro.Controls; using Microsoft.Xaml.Behaviors; namespace HalationGhost.WinApps { /// <summary>Windowを制御するBehaviorを表します。</summary> public class WindowControlBehavior : Behavior<Window> { /// <summary>WindowがCloseできるかを取得・設定します。</summary> public bool CanClose { get { return (bool)GetValue(CanCloseProperty); } set { SetValue(CanCloseProperty, value); } } /// <summary>WindowがCloseできるかを取得・設定する依存プロパティ。</summary> public static readonly DependencyProperty CanCloseProperty = DependencyProperty.Register(nameof(CanClose), typeof(bool), typeof(WindowControlBehavior), new PropertyMetadata(true, WindowControlBehavior.onPropertiesChanged)); ~ 略 ~ /// <summary>WindowをCloseします。</summary> public bool RequestClose { get { return (bool)GetValue(RequestCloseProperty); } set { SetValue(RequestCloseProperty, value); } } /// <summary>WindowをCloseする依存プロパティ。</summary> public static readonly DependencyProperty RequestCloseProperty = DependencyProperty.Register(nameof(RequestClose), typeof(bool), typeof(WindowControlBehavior), new PropertyMetadata(false, WindowControlBehavior.onRequestClose)); /// <summary>Window.Closingイベント時に呼び出されるCommandを表します。</summary> public ICommand CloseCanceledCallback { get { return (ICommand)GetValue(CloseCanceledCommandProperty); } set { SetValue(CloseCanceledCommandProperty, value); } } /// <summary>Window.Closingイベント時に呼び出されるCommandを表します。</summary> public static readonly DependencyProperty CloseCanceledCommandProperty = DependencyProperty.Register(nameof(CloseCanceledCallback), typeof(ICommand), typeof(WindowControlBehavior), new PropertyMetadata(null)); /// <summary>Behaviorが依存コントロールに接続された場合に呼び出されます。</summary> protected override void OnAttached() { if (this.AssociatedObject == null) throw new InvalidOperationException(); // Window Handleが必要な初期化処理 this.AssociatedObject.SourceInitialized += this.onSourceInitialized; base.OnAttached(); // Closingイベント this.AssociatedObject.Closing += (sender, e) => { if ((this.CloseCanceledCallback != null) && (this.CloseCanceledCallback.CanExecute(null))) { this.CloseCanceledCallback.Execute(null); } e.Cancel = !this.CanClose; }; // Closedイベント this.AssociatedObject.Closed += (sender, e) => { (this.AssociatedObject.DataContext as IDisposable)?.Dispose(); }; // PreviewMouseDoubleClickイベント this.AssociatedObject.PreviewMouseDoubleClick += (sender, e) => { if (!this.CanMaxmize) { if (e.ChangedButton == MouseButton.Left) { var mousePos = Mouse.GetPosition(this.AssociatedObject); var win = this.AssociatedObject as MetroWindow; if ((win != null) && (mousePos.Y <= win.TitlebarHeight) && (0 < win.TitlebarHeight)) e.Handled = true; } } }; } /// <summary>Behaviorが依存コントロールから取り外された場合に呼び出されます。</summary> protected override void OnDetaching() { var source = (HwndSource)HwndSource.FromVisual(this.AssociatedObject); source.RemoveHook(this.WndProc); this.AssociatedObject.SourceInitialized -= this.onSourceInitialized; base.OnDetaching(); } /// <summary>WindowのSourceInitializedイベントハンドラ。</summary> /// <param name="sender">イベントのソース。</param> /// <param name="e">イベントデータを格納しているEventArgs。</param> private void onSourceInitialized(object sender, EventArgs e) { var source = (HwndSource)HwndSource.FromVisual(this.AssociatedObject); source.AddHook(this.WndProc); this.applyProperties(); } /// <summary>RequestClose依存プロパティの変更イベントハンドラ。</summary> /// <param name="d">このBehaviorを表すDependencyObject。</param> /// <param name="e">変更されたプロパティを表すDependencyPropertyChangedEventArgs。</param> private static void onRequestClose(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as WindowControlBehavior)?.closeWindow(e); /// <summary>WindowをCloseします。</summary> /// <param name="e"></param> private void closeWindow(DependencyPropertyChangedEventArgs e) { if ((bool)e.NewValue) this.AssociatedObject?.Close(); } /// <summary>閉じる・最小化ボタン等の共通値変更イベントハンドラ。</summary> /// <param name="d">このBehaviorを表すDependencyObject。</param> /// <param name="e">変更されたプロパティを表すDependencyPropertyChangedEventArgs。</param> private static void onPropertiesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) => (d as WindowControlBehavior)?.applyProperties(); /// <summary>プロパティを適用します。</summary> private void applyProperties() { if (this.AssociatedObject == null) return; var hWnd = new WindowInteropHelper(this.AssociatedObject).Handle; MetroWindowStyle styleFlag = 0; if (this.SystemMenuVisible) styleFlag |= MetroWindowStyle.ShowSystemMenu; if (this.CanMaxmize) styleFlag |= MetroWindowStyle.CanMaxmize; if (this.CanMinimize) styleFlag |= MetroWindowStyle.CanMinimize; WindowApis.ChangeWindowStyle(hWnd, styleFlag); } /// <summary>Windowメッセージフックプロシージャを表します。</summary> /// <param name="hWnd">ウィンドウハンドルを表すIntPtr。</param> /// <param name="msg">ウィンドウメッセージを表すint。</param> /// <param name="wParam">パラメータを表すIntPtr。</param> /// <param name="lParam">パラメータを表すIntPtr。</param> /// <param name="handled">Windowメッセージ処理結果を返すbool。</param> /// <returns>Windowメッセージの処理結果を表すIntPtr。</returns> private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WindowsApiConstants.WM_INITMENU) { // 閉じるメニュー WindowApis.ChangeSystemMenuItemEnabled(hWnd, SystemMenuItem.CloseItem, this.CanClose); // 最大化メニュー WindowApis.ChangeSystemMenuItemEnabled(hWnd, SystemMenuItem.MaxmizeItem, this.CanMaxmize); // 最小化メニュー WindowApis.ChangeSystemMenuItemEnabled(hWnd, SystemMenuItem.MinimizeItem, this.CanMinimize); // 移動メニュー WindowApis.ChangeSystemMenuItemEnabled(hWnd, SystemMenuItem.MoveItem, this.CanMove); } return IntPtr.Zero; } } } |
Behavior は添付対象の依存オブジェクトを型パラメータに指定する必要があるので、この Behavior では Window を依存オブジェクトに指定していますが、基本的に MetroWindow を対象に作成したので WPF 標準の Window に添付した場合の動作までは確かめていません。
Behavior のインタフェース
src. 2 では『~ 略 ~』で省略していますが、以下のようなインタフェースを用意しています。
インタフェース | 内容 |
---|---|
CanClose | false を設定している間は Alt + F4 等でも Window の Close を中止するプロパティ。又、システムメニューの『閉じる』項目もグレーアウトします。 true を設定している間に Window Close 操作を行うと Closeされますが、false のままでは Window が Close できません。 |
CanMaxmize | false を設定している間はタイトルバーをダブルクリックしても最大化を中止するプロパティ。又、システムメニューの『最大化』項目もグレーアウトします。 true を設定している間は最大化されますし、システムメニューも有効になります。 |
CanMinimize | false を設定している間はタスクバーのアイコンをクリックしても最小化を中止するプロパティ。又、システムメニューの『最小化』項目もグレーアウトします。 true を設定している間は最小化されますし、システムメニューも有効になります。 |
CanMove | システムメニューの『移動』項目の有効 / 無効を切り替えるプロパティ。 |
SystemMenuVisible | タスクバーアイコンの右クリックからシステムメニューを表示する / しないを切り替えるプロパティ。 |
RequestClose | true を設定すると Window を閉じるプロパティ。 このプロパティに true を設定する前に CanClose プロパティが true に設定されている必要があります。 |
CloseCanceledCallback | Window を Close しようとした場合に呼び出されるこの Behavior 唯一のコマンド。 このコマンドにバインドした処理の中で CanClose プロパティに false をセットすると Window の Close を中止することができます。 |
src. 2 では一応見本を兼ねて CanClose の定義のみ残してありますが、省略したプロパティの定義はほとんど同じです。依存プロパティの定義自体はかなり長いですが、『propdp』スニペットを使用すれば、fig. 7 のように簡単に入力できます。
『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 の可否を問い合わせています。
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 クラスです。
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 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 | using System; using System.Runtime.InteropServices; namespace HalationGhost.Win32Api { /// <summary>Win32API定数を表します。</summary> internal class ApiConstants { //--- GetWindowLong internal const int GWL_STYLE = -16; //--- Window Style internal const int WS_MAXIMIZEBOX = 0x10000; internal const int WS_MINIMIZEBOX = 0x20000; internal const int WS_SYSMENU = 0x80000; internal const UInt32 SC_SIZE = 0xF000; internal const UInt32 SC_MOVE = 0xF010; internal const UInt32 SC_MINIMIZE = 0xF020; internal const UInt32 SC_MAXIMIZE = 0xF030; internal const UInt32 SC_NEXTWINDOW = 0xF040; internal const UInt32 SC_PREVWINDOW = 0xF050; internal const UInt32 SC_CLOSE = 0xF060; //--- Virtual Keyboard internal const int VK_F4 = 0x73; // メニューフラグ internal const uint MF_BYCOMMAND = 0x00000000; internal const uint MF_GRAYED = 0x00000001; internal const uint MF_ENABLED = 0x00000000; } /// <summary>Win32APIラッパーを表します。</summary> public static class WindowApis { [DllImport("user32")] private static extern int GetWindowLong(IntPtr hWnd, int nIndex); [DllImport("user32")] private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwLong); [DllImport("user32.dll")] private static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); [DllImport("user32.dll")] private static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); /// <summary>WindowのStyleを変更します。</summary> /// <param name="hWnd">ウィンドウハンドルを表すIntPtr。</param> /// <param name="styleFlag">Windowに設定するStyleを表すMetroWindowStyle列挙型フラグ。</param> public static void ChangeWindowStyle(IntPtr hWnd, MetroWindowStyle styleFlag) { var style = WindowApis.GetWindowLong(hWnd, ApiConstants.GWL_STYLE); if ((styleFlag & MetroWindowStyle.CanMaxmize) == MetroWindowStyle.CanMaxmize) style |= ApiConstants.WS_MAXIMIZEBOX; else style &= ~ApiConstants.WS_MAXIMIZEBOX; if ((styleFlag & MetroWindowStyle.CanMinimize) == MetroWindowStyle.CanMinimize) style |= ApiConstants.WS_MINIMIZEBOX; else style &= ~ApiConstants.WS_MINIMIZEBOX; if ((styleFlag & MetroWindowStyle.ShowSystemMenu) == MetroWindowStyle.ShowSystemMenu) style |= ApiConstants.WS_SYSMENU; else style &= ~ApiConstants.WS_SYSMENU; WindowApis.SetWindowLong(hWnd, ApiConstants.GWL_STYLE, style); } /// <summary>システムメニュー項目の有効/無効を設定します。</summary> /// <param name="hWnd">対象のウィンドウハンドルを表すIntPtr。</param> /// <param name="menuItem">システムメニュー項目を表すSystemMenuItem列挙型の内の1つ</param> /// <param name="isEnabled">設定する有効状態を表すbool。</param> public static void ChangeSystemMenuItemEnabled(IntPtr hWnd, SystemMenuItem menuItem, bool isEnabled) { var hMenu = WindowApis.GetSystemMenu(hWnd, false); if (hMenu == IntPtr.Zero) return; var enabled = isEnabled ? ApiConstants.MF_BYCOMMAND | ApiConstants.MF_ENABLED : ApiConstants.MF_BYCOMMAND | ApiConstants.MF_GRAYED; WindowApis.EnableMenuItem(hMenu, (uint)menuItem, enabled); } } } |
Window の最大化・最小化の可 / 不可と、システムメニューの表示 / 非表示は WindowStyle を変更することで可能なので、ChangeWindowStyle メソッドで GetWindowLong API で Window の Style を取得して、SetWindowLong API で Style を設定しています。
又、システムメニュー項目の有効 / 無効は GetSystemMenu API でシステムメニューハンドルを取得して、EnableMenuItem API で有効 / 無効を設定しています。
そしてこの Behavior を MainWindow に添付して実行すると fig. 8 のようになります。
fig. 6 では操作できていたシステム関連の動作が fig. 8 では無効になっているのが分かると思います。
補足 fig. 8 では Window Closing イベントでのメッセージボックス表示を一時的にコメントアウトしています。
この WindowControlBehavior を使用するには MainWindow.xaml へ src. 4 のハイライト部分を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <metro:MetroWindow x:Class="MetroWindowAndControls.MainWindow" ~ 略 ~ xmlns:vmBase="clr-namespace:HalationGhost.WinApps;assembly=HalationGhostWpfViewModels" ~ 略 ~ ShowIconOnTitleBar="{Binding ShowIconOnTitleBar.Value, Mode=OneWay}"> <bh:Interaction.Behaviors> <vmBase:WindowControlBehavior RequestClose="{Binding RequestClose.Value, Mode=OneWay}" CanClose="{Binding CanClose.Value, Mode=OneWay}" CanMaxmize="{Binding CanMaxmize.Value, Mode=OneWay}" CanMinimize="{Binding CanMinimize.Value, Mode=OneWay}" CanMove="{Binding IsWindowDraggable.Value, Mode=OneWay}" SystemMenuVisible="{Binding ShowSystemMenuOnRightClick.Value, Mode=OneWay}" CloseCanceledCallback="{Binding CloseCancel, Mode=OneWay}"/> </bh:Interaction.Behaviors> ~ 略 ~ </metro:MetroWindow> |
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 のプロパティに直接バインドすることもできます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <metro:MetroWindow x:Class="MetroWindowAndControls.MainWindow" ~ 略 ~ xmlns:vmBase="clr-namespace:HalationGhost.WinApps;assembly=HalationGhostWpfViewModels" ~ 略 ~ Name="AppMainWindow" ~ 略 ~ ShowIconOnTitleBar="{Binding ShowIconOnTitleBar.Value, Mode=OneWay}"> <bh:Interaction.Behaviors> <vmBase:WindowControlBehavior RequestClose="{Binding RequestClose.Value, Mode=OneWay}" CanClose="{Binding ElementName=AppMainWindow, Path=ShowCloseButton, Mode=OneWay}" CanMaxmize="{Binding CanMaxmize.Value, Mode=OneWay}" CanMinimize="{Binding CanMinimize.Value, Mode=OneWay}" CanMove="{Binding IsWindowDraggable.Value, Mode=OneWay}" SystemMenuVisible="{Binding ShowSystemMenuOnRightClick.Value, Mode=OneWay}" CloseCanceledCallback="{Binding CloseCancel, Mode=OneWay}"/> </bh:Interaction.Behaviors> ~ 略 ~ </metro:MetroWindow> |
src. 5 のように ElementName を指定すると同一 XAML 上の別コントロールのプロパティと直接バインディングすることもできるので作成するアプリの仕様に合った方を選択すれば良いと思います。
かなり長文の紹介になりましたが全ソースコードをこのエントリでは紹介できないので、必要があれば GitHub リポジトリ を見てください。
タイトルバーにコントロールを追加する WindowCommands
MetroWindow には LeftWindowCommands、RightWindowCommands と言う WindowCommands 型のプロパティがあり、fig. 9 のようにそれぞれタイトルバーの左側と右側にボタン等のコントロールを追加することができます。
WindowCommands は fig. 9 の通りボタンやテキストボックス(⁉)等も含める事ができ、XAML は src. 6 のように記述します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <metro:MetroWindow x:Class="MetroWindowAndControls.MainWindow" ~ 略 ~ xmlns:metro="http://metro.mahapps.com/winfx/xaml/controls" ~ 略 ~ ShowIconOnTitleBar="{Binding ShowIconOnTitleBar.Value, Mode=OneWay}"> ~ 略 ~ <metro:MetroWindow.LeftWindowCommands> <metro:WindowCommands ShowLastSeparator="False"> <Button Content="{iconPacks:FontAwesome Kind=GithubBrands, Width=24, Height=24}" Command="{Binding GoGitHub}" ToolTip="GitHubリポジトリを表示"/> <TextBox Width="100"/> </metro:WindowCommands> </metro:MetroWindow.LeftWindowCommands> <metro:MetroWindow.RightWindowCommands> <metro:WindowCommands ShowLastSeparator="False"> <Button Content="{iconPacks:FontAwesome Kind=HomeSolid, Width=24, Height=24}" Command="{Binding HomeCommand}" ToolTip="StartUp Viewを表示"/> </metro:WindowCommands> </metro:MetroWindow.RightWindowCommands> ~ 略 ~ </metro:MetroWindow> |
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 が開けないようになっています。
1 2 3 4 5 6 7 8 | private void onGoGitHub() { // .NET CoreでのURL呼び出し方法 Process.Start(new ProcessStartInfo("cmd", $"/c start https://github.com/YouseiSakusen/WpfUiGallery") { CreateNoWindow = true }); // .NET Framework時代の呼び出し方 //Process.Start("https://github.com/YouseiSakusen/WpfUiGallery"); } |
自分のサイトへ誘導する機能を組み込みたい場合もあると思うので .NET Core で開発する場合は注意が必要です。
元々は MetroWindow 以外に TextBox 等のコントロールも紹介する予定でしたが、Behavior と Win32API まで紹介することになるのは想定外で、今回もかなり長文のエントリになってしまいましたが、MetroWindow のプロパティ紹介は一旦ここまでにします。
MetroWindow の全プロパティを紹介できた訳ではなくグラフィック系やダイアログ・ Flyout 関連のプロパティが残っていますが、ダイアログや Flyout を紹介するエントリまで持ち越すことにします。
(グラフィック系のプロパティは管理人が理解できていないので今の所紹介する予定はありません)
ここで紹介したサンプルコードはいつものように GitHub リポジトリ に上げています。
次回記事「TextBox と Material Design In XAML Toolkit【case: 1-3 WPF UI Gallery】」