アーキテクチャ
技術スタック
- .NET 10 / C#
- Windows Forms (WinForms)
- Win32 API (P/Invoke)
- GitHub Releases API(自動更新機能)
モジュール構成
src/Launcher/
├── Core/ ドメインモデル・ビジネスロジック
├── Infrastructure/ 基盤ユーティリティ
├── UI/ WinFormsフォーム群
├── Win32/ Win32 API連携
└── Updater/ 自動更新機能モジュール分割の設計意図
- Core — UIフレームワーク(WinForms)に依存しない純粋なドメインモデルとロジック。テスト容易性と関心の分離が目的
- Infrastructure — アプリケーション基盤。シリアライズ・パス操作・ファイル操作など、ドメインやUIに属さない横断的関心事を集約。PathHelperはパス文字列操作のみ、FileHelperはファイル・ディレクトリ操作と役割を分離している
- UI — WinFormsに依存するフォーム群。ロジックはPresenterに委譲し、フォーム自体は表示と入力の橋渡しに徹する
- Win32 — P/Invoke呼び出しを隔離するモジュール。Win32 APIの複雑さ(マーシャリング、リソース管理)をアプリケーション本体から遮断する
- Updater — 自動更新機能。GitHub Releases APIとの通信、ZIPの展開、バッチスクリプトによる自己置換など、更新特有の処理を分離
主要な設計パターン
Presenterパターン
MainFormPresenter / ButtonLauncherPresenter / SchedulerPresenterがUIロジックを担当する。WinFormsのフォームクラスはイベントハンドラとコントロール操作のみを持ち、判断ロジックはPresenterに委譲する。これにより、UIロジックをWinFormsから分離してテスト可能にしている。
ConfigStore継承による永続化
Config、CommandList、ButtonLauncherData(Data)はすべてConfigStoreを継承し、XMLシリアライズで永続化される。ConfigStoreは原子的なファイル保存(一時ファイルに書き込み後File.Moveで置換)を提供し、保存中のクラッシュによるデータ破損を防止する。
制約: XMLシリアライズ対象プロパティのコレクション初期化子は変更禁止。XmlSerializerはデシリアライズ時に既存インスタンスへAddするため、初期化子で値を入れるとデシリアライズ結果と重複する。
DummyFormによるIPCハブ
DummyFormは不可視の常駐フォームで、アプリケーション全体のハブとして機能する。WM_APPMSGによるプロセス間通信(/close、/restart等のコマンドライン引数の処理)を受け付ける。加えて、子フォーム(MainForm、ButtonLauncherForm)のライフサイクルを管理する。WinFormsのメッセージループを維持するために常駐フォームが必要であり、メインウィンドウ(MainForm)は表示/非表示を繰り返すため、この役割を分離している。また、スケジューラーのタイマー(30秒間隔)を管理し、スケジュール条件に合致したタスクの自動実行も制御する。
スケジューラータスクの種類
スケジューラーはファイル実行に加え、メッセージ表示タスクをサポートする。
| 種類 | 説明 |
|---|---|
| Execute | ShellExecuteExでプログラムを起動する |
| BalloonTip | タスクトレイのバルーン通知でメッセージを表示する(自動消去) |
| MessageBox | メッセージボックスでメッセージを表示する(OKボタンで手動消去) |
Core層(SchedulerPresenter)はUI依存を持たない。 BalloonTip/MessageBoxの表示はデリゲート経由でUI層(DummyForm)に委譲する。 MessageBoxはInvoke(同期呼び出し)でダイアログが閉じるまで後続タスクをブロックする。 BalloonTipはBeginInvoke(非同期)で実行する。
設定ファイル
すべてXMLシリアライズで、アプリケーションと同じディレクトリに保存される。 基底クラスConfigStoreがシリアライズ/デシリアライズを提供。
*.cfgファイルはユーザーが設定変更したときのみ書き換わる静的な設定ファイル。*.datファイルはアプリケーション動作中に頻繁に更新されるランタイムデータ(ウィンドウハンドル、スケジューラーの最終チェック時刻など)。この分離により、大容量になりうる設定ファイルの頻繁な書き込みを避けている。
| ファイル | 内容 |
|---|---|
らんちゃ.cfg | アプリケーション設定(Config) |
らんちゃ.cmd.cfg | コマンド一覧(CommandList) |
らんちゃ.btns.cfg | ボタン型ランチャーのデータ(ButtonLauncherData) |
らんちゃ.sch.cfg | スケジューラー設定(SchedulerData) |
らんちゃ.dat | ランタイムデータ(Data) |
スレッディングモデル
| スレッド | 用途 | 備考 |
|---|---|---|
| UIスレッド (STA) | WinFormsメッセージループ、全UI操作 | Application.Run(DummyForm) |
| コマンド実行スレッド (STA) | Command.Execute()の実行 | MainForm.ExecuteCommandで生成 |
| ディレクトリ展開スレッド (STA) | Command.OpenDirectory()の実行 | MainForm.OpenDirectoryで生成 |
| アイコン読込スレッド (STAx8) | AsyncIconLoaderによる非同期アイコン取得 | 固定8本STAワーカー + リトライ(最大2回) |
| 環境変数置換スレッド | ReplaceEnvListのコマンド名置換 | MainForm.ApplyConfigで生成 |
| スケジューラー実行スレッド (STA) | SchedulerPresenter.ExecuteItemTasks | タイマーTick時に生成、アイテムごとに1本 |
| フックコールバック | キーボード/マウスフックのイベント通知 | BeginInvokeでUIスレッドへディスパッチ |
STA制約
コマンド実行・ディレクトリ展開・アイコン読込はすべてSTAスレッドで行う。ShellExecuteExやSHGetFileInfo等のShell APIはCOMのSTA(Single-Threaded Apartment)を前提としている。そのため、Task.Run(ThreadPool/MTA)では正常に動作しない。専用のSTAスレッドを生成して実行する必要がある。
アイコンローダーの並行度制限
AsyncIconLoaderのワーカー数は8本固定。SHGetFileInfo(Shell API)は高並行度で不安定になるため、ProcessorCount等の動的な値は使用せず固定値とする。ButtonLauncherFormのHandle作成は、アイコン非同期読み込み(BuildTabs→iconLoader.Load)より前に行うこと。Handle未作成時にIconLoadedイベントが到着すると、BeginInvokeの失敗によりアイコンは破棄される。アイコン読み込み完了時のInvalidateはbtn.Parent?.Invalidate(true)で親パネル全体を対象にすること。btn.Invalidate()では非選択タブのボタンが再描画されない。
フック管理
HookManagerがグローバルキーボード/マウスフックの状態管理を一元的に担当する。
- ホットキー検知:
SetWindowsHookExで登録したキーボードフックのコールバックで、KEYDOWN時に仮想キーコードと修飾キーを照合。一致したらKEYUPを抑制しつつ、BeginInvokeでUIスレッドへShowHideメッセージを送信 - ボタンランチャー起動: マウスフックでボタン押下状態(
lbuttonDown/rbuttonDown)を追跡し、設定されたトリガー(左右同時押し等)を検知。トリガー発動時はUPイベントを抑制して誤操作を防止 - UP抑制フラグ:
suppressNextLButtonUp/suppressNextRButtonUp/suppressKeyUpVKで、トリガー発動後の不要なUPイベントをフック内で消費する。フックコールバック内でUPを消費しないと、トリガー操作の直後にボタンクリックやキー入力として誤検知される
設計上の制約・選択
- cfg/dat分離の徹底:
*.cfgは設定変更時のみ、*.datは頻繁に書き換わるデータ。混在させない - STAスレッド必須: Shell API呼び出しはすべてSTAスレッドから。ThreadPool/MTAからの呼び出しは禁止
- アイコンローダー8本固定: SHGetFileInfoの安定性のため動的調整しない