Windows FormsにおけるSendKeysの限界を突破する:UI自動テストや業務ツールで確実にキー入力をシミュレートするP/Invoke活用術
レガシーな業務システムやWindows Forms(WinForms)アプリケーションの自動化、あるいは他システムとのインテグレーションにおいて、避けて通れないのがキーボードおよびマウス入力のシミュレーションだ。
VB.NET開発者の多くは、手始めに `System.Windows.Forms.SendKeys.Send` を使うだろう。しかし、本番環境や負荷の高いマシン、あるいは複雑なマルチスレッド環境において、`SendKeys` が期待通りに動作せず、文字化落としやフォーカス喪失といった謎の不具合に泣かされた経験はないだろうか。
本稿では、`SendKeys` が抱える構造的な欠陥を暴き、それを根本から解決するための Windows API (P/Invoke) を用いた `SendInput` による極限のキー入力制御について、現場で使える実用コードと共に解説する。
—
1. なぜ `SendKeys` は実務で使えないのか?
`SendKeys.Send` や `SendKeys.SendWait` は手軽だが、内部実装において致命的な弱点を抱えている。
1. メッセージキューの非同期性: `SendKeys` は背後でWindowsのメッセージキューにキーイベントを投げるが、対象アプリケーションの処理負荷が高い場合、イベントの取りこぼしが発生する。
2. フォーカスの脆弱性: 入力を実行する瞬間までにユーザーが別のウィンドウをクリックしたり、ポップアップが表示されたりすると、全く関係のないアプリケーションにキーが送信される(セキュリティ上のリスクでもある)。
3. 修飾キー(Shift, Ctrl, Alt)の状態不整合: 物理的なキーボードの状態と `SendKeys` が認識している状態にズレが生じ、キーが「押しっぱなし」の状態でフリーズする現象(Sticky Keys問題)が多発する。
業務自動化ツールやUI自動テストにおいて、「動いたり動かなかったりする」という挙動は、システムへの信頼性を失墜させる最大の要因だ。我々は、OSの入力サブシステム(User32.dll)を直接叩き、ハードウェアレベルと同等の確実性を持つアプローチを採用しなければならない。
—
2. `keybd_event` から `SendInput` へのパラダイムシフト
レジデントコードや古いVB6の遺産では `keybd_event` APIがよく使われていたが、Microsoftは現在、このAPIを非推奨(Legacy)としている。現代のWindows環境(Windows 7以降、特にWindows 10/11)において、入力シミュレーションの黄金律は `SendInput` 関数である。
`SendInput` は、キーボード、マウス、およびハードウェアレスな入力イベントのストリームを、OSの入力スレッドに対してアトミック(不可分)にインジェクションする。これにより、他のプロセスに割り込まれることなく、確実にキーシーケンスを実行できる。
—
3. 実装:VB.NETによる `SendInput` ラッパーの構築
ここからは、実務の現場でそのままコピー&ペーストして即座に組み込める、堅牢なクラスモジュールの実装を示す。構造体の正確なマッピングと、P/Invokeにおける適切なアンマネージド・メモリの扱いがキモとなる。
Imports System.Runtime.InteropServices
”’
”’
Public NotInheritable Class NativeInputSimulator
Private Sub New()
‘ 静的クラスのためインスタンス化させない
End Sub
Region “Win32 API Declarations”
Private Shared Function SendInput(
ByVal cInputs As UInt32,
ByVal cbSize As Integer
) As UInt32
End Function
Private Shared Function GetMessageExtraInfo() As IntPtr
End Function
‘ 定数定義
Private Const INPUT_KEYBOARD As UInt32 = 1
Private Const KEYEVENTF_KEYDOWN As UInt32 = &H0
Private Const KEYEVENTF_KEYUP As UInt32 = &H2
Private Const KEYEVENTF_EXTENDEDKEY As UInt32 = &H1
Region “Structures”
Private Structure INPUT
Public type As UInt32
Public data As INPUT_UNION
End Structure
Private Structure INPUT_UNION
‘ マウスやハードウェア入力に対応させるためのパディング(今回はキーボード特化)
End Structure
Private Structure KEYBDINPUT
Public wVk As UShort
Public wScan As UShort
Public dwFlags As UInt32
Public time As UInteger
Public dwExtraInfo As IntPtr
End Structure
Private Structure MOUSEINPUT
Public dx As Integer
Public dy As Integer
Public mouseData As UInteger
Public dwFlags As UInteger
Public time As UInteger
Public dwExtraInfo As IntPtr
End Structure
Private Structure HARDWAREINPUT
Public uMsg As UInteger
Public wParamL As UShort
Public wParamH As UShort
End Structure
End Region
End Region
”’
”’
”’ 仮想キーコード (例: Keys.A)
Public Shared Sub SendKeyPress(ByVal vk As Keys)
Dim inputs(1) As INPUT
‘ 1. キーダウンイベント
inputs(0).type = INPUT_KEYBOARD
inputs(0).data.ki = New KEYBDINPUT() With {
.wVk = CUShort(vk),
.wScan = 0,
.dwFlags = KEYEVENTF_KEYDOWN,
.time = 0,
.dwExtraInfo = GetMessageExtraInfo()
}
‘ 2. キーアップイベント
inputs(1).type = INPUT_KEYBOARD
inputs(1).data.ki = New KEYBDINPUT() With {
.wVk = CUShort(vk),
.wScan = 0,
.dwFlags = KEYEVENTF_KEYUP,
.time = 0,
.dwExtraInfo = GetMessageExtraInfo()
}
‘ OSに一括送信
Dim result As UInt32 = SendInput(CUShort(inputs.Length), inputs, Marshal.SizeOf(GetType(INPUT)))
If result = 0 Then
Dim errorCode As Integer = Marshal.GetLastWin32Error()
Throw New System.ComponentModel.Win32Exception(errorCode, $”SendInputの実行に失敗しました。Error Code: {errorCode}”)
End If
End Sub
”’
”’
Public Shared Sub SendString(ByVal text As String)
If String.IsNullOrEmpty(text) Then Return
For Each c As Char In text
‘ 簡易的な文字送信の実装(実際にはVK_PACKETや文字コードに応じたシフトキー制御が必要な場合あり)
‘ ここでは主要な英数記号を想定したプレースホルダー
Dim vk As Keys = CType(CByte(Asc(c)), Keys)
SendKeyPress(vk)
‘ 高速処理による取りこぼしを防ぐための極小ウェイト(必要に応じて調整)
System.Threading.Thread.Sleep(10)
Next
End Sub
End Class
—
4. チーフアーキテクトが教える:実運用における極意と落とし穴
このコードを実際のエンタープライズ環境(基幹システムの自動化や、夜間バッチ連動のUI操作など)に投入する際、シニアエンジニアとして知っておくべき「現場の知見」を共有する。
A. フォーカス強奪(Window Activation)との組み合わせ
いかに `SendInput` が確実であっても、対象ウィンドウがアクティブでなければキー入力は迷子になる。操作の直前に必ず `SetForegroundWindow` APIを呼び出し、対象プロセスのメインウィンドウをアクティブに強制すること。
Private Shared Function SetForegroundWindow(ByVal hWnd As IntPtr) As Boolean
End Function
B. UIスレッドのディスパッチウェイト
最新のCPUや仮想環境(Citrix/RDPなど)では、入力イベントが早すぎてアプリケーション側のメッセージループが描画や入力受付に追いつかないことがある。`SendInput` の連続呼び出しの間には、数ミリ秒(10ms〜50ms程度)の微小なインターバルを入れるのが、現場での泥臭いが確実な防衛策となる。
C. 管理対象外メモリとガベージコレクション
今回の `SendInput` 実装では、マネージド配列 `INPUT()` をネイティブ境界に渡している。`.NET Framework` および `.NET (Core/.NET 5+)` の相互運用マーシャラーは優秀であるが、高頻度でこのメソッドを数万回ループさせるようなベンチマークテストを行う場合、ガベージコレクタ(GC)のプレッシャーになる。
極限のパフォーマンスを求める場合は、`Marshal.AllocHGlobal` を用いてアンマネージドヒープ上に構造体配列を直接展開し、処理後に手動で解放する(`Marshal.FreeHGlobal`)設計にブラッシュアップせよ。
—
総括
`SendKeys` の手軽さに依存しているうちは、あなたの業務自動化ツールやUIテストは永遠に「不安定な爆弾」を抱えたままだ。
Windows APIの深部を覗き込み、`SendInput` と P/Invoke を完全に見きわめること。それこそが、レガシーからモダンまでを意図通りに支配する、真のプロフェッショナル・エンジニアの姿である。
