アプリケーション設定の極限:My.Settingsによるウィンドウ状態の完全復元とカスタムレイアウトの永続化
Windows Formsアプリケーションの寿命は、そのUIの持続性と忠実度によって決まる。
ユーザーが手塩にかけてカスタマイズしたグリッドの列幅、ソート順、そしてマルチモニター環境におけるウィンドウの位置と最大化状態――これらがアプリケーションの再起動によって失われるようでは、プロフェッショナルなツールとは言えない。
多くの開発者は `My.Settings` を単なる `String` や `Integer` の置き場と考えている。しかし、シリアライゼーションのメカニズムを深く理解し、Windows APIと緻密に連携させれば、`My.Settings` は堅牢な状態管理エンジンへと変貌する。
本稿では、複雑なユーザー定義クラスのシリアライズから、Windowsのドッキングステーションや解像度変更に耐えうるウィンドウ状態の完全復元まで、妥協なき実装コードと共にその極意を解説する。
—
1. なぜデフォルトの `My.Settings` では不十分なのか
`My.Settings` は内部的に `System.Configuration` インフラストラクチャを利用している。プリミティブ型や文字列であれば自動的にシリアライズ・デシリアライズされるが、業務アプリケーションで必須となる「動的な列レイアウト構造体」や「ネストされた設定オブジェクト」を扱う場合、標準機能だけでは型安全性を欠くか、あるいはビルド時のバインドエラーに悩まされることになる。
我々が目指すべきは、「型安全でありながら、変更に強く、かつバイナリまたはXMLレベルでコントロール可能な永続化レイヤー」の構築である。
—
2. ユーザー定義クラスのシリアライズ設計
複雑なUI状態(例えば、DataGridViewの列幅、表示順序、可視性)を保持するためには、シリアライズ可能な専用のデータ構造(DTO)を定義する必要がある。
ここで重要となるのが、`.NET Framework / .NET Core` におけるシリアライザの選択だ。レガシーな `BinaryFormatter` はセキュリティ上の脆弱性(CVE-2000年代以降の常識)から完全に排除すべきであり、現代においては `XmlSerializer` または `System.Text.Json`(.NETの場合は `Newtonsoft.Json` や標準JSONシリアライザ)を `My.Settings` の文字列プロパティに挟む手法が最も堅牢である。
以下に、`XmlSerializer` を用いてカスタム設定クラスを `My.Settings` に格納する堅牢なラッパーの実装を示す。
実装コード:シリアライズ可能なグリッドレイアウト構造体
Imports System.IO
Imports System.Xml.Serialization
Public Class GridColumnSetting
Public Property ColumnName As String
Public Property Width As Integer
Public Property DisplayIndex As Integer
Public Property Visible As Boolean
End Class
Public Class FormLayoutState
Public Property WindowBounds As Rectangle
Public Property WindowState As FormWindowState
Public Property Columns As List(Of GridColumnSetting) = New List(Of GridColumnSetting)()
‘ XMLシリアライズを用いた永続化文字列への変換
Public Function ToXmlString() As String
Dim serializer As New XmlSerializer(Me.GetType())
Using sw As New StringWriter()
serializer.Serialize(sw, Me)
Return sw.ToString()
End Using
End Function
‘ XML文字列からのデシリアライズ
Public Shared Function FromXmlString(xml As String) As FormLayoutState
If String.IsNullOrEmpty(xml) Then Return New FormLayoutState()
Dim serializer As New XmlSerializer(GetType(FormLayoutState))
Using sr As New StringReader(xml)
Try
Return CType(serializer.Deserialize(sr), FormLayoutState)
Catch ex As Exception
‘ 破損した設定ファイルからの防衛:デフォルトを返す
System.Diagnostics.Debug.WriteLine($”Deserialization failed: {ex.Message}”)
Return New FormLayoutState()
End Try
End Using
End Function
End Class
この `FormLayoutState` を `My.Settings` に `String` 型のプロパティ(例: `MainFormLayout`)として定義することで、スキーマの変更にも柔軟に対応できる拡張性を手に入れることができる。
—
3. ウィンドウ状態の完全復元:マルチモニターの罠とAPIの活用
ウィンドウの位置と状態を保存・復元する際、最も多いバグが「マルチモニター環境でサブディスプレイを外した状態で起動した際、ウィンドウが画面外(座標系外)に消えて二度と操作できなくなる現象」である。
これを防ぐためには、復元しようとしている矩形(Rectangle)が、現在有効なすべてのディスプレイ領域のいずれかに交差しているかを検証する `User32.dll` のAPI、あるいは `Screen.FromRectangle` メソッドを活用した境界値チェックが不可欠となる。
さらに、`FormWindowState.Minimized`(最小化)の状態でアプリケーションが終了した場合、次回起動時に最小化状態で立ち上がり、ユーザーを混乱させることがある。これも防がねばならない。
実装コード:安全かつ完璧なウィンドウ状態管理
Public Class WindowStateManager
Private ReadOnly _targetForm As Form
Private ReadOnly _settingsKey As String
Public Sub New(targetForm As Form, settingsKey As String)
_targetForm = targetForm
_settingsKey = settingsKey
‘ イベントのフック
AddHandler _targetForm.Load, AddressOf OnFormLoad
AddHandler _targetForm.FormClosing, AddressOf OnFormClosing
End Sub
Private Sub OnFormLoad(sender As Object, e As EventArgs)
Dim xmlState As String = DirectCast(My.Settings(_settingsKey), String)
Dim state As FormLayoutState = FormLayoutState.FromXmlString(xmlState)
If state.WindowBounds.IsEmpty Then Return
‘ 1. マルチモニター環境における画面外配置の防止
Dim isVisible As Boolean = Screen.AllScreens.Any(Function(s) s.WorkingArea.IntersectsWith(state.WindowBounds))
If isVisible Then
_targetForm.StartPosition = FormStartPosition.Manual
_targetForm.Bounds = state.WindowBounds
Else
‘ 画面外にいる場合は安全なデフォルト位置へフォールバック
_targetForm.StartPosition = FormStartPosition.WindowsDefaultLocation
End If
‘ 2. 最小化状態で終了していた場合の安全策(Normalとして復元)
If state.WindowState = FormWindowState.Minimized Then
_targetForm.WindowState = FormWindowState.Normal
Else
_targetForm.WindowState = state.WindowState
End If
End Sub
Private Sub OnFormClosing(sender As Object, e As FormClosingEventArgs)
Dim state As New FormLayoutState()
‘ 最小化または最大化されている場合、Normal時のBoundsを保存するために退避
If _targetForm.WindowState = FormWindowState.Normal Then
state.WindowBounds = _targetForm.Bounds
state.WindowState = _targetForm.WindowState
Else
‘ 最小化・最大化時は RestoreBounds を保存するが、
‘ まだ一度もNormalになっていない極端なケースへの配慮としてフォールバックを持たせる
state.WindowBounds = If(_targetForm.WindowState = FormWindowState.Minimized, _targetForm.RestoreBounds, _targetForm.Bounds)
state.WindowState = _targetForm.WindowState ‘ 状態自体は保持
End If
‘ ここにDataGridViewなどの状態を収集する処理を拡張可能
‘ 例: SaveGridColumns(state)
My.Settings(_settingsKey) = state.ToXmlString()
My.Settings.Save() ‘ 明示的なディスクへの書き込み
End Sub
End Class
—
4. グリッド列幅・順序の永続化と適用ロジック
実務で最も要望が多いのが、`DataGridView` のレイアウト保持だ。ユーザーが列の順序を入れ替え(`DisplayIndex`)、幅を変更し、特定の列を非表示にした設定を正確に復元させる。
先ほど定義した `FormLayoutState` にグリッドの状態を流し込むメソッドを実装する。
実装コード:DataGridViewの状態同期
Public Module DataGridViewPersistenceExtensions
‘ グリッドの状態をStateオブジェクトに書き出す
Public Sub CaptureGridState(grid As DataGridView, state As FormLayoutState)
state.Columns.Clear()
For Each col As DataGridViewColumn in grid.Columns
state.Columns.Add(New GridColumnSetting() With {
.ColumnName = col.Name,
.Width = col.Width,
.DisplayIndex = col.DisplayIndex,
.Visible = col.Visible
})
Next
End Sub
‘ Stateオブジェクトからグリッドの状態を復元する
Public Sub RestoreGridState(grid As DataGridView, state As FormLayoutState)
If state.Columns Is Nothing OrElse state.Columns.Count = 0 Then Return
‘ デザイナー側で定義された列名と設定をマッピング
For Each colSetting In state.Columns
Dim targetCol = grid.Columns(colSetting.ColumnName)
If targetCol IsNot Nothing Then
targetCol.Width = colSetting.Width
targetCol.Visible = colSetting.Visible
‘ DisplayIndexの設定は、範囲外例外を防ぐためにインデックスの順序に配慮する
‘ ただし、単純代入だと競合がおきることがあるため、例外ハンドリングまたは安全な順序適用が必要
Try
targetCol.DisplayIndex = colSetting.DisplayIndex
Catch ex As ArgumentOutOfRangeException
‘ 列数変更などのスキーマ不一致に対する防御
System.Diagnostics.Debug.WriteLine($”DisplayIndex apply failed for {colSetting.ColumnName}: {ex.Message}”)
End Try
End If
Next
End Sub
End Module
—
5. チーフアーキテクトからの提言:ライフサイクルとパフォーマンス
1. `My.Settings.Save()` の濫用を避ける
ディスク(通常はユーザープロファイルの `AppData\Local` 内のXMLファイル)へのI/O書き込みは、高頻度で行うとパフォーマンスのボトルネックになり、最悪の場合ファイルロック競合を引き起こす。設定の保存は原則として `FormClosing` イベントの1回に限定せよ。
2. ユーザープロファイルの肥大化対策
`My.Settings` はデフォルトでローカルのユーザー設定パスに蓄積される。不要になった古い設定キーや、肥大化したXML文字列を放置すると、レジストリや設定ファイルの肥大化を招く。バージョンアップ時にはマイグレーションコード(`My.Settings.Upgrade()`)の適切な呼び出しと、スキーマバージョンの管理を怠ってはならない。
3. マルチスレッド環境における注意
設定の読み書きは常にUIスレッド(主スレッド)から行われるべきである。バックグラウンドスレッドから `My.Settings` を直接操作することは、内部の構成プロバイダの競合を引き起こすため厳禁である。
結び
アプリケーションの品質は、こうした細部へのこだわり、すなわち「ユーザーのストレスをゼロにする配慮」の総和によって決まる。
今回提示したコード群は、単なるスニペットではない。レガシーからモダンまであらゆるWindows Formsシステムの現場で即座に通用する、極限まで洗練されたアーキテクチャの断片である。あなたのプロダクトに直ちに組み込み、その堅牢性を体感してほしい。
