適材適所

システム屋のくらげが気ままに書いているブログです。PowerShellやVBAなどプログラミング系の話をメインに書いています。

MENU

VBAのSendKeys,System.Windows.FormsのSendWaitなどが反応しないときに読む記事

注意

長いです!

結論だけ知りたい人は結論へ

結論PostMessage関数を使ってScanCodeもしっかり通知する

プロローグ

Windowsの作業を自動化するときに必要となるキーストローク(キーボードの操作)の自動化。

よく使われるものに、VBAのSendKeysや、.NETのSystem.Windows.Forms.SendKeysのSendwaitなどがあります。

仕事の効率化などでRPA的なことをやろうとすると、キーボードの操作を自動化しなくてはならない場面が出てくることがあります。

それをWindowsの環境でやろうとすると、上述のSendKeysなどを使うわけです。

おそらく世の中の99%のアプリケーションはこれらのキーストロークのエミュレーションを受け付けて、キー操作を実行してくれるはずです。

しかし、世の中にはどうも、そうはいかないアプリケーションがあるようで・・・

中でも、EnterキーやF3などの文字以外のキー操作を自動化しようとしたときに出会う確率が高い気がします。

この記事では不運にもキーボード操作の自動化がうまくできないアプリケーションに出会ってしまったときの確認すべき点と確認の仕方、うまく自動化できる可能性のあるやり方を紹介したいと思います。

Windowsでキーボードが押されたとき何が起こるの?

Windowsのパソコンでキーボードが押されたとき、そもそもアプリケーションはどのようにキーが押されたことや、押されたキーの種類などを知ることができるのでしょうか。

普通、インターネットで調べものをしたいときには、ブラウザに検索したい文字列を入力すると思います。

ブラウザは、一体どのようにしてあなたがキーボードを打ったことや、打った文字の種類を知ることができるのでしょうか。

ブラウザのプログラムの中で、キーボードのドライバーと密接にやりとりをしているのでしょうか。

Excelに文字を打つ場合はどうでしょうか?Excelのなかでも同様にキーボードのドライバーとやりとりをしているのでしょうか。

答えはNoです。

キーボードのキーが押されたとき、アプリケーションごとにそのことをキーボードのドライバーとやりとりをしているわけではありません。

キーボードのキーが押されたとき、オペレーションシステムであるWindowsがアプリケーションにそのことを知らせてくれています。

OSであるWindowsは、キーが押されたとき、「このキーが押し下げられたよ!!」とか「この文字のキーが押されたよ!」「このキーが上げられた(キーの押し下げが終わった)よ!!」ということを「メッセージ」という形でアプリケーションに教えてあげています。

アプリケーション側は、OSに教えてもらったキーの情報(キーが押された、どのキー、文字が押されたか)に基づいて、そのアプリケーションごとの処理を行います。

例えば、ブラウザのアプリケーションであれば、検索バーにカーソルがあるときに文字のキーが入力されたことを知らされれば、その文字を表示するでしょう。また、検索バーの中でエンターが押されたときは検索エンジンで検索を開始することでしょう。

アプリケーションはただ、Windowsから届いたメッセージに従って処理を行っているだけです。

つまり、キーボードの操作は「メッセージ」という形になってアプリケーションに伝えられている、ということです。

そしてWindowsの環境でキーの押下を自動で行うためには、

アプリケーションに、この「メッセージなるもの」を送ってあげる必要がある、ということになります。

メッセージって何?

そもそも、メッセージはどうやったら見る事ができるのでしょうか。

メッセージは、OSとアプリケーション間の会話のようなもので、

OSとアプリケーションの間で交わされる会話を我々は知ることができません。

メッセージのログを可視化することができるSpy++

では、「キーボード操作の自動化を行う際の注意点」と題して話をしている最中になぜメッセージの話を出したのか。

目に見えないものをなぜ出してきたか・・・そうです。世の中にはとっても便利なものを作ってくれている人たちがいます。

そして、そのメッセージを可視化できるツールの一つにSpy++というものがあります。

これはなんと本家本元のMicrosoftから提供されているツールで、メッセージを可視化するだけでなく、

他にも多彩な機能を持ち合わせています。スパイの名に恥じない、とても便利なアプリケーションです。

しかも無料!

Spy++ の概要 - Visual Studio (Windows) | Microsoft Docs

Spy++ですが、導入の詳しい手順についてはここでは割愛します。

検索エンジンで「Spy++」と検索すると、導入の手順をわかりやりやすく解説してくれているサイトがいくつもありますので、

その役目はそちらに譲ることとして、本題に戻ります。

Spy++でメッセージログを可視化してみる

実際にSpy++を使ってWindowsとアプリケーション間のメッセージを可視化したものが次の画像です。

f:id:shinmai_papa:20211028164423p:plain

これは、Windowsの標準のテキストエディタであるメモ帳にカーソルを合わせて

「a」というキーを押したときのメッセージの一部抜粋です。

暗号のような文字がたくさん並んでおり、これだけ見せられても何が何だかわかりませんね。

かく言う私も全ては理解していません。

ここでこの画像を引用したのは、

単純に文字を入力しようとするだけでもOSとアプリケーションの間ではこんなにたくさんのメッセージがやり取りされているんだ、

ということをイメージとして知っておいてもらいたかったからです。

アプリケーションはOSとの間の、これらのメッセージ1つ1つに対して適切に処理をしています。

上記の例では全てのメッセージを表示する設定でしたが、

Spy++ではメッセージの種類を絞り込んで可視化することができます。

ではキーボードが押されたときのメッセージがどのようになっているのか、

Spy++を使って確認してみましょう。

先ほどの例と同じように、メモ帳を起動した状態で「a」という文字を入力したときのメッセージは次のように可視化されました。

f:id:shinmai_papa:20211028164428p:plain

先ほどのように全てのメッセージを表示したときと違い、すっきりとした感じですね。

3つのメッセージが表示されています。

赤く囲ったところを見ると「WM_KEYDOWN」、「WM_CHAR」、「WM_KEYUP」と書いてあります。

つまり、これらのメッセージは、「キーが押されたよ」「文字コード 97の入力だよ」「キーが上がったよ」という一連のメッセージです。

この中でキーボード操作の自動化で重要なのは、2つめのWM_CHARというもの。

これが実際に文字の情報をアプリケーションに通知しています。

また、キーが押されたといった情報以外にも、つらつらと何か書いてあることから、

色々な情報が付加されてアプリケーションに通知されていることがおわかりになるかと思います。

その中でも、この記事で重要な意味を持つのがScanCodeです。

上の例ではScanCodeに1Eがセットされています。

このScanCodeについては後述しますが、これがこの記事の超重要ポイントになります。

さて、これで実際にキーが押されたときにOSからアプリケーションにどんなメッセージが通知されているかがわかりました。

ここまでわかったこところで、いよいよ本題です。

VBAのSendKeysやSystem.Windows.Forms#SendWaitなどを実行してもアプリケーションが反応しないことがあります。

そんなとき、実はこのOSとアプリケーション間のメッセージまで確認しなければいけないことがあるのです。

次にVBAのSendKeysやSystem.Windows.Forms#SendWaitを実行したときに

どのようなメッセージが送信されているか確認してみることにしましょう。

VBAのSendKeysやSystem.Windows.Forms#SendWaitのメッセージ

メモ帳を起動した状態で、VBAの下記のコードを実行し、

Spy++でどのようなメッセージが送信されているか確認してみます。

 
Sub SendKeysSample()
   AppActivate "メモ帳"
   SendKeys "a"
End Sub

上述のコードを実行したときにメモ帳に送られたメッセージをSpy++で可視化したものが次の画像です。

メッセージログを可視化するときは、Spy++のウィンドウで

スパイ⇒メッセージのログ出力⇒ウィンドウの選択⇒メッセージ⇒表示するメッセージ キーボードをチェックしてOK

最後にメッセージタブからログの開始をクリックします。

f:id:shinmai_papa:20211028164433p:plain

WM_CHARの行に注目してみましょう。

<000003>の次にウィンドウハンドル名があります。

ハンドル名とはウィンドウを一意に識別するための数字で、深い意味はありません。

ウィンドウ毎にその都度振られる番号です。

次にWM_CHAR、nVirtualKey'A'と続きます。

これは、Aというキーが押されたという意味です。

Windowsのメッセージの中ではキーボードのキーをVirtual Key、仮想キー、VKなどと読んだりします。

そして注目してほしいのが、ScanCodeの部分。

これはキーボードが生成して送ってくる数字で、一応意味はあるのですが、基本的にWindowsのアプリケーションの中ではこの数字は無視されます。

この数字は仮想キーと対になっており、IBM系のアプリケーションでこの数字を参照することがあります。

この記事の主題である、キーボード操作の自動化時に反応しないときに確認すべき点の一番大事な点は、このScanCodeの部分になります。

f:id:shinmai_papa:20211028164439p:plain

VBAのSendKeysではこのScanCodeは00がセットされてアプリケーションに通知されていることが上の画像で確認できます。

そして、これはSystem.Windows.Forms#SendWaitでも同じです。

また、Wscript ShellのSendKeysでも同じです。

ちなみにSystem.Windows.Forms#SendWaitやWscript ShellのSendKeyを実行するにはPowerShellが便利です。

PowerShellをSendWaitやWscriptを使うにはPowerShellに次のように打ち込みます。

 
#SendWait
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("a")
 
#Wscript ShellのSendKey
(New-Object -ComObject Wscript.Shell).SendKeys("a")

SendKeysやSendWaitのメッセージをSpy++で見てみると、

やはりScanCodeに00がセットされていることが確認できます。

先ほども述べた通り、メッセージの中のScanCodeはWindowsのアプリケーションでは基本的に無視します。

というか無視するように作るのがお作法です。

しかし、どうやら中にはこのお作法を守っていないアプリケーションがいるようで・・・。

SendKeysやSendWaitを使っていて、どうもキーボード操作が反応しないというアプリケーションがあったときは、

このScanCodeを疑ってみることをお勧めします。

そもそもこのScanCodeとは、

むか~しのIBMのパソコンのキーボードをベースにMicrosoftが作ったものらしいです。

indowsスキャンコードは、IBM PC ATの102キーのScan Code Set 1をベースにMicrosoftが作ったものだ。 (中略) ただし、このWindowsキーはあくまでも内部的なキーコードで、ユーザーが表だって使うのは、レジストリによるキーボードマッピングぐらいだ。

ASCII.jp:キーボードのキー入れ替えにおける、仮想キーコードとキーボードスキャンコード (2/2) より引用

また、このサイトには、ScanCodeと仮想キーの組み合わせの表も載っています。

この歴史的経緯からも、おそらくScanCodeまで見ているアプリケーションは

IBM系の流れを汲むアプリケーションが多いと思われます。

ちなみに私が過去に出会ったSendKeysでうまく動かなかったアプリケーションも

IBMの流れを汲むアプリケーションでした。

では、SendKeyやSendWaitでは効かないアプリケーションがあることがわかったところで、

どうすれば、キーボードの操作を自動化することができるか見ていきましょう。

【結論】PostMessage関数を使ってScanCodeもしっかり通知する

長かったですが、ここがこの記事の結論になります。

(ここからも長いですが・・・)

ScanCodeがちゃんと送られていないからキーボード操作が反応しないのであれば、

ちゃんとScanCodeを設定してメッセージを通知してあげればいいのです。

そのためには、まずはSpy++などのツールを使ってキーボードを押したときに、

自動化したいアプリケーションのウィンドウにどのようなメッセージが届いているかを確認します。

そして、そのメッセージと同じメッセージをウィンドウに対して送ってあげればいいのです。

ウィンドウにメッセージを送るためには、Windows APIの中のPostMessage関数というものを使うことで実現することができます。

PostMessage関数について

PostMessage関数とは、WindowsのAPIの中の1つで、

指定したハンドル(ウィンドウを識別するための一意の数字)にメッセージを送ることができる関数です。

PostMessageA function (winuser.h) - Win32 apps | Microsoft Docs
(英語で書かれていますが。参考程度に・・・)

キーボードの操作だけでなく、ウィンドウに対して様々なメッセージを送ることができます。

PostMessage関数の構文

構文は次の通りです。

BOOL PostMessage(
   HWND hWnd ,
   UINT Msg ,
   WPARAM wParam ,
   LPARAM lParam
);

・・・いきなり「構文はこれです!」なんて出してしまいましたが、

Windows APIを初めて見る人にとっては何が何だかわかりませんよね・・・。

私も最初はそうでした。

BOOLは関数の戻り値の型で真偽値です。

とはいったものの、数字だと思ってもらっていいです。

1はTrue、0はFalseを表します。

そして一番の謎なのは引数の型の、HWNDやUINT、WPARAM、LPARAM。

これらは単純に言ってしまうと、数字です。数字型だと思ってもらって大丈夫です。

まとめると、PostMessage関数の引数は全て数字型で、戻り値は1か0です。

PostMessage関数の引数

第一引数(hWnd)

第一引数のhWndはメッセージの送り先のハンドルです。

第二引数(Msg)

第二引数のMsgは送信するメッセージの種類です。

先ほど、PostMessageは様々なメッセージをウィンドウに送れるということを書きましたが、

この第二引数でメッセージの種類を指定することができます。

この引数で指定することができるメッセージの種類は

キーボード入力通知 - Win32 apps | Microsoft Docs

に記載があります。

この記事の中で登場するメッセージの種類は、

①キーが押された(WM_KEYDOWN=0x0100) ②押された文字のコード(WM_CHAR=0x0102)
③キーが上がった(WM_KEYUP=0x0101)

の3つです。

主に使うのはWM_KEYDOWNとWM_KEYUPです。

後述しますが、WM _ CHARメッセージはウィンドウの中で自動で生成されるメッセージになります。

WM_〇〇というのは数字だとわかりづらいので名前をつけているだけです。

定数と思ってもらって大丈夫です。

その定数の値が0x0100などです。

ちなみに0x0100などの先頭についている「0x」は16進数の数字であることを表します。

第三引数(wParam)

第三引数のwParamは第二引数によって意味が変わってきます。

aを押したときのメッセージを見てみましょう。

Spy++ではメッセージのログに生のパラメータを出力することができるので、

それをオンにしてキーボードのaを押したときのメッセージを見てみます。

f:id:shinmai_papa:20211029105505p:plain

これがそのメッセージの一部です。

f:id:shinmai_papa:20211028172817p:plain

WM_KEYDOWNの例では、第三引数はnVirtualKey 'A'という部分です。

そして、wParam:00000041とあります。

41というのは、Windowsの中で定義されている仮想キーの番号です。

この仮想キーの一覧はMicrosoftのサイトで確認することができます。

Virtual-Key Codes (Winuser.h) - Win32 apps | Microsoft Docs

上記のサイトを見ると、Aキーは0x41であることが確認できます。

f:id:shinmai_papa:20211028172836p:plain

第二引数がWM_KEYUPも考え方は同じです。

第二引数がWM_CHARの場合のメッセージは、次のように通知されました。

f:id:shinmai_papa:20211028172854p:plain

wParamが61です。

これはWM_KEYDOWNのときとは違った意味を持ちます。

この61はAscii文字コードの「a」を表す61です。

つまり、WM_CHARの場合、第三引数は文字コードを表します。

第四引数(iParam)

最後に第四引数のiParamですが、ここが一番重要になってきます。

なぜならここに先ほどから出てきているScanCodeが含まれるからです。

第四引数は二進数の32桁のビット列で構成されます。

WM_KEYDOWN、WM_KEYUP、WM_CHARの場合、おおよそ次の通りに定義されます。

32桁ですが、0から始まっている点に注意してください。

f:id:shinmai_papa:20211028172915p:plain

二進数に変換して1ビットずつ見比べないとパラメータの意味がわかりませんが、

実際のコーディングでは16進数で書かれることが多い印象です。

そのため、この後のコードも16進数で書いていくこととします。

iParamについて、簡単に解説しましたが、

「もっと詳しく知りたい!!」という奇特な方は下記を参照ください。

WM_KEYDOWN メッセージ (Winuser.h) - Win32 apps | Microsoft Docs
WM_KEYUP メッセージ (Winuser. h) - Win32 apps | Microsoft Docs
WM_CHAR メッセージ (Winuser. h) - Win32 apps | Microsoft Docs

とここまで、それぞれの引数についてみてきましたが、ここまで詳しいことはわからなくても、

実はウィンドウにメッセージを送信するプログラムを書くことはできてしまいます。

なぜなら、Spy++を使って可視化したメッセージの通りにPostMessageを組み立てるだけだからです(身も蓋もない・・・)。

では、ここまでの総括として、VBA及びPowerShell(C#でも同じように書けます)によるPostMessageの実装例を紹介したいと思います。

今回の例ではaキーを押したときのメッセージをPostMessage関数で実装してみたいと思います。

Spy++でメモ帳に対してキーボードのaを押したときのメッセージを可視化したものが次の画像です。

f:id:shinmai_papa:20211028172943p:plain

Spy++の画像から読み取れるのは次の通り。

項目 メッセージの種類
hWnd 全部 0xB0EA4
wParam WM_KEYDOWN/WM_KEYUP 0x41
iParam WM_KEYDOWN
WM_KEYUP
0x01E0001
0x0C01E0001

WM_CHARが表にないのは、

実はWM_CHARのメッセージはWM_KEYDOWNがウィンドウに通知されたときに別の機能で勝手にWM_CHARのメッセージに変換されて通知されるので、PostMessageで実装する必要がありません。

それなら、最初から、いらん解説するなというクレームは受け付けません。

実装するときは単純にこの通りに実装してあげます。

※メモ帳は普通にSendKeysなどでも反応しますけども、ここでは例としてメモ帳を取り上げました。
※ウィンドウハンドルはウィンドウを立ち上げる都度変わりますのでご注意ください。

VBAによるPostMessageによるaキー押下の実装例

変数hwndは環境によって書き換えてください。

また、標準モジュールに貼り付けて実行する必要がありますのでご注意ください。

 
Public Declare Function PostMessage Lib "user32" Alias "PostMessageA" _
(ByVal hWnd As Long, ByVal Msg As Long, ByVal wParam As Long, ByVal iParam _
As Long) As Long

Sub PostMessageSample()
   Const hwnd As Long = &HB0EA4
   Const WM_KEYDOWN As Long = &H100
   Const WM_KEYUP As Long = &H101
   Const wParam As Long = &H41
   Const iParam1 As Long = &H1E0001
   Const iParam2 As Long = &HC01E0001

   PostMessage hwnd, WM_KEYDOWN, wParam, iParam1
   PostMessage hwnd, WM_KEYUP, wParam, iParam2
End Sub

PowerShellによるPostMessageによるaキー押下の実装例

変数$hwndは環境によって書き換えてください(しつこい?)。

 
$src=@'
using System;
using System.Runtime.InteropServices;

public static class Win32{
 [DllImport("user32.dll")]
 public static extern int PostMessage(
   IntPtr hWnd,
   IntPtr Msg,
   IntPtr wParam,
   IntPtr iParam
 );
}
'@
$hwnd=0xB0EA4
$WM_KEYDOWN=0x100
$WM_KEYUP=0x101
$wParam=0x41
$iParam1=0x1E0001
$iParam2=0xC01E0001

Add-Type -TypeDefinition $src -ErrorAction SilentlyContinue
[Win32]::PostMessage($hwnd,$WM_KEYDOWN,$wParam,$iParam1)|Out-Null
[Win32]::PostMessage($hwnd,$WM_KEYUP,$wParam,$iParam2)|Out-Null

エピローグ

ScanCodeの話だけしようとしたらなんだか長くなってしまいました。

とにかくSendKeysやSendWaitで反応しないアプリケーションがあったら、PostMessage関数を使って仮想キーに対応するScanCodeまでしっかり送ってあげてください、という話でした。

Windows APIなど、込み入った話が沢山出てきて、わかりづらいところが多々あるかも知れません。

ここがわかりづらい、こういうときどうするの?的な質問がありましたらコメントやTwitterなどで知らせてください。

わかる範囲で(←ここ重要!)お答えします。

長くなりましたが、ここまでお読みいただきありがとうございました。