デスクトップ アプリで、ウィンドウの位置を保存したいという要望はちらほら来ます。
ただし、真面目に実装しようとすると、細かい挙動まで実装するのが大変面倒です。
最大化して終了したら、最大化する直前のウィンドウの位置とサイズも保存しておかなければならなかったり。
何かベスト プラクティスはないかなーと思って調べたところ、MSDN に該当するコードがあったので、やってみました。
元ネタはこちら。
http://msdn.microsoft.com/ja-jp/library/vstudio/aa972163(v=vs.90).aspx
結局 P/Invoke だった!
いかな WPF といえど所詮は Windows の民。P/Invoke の運命からは逃れられないのだ…
という冗談は置いておいて、SetWindowPlacement 関数と GetWindowPlacement 関数を使います。
以下、ちょっと長いですが、そのための下準備コードです。
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 |
public class NativeMethods { [DllImport("user32.dll")] public static extern bool SetWindowPlacement( IntPtr hWnd, [In] ref WINDOWPLACEMENT lpwndpl); [DllImport("user32.dll")] public static extern bool GetWindowPlacement( IntPtr hWnd, out WINDOWPLACEMENT lpwndpl); } [Serializable] [StructLayout(LayoutKind.Sequential)] public struct WINDOWPLACEMENT { public int length; public int flags; public SW showCmd; public POINT minPosition; public POINT maxPosition; public RECT normalPosition; } [Serializable] [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X; public int Y; public POINT(int x, int y) { this.X = x; this.Y = y; } } [Serializable] [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left; public int Top; public int Right; public int Bottom; public RECT(int left, int top, int right, int bottom) { this.Left = left; this.Top = top; this.Right = right; this.Bottom = bottom; } } public enum SW { HIDE = 0, SHOWNORMAL = 1, SHOWMINIMIZED = 2, SHOWMAXIMIZED = 3, SHOWNOACTIVATE = 4, SHOW = 5, MINIMIZE = 6, SHOWMINNOACTIVE = 7, SHOWNA = 8, RESTORE = 9, SHOWDEFAULT = 10, } |
WINDOWPLACEMENT 構造体 に、ウィンドウの位置・サイズ・状態を格納します。
normalPosition が通常状態のウィンドウの位置とサイズ、showCmd がウィンドウの状態 (最小化・最大化・通常 など) です。
どこに保存すべきか?
これがまた悩ましいのですが、もっとも簡単な方法は Settings.settings を使ってしまう方法です。
MSDN のサンプルもこれを使っています。
ただ、それでは面白くないので、今回はもう少しだけ工夫して作ります。
ウィンドウ位置の保存方法を外部から設定できるようにしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public interface IWindowSettings { WINDOWPLACEMENT? Placement { get; set; } void Reload(); void Save(); } public class WindowSettings : ApplicationSettingsBase, IWindowSettings { public WindowSettings(Window window) : base(window.GetType().FullName) { } [UserScopedSetting] public WINDOWPLACEMENT? Placement { get { return this["Placement"] != null ? (WINDOWPLACEMENT?)(WINDOWPLACEMENT)this["Placement"] : null; } set { this["Placement"] = value; } } } |
ウィンドウ位置の保存と復元のためのインターフェイスとして、IWindowSettings を用意します。
ウィンドウの位置・サイズ・状態を持つ Placement プロパティのほか、保存のための Save メソッド、復元のための Reload メソッドを定義しました。
また、その既定実装として、ApplicationSettingsBase を使ったクラスも用意しました。
ApplicationSettingsBase は Settings.settings と同じく (というか、その Settings クラスは ApplicationSettingsBase を継承しています)、%UserProfile%\AppData\Local\ に設定を自動的に保存するための機構です。
アセンブリのバージョンが変わるとパスが変わるため、通常はバージョンアップ時に設定を引き継げない、等の若干の使いにくさがありますが、特に意識しなければこれで事足ります。
もし、レジストリに保存しなければならない、指定したフォルダーに保存しなければならない、など ApplicationSettingsBase の挙動では要件を満たせない場合は、IWindowSettings を実装したクラスを別途用意しましょう。
設定自動保存ウィンドウ
以上を踏まえ、ウィンドウの位置を自動的に保存・復元できるウィンドウを作ります。
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 |
public class RestorableWindow : Window { #region WindowSettings 依存関係プロパティ public IWindowSettings WindowSettings { get { return (IWindowSettings)this.GetValue(WindowSettingsProperty); } set { this.SetValue(WindowSettingsProperty, value); } } public static readonly DependencyProperty WindowSettingsProperty = DependencyProperty.Register("WindowSettings", typeof(IWindowSettings), typeof(RestorableWindow), new UIPropertyMetadata(null)); #endregion protected override void OnSourceInitialized(EventArgs e) { base.OnSourceInitialized(e); // 外部からウィンドウ設定の保存・復元クラスが与えられていない場合は、既定実装を使用する if (this.WindowSettings == null) { this.WindowSettings = new WindowSettings(this); } this.WindowSettings.Reload(); if (this.WindowSettings.Placement.HasValue) { var hwnd = new WindowInteropHelper(this).Handle; var placement = this.WindowSettings.Placement.Value; placement.length = Marshal.SizeOf(typeof(WINDOWPLACEMENT)); placement.flags = 0; placement.showCmd = (placement.showCmd == SW.SHOWMINIMIZED) ? SW.SHOWNORMAL : placement.showCmd; NativeMethods.SetWindowPlacement(hwnd, ref placement); } } protected override void OnClosing(CancelEventArgs e) { base.OnClosing(e); if (!e.Cancel) { WINDOWPLACEMENT placement; var hwnd = new WindowInteropHelper(this).Handle; NativeMethods.GetWindowPlacement(hwnd, out placement); this.WindowSettings.Placement = placement; this.WindowSettings.Save(); } } } |
WindowSettings 依存関係プロパティは、ウィンドウ位置の保存・復元手段を外部から与えるためのものです。
何も指定しなければ (= null)、ApplicationSettingsBase を使った既定実装になります。
あとは、P/Invoke 時はお馴染みの SourceInitiazlized イベント (ウィンドウ ハンドルが取れるようになるタイミング) で設定情報を復元し、SetWindowPlacement 関数でウィンドウの位置を設定します。
WINDOWPLACEMENT の length メンバーには、必ず sizeof(WINDOWPLACEMENT) が設定されている必要があります (それ以外の場合は失敗します)。flags は 0 で構いません。
showCmd には、復元した値が SW_SHOWMINIMIZED (最小化状態) であった場合、つまり前回の起動時に最小化されたまま終了した場合は、SW_SHOWNORMAL (通常状態) で表示するための処理を加えています。
同様に、ウィンドウが閉じられる直前で、かつキャンセルされていない場合に、GetWindowPlacement 関数で現在のウィンドウの状態を取得し、保存します。
ね? 簡単でしょう?
サンプル コード
上記コードを使用したサンプル コードを用意しました。
ビルドして、起動 -> サイズ&位置変更 -> 終了 を繰り返してみてください。
サイズと位置、最大化等の状態も保存・復元されます。
設定ファイルは、以下のように %UserProfile%\AppData\Local\RestorableWindowSample に保存されるはずです。
なお、この方法は、MSDN で公開されているだけでなく、モダンなデスクトップ アプリ実装のためのライブラリである MahApps.Metro や MetroRadiance でも使われています。
最大化や最小化直前のウィンドウ位置とサイズは Window.RestoreBounds プロパティでも取得できます。
しかし、SetWindowPlacement 関数に任せておけば、復元した座標ではウィンドウが完全に画面の外に出てしまうような異常ケース (モニターが切断されたときとか) でも自動調整してくれたりと、上手いことやってくれます。
それ専門の関数に任せておいた方が楽ですし、標準で確実な挙動になるでしょう。
以上、ご参考までに。