どのキーが押されたのかはGetAsyncKeyState関数で確認
キーボードのどのキーが押されたのかをVBAで取得するには、Win32APIのGetAsyncKeyState関数を使います。
キーの押し方には、文字入力のために使う単独キー入力の場合と、ショートカットとして複数のキーが同時に押される場合がありますが、いずれの場合もGetAsyncKeyState関数で取得できます。
なお、キーを「押す」場合にはVBAのSendKeysステートメントを使います。
SendKeysの詳細については「VBAで疑似的にキーボード入力を行う(SendKeys)」をご参照ください。
GetAsyncKeyState関数は無限ループとSleep関数と一緒に使う
GetAsyncKeyState関数を単独で使うことはまずありません。ほとんどの場合は無限ループとセットになっています。
なぜかと言うと、キーが押されたことを知りたい、ということは、何かしらユーザーのキーボード操作を待っている状態であることが前提になります。ユーザーの操作を待っている、ということは、操作がされるまで待ち続けることになりますが、その「待ち続ける」処理は無限ループで実現するのが一般的です。
例えて言えば、ゲームのキャラクターをコントローラーの十字キーの代わりに矢印の↑→←↓キーで操作している場合などです。ゲームが終わるまでキー操作が行われることを常に監視しなければならないため、結果としてゲームオーバーなどのなんらかの停止条件に達するまで、無限ループをしながらキー操作の判定を行うことになります。
ただし、無限ループをずっと続けることはCPUの占有に繋がるため、Win32APIのSleep関数を使ってCPUを一定時間休止させることも必要です。そのCPUの休止を判定するためにタイマーを使ってどれだけ時間が経過したかを判定することも必要になります。
これらのことから、必然的にGetAsyncKeyState関数を使う場合は、無限ループとSleep関数と時間の経過判定を行うことになります。
もちろんそうでない場合もありますがかなり特殊なケースです。ここではそのような特殊なケースや無理やりのGetAsyncKeyState関数を動かすだけのサンプルコードではなく、一般的な使い方でのサンプルコードを紹介します。
構文
64bit版です。モジュールに以下の構文をそのまま書いておくと利用できます。一般的には標準モジュールの先頭に書くことが多いです。
Declare PtrSafe Function GetAsyncKeyState Lib “user32” (ByVal vKey As Long) As Integer
vKey | 押したことを確認したいキーを指定します。KeyCodeConstants定数を指定します。定数の数値を直接しても構いません。
KeyCodeConstants定数の詳細は「VBAのキーコード一覧」をご参照ください。 |
||||||||||
戻り値(Integer) | 4種類の数値を返します。詳細は後述します。
|
GetAsyncKeyState関数の戻り値の詳しい話
GetAsyncKeyState関数の戻り値について上では概要だけ書いてますが、少し細かく書きます。
GetAsyncKeyState関数の戻り値はC言語のshort型という2バイト(16bit)のサイズになります。short型は範囲として-32768から32767の値が使えます。VBAでは同じ2バイトのInteger型が使われています。16ビットということは2の16乗の範囲でデータを持つことが出来ます。0から65535の範囲ですが、Integer型はマイナス値も含めるため-32768から32767の範囲になります。
そして、ここが一番特殊な話ですが、GetAsyncKeyState関数の戻り値は、Integer型を2進数として扱った場合の一番左(最上位)の値が1であればキーが押されている、0であれば押されていない、と判定し、一番右(最下位)の値が1であれば前回GetAsyncKeyState関数を呼び出したあとにキーが押されている、0であれば押されていない、と判定します。
整理すると以下のようになります。
2進数 | 10進数 | 16進数 | キーが押されているか? | 前回のGetAsyncKeyState関数呼び出し後に押された? |
---|---|---|---|---|
0000 0000 0000 0000 | 0 | &H0 | 押されていない | 押されていない |
0000 0000 0000 0001 | 1 | &H1 | 押されていない | 押された(別アプリが呼び出した可能性あり) |
1000 0000 0000 0000 | -32768 | &H8001 | 押されている | 押されていない |
1000 0000 0000 0001 | -32767 | &H8000 | 押されている | 押された(別アプリが呼び出した可能性あり) |
戻り値の-32768と-32767がなんでマイナスの値なのかですが、最上位ビットに1が設定されていることが原因です。通常使う10進数のマイナスを表す場合は-記号を使うルールですが、2進数の場合はマイナス記号(負号)を使わずに一番左側にある最上位ビットが1の場合はマイナスとして扱うルールになっています。そのため、マイナス値の戻り値になります。詳しいことを知りたい場合は「2の補数」を調べてみてください。
このように2進数の各ビット値の1と0でなんらかの状況のONとOFFを表現するものがWin32APIではよくあります。
最下位ビットの判定は注意が必要です。「最下位ビットが1だから、キーが押されたことを初めて検知した」と判断したくなりますが、それは誤検知の恐れがあります。他のアプリケーションでGetAsyncKeyState関数を利用している場合はそれがOS全体に引き継がれるため、その場合には正しく判定できません。
そのため、Microsoftのヘルプページでは「最下位ビットの動作は信頼しないでください」という趣旨のことが書いてあります。どのキーが押されたかではなく、キーが押されたかどうかを優先して判定したいのであればGetAsyncKeyState関数を使うのではなく、フォームでのKeyDownイベントやKeyPressイベントを使うことをお勧めします。
サンプルコード
以下のコードはGetAsyncKeyState関数を使う際の無限ループの仕組みとSleep関数を使ったCPU占有の解放も考慮しています。
Win32APIを3つ使っています。GetAsyncKeyState関数、Sleep関数、GetTickCount関数です。Sleep関数は引数の値をミリ秒スリープします。GetTickCount関数はPCが起動してからの経過時間をミリ秒単位で返します。
Sleep関数についての詳細は「VBAの処理を一時停止する(Sleep、Wait)」をご参照ください。
VBAの関数が2つありますが、GetAsyncKeyStateTest関数が実行する側のメイン関数で、ChkKeyPush関数がGetAsyncKeyState関数を使って指定したキーが押されたかどうかを判定します。
1つ目のGetAsyncKeyStateTest関数はAキーとShiftキーが押されたときにイミディエイトウィンドウに”A”、”Shift”を出力し、Qキーが押されたときに”Quit”と出力して無限ループを終了して関数を終了します。無限ループ+スリープ+DoEventsでのWindows制御可を実装しているため一般的な常時稼働中でのキーが押されたことの判定処理として利用できます。
2つ目のChkKeyPush関数がGetAsyncKeyState関数を実際に利用している箇所です。&H8000で最上位ビットが1かどうかの判定も入れてます。最上位ビットを判定しない場合、レアケースではありますが他アプリケーションで指定キーと同じキーをGetAsyncKeyState関数で判定している場合に、戻り値の最下位ビットに1が設定され、「キーが初めて押された=True」と誤検知する恐れがあるためそれを防いでいます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
Declare PtrSafe Sub Sleep Lib "kernel32" (ByVal dwMilliseconds As Long) Declare PtrSafe Function GetAsyncKeyState Lib "user32" (ByVal vKey As Long) As Integer Declare PtrSafe Function GetTickCount Lib "kernel32" () As Long Sub GetAsyncKeyStateTest() Dim bEndFlg As Boolean '// ループ終了フラグ(True:ループ継続、False:ループ終了) Dim lStartTimer As Long '// 基点時刻 Dim sMsg As String '// 出力MSG '// ループ継続 bEndFlg = True '// 基点時刻を取得 lStartTimer = GetTickCount Do '// ループ終了フラグが「ループ終了」の場合 If bEndFlg = False Then '// ループを抜ける Exit Do End If Dim bKeyA As Boolean Dim bKeyShift As Boolean sMsg = "" '// Qが押された場合 If (ChkKeyPush(vbKeyQ) = True) Then '// ループ終了フラグを「ループ終了」に更新 bEndFlg = False Debug.Print "Quit" End If '// Aが押された場合 If (ChkKeyPush(vbKeyA) = True) Then bKeyA = True Else bKeyA = False End If '// Shiftが押された場合 If (ChkKeyPush(vbKeyShift) = True) Then bKeyShift = True Else bKeyShift = False End If '// Aが押されていた場合 If bKeyA = True Then sMsg = "A " End If '// Shiftが押されていた場合 If bKeyShift = True Then sMsg = sMsg & "Shift " End If If sMsg <> "" Then Debug.Print sMsg End If '// Windowsに制御を渡す DoEvents '// 0.1秒経過するまでスリープ Do While GetTickCount - lStartTimer < 100 '// CPUを休ませる(ループ処理にCPUが占有されないようにして負荷を下げる) Call Sleep(1) Loop '// 基点時刻を再取得 lStartTimer = GetTickCount Loop End Sub Function ChkKeyPush(a_iKeyCode) '// 指定キーが押された If (GetAsyncKeyState(a_iKeyCode) And &H8000) Then ChkKeyPush = True '// 指定キーが押されていない Else ChkKeyPush = False End If End Function |
実行結果
AキーとShiftキーをそれぞれ押して、Qキーで終了した場合の例です。キーを押し続けていると連続して出力されます。
A
A
Shift
Shift
Shift
A Shift
A Shift
A Shift
A Shift
A Shift
A Shift
Shift
Quit