ウィンドウの位置やサイズを記憶しよう。

開発において、あたりまえに使用するウィンドウは、基本的にユーザが位置やサイズを自由に変更できる。
しかし、ユーザが変えた位置やサイズは、「実行時に」設定された値であるため、意図的に記憶しない限りは、前のまま(ビルド時に設定した値)が適用され、起動される。ユーザによっては、これを煩わしく感じるので、当然ながら、「記憶してくれ!」という要望が出てくる。
記憶する為には、
1.画面終了時に、サイズ、位置をファイルに保存する。
2.次回起動時に、1で記憶したサイズや位置を読み込んで、反映させる。
となる。仕事で使ったロジックをシンプル化して、添付プロパティで実現してみた。
○添付プロパティ実装
using System;
using System.Windows;

namespace TawamureDays {

/// <summary>
/// ウィンドウ(Window)用ビヘイビアクラス
/// </summary>
public static class WindowBehavior {

/// <summary>
/// ウィンドウ状態(位置、サイズ)を記憶するかどうかを取得します。
/// </summary>
/// <param name="obj">対象オブジェクト(Window)</param>
/// <returns>true:記憶する。</returns>
public static bool GetSaveWindowState(DependencyObject obj) {
return (bool)obj.GetValue(SaveWindowStateProperty);
}

/// <summary>
/// ウィンドウ状態(位置、サイズ)を記憶するかどうかを設定します。
/// </summary>
/// <param name="obj">対象オブジェクト(Window)</param>
/// <param name="value">true:記憶する。</param>
public static void SetSaveWindowState(DependencyObject obj, bool value) {
obj.SetValue(SaveWindowStateProperty, value);
}

/// <summary>ウィンドウ状態(位置、サイズ)を記憶するかどうか</summary>
public static readonly DependencyProperty SaveWindowStateProperty =
DependencyProperty.RegisterAttached("SaveWindowState", typeof(bool),
typeof(WindowBehavior),
new UIPropertyMetadata(false, OnSaveWindowStatePropertyChanged));

/// <summary>
/// SaveWindowStateプロパティ値変更イベントハンドラ
/// </summary>
/// <param name="dpObj">イベント発生元</param>
/// <param name="e">イベント引数</param>
private static void OnSaveWindowStatePropertyChanged(
DependencyObject dpObj, DependencyPropertyChangedEventArgs e) {
//...
}
}
}

デフォルトをfalseにして、XAML上でtrueに設定させる事で、プロパティ変更値イベントが発生する。
そのイベント発生によりOnSaveWindowStatePropertyChangedメソッドが実行される。
using System;
using System.Windows;

namespace TawamureDays {

/// <summary>
/// ウィンドウ(Window)用ビヘイビアクラス
/// </summary>
public static class WindowBehavior {

/// <summary>
/// SaveWindowStateプロパティ値変更イベントハンドラ
/// </summary>
/// <param name="dpObj">イベント発生元</param>
/// <param name="e">イベント引数</param>
private static void OnSaveWindowStatePropertyChanged(
DependencyObject dpObj, DependencyPropertyChangedEventArgs e) {

var window = dpObj as Window;

if (window == null) {
return;
}

if ((bool)e.NewValue) {

if (!window.IsLoaded) {
window.Loaded += new RoutedEventHandler(SaveStateWindow_Loaded);
} else {
//Load済
WindowBehavior.SaveStateWindow_Loaded(window, new RoutedEventArgs());
}
}

return;
}
}
}

Loadedイベントにメソッドを登録する。登録したメソッド内で処理を行う。
なお、この実装は、一回だけtrueに変更される事を想定している。動的な切り替えは、本稿では考慮しない。

○Load時の処理
using System;
using System.Windows;

namespace TawamureDays {

/// <summary>
/// ウィンドウ(Window)用ビヘイビアクラス
/// </summary>
public static class WindowBehavior {

/// <summary>
/// Loadedイベントハンドラ
/// </summary>
/// <param name="sender">イベント発生元</param>
/// <param name="e">イベント引数</param>
private static void SaveStateWindow_Loaded(object sender, RoutedEventArgs e) {
//ロード時にファイルを検索・設定読み込み
//終了時にファイルへ設定を書き込み

var window = sender as Window;
//イベントハンドラを削除します。
window.Loaded -= new RoutedEventHandler(SaveStateWindow_Loaded);
//終了時イベントにハンドラを登録します。
window.Closed += new EventHandler(SaveStateWindow_Closed);

var settingPath = window.ResolveSetringPath();

if (System.IO.File.Exists(settingPath)) {
//ロードします。
var setting = System.IO.File.ReadAllText(settingPath);

if (!string.IsNullOrWhiteSpace(setting)) {
//カンマ区切りで分割
var settingList = setting.Split(new []{','});

try {
window.Left = double.Parse(settingList[0]);
window.Top = double.Parse(settingList[1]);
window.Width = double.Parse(settingList[2]);
window.Height = double.Parse(settingList[3]);

var state = (WindowState)Enum.Parse(typeof(WindowState), settingList[4]);

if (state == WindowState.Maximized) {
//最大化の時のみ反映させよう。
window.WindowState = WindowState.Maximized;
}

} catch (Exception) {
//反映失敗(でも黙殺)
}

}
}

return;
}
}
}
・Loadedの実行が1回になるようにハンドラを削除する。
・設定ファイルが存在すれば、設定を読み込む。ここで発生するエラーは無視する。
(ロギング機能があれば、警告かエラーでファイルに出力しておく方が良い)
・上記コードでは、カンマ区切りとみなして処理しているけど、これは保存時の処理に合わせる必要がある。

○終了時の処理
using System;
using System.Windows;

namespace TawamureDays {

/// <summary>
/// ウィンドウ(Window)用ビヘイビアクラス
/// </summary>
public static class WindowBehavior {

/// <summary>
/// ウィンドウ終了イベントハンドラ
/// </summary>
/// <param name="sender">イベント発生元</param>
/// <param name="e">イベント引数</param>
private static void SaveStateWindow_Closed(object sender, EventArgs e) {
var window = sender as Window;
//イベントハンドラを削除します。
window.Closed -= new EventHandler(SaveStateWindow_Closed);
//保存先
var settingPath = window.ResolveSetringPath();
const string FORMAT = "#0";
var minWidth = 30D;//デフォルト値
var minHeight = 30D;//デフォルト値

if (!double.IsNaN(window.MinWidth)) {
minWidth = window.MinWidth;
}

if (!double.IsNaN(window.MinHeight)) {
minHeight = window.MinHeight;
}

var settingList = new []{
Math.Max(0D, window.Left).ToString(FORMAT),//0:左座標
Math.Max(0D, window.Top).ToString(FORMAT),//1:上座標
Math.Max(minWidth, window.Width).ToString(FORMAT),//2:幅
Math.Max(minHeight, window.Height).ToString(FORMAT),//3:高さ
window.WindowState.ToString()//4:状態
};

//保存します。(カンマ区切りで)
var fileInfo = new System.IO.FileInfo(settingPath);

//ディレクトリなければ作る。
if (!fileInfo.Directory.Exists) {
fileInfo.Directory.Create();
}

System.IO.File.WriteAllText(settingPath,
Utils.Concat(settingList, ","));
return;
}
}
}

・終了時に、座標、サイズをカンマ区切りでファイルに出力させている。
・Utils.Concatメソッドは、前に書いた記事のコードを流用している。
文字列の連結とか。
ここで肝要なのが、保存先と読込元のファイルをウィンドウ毎に保持すること。
ファイルパスを決めるメソッド(ResolveSetringPath)は↓のような感じ。
using System;
using System.Windows;

namespace TawamureDays {

/// <summary>
/// ウィンドウ(Window)用ビヘイビアクラス
/// </summary>
public static class WindowBehavior {

/// <summary>
/// 指定のウィンドウに関する情報を保存するための設定ファイルパスを取得します。
/// thisをつけて、拡張メソッド風に使えるようにしています。
/// </summary>
/// <param name="window">ウィンドウ</param>
/// <returns>設定ファイルパス</returns>
private static string ResolveSetringPath(this Window window) {
//アプリケーション用フォルダ
var folder = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
//エントリポイントを持つアセンブリ(大抵*.exe)の名前
var appName = System.Reflection.Assembly.GetEntryAssembly().GetName();
//ファイル区切り文字
var separator = System.IO.Path.DirectorySeparatorChar.ToString();

return string.Concat(new []{
folder,
separator,
appName.Name,
separator,
window.GetType().Name,
".config"
});
}
}
}
アプリ用共通リポジトリ用フォルダ\exe名をフォルダパス
Windowのクラス名+".config"をファイル名にしている。
Window毎にファイルを持つ事で、不具合、トラブル等でファイルが壊れても、被害の範囲を最小限にする為。
以前の仕事で、同一アプリ内の全ての設定を1つのファイルで管理しようという機能があったんだけど、壊れたが最後、全画面において設定がリセットされるという動きになり、そのとき、非常に不評であった…。
アプリ用共通リポジトリ用フォルダは、Windows7の場合、
C:\Users\{ユーザ名}\AppData\Roaming
になる。AppDataは隠しフォルダなので、フォルダオプションの設定変更で見えるようになる。
XAML上で使うときは、↓のような感じ

xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:local="clr-namespace:TawamureDays"
Title="MainWindow" Height="213" Width="385"
local:WindowBehavior.UseMessageCommand="True"
local:FrameworkElementBehavior.OnLoadedCommand="{Binding LoadedCommand}"
local:WindowBehavior.SaveWindowState="True"
>
...
開発者各々が実装する必要がないのがメリットかな。
今回は保存形式をカンマ区切りにしたけど、別にXAMLでもJSONでも大丈夫。でも、その場合だと、クラスなんかを用意する必要がある。速度的には、カンマ区切りが速いと思っているので、速度優先ってことで。
スポンサーサイト
当サイトは基本をすっ飛ばしてます。基本文法等は、@ITをどうぞ
カテゴリー: WPF4 | コメント: 0 | トラックバック: 0


この記事へのコメント

コメントの投稿

非公開コメント


サイドバー背後固定表示サンプル

当ブログに書かれたソースコードは流用自由です。

バグ、スペルミス等はありうる事です。

ご利用の際は自己責任でお願いしますm(_ _)m