マルチディスプレイ地獄からの脱却:ウィンドウ座標記憶と「画面外消失」を完全に防ぐ堅牢なUI設計
こんにちは。業務システム開発の現場で、数々の「ユーザーからの理不尽なクレーム」をアーキテクチャの力でねじ伏せてきたチーフアーキテクトだ。
Windows Forms(VB.NET)を使った業務アプリケーション開発において、ウィンドウの位置やサイズを保存・復元する要件はもはや標準装備と言っていい。だが、この実装を舐めてかかると痛い目を見る。
「昨日までトリプルモニターの右端で使っていたアプリを、今日はノートPC単体で起動したら、ウィンドウが画面の外へ完全にはみ出して二度と操作できなくなった」
……この問い合わせ、何度対応したことか。ユーザーはパニックになり、タスクマネージャーで強制終了する羽目になる。あるいは、会社支給のデュアルモニター環境から、出先でシングルモニターに変えた瞬間にアプリが消滅する。
今回は、複数モニター環境(マルチディスプレイ)の座標系の罠を完全に理解し、いかなるディスプレイ構成の変更であっても絶対に画面外へ飛ばない、プロダクションクオリティの座標補正ロジックを伝授する。
—
なぜ従来の「単純な座標復元」は破綻するのか?
多くの初学者が書くコードはこうだ。
.net
‘ 【悪手】単に保存された座標をそのまま適用する例
Me.Location = New Point(My.Settings.WindowLeft, My.Settings.WindowTop)
Me.Size = New Size(My.Settings.WindowWidth, My.Settings.WindowHeight)
これがなぜ非効率かつ危険なのか?理由は明確だ。
Windowsの座標系は、プライマリモニターの左上を `(0, 0)` とし、セカンダリモニターなどが左側や上側にある場合はマイナス座標を取り得る。さらに、ユーザーがディスプレイを取り外したり、解像度を変更したりすると、保存された `(Left, Top)` は「存在しない空間」を指し示すことになる。
結果として、ウィンドウはレンダリングされているものの、人間の目に見えない領域(画面外)に描画され、ユーザーは手出しできなくなるのだ。
堅牢なUI設計の原則
1. 「画面外判定」を起動時に必ず行うこと:保存された座標が、現在有効な「すべてのディスプレイの作業領域(WorkingArea)の合算領域」に含まれているかを検証する。
2. 含まれていない場合は安全なデフォルト位置へフォールバックする:プライマリモニターの中心等へ強制送還する。
—
実装:マルチディスプレイ完全対応クラス
ここからは、実務でそのままコピー&ペーストして使える、堅牢なウィンドウ状態管理の決定版コードを公開する。
フォームの `Load` イベントおよび `FormClosing` イベントで呼び出すだけで、完璧なライフサイクル管理を実現できる。
.net
Imports System.Windows.Forms
Public Class WindowManager
”’
”’
”’ 対象のフォーム
Public Shared Sub RestoreWindowPosition(frm As Form)
‘ ユーザー設定から前回の状態を読み込み(なければデフォルト値)
Dim savedLeft As Integer = My.Settings.WindowLeft
Dim savedTop As Integer = My.Settings.WindowTop
Dim savedWidth As Integer = My.Settings.WindowWidth
Dim savedHeight As Integer = My.Settings.WindowHeight
Dim savedState As FormWindowState = CType(My.Settings.WindowState, FormWindowState)
‘ 初回起動時(設定値が初期値のままで無効な場合など)のハンドリング
If savedWidth < 100 OrElse savedHeight < 100 Then
frm.StartPosition = FormStartPosition.CenterScreen
Return
End If
' 一旦サイズを復元
frm.Width = savedWidth
frm.Height = savedHeight
frm.WindowState = savedState
' 最大化・最小化時は位置の厳密な計算が不要なためそのまま適用
If savedState <> FormWindowState.Normal Then
frm.Location = New Point(savedLeft, savedTop)
Return
End If
‘ 復元しようとしているウィンドウの矩形(Rectangle)
Dim restorationRect As New Rectangle(savedLeft, savedTop, savedWidth, savedHeight)
‘ 【最重要】現在接続されている全てのディスプレイの領域を走査し、
‘ ウィンドウが「少なくとも一部でも」画面内に存在するかチェックする
Dim isVisible As Boolean = False
For Each screen As Screen In Screen.AllScreens
‘ WorkingAreaはタスクバーを除いた領域。Boundsを使う場合はタスクバー考慮が必要
If screen.WorkingArea.IntersectsWith(restorationRect) Then
isVisible = True
Exit For
End If
Next
If isVisible Then
‘ 画面内に収まることが確認できたので安全に適用
frm.Location = New Point(savedLeft, savedTop)
Else
‘ 【安全装置】画面外に消滅していると判定された場合、プライマリ画面の中央に強制配置
frm.StartPosition = FormStartPosition.CenterScreen
‘ またはプライマリのWorkingAreaの左上に安全に配置
‘ frm.Location = Screen.PrimaryScreen.WorkingArea.Location
End If
End Sub
”’
”’
”’ 対象のフォーム
SaveWindowPosition(frm As Form)
‘ 最大化・最小化されている場合は、その状態の座標を保存しない(元のNormal時の位置を保持するため)
If frm.WindowState = FormWindowState.Normal Then
My.Settings.WindowLeft = frm.Location.X
My.Settings.WindowTop = frm.Location.Y
My.Settings.WindowWidth = frm.Width
My.Settings.WindowHeight = frm.Height
My.Settings.WindowState = FormWindowState.Normal
Else
‘ 最大化・最小化時はWindowStateのみ保存し、Boundsは前回のNormal時のものを維持
My.Settings.WindowState = frm.WindowState
End If
‘ 設定を永続化ファイルへ書き出し
My.Settings.Save()
End Sub
End Class
—
フォーム側の実装:これ以上なくシンプルに
上記の `WindowManager` を使えば、実際のフォーム側のコードは驚くほどすっきりと、そして堅牢になる。
.net
Public Class MainForm
Private Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load
‘ 起動時に座標を安全に復元
WindowManager.RestoreWindowPosition(Me)
End Sub
Private Sub MainForm_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing
‘ 終了時に座標を確実に保存
WindowManager.SaveWindowPosition(Me)
End Sub
End Class
—
アーキテクトからのワンポイント・アドバイス
1. `Screen.WorkingArea` と `Screen.Bounds` の使い分け
今回のコードではタスクバーやサイドバーを除外した領域である `WorkingArea` を使用している。これにより、ウィンドウがタスクバーの下に潜り込んで操作できなくなる現象も同時に防ぐことができる。全画面表示(キオスク端末など)を作る場合のみ `Bounds` を検討してほしい。
2. 設定ファイルの永続化 (`My.Settings`) の罠
VB.NETの `My.Settings` は非常に便利だが、アプリケーションのアップデート時(ClickOnceやインストーラーによるバージョンアップ)に設定がリセットされる、あるいは古いスキーマが残るトラブルが起きやすい。
業務アプリで堅牢性を担保するため、重要な設定値は必要に応じてローカルのJSONやSQLite、あるいはレジストリへ退避する設計も視野に入れておくと、現場でのトラブルシューティングが劇的に楽になる。
—
まとめ
マルチディスプレイ対応は、細部へのこだわりがプロとアマを分けるポイントだ。「動けばいいや」で作ったコードは、必ず顧客の環境変更時に牙を向く。
今回紹介した `IntersectsWith` による交差判定ロジックを取り入れれば、ユーザーがどれだけディスプレイを抜き差ししようとも、アプリが画面外へ蒸発することは二度となくなる。
さあ、今すぐ既存のプロジェクトのウィンドウ復元処理を見直し、ワンランク上の堅牢なアプリケーションへとアップデートしてほしい。
