- C# 91.2%
- JavaScript 7%
- PowerShell 1.8%
| extension | ||
| src | ||
| tests/SaveWindow.Tests | ||
| .gitignore | ||
| icon_base.png | ||
| icon_base300x300.png | ||
| README.md | ||
| RESTORE-CORE-V1-REPORT.md | ||
| SaveWindow.sln | ||
| THIRD-PARTY-NOTICES.md | ||
SaveWindow
Windows 11 向けの軽量常駐型ウィンドウ位置・サイズ復元ユーティリティです。 PersistentWindows に着想を得て設計されており、スリープ復帰時や外部ディスプレイの再接続時、および Windows 仮想デスクトップごとの独立したウィンドウ配置を確実に記憶・復元することを目指しています。
主な特徴
- 超軽量・省リソース設計:
- 不要な常時ポーリング(100ms毎のEnumWindows等)を完全排除
- 常駐時アイドル CPU 使用率 0%、プライベートメモリ 30〜60 MB を目標
- 外部依存パッケージ(SQLite、WebView2、テレメトリ等)ゼロのピュア .NET 10 実装
- 堅牢な状態管理:
- トポロジ変更時(ディスプレイの抜き差しや解像度変更)の一時的なフォールバック座標の誤保存を防止
- 原子的な JSON 書き込み(Atomic Save)によりクラッシュや電源断時も設定・レイアウト破損を防止
- 単一インスタンス保護 & トレイ常駐:
- Mutex による二重起動の完全抑止
- 設定画面は起動時には読み込まず、必要な時にのみ生成する Lazy ロード設計
- タスクバー通知領域(トレイ)からのクイック操作と診断情報コピー
動作要件
- OS: Windows 11 (64-bit, 23H2 / 24H2 Build 26100/26200 以降推奨)
- ランタイム: .NET 10 Desktop Runtime (x64)
- ビルド環境: .NET 10 SDK (x64)
ビルド手順
リポジトリルートで以下を実行します。
dotnet build SaveWindow.sln -c Release -p:Platform=x64
出力先: src/SaveWindow/bin/x64/Release/net10.0-windows/SaveWindow.exe
ディレクトリと設定ファイル
- 設定およびレイアウト保存先:
%LocalAppData%\SaveWindow\settings.json: アプリケーション全般の設定(自動起動、復元トリガー、デバウンス値、除外リスト等)layouts.json: 各デスクトップ・モニタ構成ごとのウィンドウ配置キャッシュ
- ログ保存先:
%LocalAppData%\SaveWindow\logs\savewindow.log(最大2MB、最大2世代までの自動ローテーションに対応)
トレイメニューの機能
タスクバーのトレイアイコンを右クリックすることで以下の操作が可能です。
- 稼働状態表示: 現在の稼働状況(稼働中 / 一時停止中)の確認
- 今すぐレイアウト保存: 現在の追跡対象ウィンドウの位置・サイズをキャプチャし、即時保存
- 今すぐレイアウト復元: 現在の仮想デスクトップに保存した配置へ非アクティブで復元
- 一時停止: 位置監視および自動復元の一時中断
- Windows 起動時に実行: HKCU Run レジストリへの自動起動登録切り替え
- 設定と診断...: 各種設定および詳細診断情報(追跡ウィンドウ数、イベント集約カウンタ等)の閲覧
- 終了: アプリケーションの安全な終了(フック解除、設定・レイアウトを即時フラッシュしてリソース解放)
Phase 2: ウィンドウ追跡アーキテクチャと API 仕様
1. 使用する Windows API / P-Invoke 境界
Microsoft Learn 公式仕様および Windows SDK に基づき以下の P/Invoke を定義(NativeMethods.cs):
SetWinEventHook/UnhookWinEvent:WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESSで登録。- 登録スレッドの WinForms メッセージループ上でコールバックが実行され、コンテキストスイッチ・余分なスレッド生成を回避。
- 対象イベント(広範囲一括登録を廃止し、個別の極小範囲でフック登録&callback許可リスト判定):
EVENT_SYSTEM_MOVESIZEEND(0x000B): ユーザー移動・リサイズ完了時EVENT_SYSTEM_MINIMIZESTART(0x0016) /EVENT_SYSTEM_MINIMIZEEND(0x0017): 最小化および復帰時EVENT_OBJECT_DESTROY(0x8001): ウィンドウ破棄(追跡破棄)EVENT_OBJECT_STATECHANGE(0x800A): 最大化等の状態変更EVENT_OBJECT_LOCATIONCHANGE(0x800B): runtime observation と topology probe。単独では trusted layout に commit しない
GetWindowPlacement:WINDOWPLACEMENT(44 bytes,SW_SHOWNORMAL,SW_SHOWMAXIMIZED,SW_SHOWMINIMIZED,rcNormalPosition等) を取得。- 最大化や最小化状態であっても「通常の復元座標」を正確に保持。
EnumWindows:- アプリ起動時のメモリ上トラッカー初期化、および手動「今すぐレイアウト保存」実行時の全体スナップショット取得(起動時に既存の永続化レイアウトを上書き破壊しない保護機構を実装)。
DwmGetWindowAttribute(DWMWA_CLOAKED):- Windows 11 の仮想デスクトップ裏画面・UWP 非表示状態の判別に利用可能。
2. Window Identity(識別子設計)
- HWND やタイトル文字列単独を永続キーにせず、以下の複合識別モデルを採用:
ProcessName: 実行ファイル名(小文字正規化)ClassName: ウィンドウクラス名ExecutablePath(取得できる場合)と補助情報としてのTitle
- タイトル変更時の fallback は一意な process/class/path に限定し、曖昧な複数Windowは移動しない。
3. イベント Debounce / Coalesce & ディスク書き込み抑制
- LocationChange は候補commitせず、MoveSizeEnd / 状態変更時に取得した snapshot を
DebounceMs(デフォルト500ms)後、topology・desktop・HWND identity を再検証してから commit。 - ディスク保存はイベント毎に行わず、Dirty フラグが立ったときだけ保存タイマーを開始し、一定間隔(
SaveIntervalMs: デフォルト 10秒)または終了時に原子書き込み(Atomic Save)。保存成功後はタイマーを停止し、アイドル中は周期処理を行わない。
4. RestoreGuard & 保存レイアウト保護
- ウィンドウ復元処理中および終了後に遅延到着する対象 HWND の移動イベントを、
RestoreGuardの bounded suppression window で保存経路から抑止。 - 起動時の初期ウィンドウ列挙による保存レイアウトの上書きを抑止し、復元用データの保全性を担保。
開発時の自動テスト
外部 NuGet 依存のないコンソールテストを使います。Restore Core v1 の最終検証では25テスト群が成功しました。通常の Release x64 ビルドは SaveWindow.sln のみで実行できます。
Phase 3: ディスプレイ・電源・トポロジ安定化アーキテクチャ
1. 公式資料・Windows SDK / 既存 OSS 参照元
- Microsoft Learn:
- 既存 OSS 照合:
kristoffer-t/PersistentWindows(Display topology stabilization & QueryDisplayConfig buffer retry logic)
2. MonitorIdentity(安定モニタ識別規則)
\\.\DISPLAY1 や \\.\DISPLAY2 などの GDI デバイス名や列挙順は、ディスプレイの抜き差しや電源再投入で容易に入れ替わります。
そのため、以下の優先順位で安定したハードウェア一意識別キー(StableKey)を生成します:
- Target Device Path:
\\?\DISPLAY#DELA123#5&28b8e01d&0&UID4354#{e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}(最も安定・永続的) - EDID + Connector Instance:
FriendlyName_EdidMfg:EdidProd_ConnectorInstance(DevicePath 取得不可時のフォールバック) - FriendlyName + GDI DeviceName: 最終フォールバック。回転で変わるBounds/解像度はStableKeyに含めません。
Screen.Bounds / Screen.WorkingArea をrotation適用後のWindows desktop geometryとして使います。DISPLAYCONFIG_SOURCE_MODEのraw寸法とtargetInfo.rotationは分けて保持し、DiagnosticsでもDesktop Bounds、Source Mode、Rotationを区別します。
3. DisplayTopologyStabilizer(トポロジ安定化ステートマシン)
- トリガー:
WM_DISPLAYCHANGE,WM_POWERBROADCAST(PBT_APMRESUMEAUTOMATIC,PBT_APMRESUMESUSPEND,PBT_APMSUSPEND) - 動作:
- トポロジ変更を検知すると
TopologyChanging状態へ遷移し、TopologyGateを閉鎖。 - 常時ポーリングや
Thread.Sleep、Task.Runループは一切使わず、WinForms UI スレッドタイマー(TopologyStabilizeDelayMs: 初期値 2000ms)で有限回数の確認を開始。 - 各 Tick で
QueryDisplayConfigからアクティブトポロジを取得し、前回サンプルと比較。 - 複数回連続一致(デフォルト2回) を確認して「安定(Stabilized)」と判定し、
TopologyGateを解放。 - 最大試行回数までに安定サンプルを得られない場合は Restore を実行せず、
TopologyGateを閉じたまま最後の trusted topology を保持します。次の正常な表示イベントで再試行します。 - スリープ移行時(
PBT_APMSUSPEND)はTopologyGateを閉じた状態(IsBlocked = true)を維持したままタイマーを停止。復帰時(PBT_APMRESUMEAUTOMATIC/PBT_APMRESUMESUSPEND)に再安定化を開始し、トポロジが安定した時のみ Gate を解放。 WM_POWERBROADCASTのメッセージ処理後は、Microsoft Learn の規定に従いm.Result = (IntPtr)1(TRUE) を返却。
- トポロジ変更を検知すると
4. TopologyGate と誤保存抑止 & Trusted Layout 保存
- 課題: ディスプレイ切断時やスリープ復帰時、Windows はマルチモニタ上の全ウィンドウをプライマリモニタへ強制集約するため、大量の過渡的移動イベントが発生します。これを追跡・保存すると本来の配置が破壊されます。
- 解決策:
- トポロジ変更開始時、直前までに確定していた正常なレイアウト(trusted layout)が dirty の場合はディスクへフラッシュ(保存失敗時は dirty フラグを保持)。
- pending candidate を topology fingerprint とイベント種別で再検証し、安全な配置だけを commit します。unsafe candidate は明示的に破棄し、位置変更イベントは安定化後に再列挙します。
TopologyGate.IsBlockedが true の間、WindowTrackerのOnWinEventコールバックは移動イベントをすべて破棄(Drop)。- Phase 4 の復元処理(Restore)完了まで安全に Gate を閉じた状態を維持できる設計。
Phase 4: ウィンドウ復元(Window Restore)アーキテクチャ
1. 公式仕様・Windows SDK 参照元
- Microsoft Learn:
GetWindowPlacement/SetWindowPlacement:rcNormalPositionは通常のトップレベルウィンドウにおいて Workspace Coordinates(作業領域座標系) であり、SetWindowPosが期待する Screen Coordinates(スクリーン絶対座標系) とは異なります。Workspace 座標をSetWindowPosに直接渡すことは禁止されています。- Windows SDK
winuser.hにおいてWINDOWPLACEMENTのサイズは 44 bytes(length = sizeof(WINDOWPLACEMENT)が必須)。
SetWindowPos:SWP_NOACTIVATE(0x0010) を指定することで、ウィンドウをアクティブ化(フォーカス奪取)せずに位置・サイズを変更可能。SWP_NOZORDER(0x0004) およびSWP_NOOWNERZORDER(0x0200) により Z オーダーを保持。
GetDpiForWindow:- Per-Monitor V2 aware ウィンドウが存在するモニタの DPI を取得。
GetMonitorInfo:MONITORINFO(40 bytes)を用いて各モニタのrcMonitor(物理境界)およびrcWork(作業領域)を取得。
2. 座標系の分離と DPI スケーリング
- 座標系の型・API 境界分離:
NormalBounds: 保存された通常位置を Screen 絶対物理ピクセル座標で保持。NormalPosition:WINDOWPLACEMENT.rcNormalPositionの Workspace 座標。復元直前に target monitor の WorkArea 基準へ変換。- Normal / Maximized / Minimized の show state と normal bounds は独立して保持し、全状態に同じ
SetWindowPlacementexecution path を使います。
- DPI スケーリング計算:
- 保存時の
Dpi(通常 96 DPI = 100%)と、解決した復元先モニタの DPI を比較(現在 HWND が一時的にいるモニタの DPI は使いません)。 - 異なる DPI のモニタへウィンドウが移動した場合、サイズおよび相対オフセットを
targetDpi / sourceDpi比率で適切にスケーリング。
- 保存時の
3. 一対一ウィンドウ照合(Window Matching)
WindowIdentity(ProcessName,ClassName,Title)を用いた 2 パス照合アルゴリズム:- executable path(利用可能な場合)・process・class・title の一意な一致を優先。
- title 変更時の fallback は同じ process/class/path の saved/current がそれぞれ1つの場合だけ許可。
- 同一アプリの複数ウィンドウを一意に識別できない場合は誤移動せず skip。
4. モニタ切断時のフォールバック & Offscreen 防止クランプ
- 保存された
MonitorId(StableKey)を exact match、保存 geometry との overlap/nearest、primary の順で解決。 - 通常位置は monitor bounds ではなく WorkArea を基準に clamp。操作できる領域を残し、元のサイズを不必要に縮小しません。
5. 非アクティブな Window Placement
SetForegroundWindowは呼び出しません。- Normal / Minimized は
SW_SHOWNOACTIVATE/SW_SHOWMINNOACTIVE、Maximized はSetWindowPlacementの正式な maximized show state と target monitor の normal restore bounds を使用します。 - HWND・process identity・desktop membership を API 直前に再確認し、対象 HWND ごとの bounded mutation suppression で遅れて届く WinEvent も通常保存経路から除外します。
6. 二重保護(RestoreGuard & TopologyGate)
- 復元実行時は全体の再入防止、対象 HWND ごとの
RestoreGuardsuppression、およびTopologyGate.BeginBlock()を使用。 - 復元で発生する同期・遅延 WinEvent を trusted layout に戻さず、復元後に状態を検証します。
7. 起動時の既存ウィンドウ復元
- 起動時に topology の安定を確認した後、その時点で存在する top-level windowを列挙し、各HWNDのDesktop GUIDとsaved Desktop layoutを照合して同じRestoreEngineで復元します。Manual Restoreもすべての既知Desktopを対象にします。
- Background Desktop上の既存WindowはDesktopを切り替えず、per-window membershipが保存GUIDと一致する場合に限りplacementを復元します。
- 起動後に新しく作成された window を監視して自動配置する Restore Watch は含みません。
Phase 5: 仮想デスクトップ(Virtual Desktop)アーキテクチャ
1. 公式資料・Windows SDK / 内部 COM 調査結果と Source Mapping
- Microsoft Learn 公開 API:
IVirtualDesktopManagerCLSID_VirtualDesktopManager:AA509086-5CA9-4C25-8F95-589D3C07B48AIID_IVirtualDesktopManager:A5CD92FF-29BE-454C-8D04-D82879FB3F1BIsWindowOnCurrentVirtualDesktop(HWND, out bool): 指定ウィンドウが現在アクティブな仮想デスクトップに存在するか確認。GetWindowDesktopId(HWND, out Guid): 指定ウィンドウの仮想デスクトップ一意 GUID を取得。- ※ Microsoft Learn はアクティブな仮想デスクトップ GUID を直接取得する公開 getter API を提供していません。
EVENT_SYSTEM_DESKTOPSWITCH(0x0020):- 仮想デスクトップ切り替え時に OS から発火される標準 WinEvent。
- ポーリングを行わず、完全イベント駆動(Event-driven) で切り替えを検知。コールバック内では直接 COM 呼び出しやファイル I/O を行わず、UI スレッドのタイマーでデバウンス集約します。
- Windows 11 Build 26100 / 26200 (24H2) 向け内部 COM:
MScholtes/VirtualDesktop(VirtualDesktop11-24H2.cs、MIT License、詳細は THIRD-PARTY-NOTICES.md 参照)に基づき、Windows 11 24H2 (Build 26100, 26200) 向け確定定義を採用:IServiceProviderGUID:6D5140C1-7436-11CE-8034-00AA006009FACLSID_ImmersiveShell:C2F03A33-21F5-47FA-B4BB-156362A2F239CLSID_VirtualDesktopManagerInternal:C5E0CDCA-7B6E-41B2-9FC4-D93975CC467BIID_IVirtualDesktopManagerInternal:53F5CA0B-158F-4124-900C-057158060B27IID_IVirtualDesktop:3F07F4BE-B107-441A-AF0F-39D82529072CIVirtualDesktopManagerInternalvtable 順: Slot 0GetCount()(PreserveSig false / 戻り値 int、マネージド呼び出しで HRESULT 成功時に戻り値を取得), Slot 1MoveViewToDesktop(view, desktop), Slot 2CanViewMoveDesktops(view), Slot 3GetCurrentDesktop(out desktop), Slot 4GetDesktops(out desktops)IVirtualDesktopvtable 順: Slot 0IsViewVisible(view), Slot 1GetId(out pGuid)(Slot 2 はGetNameでありダミースロットは不要。未使用の Slot 2 以降は省略)- これにより、空のデスクトップや可視ウィンドウが存在しないデスクトップでもアクティブ GUID を直接正確に取得可能です。
- 取得した内部 Desktop COM オブジェクトは各呼出直後に
Marshal.ReleaseComObjectで解放し、プロバイダー破棄時にも Manager の RCW を安全に解放(二重解放ガード付き)してリークを防止します。
2. プロバイダー抽象化とフォールバック階層(VirtualDesktopProviderFactory)
VirtualDesktopProviderFactory:- 実行環境の OS ビルド番号を判定し、最適なプロバイダーを透過的に生成します。
- Windows 11 24H2 確認済み base build (26100 または 26200):
Build26100VirtualDesktopProviderを使用。 - 初期化失敗時・26300 以上の将来/未知ビルド: 未ドキュメント COM の使用を避け、自動的に公開 API ベースの
PublicVirtualDesktopProviderへ安全にフォールバック。 - COM 完全不可環境:
FallbackVirtualDesktopProviderはUnknownを返します。誤った Desktop key へ保存・復元せず、レイアウト操作を安全に停止します。 - トレイ、設定、ログ、表示トポロジ診断は継続します。Window layout の自動保存・復元は Desktop identity を確認できる場合だけ有効です。
3. Tri-state 判定とウィンドウ混入防止
- 単純な
boolでは「別デスクトップ」と「COM 例外/判定不能」を区別できず、例外時に true を返すと別デスクトップのウィンドウを誤保存するリスクがあります。 TryIsWindowOnCurrentDesktop(IntPtr hWnd, out bool onCurrentDesktop)(Tri-state):- 判定成功かつ
onCurrentDesktop == true: 現在デスクトップ対象として採用。 - 判定成功かつ
onCurrentDesktop == false: 別デスクトップとして除外。 - 判定不能(COM エラーや破棄中ウィンドウ): 誤保存・誤上書き防止のため追跡から除外(スキップ)。
- Provider failure / active desktop identity unknown 時は保存・復元をスキップし、
defaultや前回 GUID を流用しません。
- 判定成功かつ
WindowFilterに DWM Cloaked 判定(DwmGetWindowAttribute(hWnd, DWMWA_CLOAKED))を組み込み、裏画面や非表示デスクトップのウィンドウを多重に除外。
4. LayoutStore の Per-Desktop Key 統合
LayoutStore.Desktops: 仮想デスクトップ GUID(または"default")をキーとする辞書構造。WindowTracker:- 初期 EnumWindows、WinEvent デバウンス、手動キャプチャ(CaptureAllNow)、デスクトップ切り替え時の全経路で、現在デスクトップ以外のウィンドウを完全に除外。
- デスクトップ切り替え通知時、直前のデスクトップの未保存変更をフラッシュし、過渡的イベントキューをクリアして新デスクトップの追跡マップを再構築(既存保存レイアウトは上書きせず保持)。
- A → B → A のように切り替えても、デスクトップ A 用と B 用のレイアウトが混同されることはありません。
WindowRestoreService:- デスクトップ切り替え時は、設定
RestoreOnDesktopSwitch == trueであれば新current Desktopだけを復元。 - Startup / Manual Restoreでは、既存Windowの
GetWindowDesktopIdと全既知Desktopのlayoutを照合し、background DesktopのWindowもその場に留めたままrestore。 - 対象 Desktop に layout がない場合は何も復元しません。別 Desktop や
defaultlayout は流用しません。 - 復元候補列挙時にも他デスクトップの裏ウィンドウを確実に除外。
- デスクトップ切り替え時は、設定
Note
現在の実装は Windows 11 Build 26200 上での COM 生成・IID/CLSID 整合・単体テスト(Fake プロバイダーによる A/B 分離および同一 identity 分離)をパスしていますが、実機での物理的な仮想デスクトップ切り替え操作(Win+Ctrl+左右キー)に伴うユーザーランドでの長期的な動作挙動は、今後手動検証チェックリストに沿って確認を行う必要があります。
Phase 6: 設定管理・スタートアップ登録・トリガー分離アーキテクチャ
1. トリガー経路の厳密な分離 (RestoreOnResume vs RestoreOnMonitorReconnect)
- トリガー理由の伝播:
DisplayTopologyStabilizerはトポロジ変更検知時にトリガー要因(TopologyTriggerReason.DisplayChangeまたはTopologyTriggerReason.PowerResume)を内部ステートとして記録。- 安定化完了イベント
TopologyStabilizedWithReason(reason)を通じて、上位のSaveWindowApplicationContextへ通知。
- 復元フラグの独立評価:
- スリープ・休止復帰由来(
PowerResume):RestoreOnResumeフラグが有効な場合のみ復元を実行。 - ディスプレイ再接続・解像度変更由来(
DisplayChange):RestoreOnMonitorReconnectフラグが有効な場合のみ復元を実行。 - これにより、スリープ復帰時のみ復元したい、あるいは外部モニタ接続時のみ復元したいというユーザー設定が 100% 正確に反映されます(
AutoRestoreおよび!IsPausedも同時に尊重)。
- スリープ・休止復帰由来(
2. 設定変更の即時反映(Dynamic Settings Application)
SettingsFormで各種設定(デバウンス時間DebounceMs、トポロジ安定化間隔TopologyStabilizeDelayMsなど)を保存した際、コールバックonSettingsSavedを経由して稼働中のサービス群へ即座に伝達。WindowTracker.ApplySettings(AppSettings): デバウンスタイマー(DebounceMs)およびディスク保存タイマー(SaveIntervalMs)の間隔を稼働中のタイマーへ即時再設定。
DisplayTopologyStabilizer.ApplySettings(AppSettings): 安定化サンプリングタイマーの間隔を即時再設定。- 電源復帰中にディスプレイ変更通知も重なった場合、両方の原因を保持し、それぞれの設定が有効な原因が1つでもあれば復元。
- アプリケーションを再起動することなく、設定ダイアログの変更が直ちにバックグラウンド処理へ反映されます。
3. HKCU Run スタートアップ登録と安全性
- Windows の
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Runレジストリキーを使用。 - 登録パスは空白を含むパスに対応するため、
"\"" + exePath + "\""の形式で二重引用符(ダブルクォート)で囲んで保存。 - 設定ファイル内の
StartWithWindowsフラグとレジストリ実登録状態を同期。 - 管理者権限不要(UAC プロンプトなし)で安全にユーザーごとの自動起動を切り替え可能。
4. 設定ファイルの堅牢性(フォールバック保護)
- 設定ファイル
settings.jsonの読み込み時、万が一ファイルが破損(不正な JSON や構文エラー)していた場合でも、例外をキャッチして安全にデフォルト設定(AppSettings.CreateDefault())へ復帰。 - エラー内容はログに警告として記録され、アプリケーションのクラッシュを防止します。
Phase 7: 診断画面と保守性アーキテクチャ
1. オンデマンド診断情報(Diagnostic Telemetry)
- アプリケーション常駐中のオーバーヘッドを完全に排除するため、診断文字列の生成はバックグラウンドでは一切行わず、ユーザーが「設定と診断...」ダイアログを開いた際、またはダイアログ上の「更新」ボタンをクリックした時のみオンデマンドで生成。
- 診断内容:
- バージョン・稼働状態: バージョン番号、稼働状態(Active / Paused)、プロセスメモリ使用量
- 仮想デスクトップ情報: 使用中のプロバイダー名(
Build26100InternalCom,PublicCom,Fallback)、現在のデスクトップ GUID / キー、サポート状態 - ウィンドウ追跡統計: 現在追跡中のウィンドウ数、総受信 WinEvent 数、集約(Coalesced)イベント数、除外イベント数
- ディスプレイ・トポロジ: 現在のアクティブトポロジキー、モニタ数、各モニタのハードウェアパス・解像度・プライマリ判定
- 復元統計: 直近の復元実行結果、対象ウィンドウ数、移動成功数、スキップ数、復元失敗数、およびアプリ起動時からの 累積復元回数(Cumulative Restore Count)
2. ワンクリック・クリップボードコピー
- 診断画面に「診断情報をクリップボードへコピー」ボタンを配置。
- サポートやトラブルシューティング時に、現在のトポロジ、ウィンドウ追跡数、COM プロバイダー種別などの環境情報を素早く共有可能。
Phase 8: 総合運用・障害耐性・ライフサイクル設計
1. 単一インスタンス保護(Mutex 管理)
Program.csにおいて名前付きシステム Mutex(Local\SaveWindow_SingleInstance_Mutex)を確保。- 二重起動を検知した場合はログへ記録して終了し、レジストリやレイアウトファイルの競合を防止。
2. 多重例外ハンドリングとクラッシュフリー設計
Application.ThreadExceptionおよびAppDomain.CurrentDomain.UnhandledExceptionを捕捉し、予期せぬ例外が発生した場合でもスタックトレースをログへ書き込んで安全に記録。- WinEvent コールバック、トポロジ安定化タイマー、COM デスクトップ判定など、あらゆる OS イベント境界において try-catch による保護を実施。
3. シャットダウン・リソース解放の完全性
- アプリケーション終了(トレイメニューの「終了」や OS シャットダウン・ログオフ)時:
WindowTracker:UnhookWinEventを実行して OS フックを安全に解除。未保存レイアウトをディスクへ原子書き込み(Atomic Save)。DisplayTopologyStabilizer: タイマーを停止し、保留中状態をリセット。VirtualDesktopProvider: 保持している内部 Manager や Desktop の COM RCW を確実に解放。TrayIconManager: トレイアイコンを非表示にして破棄(タスクバー上のゴーストアイコン発生を防止)。- 単一インスタンス Mutex を解放。
既知の制約
- 公開 Virtual Desktop API が現在 GUID を特定できない空 Desktop、または switch hook 登録失敗時は、Desktop 単位の自動復元を安全側に停止します。
- Fullscreen 判定は DWM bounds・monitor bounds・window style を組み合わせた conservative heuristic です。実機での Game/RDP/Video と maximized app の判別確認が必要です。
- Monitor unplug/sleep、DPI、Virtual Desktop、最大化・最小化の実機動作は、以下の手動チェックリストで確認してください。
手動検証チェックリスト (Phase 2 〜 Phase 8)
Phase 2: ウィンドウ追跡
- 追跡対象の自動検出: アプリ起動後、エクスプローラーやメモ帳を開いた状態で「設定と診断...」を開き、「Tracked Windows Count」が正しくカウントされていること。
- イベント集約の確認: ウィンドウをドラッグ移動した際、診断情報の「Total WinEvents Received」が増加し、「Coalesced Events」によって重複が排除されていること。
- 手動レイアウト保存: トレイメニューから「今すぐレイアウト保存」をクリックし、バルーン通知が表示され、
%LocalAppData%\SaveWindow\layouts.jsonにウィンドウ情報が記録されること。 - 一時停止の連動: トレイメニューで「一時停止」にした場合、ウィンドウ移動を行っても追跡イベントがバイパスされること。
- アプリ終了時の確実な解放: アプリ終了時に UnhookWinEvent が呼ばれ、ログに
WindowTracker disposed.が記録されること。
Phase 3: ディスプレイ・電源・トポロジ安定化
- アクティブモニタ情報の表示: 「設定と診断...」を開き、「Active Monitors」に FriendlyName、Primary 判定、解像度、DevicePath、StableKey が正しく表示されること。
- 解像度変更時の安定化: 画面の解像度を変更した際、ログに
DisplayTopologyStabilizer: Topology stabilized successfully!が記録され、診断情報の TopologyKey が更新されること。 - モニタ切断時の誤保存抑止: 外部ディスプレイを切断した際、Windows によってプライマリに集約された過渡的座標が
layouts.jsonに誤保存されないこと。 - スリープ復帰検知: PC をスリープさせて復帰させた際、
WM_POWERBROADCAST (PBT_APMRESUMEAUTOMATIC / PBT_APMRESUMESUSPEND)が受信され、安定化タイマーが動作すること。
Phase 4: ウィンドウ復元
- 手動レイアウト復元: ウィンドウを別の場所にドラッグした後、トレイメニューから「今すぐレイアウト復元」をクリックし、バルーン通知が表示され、保存時の位置へ非アクティブで瞬時に復帰すること。
- フォーカス非奪取の確認: 他のウィンドウ(メモ帳等)に入力フォーカスがある状態で復元を実行しても、フォーカスが奪われないこと(Foreground Steal が発生しないこと)。
- Tolerance(差分判定)による不要移動抑止: ウィンドウがすでに保存位置にある状態で復元を実行した際、ログおよびバルーン通知でスキップ(変更なし)とカウントされること。
- トポロジ安定化後の自動復元: 画面解像度の変更やディスプレイ再接続後、トポロジが安定したタイミングで自動的にウィンドウ配置が復元されること。
- 診断画面での復元結果確認: 「設定と診断...」を開き、「Window Restoration State」に直近の復元成否、復元件数、スキップ件数が表示されること。
Phase 5: 仮想デスクトップ
- 仮想デスクトップごとの独立保存: デスクトップ 1 でウィンドウ A を左側に配置して保存し、デスクトップ 2 でウィンドウ B を右側に配置して保存した際、
layouts.jsonにデスクトップごとのエントリが作成されること。 - デスクトップ切り替え時の独立復元 (A→B→A): デスクトップ 1 から 2、そして 1 へ戻した際、それぞれのデスクトップに記録された配置が正しく維持・復元されること。
- 他デスクトップウィンドウの除外: デスクトップ 1 の復元実行時に、デスクトップ 2 にある非表示ウィンドウが誤って移動されないこと。
- COM 例外フォールバック: 仮想デスクトップ API がサポートされない、または取得不能な環境でも
"default"キーとして正常に動作を継続すること。
Phase 6: 設定管理・スタートアップ登録
- HKCU スタートアップ登録: 「設定と診断...」で「Windows 起動時に実行」を ON にして保存した際、
HKCU\Software\Microsoft\Windows\CurrentVersion\Runにパスが二重引用符付きで登録されること。OFF にした際にキーが削除されること。 - トリガー経路の独立動作:
- モニタ構成を変えず、
RestoreOnResumeを OFF、RestoreOnMonitorReconnectを ON にした状態でスリープ復帰を行い、自動復元がトリガーされないこと。 - ディスプレイの解像度変更を行い、解像度変更後には自動復元がトリガーされること。
- モニタ構成を変えず、
- デバウンス設定の即時反映: 「設定と診断...」でデバウンス時間を変更して保存した後、アプリを再起動することなく、ウィンドウ移動時のイベント集約間隔が即座に反映されること。
- 破損 JSON の耐性:
%LocalAppData%\SaveWindow\settings.jsonとlayouts.jsonの内容を不正な文字列に書き換えてアプリを起動した際、クラッシュせずデフォルト値へ復旧し、破損ファイルのバックアップと警告ログが作られること。
Phase 7: 診断画面・クリップボード
- オンデマンド診断表示: 「設定と診断...」を開いた際、Virtual Desktop Provider 名、Current Desktop GUID/Key、Cumulative Restore Count などの最新診断情報が表示されること。
- 診断情報コピー: 「診断情報をクリップボードへコピー」をクリックした際、クリップボードに診断テキスト全体がコピーされ、エディタ等に貼り付けできること。
- 累積復元回数の加算: トレイから「今すぐレイアウト復元」を実行するか自動復元が行われるたびに、診断画面の「Cumulative Restore Count」が 1 ずつ増加すること。
Phase 8: 総合運用・障害耐性
- 単一インスタンス(二重起動防止): アプリ起動中に、別のコマンドプロンプトやエクスプローラーから
SaveWindow.exeを再起動した際、多重起動が防止され、終了がログに記録されること。 - 急激なディスプレイ/電源イベント耐性: スリープ・復帰や解像度変更を短時間に連続で行っても、プロセスがクラッシュしたり CPU 使用率が高止まりしないこと。
- 安全な終了とゴーストアイコン防止: トレイメニューから「終了」を選択した際、タスクバーのトレイアイコンが即座に消去され、ログに正常終了が記録されること。