【テクニカル・上級編】アプリケーション設定(My.Settings)の活用:ユーザーごとのウィンドウサイズ・位置や入力履歴を自動保存・復元する実装手順 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【Windows Forms極意】My.Settingsを極限まで使い倒す:ウィンドウ状態・入力履歴の自動永続化アーキテクチャ

レガシーシステムの保守からモダンなWindows Forms(WinForms)開発まで、我々が直面する最も普遍的な課題の一つが「状態の永続化」だ。
ユーザーが前回の作業を中断した状態――ウィンドウの位置、サイズ、最大化状態、そして入力履歴――を、次の起動時に寸分たがわず復元させる。この「当たり前の体験」の裏側には、WinFormsのイベントライフサイクルと、フレームワークが隠蔽するシリアライゼーションのメカニズムに対する深い理解が必要となる。

今回は、VB.NETの隠し宝石とも言える `My.Settings` を単なるプロパティの器としてではなく、堅牢な状態管理エンジンとして極限まで活用する手法を解説する。

—

1. なぜ「ただのMy.Settings保存」では実務で破綻するのか?

多くの開発者は、`Form_Closing` イベントで `My.Settings.WindowLocation = Me.Location` と書き、`Form_Load` でそれを戻して満足する。しかし、以下のエッジケースに直面したことはないだろうか?

  • マルチモニター環境からの移行時: ユーザーが外部モニター(2画面目)でアプリを最大化終了し、翌日会社に出社してノートPC単体(1画面)で起動した際、ウィンドウが画面外(座標 (3000, 1000) など)に消え、操作不能(ゾンビプロセス化)になる。
  • DPIスケーリングの罠: 高解像度ディスプレイ(4K)と標準解像度を行き来する際、座標とサイズの変換不整合により、起動のたびにウィンドウが徐々に小さくなる。

これらを完全に克服するためには、「OSのディスプレイ構成へのフォールバック機構」と「ライフサイクル管理」をコードに組み込まなければならない。

—

2. 実装アーキテクチャ:堅牢なウィンドウ永続化クラス

まずは、デザイナ任せにせず、コード側から `My.Settings` とウィンドウのライフサイクルを完全に制御する実装パターンを示す。

ステップ 1: プロジェクトの設定(Settings.settings)の定義

Visual Studioのプロジェクトプロパティから「設定」タブを開き、以下のスコープ(User)で変数を定義する。

| 名前 | 型 | スコープ | 初期値 |
| :— | :— | :— | :— |
| `WinLeft` | Integer | ユーザー | -1 |
| `WinTop` | Integer | ユーザー | -1 |
| `WinWidth` | Integer | ユーザー | 800 |
| `WinHeight` | Integer | ユーザー | 600 |
| `WinState` | System.Windows.Forms.FormWindowState | ユーザー | Normal |
| `SearchHistory` | System.Collections.Specialized.StringCollection | ユーザー | (空) |

—

ステップ 2: フォーム側の実装コード

以下のコードをメインフォームに実装する。ここでは、画面外配置を防ぐ境界チェック(Bounds Check)のロジックを組み込んでいる点がチーフアーキテクトとしてのこだわりだ。

Public Class MainForm

Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ 1. ウィンドウ状態の復元(画面外ハングアップ対策を含む)
RestoreWindowGeometry()

‘ 2. 入力履歴(StringCollection)のコンボボックスへのバインド
LoadSearchHistory()
End Sub

Private Sub MainForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
‘ 1. ウィンドウ状態の退避
SaveWindowGeometry()

‘ 2. 入力履歴の永続化
SaveSearchHistory()

‘ 3. 設定の物理ストレージへのフラッシュ
My.Settings.Save()
End Sub

”’

”’ ウィンドウの位置とサイズを安全に復元する
”’

Private Sub RestoreWindowGeometry()
‘ 初回起動時や設定が無効な場合のフォールバック
If My.Settings.WinWidth < 200 OrElse My.Settings.WinHeight < 200 Then Exit Sub End If ' 最大化・最小化状態の復元 Me.WindowState = My.Settings.WinState ' Normal状態での位置とサイズを復元 Dim restoredBounds As New Rectangle( My.Settings.WinLeft, My.Settings.WinTop, My.Settings.WinWidth, My.Settings.WinHeight ) ' 【極意】現在のディスプレイのいずれかに、ウィンドウの少なくとも一部が含まれているか検証 Dim isVisible As Boolean = False For Each screen As Screen In Screen.AllScreens If screen.WorkingArea.IntersectsWith(restoredBounds) Then isVisible = True Exit For End If Next If isVisible Then Me.StartPosition = FormStartPosition.Manual Me.Bounds = restoredBounds Else ' 画面外に飛んでいる場合は安全なデフォルト位置へ Me.StartPosition = FormStartPosition.CenterScreen End If End Sub '''

”’ 現在のウィンドウ状態を My.Settings にシリアライズする
”’

Private Sub SaveWindowGeometry()
If Me.WindowState = FormWindowState.Normal Then
My.Settings.WinLeft = Me.RestoreBounds.X
My.Settings.WinTop = Me.RestoreBounds.Y
My.Settings.WinWidth = Me.RestoreBounds.Width
My.Settings.WinHeight = Me.RestoreBounds.Height
My.Settings.WinState = FormWindowState.Normal
Else
‘ 最大化・最小化時は RestoreBounds(元のサイズ)を保存する
My.Settings.WinLeft = Me.RestoreBounds.X
My.Settings.WinTop = Me.RestoreBounds.Y
My.Settings.WinWidth = Me.RestoreBounds.Width
My.Settings.WinHeight = Me.RestoreBounds.Height
My.Settings.WinState = Me.WindowState
End If
End Sub

Private Sub LoadSearchHistory()
cmbSearchBox.Items.Clear()
If My.Settings.SearchHistory IsNot Nothing Then
For Each item As String In My.Settings.SearchHistory
cmbSearchBox.Items.Add(item)
Next
End If
End Sub

Private Sub SaveSearchHistory()
If My.Settings.SearchHistory Is Nothing Then
My.Settings.SearchHistory = New Specialized.StringCollection()
End If

‘ 現在の入力値を履歴の先頭に追加(重複排除と最大件数制御)
Dim currentText As String = cmbSearchBox.Text.Trim()
If Not String.IsNullOrEmpty(currentText) Then
If My.Settings.SearchHistory.Contains(currentText) Then
My.Settings.SearchHistory.Remove(currentText)
End If
My.Settings.SearchHistory.Insert(0, currentText)

‘ 履歴は最大20件までとする(メモリ肥大化防止)
While My.Settings.SearchHistory.Count > 20
My.Settings.SearchHistory.RemoveAt(My.Settings.SearchHistory.Count – 1)
End While
End If
End Sub

End Class

—

3. チーフアーキテクトが警鐘を鳴らす「My.Settingsの裏側」

一見、非常にエレガントに見えるこの `My.Settings` だが、内部構造を知らずに大規模システムで乱用すると、予期せぬパフォーマンス劣化やファイル破損を引き起こす。

① 物理ファイルの保存場所とバージョン問題

`My.Settings` は、裏側で `System.Configuration.ClientSettingsStore` を使っており、最終的に以下のパスに `user.config` という名前のXMLファイルとして保存される。

> `%LOCALAPPDATA%\[会社名またはアセンブリ名]\[AppName]_[EvidenceHash]\[Version]\user.config`

ここで恐ろしいのは [Version] ディレクトリだ。アプリケーションのバージョン(例: `1.0.0.0` から `1.1.0.0`)を更新するたびに、別のディレクトリが新規作成される。
そのままでは前回のバージョンで保存した設定(ウィンドウ位置や履歴)が消滅するという現象が発生する。

これを解決するためには、アプリケーションの起動時(通常は `ApplicationStartup` イベントや Mainメソッド)に、以下のコードで前バージョンからの設定マイグレーション(引き継ぎ)を明示的に実行する必要がある。

Friend Namespace My
Partial Friend Class MyApplication
Private Sub MyApplication_Startup(sender + As Object, e As Microsoft.VisualBasic.ApplicationServices.StartupEventArgs) Handles Me.Startup
‘ バージョンアップ時に前回の設定を引き継ぐ
If My.Settings.UpgradeRequired Then
My.Settings.Upgrade()
My.Settings.UpgradeRequired = False
My.Settings.Save()
End If
End Sub
End Class
End Namespace

(※あらかじめ設定項目に Boolean型の `UpgradeRequired` を用意し、初期値を `True` に設定しておくこと)

② メモリリークとガベージコレクションの観点

`StringCollection` などのコレクションオブジェクトを `My.Settings` にバインドして頻繁に保存・書き込みを行うと、内部でシリアライゼーションのオーバーヘッドが発生し、マネージドヒープに一時オブジェクトが蓄積される。
ウィンドウのドラッグ中などにこれを呼び出すのは絶対に御法度である。保存のタイミングは必ず `FormClosing` や、ユーザーが明示的に「保存」ボタンを押したタイミングなど、非同期またはイベント駆動のライフサイクルの終端に限定すべきだ。

—

4. レガシー・VBAからの移行組へのメッセージ

Excel VBAの `SaveSetting` / `GetSetting` や、レガシーなINIファイル読み書きに慣れ親しんだエンジニアにとって、`My.Settings` はタイプセーフであり、インテリセンスの恩恵を受けられる最高の進化形に見えるだろう。

しかし、フレームワークがすべてを隠蔽してくれるからこそ、その裏で何が行われているか(XMLのシリアライズ、ユーザープロファイルディレクトリへのI/O、バージョンごとのストア分離)をエンジニアが把握していなければならない。

堅牢なアプリケーションとは、ユーザーがどんな過酷な操作(マルチモニタの抜き差し、強制終了、バージョンアップ)を行っても、何食わぬ顔で前回の状態を再現できるもののことだ。
今日のコードをあなたのソリューションに組み込み、ワンランク上の信頼性を勝ち取ってほしい。

タイトルとURLをコピーしました