提督業プラグインを作ってみよう(4)ウィンドウ位置とサイズを保存・復元しよう
9/12追記 うっかり設定の保存・復元処理を追加し忘れていたのを修正しました。
ウィンドウ位置とサイズを保存・復元する機能自体はすでにMetroWindowに実装されています。 ただし、デフォルトの実装だと%UserProfile%\AppData\Local\ にファイルが作成されてしまうので、この連載ではプラグインdllと同じフォルダに保存するようにプログラムを改造していきます。
1.ウィンドウ位置とサイズを保存・復元するためのクラスを追加
Models\SettingsフォルダにSerializableSetting.cs、WindowSetting.csを追加します。 bool TopMostでウィンドウが最前面かどうかを、Nullable<WINDOWPLACEMENT>Placementでウィンドウ位置とサイズを保存します。
- SerializableSetting.cs
/// <summary> /// シリアライズする設定項目の基本クラス /// </summary> /// <seealso cref="System.ComponentModel.INotifyPropertyChanged" /> public abstract class SerializableSetting : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChanged([CallerMemberName]string propertyName = null) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); /// <summary> /// シリアライズするクラスを識別するための文字列(デフォルトはクラス名) /// </summary> public virtual string CategoryName => this.GetType().Name; }
- WindowSetting.cs
/// <summary> /// ウインドウ設定用クラス /// </summary> /// <seealso cref="MetroRadiance.UI.Controls.IWindowSettings" /> public class WindowSetting : SerializableSetting, IWindowSettings { /// <summary> /// ウィンドウを常に最前面に表示するかどうかを示す設定値 /// </summary> #region TopMost変更通知プロパティ private bool _TopMost; public bool TopMost { get { return _TopMost; } set { if (_TopMost == value) return; _TopMost = value; RaisePropertyChanged(nameof(TopMost)); } } #endregion /// <summary> /// ウィンドウの位置・サイズ・状態を示す設定値 /// </summary> #region Placementプロパティ private WINDOWPLACEMENT? _Placement; public WINDOWPLACEMENT? Placement { get { return _Placement; } set { if (_Placement.Equals(value)) return; _Placement = value; } } #endregion /// <summary> /// シリアライズするクラスを識別するための文字列(ウィンドウごとに一意な文字列にすること) /// </summary> public override string CategoryName { get; } public WindowSetting() : this(null) { } public WindowSetting(string key) { this.CategoryName = key ?? this.GetType().Name; } /// <summary> /// IWindowSettings.Reload /// </summary> public virtual void Reload() { } /// <summary> /// IWindowSettings.Save /// </summary> public virtual void Save() { } }
2.設定データをファイルへの読み書きするためのクラスを追加
Models\SettingsフォルダにSettingsRoot.cs、SettingsRoot.cs、SettingsHost.csを追加します。 SettingsHost.cached_instancesに入っている設定データをファイルへ保存・復元します。
- SettingPath.cs
/// <summary> /// パス、ファイル名等の定数を管理するクラス /// </summary> public static class SettingPath { /// <summary> /// このプラグインのあるフォルダを取得 /// </summary> public static string GetDllFolder() => Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName; /// <summary> /// アセンブリ名 /// </summary> public static readonly string DLLName = Assembly.GetExecutingAssembly().GetName().Name; /// <summary> /// 設定ファイル名 /// </summary> private static readonly string SettingFileName = $"{DLLName}.xml"; /// <summary> /// ローカルApplication Dataフォルダに /// \grabacr.net\KanColleViewer\Plugins\KanburaLike\アセンブリ名.xaml /// を繋げたもの(コンパイルオプションで使用可能) /// </summary> private static string LocalAppFilePath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "grabacr.net", "KanColleViewer", "Plugins", DLLName, SettingFileName); /// <summary> /// このプラグインがあるフォルダに /// \アセンブリ名.xaml /// を繋げたもの(デフォルトはこれ) /// </summary> private static string PluginPath { get; } = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location).FullName, SettingFileName); #if SETTING_LOCALAPP public static string Current { get; } = LocalAppFilePath; #else public static string Current { get; } = PluginPath; #endif }
SettingPath.Currentで設定ファイルのフルパスが取れます。
- SettingsRoot.cs
/// <summary> /// 設定用データのDictionaryをシリアライズするためのクラス /// </summary> [SerializableAttribute()] [XmlType(AnonymousType = true)] [XmlRoot(Namespace = "", IsNullable = false)] [XmlInclude(typeof(WindowSetting))] public class SettingsRoot { [XmlArrayItem(IsNullable = false)] public string[] Keys { get; set; } [XmlArrayItem(IsNullable = false)] public SerializableSetting[] Values { get; set; } }
シリアライズするクラスが増えたら、XmlIncludeに追加していきます。
- SettingsHost.cs
/// <summary> /// 設定データをまとめてセーブ、ロードするクラス /// </summary> public static class SettingsHost { /// <summary> /// 設定データのインスタンスのキャッシュ /// </summary> private static Dictionary<string, SerializableSetting> cached_instances = new Dictionary<string, SerializableSetting>(); private static readonly object _sync = new object(); /// <summary> /// 現在のcached_instancesにキャッシュされている <see cref="{T}"/> /// を取得します。 キャッシュがない場合は <see cref="create"/> に従って生成します。 /// </summary> /// <returns></returns> public static T Cache<T>(Func<string, T> create, string key) where T : SerializableSetting { if (cached_instances.TryGetValue(key, out SerializableSetting obj) && obj is T) return (T)obj; var instance = create(key); cached_instances[key] = instance; return instance; } #region Load / Save /// <summary> /// ファイルからロード /// </summary> public static void LoadFile() { try { if (File.Exists(SettingPath.Current)) { using (var stream = new FileStream(SettingPath.Current, FileMode.Open, FileAccess.Read)) { lock (_sync) { // 読み込み var serializer = new XmlSerializer(typeof(SettingsRoot)); var root = (SettingsRoot)serializer.Deserialize(stream); var dic = root.Keys.Zip(root.Values, (k, v) => new KeyValuePair<string, SerializableSetting>(k, v)) .ToDictionary(kv => kv.Key, kv => kv.Value); cached_instances = dic; } } } else { lock (_sync) { cached_instances = new Dictionary<string, SerializableSetting>(); } } } catch (Exception) { File.Delete(SettingPath.Current); cached_instances = new Dictionary<string, SerializableSetting>(); } } /// <summary> /// ファイルにセーブ /// </summary> public static void SaveFile() { try { if (cached_instances.Count == 0) return; lock (_sync) { using (var stream = new FileStream(SettingPath.Current, FileMode.Create, FileAccess.ReadWrite)) using (var writer = new StreamWriter(stream, Encoding.UTF8)) { SettingsRoot root = new SettingsRoot { Keys = cached_instances.Keys.ToArray(), Values = cached_instances.Values.ToArray() }; var serializer = new XmlSerializer(typeof(SettingsRoot)); serializer.Serialize(writer, root); writer.Flush(); } } } catch (Exception) { //例外が出た } } #endregion }
本家KanColleViewerの設定ファイル保存には Grabacr07.KanColleViewer.Models.Settings一式と MetroTrilithon.Desktop/Serialization/SerializableProperty.csと MetroTrilithon.Desktop/Serialization/FileSettingsProvider.csが使われていますが、構造体を保存できないので、この連載では独自にクラスを実装しました。
3.ViewModelから設定データを扱うクラスを追加
ViewModels\SettingsフォルダにWindowSettingViewModel.csを追加 ViewModelsフォルダにInformationWindowViewModel.csを追加
- WindowSettingViewModel.cs
public abstract class WindowSettingViewModel : Livet.ViewModel { /// <summary> /// MetoroWindowのWindowSettingsにバインドするとファイルに保存できる /// </summary> public WindowSetting Setting { get; } public WindowSettingViewModel(string key) { Setting = SettingsHost.Cache<WindowSetting>(_ => new WindowSetting(key), key); } }
- InformationWindowViewModel.cs
public class InformationWindowViewModel : WindowSettingViewModel { public InformationViewModel Info { get; } = new InformationViewModel(); public InformationWindowViewModel(string key) : base(key) { } }
4.Viewと設定データをバインディングする
IsRestoringWindowPlacementをtrueに WindowSettingsにInformationWindowViewModel.Settingをバインドすることによってウィンドウ位置とサイズを保存・復元できます。 Topmost、ピン止めボタンのIsCheckedをInformationWindowViewModel.TopMostとバインドすることでウィンドウが最前面かどうかを保存・復元できます。
艦隊情報表示ユーザーコントロールのDataContextに明示的にInformationViewModelをバインドします。
- InformationWindow.xaml
<metro:MetroWindow xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:metro="http://schemes.grabacr.net/winfx/2014/controls" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:プロジェクト名.Views" xmlns:ViewModels="clr-namespace:プロジェクト名.ViewModels" x:Class="プロジェクト名.Views.InformationWindow" mc:Ignorable="d" Background="{DynamicResource ThemeBrushKey}" Foreground="{DynamicResource ActiveForegroundBrushKey}" Title="InformationWindow" Height="400" Width="300" d:DataContext="{d:DesignInstance {x:Type ViewModels:InformationWindowViewModel}}" Topmost="{Binding Setting.TopMost, Mode=TwoWay}" WindowSettings="{Binding Setting, Mode=OneWay}" IsRestoringWindowPlacement="True"> <metro:MetroWindow.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="KanColleResource.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </metro:MetroWindow.Resources> <DockPanel> <!-- キャプションバー ここから --> <DockPanel metro:MetroWindow.IsCaptionBar="True" DockPanel.Dock="Top"> <Border DockPanel.Dock="Bottom" Height="4" /> <StackPanel Orientation="Horizontal" DockPanel.Dock="Right"> <metro:CaptionButton Style="{DynamicResource PinButtonStyleKey}" VerticalAlignment="Top" IsChecked="{Binding Setting.TopMost, Mode=TwoWay}" /> <metro:CaptionButton WindowAction="Minimize" VerticalAlignment="Top" > <Path Style="{DynamicResource MinimizeIconKey}"/> </metro:CaptionButton> </StackPanel> <TextBlock Text="艦隊情報 - プラグインテスト" HorizontalAlignment="Left" Style="{DynamicResource CaptionTextStyleKey}" Margin="2,0,8,0" /> </DockPanel> <!-- キャプションバー ここまで --> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <local:Information DataContext="{Binding Info, Mode=OneWay}"/> </ScrollViewer> </DockPanel> </metro:MetroWindow>
5.プラグイン本体を修正
メインのViewModelをInformationWindowViewModelに差し替えて、 InformationWindowViewModelの識別キーはメインのViewのクラス名にしています。
class Plugin : IPlugin, IDisposable { private Views.InformationWindow info; //メインのView private ViewModels.InformationWindowViewModel infovm; //メインのViewModel public Plugin() { } //プラグインが読み込まれたときに呼ばれる public void Initialize() { SettingsHost.LoadFile(); //設定をロード infovm = new ViewModels.InformationWindowViewModel(nameof(Views.InformationWindow)); info = new Views.InformationWindow { DataContext = infovm }; info.Show(); } public void Dispose() { //ここに終了時の処理を書く SettingsHost.SaveFile(); //設定をセーブ } }
ちょっと複雑ですが、これで設定情報をXMLファイルに保存・復元できるようになります。 保存するファイル名やパスはSettingPath.cs、XMLのRootElement名はSettingsRoot.cs、XMLのElement名はSerializableSettingを継承したクラスをカスタマイズすることで変更できます。
カスタマイズ方法は以下のサイトを参考にしてください(順不同)
現在実行しているDLLのパスを取得する。(GetExecutingAssembly ) - tekkの日記 C#,VB.NET
[C#]属性とテキストで構成されるXMLタグをデシリアライズする為のクラス設計 | Zero Configuration
XmlRootAttribute クラス (System.Xml.Serialization)