【VBA×WindowsAPI】メニューバーを操作する(メニュー内項目のコマンド実行)

Windows APIのFindWindowEx関数やSendMessage関数を使うことで、VBAで任意のダイアログ内にあるEditコントロールButtonコントロールComoboxコントロールなどのRPAライクな操作を行うことができます。このとき対象のダイアログは既に画面上に表示されている必要がありますが、同じくWindows APIでメニューバーの識別IDを取得することで、メニューバー内あるコマンドを実行しダイアログを表示させることができます。

本ページではそんなメニューバーのIDを取得してコマンド実行する方法を解説していきます。
VBAでRPAライクの操作を行う方法は下記メインページにまとめているので合わせて参照下さい。

メニュバーのコマンドを実行

メニューバーはダイアログ上部にあるドロップダウン形式のメニューです。このドロップダウンメニューにはアプリケーションを操作するための様々なコマンドが用意されており、コマンドによっては実行することで新たなダイアログが表示されるものもあります。

本ページでは、このメニューバー内にある各項目の識別IDを取得して任意のコマンドを実行する方法についてを解説します。RPAはGUI経由で操作を自動化するシステムであるため、ダイアログが画面上に存在してはじめて成立するものです。メニューバーのコマンドを実行するということはそのダイアログを表示させることであるため、いわばRPAの取っ掛かりの処理ともいえます。

VBAで別アプリケーションのメニューバー内のコマンドを実行するには下記のWindows APIを利用します。それぞれ関数のより詳細な使い方の解説は各関数のリンクページを参照下さい。

icon-check-square FindWindow関数            :指定のウィンドウのハンドルを取得する
icon-check-square FindWindowEx関数        :指定のウィンドウ内にある子ウィンドウのハンドルを取得する
icon-check-square SendNotifyMessage関数 :指定のウィンドウにメッセージを送信する
icon-check-square GetMenu関数                  :指定のウィンドウのメニューのハンドルを取得する
icon-check-square GetSubMenu関数            :指定のウィンドウのサブメニューのハンドルを取得する
icon-check-square GetMenuItemCount関数 :指定したメニュー内の項目数を取得する
icon-check-square GetMenuItemInfo関数    :指定したメニュー項目の情報を取得する

「そもそもWindows APIって何?」という方はコチラ(メインページ)も併せて参照下さい。

サンプルコード

メニューバー内の指定の項目を実行するサンプルコードは下記の通りです。
メモ帳ウィンドウを開いた状態で下記コードを実行すると「ページ設定」ウィンドウを開くことができます。サンプルコードのLaunchMenuCommand関数の第2引数の文字列を別の文字列に変更することで別のコマンドを実行することもできます。(※入力文字列を含むメニュー項目が実行される)

コード解説

Windows APIを使ってメニューバー内のコマンドを実行するには下記の手順を行います。
操作対象のウィンドウ(アプリケーション)のハンドルの取得はFindWindow関数を使うことで取得が可能です。サンプルコードではメモ帳(Notepad)を取得していますが、メニューバーを持つウィンドウであればその他のアプリケーションのウィンドウでも問題ありません。

1. 操作対象のメニューバーを持つウィンドウのハンドルを取得する
2. メニューバーのハンドルを取得する
3. メニューの項目数を調べる
4. メニューのテキストと識別IDを取得する
5. メニューのテキストが指定の文字列を含んでいる場合は識別IDを取得
6. サブメニューが存在する場合はサブメニューのハンドルを取得
7. 再帰処理で該当のメニューが見つかるまですべてのメニューを探索する
8. 識別IDを送信してコマンドを実行する

 
以下では上記のうち重要な部分を抜粋して解説していきます。
 

icon-edit メニューバーのハンドルを取得する

メニューバーのハンドルを取得するにはGetMenu関数を使い下記のように書きます。

icon-code GetMenu関数 

Dim  hMenu As LongPtr
hMenu = GetMenu(hwnd)

引数のhwndにメニューバーを持つウィンドウのハンドルを入力することで、そのウィンドウが持つメニューバーのハンドルを取得することができます。このとき、戻り値の型はウィンドウハンドルと同じくLongPtr型になるので注意が必要です。(※サンプルコードでは関数ごと引数に入力しています)
 

icon-edit メニューの項目数を調べる

該当のメニューがいくつの項目を持っているかをカウントします。
メニューの項目数はGetMenuItemCount関数を使い下記のように書きます。

icon-code GetMenuItemCount関数 

Dim lCntMenu As Long
lCntMenu = GetMenuItemCount(hMenu)

引数のhMenuには項目数をカウントする親のメニューのハンドルを入力します。
親メニューと子項目の関係は下画像の通りで青い四角形が親メニューでピンク四角形が子項目となり、GetMenuItemCount関数を使うことで親メニューに対する子項目の数を取得することができます。

メニューバーのハンドルを渡した場合はメニューバーの項目数、指定のメニューのハンドルを渡した場合はそのメニューが持つサブメニュー(メニュー内のメニュー)の項目数となります。このとき、メニューを区切るための線[―――]も項目の1つとしてカウントされるので注意が必要です。
 

icon-edit メニューのテキストと識別IDを取得する

各メニュー項目の情報を取得するにはGetMenuItemInfo関数を使って下記のように書きます。

icon-code GetMenuItemInfo関数 

Call GetMenuItemInfo(hMenu, lIndex, 1, tMII)

第1引数のhMenuには情報を取得する項目の親のメニューハンドル、第2引数のlIndexには情報を取得する項目のインデックス、第3引数には定数値で「1」、第4引数のtMIIには取得したメニューの情報を格納するためのMENUITEMINFO構造体をそれぞれ入力します。

hMenulIndexの関係は下画像の通りです。インデックスは0始まりで、項目数取得時と同じくメニューを区切るための線[―――]は項目の1つとしてカウントされているので注意が必要です。

 
tMII
には情報を受け取るため構造体を入力します。
MENUITEMINFO構造体はTypeステートメントを使ってあらかじめ定義しておく必要があります。構造体定義後は「Dim tMII As MENUITEMINFO」と書くことでMENUITEMINFO構造体の変数を用意することができます。MENUITEMINFO構造体の変数が用意出来たら下記の通り値を設定します。

icon-code GetMenuItemInfo関数 

With tMII
    .cbSize = Len(tMII)
    .fMask = MIIM_ID Or MIIM_STRING
    .dwTypeData = String(255, vbNullChar)
    .cch = Len(tMII.dwTypeData)
End With

上記のうち「fMask」に入力している「MIIM_ID Or MIIM_STRING」という値がメニューの識別IDとテキストを取得するということを表しています。また、取得したテキストは「dwTypeData」で設定している値に格納されるため、VBAでは固定長文字列を用意して入力します。(※この書き方の場合255文字を超えるメニューテキストがあった場合255文字目以降の取得は不可)

これによりGetMenuItemInfo関数の実行後、引数として入力したtMIIには上記で設定した通りの情報が格納されます。今回のケースではメニューのテキストと識別IDのみを取得しているため、下記のように書くことでそれぞれの値を取得することができます。

icon-code メニューテキストと識別IDの取得関数 

sLab = Left(tMII.dwTypeData, InStr(tMII.dwTypeData, vbNullChar) – 1)
lMenuID = tMII.wID

テキストは前述の通り「dwTypeData」に格納されていますが、255文字分の空文字の中に格納されているため、そのまま抜き出すと不要な空文字まで含まれてしまいます。そのため、上記の通り、空文字より左にある文字列のみを抽出しています。識別IDは構造体の「wID」に格納されています。
 

icon-edit サブメニューが存在する場合はサブメニューのハンドルを取得

再帰的にメニューバー内のすべての項目を探索するため、サブメニューが存在する場合はサブメニューのハンドルを取得します。サブメニューのハンドルを取得するにはGetSubMenu関数を使い下記のように書きます。(※サブメニューの有無は前述のメニューの項目数を調べることで判定が可能)

icon-code サブメニューハンドルの取得 

Dim  hSubMenu As LongPtr
hSubMenu = GetSubMenu(hMenu, lIndex)

第1引数のhMenuには親メニューのハンドル、第2引数にはサブメニューのインデックスをそれぞれ入力することで、対象のサブメニューのハンドルを取得することができます。

hMenulIndexの関係は前項のGetMenuItemInfo関数と同じ考えです。
 

icon-edit 識別IDを送信してコマンドを実行する

実行したいコマンドの識別IDが取得できたら、SendNotifyMessage関数を使ってメニューバーを持つウィンドウのハンドルにコマンド実行をするためのメッセージと識別IDを送信します。

icon-code メニュー内コマンドの実行 

Call SendNotifyMessage(hwnd, WM_COMMAND, lMenuID, 0)

第1引数のhwndにはメニューバーを持つウィンドウのハンドル、第2引数にはコマンド実行のメッセージ「WM_COMMAND」(定数値)、第3引数のlMenuIDにはコマンド実行する対象のメニュー項目の識別ID、第4引数は今回の場合では使用しないので「0」をそれぞれ入力することで、指定のメニュー内コマンドを実行することができます。

SendNotifyMessage関数はSendMessage関数と同じくウィンドウにメッセージを送信するための関数ですが実行後の処理の動きに違いがあります。SendNotifyMessage関数は実行後すぐにVBAの処理に戻りますが、SendMessage関数は実行後にメッセージを受信した側の処理が終了するまでVBAの処理は待機されます

たとえばサンプルコードのようにコマンド実行で新しいウィンドウを開く場合、SendNotifyMessage関数で開くとすぐにVBAに処理が戻ってくるため、そのウィンドウの操作を行うことができます。対してSendMessage関数でウィンドウを開くと、そのウィンドウが閉じられるまでVBAの処理は待機されてしまうため、ウィンドウの操作を行うことができません。

ウィンドウを開きたいだけであればSendMessage関数で実行しても良い内容であるため、これら関数の処理の違いを理解して状況に応じて使い分ける必要があります。サンプルコードにはSendMessage関数版のコードもコメントアウトしているので実際に処理の違いを確かめてみて下さい。

関連情報

icon-share-square VBA×WindowsAPIまとめページ

その他のWindowsAPI関数は下記ページにまとまっているので合わせて参照下さい。

icon-share-square 参考

Microsoft公式:ウィンドウ メッセージ (Windows とメッセージ) – Win32 apps

Excel, RPA, VBA, Windows API

Posted by Lic