PowerShell 5.1 + Windows Forms で作成中の既存アプリに、**Capsキーのダブルタップでフォームを表示/非表示するタスクトレイ常駐機能**を追加してください。
この機能は単体テスト用スクリプトですでに動作確認できています。
以下の実機確認済み条件を変更せず、既存アプリへ組み込んでください。
# 1. 実行環境
以下の環境を前提としてください。
* Windows PowerShell 5.1
* Windows Forms
* Windows 10 / 11
* `.ps1` / `.psm1` は UTF-8 BOM付きで保存する前提
* PowerShell 7専用構文は使用しない
* Win32 APIは `Add-Type` 内のC#から使用してよい
以下のようなPowerShell 7以降の構文は使用しないでください。
```powershell
??
?.
# PowerShell 7の三項演算子
$condition ? $a : $b
```
# 2. コード内のコメントは日本語にする
重要です。
作成するPowerShellコードの、
* セクション見出し
* 処理説明
* 注意事項
* 変更理由
などのコメントは、**原則として日本語で記述してください。**
関数名、クラス名、Win32 API名など、プログラム上英語である必要があるものは英語のままで構いません。
例えば、コード内の見出しは次のようにしてください。
```powershell
# ============================================================
# 必要なアセンブリの読み込み
# ============================================================
# ============================================================
# Capsキー ダブルタップ検出
# ============================================================
# ============================================================
# Win32 API
# ============================================================
# ============================================================
# アプリの状態管理
# ============================================================
# ============================================================
# メインフォーム
# ============================================================
# ============================================================
# タスクトレイ
# ============================================================
# ============================================================
# タスクトレイメニュー
# ============================================================
# ============================================================
# ウィンドウを隠す処理
# ============================================================
# ============================================================
# ウィンドウをカーソル横に表示する処理
# ============================================================
# ============================================================
# Capsキー ダブルタップ時の処理
# ============================================================
# ============================================================
# ×ボタンを押したときの処理
# ============================================================
# ============================================================
# タスクトレイ「表示」
# ============================================================
# ============================================================
# タスクトレイ「隠す」
# ============================================================
# ============================================================
# タスクトレイ「終了」
# ============================================================
# ============================================================
# Capsキー ダブルタップ判定時間
# ============================================================
# ============================================================
# キーボードフック開始
# ============================================================
# ============================================================
# アプリ起動
# ============================================================
# ============================================================
# 終了時の後始末
# ============================================================
```
後からコードを見た人が、どの部分が何を担当しているか分かる構成にしてください。
# 3. Capsキーについての重要な実機確認結果
この環境では、Capsキーを押したときに通常の、
```text
VK_CAPITAL = 0x14
```
として取得されません。
低レベルキーボードフックで実際に確認したCapsキーの入力は、
```text
VK = 0xF0
ScanCode = 0x3A
Flags = 0x00
DOWN
```
です。
つまりCapsキーの判定条件は、
```text
VK = 0xF0
ScanCode = 0x3A
KEYDOWN
```
です。
この条件は実機で確認済みなので、通常の、
```csharp
VK_CAPITAL = 0x14
```
へ勝手に変更しないでください。
# 4. CapsキーのUPイベントを使用しない
これも非常に重要です。
この環境ではCapsキーのUPイベントが通常の形で取得できませんでした。
そのため、
```text
Caps DOWN
Caps UP
Caps DOWN
Caps UP
```
をダブルタップとして判定する方式は使用しないでください。
実機で正常動作した方式は、
```text
Caps DOWN
↓
Caps DOWN
↓
ダブルタップ成立
```
です。
つまり、
```text
VK=0xF0
ScanCode=0x3A
DOWN
```
が2回発生した時間差だけでダブルCapsを判定してください。
# 5. 低レベルキーボードフックを使用する
Windows全体でCapsキーを検出するため、
```text
WH_KEYBOARD_LL
```
による低レベルキーボードフックを使用してください。
最低限、以下のWin32 APIを使用してください。
```text
SetWindowsHookEx
CallNextHookEx
UnhookWindowsHookEx
GetModuleHandle
```
C#側では `KBDLLHOOKSTRUCT` を定義し、
```text
vkCode
scanCode
flags
time
dwExtraInfo
```
を正しく取得できるようにしてください。
フック用デリゲートはGCで破棄されないよう、staticフィールドとして保持してください。
また、フックの二重登録を防止してください。
# 6. ダブルCapsの判定時間
Capsキーの2回のDOWNイベントの時間差で判定してください。
初期値は、
```text
100ms未満
↓
長押し・キーリピート等として無視
100ms以上650ms以下
↓
ダブルCaps成立
650ms超
↓
時間切れ
↓
今回のCaps DOWNを新しい1回目として扱う
```
としてください。
C#側で例えば、
```csharp
public static int MinimumIntervalMilliseconds = 100;
public static int MaximumIntervalMilliseconds = 650;
```
のように、PowerShell側から後で変更できるようにしてください。
# 7. ダブルタップ判定はC#側で行う
Capsキーの、
```text
VK判定
ScanCode判定
DOWN判定
時間差判定
ダブルタップ成立判定
```
は、できるだけ `Add-Type` 内のC#側で完結させてください。
ダブルCapsが成立したら、
```csharp
DoubleCaps
```
のようなイベントを発生させ、PowerShell側ではフォームの表示/非表示処理だけを担当する構造にしてください。
# 8. 現段階ではCaps入力をブロックしない
重要です。
ダブルCaps検出は、この方式ですでに実機で正常動作しています。
現段階ではCaps入力に対して、
```csharp
return (IntPtr)1;
```
を使用して、入力をWindowsから抑止しないでください。
Caps入力についても最終的に、
```csharp
CallNextHookEx(...)
```
へ流してください。
以前、Caps入力を抑止する実装を試したところ、状態判定が崩れた際に他のキーまで反応しなくなる問題が発生しました。
そのため、
**まずは現在動作しているダブルCaps検出を壊さないことを最優先**
としてください。
Caps本来の機能を無効化する処理は、必要になった場合に別工程として追加します。
# 9. Capsダブルタップでフォームを表示/非表示する
Capsを素早く2回押したら、フォームの表示状態をトグルしてください。
## フォーム表示中
```text
Caps
Caps
```
↓
フォームを非表示。
最小化ではなく、
```powershell
$form.Hide()
$form.ShowInTaskbar = $false
```
を使用してください。
結果として、
```text
フォーム
→ 消える
タスクバー
→ 消える
タスクトレイ
→ 常駐継続
```
となるようにしてください。
## フォーム非表示中
再び、
```text
Caps
Caps
```
↓
フォームを再表示してください。
再表示位置は、**現在のマウスカーソルの右側**です。
# 10. カーソル右側への表示
カーソル位置取得には、Win32 APIの、
```text
GetCursorPos
```
を使用してください。
基本表示位置は、
```text
X = カーソルX + 20px
Y = カーソルY
```
としてください。
フォームの移動には必要に応じて、
```text
SetWindowPos
```
を使用してください。
# 11. マルチモニターと画面外補正
マルチモニター環境に対応してください。
現在のカーソル位置から、
```powershell
[System.Windows.Forms.Screen]::FromPoint(...)
```
を使用して対象モニターを取得してください。
そのモニターの、
```powershell
$screen.WorkingArea
```
を基準として、フォームが画面外へはみ出さないようにしてください。
補正ルールは、
```text
カーソル右側にフォームが入る
↓
カーソル右側へ表示
右側にはみ出す
↓
カーソル左側へ表示
左端にはみ出す
↓
WorkingArea.Leftへ補正
下端にはみ出す
↓
WorkingArea.Bottom - Form.Height
上端にはみ出す
↓
WorkingArea.Top
```
としてください。
# 12. 再表示したフォームを前面へ出す
フォーム再表示後は、
```text
SetForegroundWindow
$form.Activate()
$form.BringToFront()
```
などを使用して、可能な範囲でフォームを前面へ出してください。
# 13. UIスレッドへ処理を渡す
低レベルキーボードフックのコールバックからWindows FormsのUIを直接操作しないでください。
ダブルCaps成立後はPowerShell側で、
```powershell
$form.BeginInvoke(...)
```
などを使用し、UIスレッドへ処理を渡してください。
# 14. フォーム表示状態を管理する
必要に応じて、
```powershell
$script:FormVisible = $true
```
のような状態変数を使用してください。
動作イメージ:
```text
FormVisible = true
↓
Capsダブルタップ
↓
Hide
↓
FormVisible = false
FormVisible = false
↓
Capsダブルタップ
↓
Show
↓
FormVisible = true
```
# 15. タスクトレイへ常駐させる
`System.Windows.Forms.NotifyIcon` を使用してください。
例えば、
```powershell
$tray = New-Object System.Windows.Forms.NotifyIcon
$tray.Icon = [System.Drawing.SystemIcons]::Application
$tray.Text = "アプリ名"
$tray.Visible = $true
```
のような構造です。
Windows側の仕様で、アイコンが通知領域の「^」内に収納されることは問題ありません。
# 16. タスクトレイの右クリックメニュー
最低限、
```text
表示
隠す
----------------
終了
```
を作成してください。
## 「表示」
フォームが非表示なら、
```text
現在のカーソル右側へ表示
```
してください。
すでに表示されている場合は、
```powershell
$form.Activate()
$form.BringToFront()
```
などで前面へ出してください。
## 「隠す」
フォームを、
```powershell
$form.Hide()
$form.ShowInTaskbar = $false
```
で非表示にしてください。
## 「終了」
本当にアプリを終了してください。
終了時には最低限、
```text
キーボードフック解除
↓
NotifyIcon.Visible = false
↓
NotifyIcon.Dispose()
↓
フォームを閉じる
↓
Application.Exit()
```
を確実に実行してください。
# 17. ×ボタンではアプリを終了しない
フォーム右上の×ボタンを押してもアプリを終了させないでください。
`FormClosing` で通常終了をキャンセルし、
```powershell
$e.Cancel = $true
```
として、フォームをタスクトレイへ収納してください。
つまり、
```text
×ボタン
↓
フォーム非表示
↓
タスクバーから消える
↓
PowerShellプロセスは生存
↓
タスクトレイも生存
↓
キーボードフックも生存
```
としてください。
本当に終了するのは、
```text
タスクトレイ
↓
右クリック
↓
終了
```
の場合だけです。
必要なら、
```powershell
$script:ReallyExit = $false
```
のような終了フラグを使用してください。
# 18. ShowDialogは使用しない
これは重要です。
タスクトレイ常駐版では、
```powershell
$form.ShowDialog()
```
を使用しないでください。
以前、`ShowDialog()` で表示していたフォームに、
```powershell
$form.Hide()
```
を実行したところ、`ShowDialog()` が終了し、そのままPowerShellスクリプト自体が終了してしまいました。
そのため、
```powershell
$form.Show()
[System.Windows.Forms.Application]::Run()
```
または同等の構造を使用してください。
フォームが非表示になっても、
```text
PowerShellプロセス
Windows Formsメッセージループ
NotifyIcon
キーボードフック
```
がすべて生存し続けることが必須です。
# 19. 終了時の後始末
`try / finally` 等を使用して、正常終了時だけでなく可能な範囲で後始末をしてください。
最低限、
```text
UnhookWindowsHookEx
イベント解除
NotifyIcon.Visible = false
NotifyIcon.Dispose()
```
を実行してください。
# 20. 既存アプリへ組み込む場合
既存アプリ全体をいきなり書き換えないでください。
まず既存コードを確認し、
```text
現在のフォーム生成方法
現在のShowDialog / Application.Runの使用状況
現在の終了処理
FormClosing処理
既存のWin32 API
既存のAdd-Type
既存のNotifyIcon
```
との競合がないか確認してください。
そのうえで、概ね次の順番で統合してください。
```text
1. Capsキー ダブルタップ検出クラス
2. 必要なWin32 API
3. アプリの状態管理
4. NotifyIcon
5. タスクトレイメニュー
6. Hide-MainWindow
7. Show-MainWindowAtCursor
8. DoubleCapsイベント
9. FormClosing
10. 終了処理
11. Application.Run方式への統合
```
# 21. 既存アプリがShowDialogを使っている場合
既存コードの最後などに、
```powershell
[void]$form.ShowDialog()
```
が存在する場合、そのままタスクトレイ常駐機能を追加しないでください。
まず常駐方法との競合を確認し、
```powershell
$form.Show()
[System.Windows.Forms.Application]::Run()
```
を基本とした構造へ適切に変更してください。
ただし、既存アプリの処理を壊さないように変更してください。
# 22. コードの見出し・説明コメント
生成するコードの見出しや説明コメントは、原則日本語にしてください。
特に、以下のような日本語見出しを使用してください。
```powershell
# ============================================================
# 必要なアセンブリの読み込み
# ============================================================
# ============================================================
# Capsキー ダブルタップ検出
# ============================================================
# ============================================================
# Win32 API
# ============================================================
# ============================================================
# アプリの状態管理
# ============================================================
# ============================================================
# メインフォーム
# ============================================================
# ============================================================
# タスクトレイ
# ============================================================
# ============================================================
# タスクトレイメニュー
# ============================================================
# ============================================================
# ウィンドウを隠す処理
# ============================================================
# ============================================================
# ウィンドウをカーソル横に表示する処理
# ============================================================
# ============================================================
# Capsキー ダブルタップ時の処理
# ============================================================
# ============================================================
# ×ボタンを押したときの処理
# ============================================================
# ============================================================
# タスクトレイ「表示」
# ============================================================
# ============================================================
# タスクトレイ「隠す」
# ============================================================
# ============================================================
# タスクトレイ「終了」
# ============================================================
# ============================================================
# Capsキー ダブルタップ判定時間
# ============================================================
# ============================================================
# キーボードフック開始
# ============================================================
# ============================================================
# アプリ起動
# ============================================================
# ============================================================
# 終了時の後始末
# ============================================================
```
# 23. 完成後に必要な動作
最終的に、以下の動作にしてください。
```text
アプリ起動
↓
フォーム表示
↓
Capsを素早く2回
↓
フォーム非表示
↓
タスクバーから消える
↓
タスクトレイには常駐
↓
Capsをもう一度素早く2回
↓
現在のマウスカーソル右側へフォーム表示
```
さらに、
```text
×ボタン
→ 終了せず非表示
タスクトレイ「表示」
→ フォーム表示
タスクトレイ「隠す」
→ フォーム非表示
タスクトレイ「終了」
→ 完全終了
```
としてください。
# 24. 将来的なCaps+キーのショートカット
将来的にはCapsキーを独自ショートカットレイヤーとして使用する予定です。
例えば、
```text
Caps + A
Caps + S
Caps + D
Caps + 1
Caps + 2
```
などです。
そのため、将来的に、
```text
Capsダブルタップ
→ フォーム表示/非表示
Caps + 任意キー
→ 個別ショートカット
```
を共存させられる設計にしてください。
ただし、**現段階ではCaps+キーのショートカット機能を無理に実装しないでください。**
まず、
```text
VK = 0xF0
ScanCode = 0x3A
DOWN
```
の2回検出による、現在正常動作しているダブルCaps機能を維持することを優先してください。
# 25. 特に変更してはいけないポイント
以下は実機テストによって判明した重要事項です。
勝手に一般的なCapsLock実装へ変更しないでください。
```text
× VK_CAPITAL = 0x14 のみで判定
× Caps DOWN → Caps UP を1タップとして判定
× UPイベントを待ってダブルタップ判定
× Caps入力を最初から return (IntPtr)1 で抑止
```
現在動作確認済みなのは、
```text
VK = 0xF0
ScanCode = 0x3A
DOWN
```
を検出し、
```text
1回目のDOWN
↓
100~650ms
↓
2回目のDOWN
↓
ダブルCaps成立
```
とする方式です。
**この動作確認済み部分を維持してください。**
# 26. 出力してほしいもの
既存アプリのコードを渡した場合は、まずコードを確認してください。
そのうえで、
```text
・どこへ追加するのか
・どこを変更するのか
・どこを削除/置換するのか
・既存処理と競合する部分はないか
・ShowDialogを変更する必要があるか
```
を説明してください。
その後、**PowerShell 5.1でそのまま実行できる完全な修正版コードを全文で提示してください。**
差分だけではなく、可能であれば完成した全文を提示してください。
また、コード内のセクション見出しと説明コメントは原則として日本語にしてください。