【VBA×WindowsAPI】メニューバーを操作する(メニュー内項目のコマンド実行)
Windows APIのFindWindowEx関数やSendMessage関数を使うことで、VBAで任意のダイアログ内にあるEditコントロールやButtonコントロール、ComoboxコントロールなどのRPAライクな操作を行うことができます。このとき対象のダイアログは既に画面上に表示されている必要がありますが、同じくWindows APIでメニューバーの識別IDを取得することで、メニューバー内あるコマンドを実行しダイアログを表示させることができます。
本ページではそんなメニューバーのIDを取得してコマンド実行する方法を解説していきます。
VBAでRPAライクの操作を行う方法は下記メインページにまとめているので合わせて参照下さい。
メニュバーのコマンドを実行
メニューバーはダイアログ上部にあるドロップダウン形式のメニューです。このドロップダウンメニューにはアプリケーションを操作するための様々なコマンドが用意されており、コマンドによっては実行することで新たなダイアログが表示されるものもあります。
本ページでは、このメニューバー内にある各項目の識別IDを取得して任意のコマンドを実行する方法についてを解説します。RPAはGUI経由で操作を自動化するシステムであるため、ダイアログが画面上に存在してはじめて成立するものです。メニューバーのコマンドを実行するということはそのダイアログを表示させることであるため、いわばRPAの取っ掛かりの処理ともいえます。
VBAで別アプリケーションのメニューバー内のコマンドを実行するには下記のWindows APIを利用します。それぞれ関数のより詳細な使い方の解説は各関数のリンクページを参照下さい。
FindWindow関数 :指定のウィンドウのハンドルを取得する
FindWindowEx関数 :指定のウィンドウ内にある子ウィンドウのハンドルを取得する
SendNotifyMessage関数 :指定のウィンドウにメッセージを送信する
GetMenu関数 :指定のウィンドウのメニューのハンドルを取得する
GetSubMenu関数 :指定のウィンドウのサブメニューのハンドルを取得する
GetMenuItemCount関数 :指定したメニュー内の項目数を取得する
GetMenuItemInfo関数 :指定したメニュー項目の情報を取得する
「そもそもWindows APIって何?」という方はコチラ(メインページ)も併せて参照下さい。
サンプルコード
メニューバー内の指定の項目を実行するサンプルコードは下記の通りです。
メモ帳ウィンドウを開いた状態で下記コードを実行すると「ページ設定」ウィンドウを開くことができます。サンプルコードのLaunchMenuCommand関数の第2引数の文字列を別の文字列に変更することで別のコマンドを実行することもできます。(※入力文字列を含むメニュー項目が実行される)
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 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
Option Explicit Private Declare PtrSafe Function FindWindowEx Lib "user32" Alias "FindWindowExA" (ByVal hWnd1 As LongPtr, ByVal hWnd2 As LongPtr, ByVal lpsz1 As String, ByVal lpsz2 As String) As LongPtr Private Declare PtrSafe Function FindWindow Lib "user32" Alias "FindWindowA" (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr Private Declare PtrSafe Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hwnd As LongPtr, ByVal wMsg As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr Private Declare PtrSafe Function SendNotifyMessage Lib "user32" Alias "SendNotifyMessageA" (ByVal hwnd As LongPtr, ByVal msg As Long, ByVal wParam As LongPtr, ByVal lParam As LongPtr) As Long Private Declare PtrSafe Function GetMenu Lib "user32" (ByVal hwnd As LongPtr) As LongPtr Private Declare PtrSafe Function GetSubMenu Lib "user32" (ByVal hMenu As LongPtr, ByVal nPos As Long) As LongPtr Private Declare PtrSafe Function GetMenuItemCount Lib "user32" (ByVal hMenu As LongPtr) As Long Private Declare PtrSafe Function GetMenuItemInfo Lib "user32" Alias "GetMenuItemInfoA" (ByVal hMenu As LongPtr, ByVal un As Long, ByVal b As Long, lpMenuItemInfo As MENUITEMINFO) As Long Private Type MENUITEMINFO cbSize As Long '構造体サイズ fMask As Long '取得または設定するメンバ fType As Long 'メニュー項目のタイプ fState As Long 'メニュー項目の状態 wID As Long 'メニュー項目を識別するアプリケーション定義のID hSubMenu As LongPtr 'サブメニューのハンドル hbmpChecked As LongPtr '項目の横に表示するビットマップハンドル(選択時) hbmpUnchecked As LongPtr '項目の横に表示するビットマップハンドル(非選択時) dwItemData As LongPtr 'メニュー項目に関連付けられているアプリケーション定義の値 dwTypeData As String 'メニュー項目の内容 cch As Long 'メニュー項目のテキストの長さ(文字数) hbmpItem As LongPtr '表示するビットマップのハンドル End Type Private Const MIIM_ID As Long = &H2 Private Const MIIM_STRING As Long = &H40 Private Const WM_COMMAND As Long = &H111 '---------------------------------------------------------------- ' メイン処理 '---------------------------------------------------------------- Sub main() Dim hWndNotepad As LongPtr 'メモ帳のウィンドウハンドル取得 hWndNotepad = FindWindow("notepad", vbNullString) 'メモ帳のメニューバー内にある[ページ設定(U)...]を実行 Call LaunchMenuCommand(hWndNotepad, "ページ設定") End Sub '-------------------------------------------------------------------------- ' メニューバー内コマンドの実行 ' hwnd :操作対象ウィンドウのハンドル ' sWord :実行するメニュー項目の名称 (部分一致) '-------------------------------------------------------------------------- Private Sub LaunchMenuCommand(ByVal hwnd As LongPtr, ByVal sWord As String) Dim lMenuID As Long '文字列から該当のメニュー項目の識別IDを取得 Call GetMenuID(GetMenu(hwnd), sWord, lMenuID) '識別IDによるメニュー項目の実行(※実行後すぐに処理が戻る) Call SendNotifyMessage(hwnd, WM_COMMAND, lMenuID, 0) ' '識別IDによるメニュー項目の実行(※実行先の処理が終わるまで処理は戻ってこない) ' Call SendMessage(hwnd, WM_COMMAND, lMenuID, 0) End Sub '----------------------------------------------------------------- ' 指定文字を含むメニュー項目の識別IDを取得する ' hMenu :親メニューハンドル ' sLabel :取得するメニュー項目のテキスト(部分一致) ' lMenuID :メニューテキストにsLabelの文字列を含むメニューの識別ID [戻り値] '----------------------------------------------------------------- Private Sub GetMenuID(ByVal hMenu As LongPtr, ByVal sLabel As String, _ ByRef lMenuID As Long) Dim hSubMenu As LongPtr Dim tMII As MENUITEMINFO Dim lCntMenu As Long Dim lCntSubMenu As Long Dim sLab As String Dim i As Long If lMenuID <> 0 Then Exit Sub 'メニュー項目数を取得 lCntMenu = GetMenuItemCount(hMenu) '再帰処理でメニュー項目の情報を網羅的に取得 For i = 0 To lCntMenu - 1 'メニューテキストと識別IDを取得 With tMII .cbSize = Len(tMII) .fMask = MIIM_ID Or MIIM_STRING .dwTypeData = String(255, vbNullChar) .cch = Len(tMII.dwTypeData) End With Call GetMenuItemInfo(hMenu, i, 1, tMII) sLab = Left(tMII.dwTypeData, InStr(tMII.dwTypeData, vbNullChar) - 1) 'メニューテキストと識別IDをイミディエイトウィンドウに出力(確認用) Debug.Print "Menu Text : " & sLab Debug.Print "Menu ID : " & tMII.wID 'メニューテキストに引数の文字列を含むメニューの識別IDを取得 If InStr(sLab, sLabel) <> 0 And sLab <> "" Then lMenuID = tMII.wID Exit Sub End If 'サブメニューハンドルと項目数取得 hSubMenu = GetSubMenu(hMenu, i) lCntSubMenu = GetMenuItemCount(hSubMenu) 'サブメニュー項目数が0でない場合は再帰処理 If lCntSubMenu = 0 Then Exit Sub Else Call GetMenuID(hSubMenu, sLabel, lMenuID) End If Next End Sub |
コード解説
Windows APIを使ってメニューバー内のコマンドを実行するには下記の手順を行います。
操作対象のウィンドウ(アプリケーション)のハンドルの取得はFindWindow関数を使うことで取得が可能です。サンプルコードではメモ帳(Notepad)を取得していますが、メニューバーを持つウィンドウであればその他のアプリケーションのウィンドウでも問題ありません。
2. メニューバーのハンドルを取得する
3. メニューの項目数を調べる
4. メニューのテキストと識別IDを取得する
5. メニューのテキストが指定の文字列を含んでいる場合は識別IDを取得
6. サブメニューが存在する場合はサブメニューのハンドルを取得
7. 再帰処理で該当のメニューが見つかるまですべてのメニューを探索する
8. 識別IDを送信してコマンドを実行する
以下では上記のうち重要な部分を抜粋して解説していきます。
メニューバーのハンドルを取得する
メニューバーのハンドルを取得するにはGetMenu関数を使い下記のように書きます。
Dim hMenu As LongPtr
hMenu = GetMenu(hwnd)
引数のhwndにメニューバーを持つウィンドウのハンドルを入力することで、そのウィンドウが持つメニューバーのハンドルを取得することができます。このとき、戻り値の型はウィンドウハンドルと同じくLongPtr型になるので注意が必要です。(※サンプルコードでは関数ごと引数に入力しています)
メニューの項目数を調べる
該当のメニューがいくつの項目を持っているかをカウントします。
メニューの項目数はGetMenuItemCount関数を使い下記のように書きます。
Dim lCntMenu As Long
lCntMenu = GetMenuItemCount(hMenu)
引数のhMenuには項目数をカウントする親のメニューのハンドルを入力します。
親メニューと子項目の関係は下画像の通りで青い四角形が親メニューでピンク四角形が子項目となり、GetMenuItemCount関数を使うことで親メニューに対する子項目の数を取得することができます。
メニューバーのハンドルを渡した場合はメニューバーの項目数、指定のメニューのハンドルを渡した場合はそのメニューが持つサブメニュー(メニュー内のメニュー)の項目数となります。このとき、メニューを区切るための線[―――]も項目の1つとしてカウントされるので注意が必要です。
メニューのテキストと識別IDを取得する
各メニュー項目の情報を取得するにはGetMenuItemInfo関数を使って下記のように書きます。
Call GetMenuItemInfo(hMenu, lIndex, 1, tMII)
第1引数のhMenuには情報を取得する項目の親のメニューハンドル、第2引数のlIndexには情報を取得する項目のインデックス、第3引数には定数値で「1」、第4引数のtMIIには取得したメニューの情報を格納するためのMENUITEMINFO構造体をそれぞれ入力します。
hMenuとlIndexの関係は下画像の通りです。インデックスは0始まりで、項目数取得時と同じくメニューを区切るための線[―――]は項目の1つとしてカウントされているので注意が必要です。
tMIIには情報を受け取るため構造体を入力します。
MENUITEMINFO構造体はTypeステートメントを使ってあらかじめ定義しておく必要があります。構造体定義後は「Dim tMII As MENUITEMINFO」と書くことでMENUITEMINFO構造体の変数を用意することができます。MENUITEMINFO構造体の変数が用意出来たら下記の通り値を設定します。
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のみを取得しているため、下記のように書くことでそれぞれの値を取得することができます。
sLab = Left(tMII.dwTypeData, InStr(tMII.dwTypeData, vbNullChar) – 1)
lMenuID = tMII.wID
テキストは前述の通り「dwTypeData」に格納されていますが、255文字分の空文字の中に格納されているため、そのまま抜き出すと不要な空文字まで含まれてしまいます。そのため、上記の通り、空文字より左にある文字列のみを抽出しています。識別IDは構造体の「wID」に格納されています。
サブメニューが存在する場合はサブメニューのハンドルを取得
再帰的にメニューバー内のすべての項目を探索するため、サブメニューが存在する場合はサブメニューのハンドルを取得します。サブメニューのハンドルを取得するにはGetSubMenu関数を使い下記のように書きます。(※サブメニューの有無は前述のメニューの項目数を調べることで判定が可能)
Dim hSubMenu As LongPtr
hSubMenu = GetSubMenu(hMenu, lIndex)
第1引数のhMenuには親メニューのハンドル、第2引数にはサブメニューのインデックスをそれぞれ入力することで、対象のサブメニューのハンドルを取得することができます。
hMenuとlIndexの関係は前項のGetMenuItemInfo関数と同じ考えです。
識別IDを送信してコマンドを実行する
実行したいコマンドの識別IDが取得できたら、SendNotifyMessage関数を使ってメニューバーを持つウィンドウのハンドルにコマンド実行をするためのメッセージと識別IDを送信します。
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関数版のコードもコメントアウトしているので実際に処理の違いを確かめてみて下さい。
関連情報
VBA×WindowsAPIまとめページ
その他のWindowsAPI関数は下記ページにまとまっているので合わせて参照下さい。
参考
Microsoft公式:ウィンドウ メッセージ (Windows とメッセージ) – Win32 apps