【実務・中級編】Windows FormsにおけるSendKeysの限界を突破する:UI自動テストや業務ツールで確実にキー入力をシミュレートするP/Invoke活用術 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

Windows FormsにおけるSendKeysの限界を突破する:UI自動テストや業務ツールで確実にキー入力をシミュレートするP/Invoke活用術

レガシーな業務システムの自動化や、Windows Forms(WinForms)におけるUI自動テストの構築で、幾度となく開発者の心を折ってきた存在――それが `SendKeys.Send` メソッドだ。

「開発環境では動いたのに、本番のクライアント端末(タスクスケジューラーやリモートデスクトップ接続時)でキー入力が明後日の方向へ飛んでいく」
「処理速度を上げようと連続して呼び出すと、キーの取りこぼしや修飾キー(Shift, Ctrl)のロストが頻発する」

もしあなたがこうした現場の闇に直面しているなら、今すぐ `SendKeys` を捨てるべきだ。あれは「お祈りプログラミング」の温床にすぎない。

今回は、Windows OSの深部へとダイレクトにアプローチし、数千回の連続入力であっても1ミリ秒の狂いもなくキーイベントを注入する P/Invoke(`SendInput` API) を用いた、極限まで堅牢なキーボード・マウス自動化の極意を伝授する。

—

なぜ `SendKeys` は実務で使えないのか?

`SendKeys.Send` や `SendKeys.SendWait` の根本的な問題は、そのアーキテクチャの脆弱さにある。

1. アクティブウィンドウへの依存性
`SendKeys` は、OSが現在「アクティブ」と認識しているウィンドウに対してメッセージキューを送りつける。そのため、バックグラウンド処理や、画面描画のわずかな遅延、ポップアップの割り込みによって、意図しない別のアプリケーションに機密データやコマンドが打ち込まれるという致命的なインシデントを引き起こす。
2. 非決定的なタイミング(Timing Issues)
内部でメッセージの処理を待機する仕組みが非常に曖昧であり、特に負荷の高い業務サーバーや仮想環境(CitrixやRDP)では、キーの「押下(Down)」と「解放(Up)」のペアが崩れやすい。結果として「Ctrlキーが押しっぱなしになったままになる」といった現象が起きる。

業務自動化ツールやテスト自動化において「確率で動く」は「動かない」と同義だ。我々は決定論的なコードを書かなければならない。

—

解決策:`SendInput` API による完全制御

Windows APIの `SendInput` 関数は、キーボードやマウスのイベントをハードウェアドライバレベルと同等の精度で入力ストリームに注入する。`keybd_event` という古いAPIもあるが、Microsoftは現在、よりモダンで拡張性の高い `SendInput` の使用を推奨している。

これをVB.NETから安全かつエレガントに呼び出すための設計を見ていこう。

プロダクションコード:堅牢な `InputSimulator` クラス

以下のコードは、そのままプロジェクトに組み込んで即座に利用できる、洗練されたラッパーモジュールである。エラーハンドリングと構造体の正確なアラインメントを考慮している。

Imports System.Runtime.InteropServices
Imports System.Threading

Public NotInheritable Class InputSimulator

‘ — Win32 API 定数定義 —
Private Const INPUT_KEYBOARD As Integer = 1

Private Const KEYEVENTF_KEYDOWN As UInteger = &H0
Private Const KEYEVENTF_KEYUP As UInteger = &H2
Private Const KEYEVENTF_EXTENDEDKEY As UInteger = &H1

‘ — Win32 API 構造体 —

Private Structure INPUT
Public type As Integer
Public u As InputUnion
End Structure


Private Structure InputUnion
Public ki As KEYBDINPUT
‘ マウスやハードウェア入力を使う場合の拡張領域は省略
End Structure


Private Structure KEYBDINPUT
Public wVk As UShort
Public wScan As UShort
Public dwFlags As UInteger
Public time As UInteger
Public dwExtraInfo As IntPtr
End Structure

‘ — P/Invoke 宣言 —

Private Shared Function SendInput(
ByVal nInputs As UINT,
ByVal pInputs() As INPUT,
ByVal cbSize As Integer
) As UINT
End Function

‘ 互換性のための定義
Private Shadows Enum UINT As UInt32
[Default] = 0
End Enum

”’

”’ 指定した仮想キーを確実に入力(押下して離す)します。
”’

”’ VirtualKey (Keys列挙体) Public Shared Sub PressKey(key As Keys)
Dim inputs(1) As INPUT

‘ 1. キーを押す
inputs(0) = CreateKeyboardInput(key, KEYEVENTF_KEYDOWN)

‘ 2. キーを離す
inputs(1) = CreateKeyboardInput(key, KEYEVENTF_KEYUP)

‘ 一括送信(アトミックな実行に近い挙動を保証)
Dim result = SendInput(CUInt(inputs.Length), inputs, Marshal.SizeOf(GetType(INPUT)))

If result <> inputs.Length Then
Throw New InvalidOperationException($”SendInput failed. ErrorCode: {Marshal.GetLastWinError()}”)
End If

‘ 連続入力時のOS側処理バッファ溢れを防ぐ微小ウェイト
Thread.Sleep(10)
End Sub

”’

”’ 文字列を一文字ずつ確実に送信します(特殊文字エスケープ考慮版)
”’

Public Shared Sub SendString(text As String)
For Each c As Char In text
‘ 文字から仮想キーコードと言語レイアウトへの変換は複雑なため、
‘ 基本的なASCII文字と数値、記号に絞ったマッピング、あるいは
‘ VK_PACKET (Unicode直接注入) を利用するのがプロの技。
SendUnicodeChar(c)
Thread.Sleep(5) ‘ 描画・処理落ちを防ぐインターバル
For Next
End Sub

”’

”’ VK_PACKETを使用して、キーボードレイアウトに依存せず任意のUnicode文字を送信する
”’

Private Shared Sub SendUnicodeChar(c As Char)
Dim inputs(1) As INPUT
Dim vk As UShort = Convert.ToUInt16(c)

‘ VK_PACKETを使ったUnicode直打ち
‘ 押下
inputs(0).type = INPUT_KEYBOARD
inputs(0).u.ki.wVk = 0
inputs(0).u.ki.wScan = vk
inputs(0).u.ki.dwFlags = &H4 ‘ KEYEVENTF_UNICODE
inputs(0).u.ki.time = 0
inputs(0).u.ki.dwExtraInfo = IntPtr.Zero

‘ 解放
inputs(1).type = INPUT_KEYBOARD
inputs(1).u.ki.wVk = 0
inputs(1).u.ki.wScan = vk
inputs(1).u.ki.dwFlags = &H4 Or KEYEVENTF_KEYUP
inputs(1).u.ki.time = 0
inputs(1).u.ki.dwExtraInfo = IntPtr.Zero

SendInput(CUInt(inputs.Length), inputs, Marshal.SizeOf(GetType(INPUT)))
End Sub

Private Shared Function CreateKeyboardInput(key As Keys, flag As UInteger) As INPUT
Dim inp As New INPUT
inp.type = INPUT_KEYBOARD
inp.u.ki.wVk = CUShort(key)
inp.u.ki.wScan = 0
inp.u.ki.dwFlags = flag
inp.u.ki.time = 0
inp.u.ki.dwExtraInfo = IntPtr.Zero
Return inp
End Function

End Class

—

アーキテクトが教える:実務導入における3つの鉄則

この `InputSimulator` を実際の業務システムやDB連携ツールに組み込む際、以下の設計原則を遵守してほしい。

1. ターゲットウィンドウの強制フォーカス制御(安全弁)

`SendInput` はウィンドウを限定しないため、誤作動を防ぐために「操作対象のプロセス・ウィンドウが確実に手前に来ていること」をコード側で担保してから実行する必要がある。

‘ 対象のプロセスをアクティブにする処理の例
Dim p() As Process = Process.GetProcessesByName(“TargetApp”)
If p.Length > 0 Then
Dim hWnd As IntPtr = p(0).MainWindowHandle
‘ 最小化されている場合は元に戻す
ShowWindow(hWnd, SW_RESTORE)
‘ 前面へ強制移動
SetForegroundWindow(hWnd)

‘ ウィンドウがアクティブになるのを数ミリ秒待機
Thread.Sleep(100)

‘ ここで初めて自動入力を実行
InputSimulator.SendString(“AutoGeneratedData_001”)
End If

(※ `ShowWindow` や `SetForegroundWindow` も同様に `user32.dll` からP/Invokeでインポート可能)

2. データベースやファイル連携時のトランザクション整合性

業務ツールでよくある要件として、「DBからデータを取得し、画面にキー入力で転記し、完了したらDBのステータスを更新する」というシナリオがある。
ここでキー入力が途中で失敗した場合、DBだけが「処理済み」になってしまう不整合(ゴーストデータ)が起きる。

  • 対策: キー入力を伴うUI自動化の前後を、必ず堅牢なログ出力と例外ハンドリングで囲むこと。また、可能であればUI操作ではなく、直接APIや内部ロジックを叩くアーキテクチャ(レイヤードアーキテクチャの適用)に変更すべきだが、どうしても画面操作が必要なレガシー環境では、UI操作部分を「リトライ可能な独立したトランザクション単位」としてカプセル化せよ。

3. ハードコードされたウェイト(Sleep)の排除とイベント同期待ち

「とりあえず動かすために `Thread.Sleep(1000)` を大量に挟む」という実装は、開発者が最もやってはいけない怠慢だ。システムが重い環境では1秒でも足りず、軽い環境では無駄に処理が遅くなる。
画面のコントロールの有効化・無効化状態、またはテキストボックスの値が変化したことを検知するポーリング処理(Timeout付きの `While` ループ)を組み合わせ、「状態が変化したら即座に進む」非同期的な同期設計を心がけよう。

—

まとめ

`SendKeys` という過去の遺物に頼る時代は終わった。
P/Invokeを活用した `SendInput` の実装は、一見するとコード量が増え、アンマネージドコードの知識が必要なためハードルが高く見えるかもしれない。しかし、一度この堅牢なラッパーを手に入れてしまえば、二度と「本番環境での謎の入力ミス」に怯える夜は訪れなくなる。

プロのエンジニアとしての誇りにかけて、不安定な自動化スクリプトから脱却し、OSの深部を掌握した強靭なシステムを構築してほしい。

タイトルとURLをコピーしました