Windows Formsの呪縛を解く:My.Settingsとカスタムシリアライズで実現する「真のウィンドウ状態完全復元」
Windows Forms(WinForms)による業務アプリケーション開発において、ユーザーの利便性を左右する隠れた重要ファクターをご存知だろうか。それは「前回終了時のウィンドウ位置、サイズ、そして複雑なグリッドのレイアウトが、再起動後も寸分たがわず復元されること」だ。
「なんだ、`Form.WindowState`や`My.Settings`を使えば数行で終わる話じゃないか」と思ったならば、あなたは現場で数々のユーザーからの「マルチモニター環境で起動したら画面外にウィンドウが消えて二度と操作できなくなった」「グリッドの列幅をカスタマイズしたのにリセットされる」というクレームの修羅場をくぐり抜けていない証拠だ。
素朴な`My.Settings`の自動保存機能は、複雑なオブジェクトや動的なUIレイアウトの前には無力である。今回は、VB.NETの型安全性を保ちながら、カスタムクラスをシリアライズして永続化し、プロ仕様の堅牢なUI状態復元メカニズムを実装する極意を伝授しよう。
—
1. なぜ「素朴な My.Settings」では実務に耐えないのか?
`My.Settings`は非常に強力だが、デフォルトの機能のままプリミティブ型(String, Integer, Booleanなど)以外を扱おうとすると、以下の致命的な壁に突き当たる。
1. シリアライズの暗黙的失敗: 複雑なオブジェクトやコレクションをそのまま設定にバインドしようとすると、シリアライズ例外がバックグラウンドで握りつぶされたり、設定ファイル(`user.config`)が破損してアプリケーションが起動しなくなる。
2. マルチモニター問題の欠落: 単に`Left`と`Top`を保存・復元するだけでは、前回の作業時に接続していた外部モニターが取り外された環境で起動した際、ウィンドウが画面外(座標(2000, 1000)など)に描画され、ユーザーが手出しできなくなる。
3. DataGridView等の列状態の放置: グリッドの「列の並び順(DisplayIndex)」「幅(Width)」「表示/非表示(Visible)」は、ユーザーごとの業務効率に直結するにもかかわらず、標準のプロパティには存在しない。
これらを解決するには、「状態を保持する専用のDTO(Data Transfer Object)クラスを設計し、XMLシリアライズまたはJSONで安全に`My.Settings`(文字列型)へマッピングする」という設計アプローチが不可欠である。
—
2. アーキテクチャ設計:堅牢なUI状態管理の全体像
今回構築するアーキテクチャの要件は以下の通り。
- Serializableな設定用DTOクラス: ウィンドウの位置・サイズだけでなく、DataGridViewの列レイアウト(列名、幅、表示順)を保持する。
- 安全なデシリアライズ(フォールバック機構付き): 設定ファイルが破損している、あるいはモニター環境が変化している場合は、安全なデフォルト値にフォールバックする。
- ライフサイクルに合わせた正確なフック: フォームの`Load`イベントで復元し、`FormClosing`イベントで永続化する。
—
3. プロダクションコード実装
実際のプロジェクトにそのまま組み込める、洗練されたVB.NETコードを提示する。
ステップ 1: ウィンドウとグリッドの状態を保持するDTOクラス
まずは、永続化対象となるデータをカプセル化したクラスを作成する。シリアライズを確実に行うため、`
Option Strict On
Option Explicit On
Imports System.IO
Imports System.Xml.Serialization
Public Class UserWindowSettings
‘ ウィンドウ基本情報
Public Property Left As Integer = 100
Public Property Top As Integer = 100
Public Property Width As Integer = 800
Public Property Height As Integer = 600
Public Property WindowState As FormWindowState = FormWindowState.Normal
‘ DataGridViewの列設定リスト
Public Property ColumnSettings As List(Of ColumnSetting) = New List(Of ColumnSetting)()
Public Class ColumnSetting
Public Property Name As String = String.Empty
Public Property Width As Integer = 100
Public Property DisplayIndex As Integer = 0
Public Property Visible As Boolean = True
End Class
”’
オブジェクトを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 UserWindowSettings
If String.IsNullOrEmpty(xml) Then Return New UserWindowSettings()
Try
Dim serializer As New XmlSerializer(GetType(UserWindowSettings))
Using sr As New StringReader(xml)
Dim result = CType(serializer.Deserialize(sr), UserWindowSettings)
Return If(result, New UserWindowSettings())
End Using
Catch ex As Exception
‘ ログ出力基盤へ接続すべきだが、ここでは安全にデフォルトを返す
System.Diagnostics.Debug.WriteLine($”設定の復元に失敗しました: {ex.Message}”)
Return New UserWindowSettings()
End Try
End Function
End Class
ステップ 2: フォーム側でのライフサイクル制御と安全な座標検証
次に、メインフォーム側に復元と保存のロジックを実装する。ここで最も重要なのが、「復元した座標が現在の有効なディスプレイ領域内に収まっているか」を検証するバリデーション(Screen.FromRectangle)である。
Option Strict On
Option Explicit On
Public Class MainForm
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ 1. 設定の読み込みと復元
LoadApplicationSettings()
End Sub
Private Sub MainForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
‘ 2. 状態の保存
SaveApplicationSettings()
End Sub
”’
”’
Private Sub LoadApplicationSettings()
‘ My.Settings.MainFormLayout には String型 でXMLが保存されている想定
Dim xmlSettings As String = My.Settings.MainFormLayout
Dim settings = UserWindowSettings.FromXmlString(xmlSettings)
‘ ウィンドウステートの適用(Minimizedでの起動を防ぐためのガード)
If settings.WindowState = FormWindowState.Minimized Then
Me.WindowState = FormWindowState.Normal
Else
Me.WindowState = settings.WindowState
End If
‘ 通常時のみ位置とサイズを復元
If Me.WindowState = FormWindowState.Normal Then
Dim restoreBounds As New Rectangle(settings.Left, settings.Top, settings.Width, settings.Height)
‘ 【重要】マルチモニター環境や解像度変更に対応:画面内にウィンドウが存在するか検証
If IsVisibleOnAnyScreen(restoreBounds) Then
Me.StartPosition = FormStartPosition.Manual
Me.Location = restoreBounds.Location
Me.Size = restoreBounds.Size
Else
‘ 画面外に出てしまっている場合はデフォルト位置へ
Me.StartPosition = FormStartPosition.WindowsDefaultLocation
End If
End If
‘ DataGridViewの列レイアウト復元
RestoreGridColumns(settings.ColumnSettings)
End Sub
”’
”’
Private Sub SaveApplicationSettings()
Dim settings As New UserWindowSettings()
‘ 最小化・最大化状態にかかわらず、復元用のNormal位置を保持する工夫
If Me.WindowState = FormWindowState.Normal Then
settings.Left = Me.Left
settings.Top = Me.Top
settings.Width = Me.Width
settings.Height = Me.Height
Else
‘ 最大化時はRestoreBoundsを保存する
settings.Left = Me.RestoreBounds.Left
settings.Top = Me.RestoreBounds.Top
settings.Width = Me.RestoreBounds.Width
settings.Height = Me.RestoreBounds.Height
End If
settings.WindowState = Me.WindowState
‘ DataGridViewの列状態をキャプチャ
settings.ColumnSettings = CaptureGridColumns()
‘ My.Settings へ永続化
My.Settings.MainFormLayout = settings.ToXmlString()
My.Settings.Save()
End Sub
”’
”’
Private Function IsVisibleOnAnyScreen(rect As Rectangle) As Boolean
For Each scr As Screen In Screen.AllScreens
‘ WorkingArea(タスクバーを除く領域)または Bounds との交差判定
If scr.WorkingArea.IntersectsWith(rect) Then
Return True
End If
Next
Return False
End Function
”’
”’
Private Function CaptureGridColumns() As List(Of UserWindowSettings.ColumnSetting)
Dim list As New List(Of UserWindowSettings.ColumnSetting)()
‘ 例として “DataGridView1″ を対象とする
For Each col As DataGridViewColumn In DataGridView1.Columns
list.Add(New UserWindowSettings.ColumnSetting() With {
.Name = col.Name,
.Width = col.Width,
.DisplayIndex = col.DisplayIndex,
.Visible = col.Visible
})
Next
Return list
End Function
”’
”’
Private Sub RestoreGridColumns(savedColumns As List(Of UserWindowSettings.ColumnSetting))
If savedColumns Is Nothing OrElse savedColumns.Count = 0 Then Return
‘ ユーザーが列を追加・削除している可能性を考慮し、名前でマッチングさせる
For Each savedColSetting In savedColumns
Dim targetCol = DataGridView1.Columns(savedColSetting.Name)
If targetCol IsNot Nothing Then
targetCol.Width = savedColSetting.Width
targetCol.Visible = savedColSetting.Visible
‘ DisplayIndexの設定は、競合を防ぐために順序に配慮が必要な場合があるが
‘ 基本プロパティとして設定可能
Try
targetCol.DisplayIndex = savedColSetting.DisplayIndex
Catch ex As Exception
‘ 列数が動的に変わるなどの理由で例外が出た場合は無視して安全性を担保
System.Diagnostics.Debug.WriteLine($”DisplayIndexの設定に失敗しました: {targetCol.Name}”)
End Try
End If
Next
End Sub
End Class
—
4. エンジニアリングの現場における注意点とベストプラクティス
この実装を導入するにあたり、プロの視点からいくつか実務的なアドバイスを添えておく。
- `My.Settings.Save()` の濫用を避ける:
アプリケーション設定の保存はディスクへのI/Oが発生する。タイマーなどで毎秒保存するような愚を犯してはならない。保存のタイミングは「フォームが閉じるとき(FormClosing)」、あるいは「ユーザーが明示的に『設定を保存する』ボタンを押したとき」に限定すべきである。
- ユーザープロファイルの競合(ClickOnceやRoaming):
WinFormsアプリの配布にClickOnceを使用している場合、`user.config`はバージョンごとに異なるフォルダに保存される。アプリがバージョンアップするたびに設定がリセットされる仕様になるため、もしバージョン間で設定を引き継ぎたい場合は、`My.Settings.Upgrade()`をアプリケーションの初回起動時(`Sub Main`など)に呼び出すロジックを挟むことを忘れないこと。
- 例外耐性:
DTOのシリアライズ・デシリアライズ処理には、必ず`Try-Catch`を巻き、設定ファイルが破損して例外が発生しても、アプリケーションがクラッシュするのではなく、デフォルト値で何食わぬ顔して起動する「フェイルセーフ」な設計を徹底すること。これがプロフェッショナルとアマチュアを分かつ境界線である。
—
5. まとめ
今回は、単なるプリミティブな値の保存にとどまらない、カスタムクラスのシリアライズを活用した高度なウィンドウ状態およびUIレイアウトの完全復元手法を解説した。
堅牢な業務アプリケーションとは、ユーザーにストレスを与えないものである。「前回作業していた状態がそのまま再現される」という当たり前のようで実装の難しい要件をスマートにクリアすることで、ツール自体の信頼性は飛躍的に向上する。
ぜひこの設計パターンをあなたのプロジェクトに持ち帰り、ワンランク上のWindows Formsアプリケーションを構築してほしい。
