【VBA×WindowsAPI】ComboBoxコントロールを操作する
ComboBoxコントロールは、ユーザーが選択できるリストを表示するためのコントロールです。本来、Excel VBAだけでは操作不能なExcel以外のアプリケーションのComboBoxコントロールですが、Windows APIと組み合わせることでプログラムとして操作することが可能になります。これにより、ユーザーが手入力している作業をExcel外の領域でも自動化することができます。
本ページではそんなComboBoxコントロールに対して、VBAで値の入力と現在の値を取得する方法を解説していきます。コントロールの種類の調べ方をはじめ、ボタンやテキストボックス等のその他コントロールの自動化は下記メインページを参照下さい。
ComboBoxコントロールを操作
ComboBoxコントロールはクリックすることでプルダウンメニューが表示されるコントロールのことです。メモ帳の[ページ設定]ウィンドウの用紙サイズや給紙方法のコントロールや、[名前を付けて保存]ウィンドウの文字コード選択のコントロールなどがこれにあたります。
本ページではComboBoxコントロールに対して「指定の値に変更する方法」と「現在の値を取得する方法」の2つの方法を解説します。コンボボックスの操作はRPA操作においてもアプリケーションの設定変更時に必須の処理となるため重要な要素の1つとなります。
VBAで別アプリケーションのComboBoxコントロールを操作するには下記のWindows APIを利用します。それぞれ関数のより詳細な使い方の解説は各関数のリンクページを参照下さい。
FindWindow関数 :指定のウィンドウのハンドルを取得する
FindWindowEx関数 :指定のウィンドウ内にある子ウィンドウのハンドルを取得する
GetParent関数 :指定のウィンドウの親ウィンドウのハンドルを取得する
GetWindowLongPtr関数:指定のウィンドウの情報を取得する
SendMessage関数 :指定のウィンドウにメッセージを送信する
「そもそもWindows APIって何?」という方はコチラ(メインページ)も併せて参照下さい。
サンプルコード
指定のComboBoxコントロールに対して値を変更、取得するサンプルコードは下記の通りです。
メモ帳の[名前を付けて保存]ウィンドウを開いた状態で下記コードを実行すると、SetValueToComboBox関数でComboBoxコントロールの値を変更、GetValueFromComboBox関数で現在のComboBoxコントロールの値(文字列)を取得することができます。
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 |
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 GetParent Lib "user32" (ByVal hwnd As LongPtr) As LongPtr Private Declare PtrSafe Function GetWindowLongPtr Lib "user32" Alias "GetWindowLongPtrA" (ByVal hwnd As LongPtr, ByVal nIndex As Long) As LongPtr 'ComboBoxコントロール Private Const CB_SELECTSTRING As Long = &H14D 'コンボボックスの指定項目を選択 Private Const CB_GETCURSEL As Long = &H147 '選択中のコンボボックス項目のインデックスを取得 Private Const CB_GETLBTEXT As Long = &H148 'コンボボックス項目の文字列を取得 Private Const CB_GETLBTEXTLEN As Long = &H149 'コンボボックス項目の文字列の長さを取得 Private Const WM_LBUTTON_DOWN As Long = &H201 'マウス左ボタンを押下 Private Const WM_LBUTTON_UP As Long = &H202 'マウス左ボタンを離す Private Const WM_COMMAND As Long = &H111 Private Const CBN_SELCHANGE As Long = &H1 Private Const CBN_SELENDOK As Long = &H9 Private Const GWL_ID As Long = -12 '---------------------------------------------------------------- ' メイン処理 '---------------------------------------------------------------- Sub main() Dim hWndSaveAs As LongPtr Dim hWndCmbBox As LongPtr '[名前を付けて保存]ウィンドウハンドル取得 hWndSaveAs = FindWindow(vbNullString, "名前を付けて保存") 'ComboBoxコントロールのハンドルを取得 (メモ帳の場合は文字コード部) hWndCmbBox = FindWindowEx(hWndSaveAs, 0, "ComboBox", vbNullString) 'ComboBoxコントロールに指定文字列を入力 Call SetValueToComboBox(hWndCmbBox, "ANSI") 'ComboBoxコントロールの文字列を取得 Debug.Print GetValueFromComboBox(hWndCmbBox) End Sub '---------------------------------------------------------------- ' ComboBoxコントロールの文字列を取得する '---------------------------------------------------------------- Private Function GetValueFromComboBox(ByVal hWndCmbBox As LongPtr) As String Dim lIndex As LongPtr Dim lLen As LongPtr Dim sBuf As String Dim sText As String '現在の項目のインデックスを取得 lIndex = SendMessage(hWndCmbBox, CB_GETCURSEL, 0, 0) '文字列の受け取り用に文字数分のバッファを用意 lLen = SendMessage(hWndCmbBox, CB_GETLBTEXTLEN, lIndex, 0) sBuf = String(CLng(lLen), vbNullChar) '文字列の取得 Call SendMessage(hWndCmbBox, CB_GETLBTEXT, lIndex, ByVal sBuf) sText = sBuf GetValueFromComboBox = sText End Function '---------------------------------------------------------------- ' ComboBoxコントロールに指定文字列を入力する '---------------------------------------------------------------- Private Sub SetValueToComboBox(ByVal hWndCmbBox As LongPtr, ByVal sText As String) Dim lID As LongPtr Dim hWndParent As LongPtr Dim wParam As LongPtr 'コンボボックスを指定文字列の項目に切り替え Call SendMessage(hWndCmbBox, CB_SELECTSTRING, 0, ByVal sText) '親ウィンドウのハンドルを取得 hWndParent = GetParent(hWndCmbBox) 'コンボボックスの識別IDを取得 lID = GetWindowLongPtr(hWndCmbBox, GWL_ID) 'コンボボックスの選択項目が有効であることを親ウィンドウに送信 wParam = (CBN_SELENDOK * &H10000) Or (lID And &HFFFF) Call SendMessage(hWndParent, WM_COMMAND, wParam, ByVal hWndCmbBox) 'コンボボックスの値が変更したことを親ウィンドウに送信 wParam = (CBN_SELCHANGE * &H10000) Or (lID And &HFFFF) Call SendMessage(hWndParent, WM_COMMAND, wParam, ByVal hWndCmbBox) End Sub '上記コードで値変更のイベントが発生しない場合の代替関数 ''---------------------------------------------------------------- '' ComboBoxコントロールに指定文字列を入力する ''---------------------------------------------------------------- 'Private Sub SetValueToComboBox(ByVal hWndCmbBox As LongPtr, ByVal sText As String) ' ' 'コンボボックスをクリック (コンボボックスを開く) ' Call SendMessage(hWndCmbBox, WM_LBUTTON_DOWN, 0, 0) ' Call SendMessage(hWndCmbBox, WM_LBUTTON_UP, 0, 0) ' ' 'コンボボックスを指定文字列の項目に切り替え ' Call SendMessage(hWndCmbBox, CB_SELECTSTRING, 0, ByVal sText) ' ' 'コンボボックスをクリック (コンボボックスを閉じる) ' Call SendMessage(hWndCmbBox, WM_LBUTTON_DOWN, 0, 0) ' Call SendMessage(hWndCmbBox, WM_LBUTTON_UP, 0, 0) ' 'End Sub |
コード解説
Windows APIを使ってComboBoxコントロールを操作するには、操作対象となるComboBoxコントロールのウィンドウハンドルを取得する必要があります。ハンドルの取得はウィンドウ(アプリケーション)の構造により取得手順が変わるので、Spy++等のツールを使用してウィンドウの構造を調べる必要があります。構造の確認ができたらFindWindowEx関数を使うことで該当コントロールのハンドルを取得することが可能です。
※以降ではいくつかの定数値が出てきますが、いずれも該当のヘッダーファイル内に定義されているものです。VBAではWindowsAPIの関数の呼び出しているだけであり定数値の呼び出しはされていません。そのため利用するすべての定数値は予め明記しておく必要があるので注意が必要です。(C言語などでWindowsAPIを使う場合は該当ヘッダーファイルをincludeすれば定数値も認識されます)
ComboBoxコントロールの値取得
ComboBoxコントロールの現在の項目の値を取得するには下記の手順を行います。
2. 現在選択されている項目のインデックスを取得
3. 項目の文字列を受け取る用のバッファを用意
4. インデックスとバッファを使って文字列を取得
少し回りくどい処理に感じますが、WindowsAPIは文字列を取得する際にその文字列を格納だけのメモリを確保する必要があるため、文字数の取得とそれに関わるインデックス取得の処理が発生します。
現在選択されている項目のインデックスを取得
ComboBoxコントロールのハンドルに対して”現在の項目インデックスを取得するためのメッセージ”を送信することで、そのComboBoxコントロールの現在の項目インデックスを取得することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。
Dim lIndex As LongPtr
lIndex = SendMessage(hWndCmbBox, CB_GETCURSEL, 0, 0)
第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数には現在の項目のインデックスを取得するメッセージ「CB_GETCURSEL」(定数値)、第3引数、第4引数は今回の場合では使用しないので「0」をそれぞれ入力します。
これにより、コンボボックスの現在設定されている項目がプルダウンメニューの何番目の値(インデックス)かを取得することができます。SendMessage関数の戻り値はLongPtr型のため、インデックスを受け取るlIndexは変数の型宣言時にLongPtr型にしておく必要があります。
項目の文字列を受け取る用のバッファを用意
ComboBoxの値(文字列)を取得するには、その値を受け取る用のバッファ(メモリ)を確保しておく必要があります。バッファの確保とは、たとえば”Text”という文字列を受け取る際にその文字数の4文字分の値を格納できるメモリをあらかじめ確保しておくことをいいます。
バッファを確保するにはそもそも文字数が何文字なのかを予め確認しておく必要があります。
ComboBoxコントロールのハンドルに対して”項目の文字数を取得するためのメッセージ”と合わせて項目インデックスを送信することで、そのComboBoxコントロールの指定項目の文字数を取得することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。
Dim lLen As LongPtr
lLen = SendMessage(hWndCmbBox, CB_GETLBTEXTLEN, lIndex, 0)
第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数には項目の文字数を取得するためのメッセージ「CB_GETLBTEXTLEN」(定数値)、第3引数のlIndexには文字数を取得する対象の項目インデックス、第4引数は今回の場合では使用しないので「0」をそれぞれ入力します。
lIndexに上記で取得した現在の項目インデックスを入力することで、コンボボックスコントロールの現在値の文字数を取得することができます。SendMessage関数の戻り値はLongPtr型のため、インデックスを受け取るlLenは変数の型宣言時にLongPtr型にしておく必要があります。
取得する値の文字数が取得できたら、その文字数分のバッファを用意します。
VBAでは文字数を受け取るためのバッファとして“固定長文字列”というものを使います。String関数を使って下記のように書くことで、第1引数で指定した文字数分の空文字列を作成することができます。
Dim sBufAs String
sBuf= String(CLng(lLen), vbNullChar) ‘※lLenはLong型に変換
これにより、コンボボックスの現在値の文字数と同じだけのメモリを確保した変数sBufを用意することができます。最終的に取得したコンボボックスの値はこの変数sBuf内に格納されます。
インデックスとバッファを使って文字列を取得
取得する項目のインデックスと文字数分のバッファが用意出来たら実際に文字列を取得します。
ComboBoxコントロールのハンドルに対して”文字列を取得するためのメッセージ”と合わせて対象の項目インデックスと値を受け取るバッファを送信することで、対象の文字列することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。
Call SendMessage(hWndCmbBox, CB_GETLBTEXT, lIndex, ByVal sBuf)
第1引数のhWndCmbBoxには値を取得する対象のComboBoxコントロールのハンドル、第2引数には文字列を取得するメッセージ「CB_GETLBTEXT」(定数値)、第3引数のlIndexには文字数を取得する対象の項目インデックス、第4引数のsBufには文字列を受け取るためのバッファである固定長文字列をByVal(値渡し)でそれぞれ入力します。
これにより、指定のコンボボックスコントロールの値を取得してsBufに格納することができます。
このとき、sBufのサイズが間違っていると文字列が途中で途切れたり、余分な空白文字(vbNullString)が含まれた文字列となってしまうので注意が必要です。ただし、あとで余分な空白文字を除去することを前提として大きめの定数値のバッファ(たとえば100文字分など)を用意するのであれば、前項の文字数カウントの処理はスキップすることができます。
ComboBoxコントロールの値変更
ComboBoxコントロールの項目を指定の値に変更には下記の手順を行います。
2. ComboBoxコントロールの値を変更
3. ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信する
ComboBoxコントロールの値を変更するためメッセージである「CB_SETCURSEL」や「CB_SELECTSTRING」は送信するだけでコンボボックスの値を変更することができますが、画面上の表示が切り替わるだけで内部的な値の変更はされません。
これはComboBoxコントロールの値変更イベントを親のウィンドウが持っている(VBAのUserFormと同じ)ためで、値を内部的にも変更するにはコントロールの値を変更したということを明示的に親ウィンドウに伝えて値変更イベント(CBN_SELCHANGE)を行わせる必要があります。
ComboBoxコントロールの値を変更
ComboBoxコントロールのハンドルに対して”現在の値を変更するためのメッセージ”と合わせてを送信することで、そのComboBoxコントロールの値を指定の値に変更することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。
Call SendMessage(hWndCmbBox, CB_SELECTSTRING, 0, ByVal sText)
第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数にはコントロールの値を変更するメッセージ「CB_SELECTSTRING」(定数値)、第3引数は今回の場合では使用しないので「0」、第4引数のsTextには指定する項目の文字列をByVal(値渡し)でそれぞれ入力します。
これにより、sTextを含む項目がコンボボックスの値として設定されます。このとき、sTextを含む項目が複数存在する場合はインデックスの最も小さい項目が値となり、sTextを含む項目が1つも存在しない場合は値の変更は行われません。
ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信する
ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信するには、その親ウィンドウのハンドルをあらかじめ取得しておく必要があります。親ウィンドウのハンドルを取得するにはGetParent関数を使い下記のように書きます。使い方としては引数にハンドルを入力するだけです。
hWndParent = GetParent(hWndCmbBox)
次にComboBoxコントロールの識別IDを取得します。
識別IDを取得するにはGetWindowLongPtr関数を使い下記のように書きます。
lID = GetWindowLongPtr(hWndCmbBox, GWL_ID)
第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数には識別IDを取得することを表す定数値「GWL_ID」を入力することで、指定のComboBoxコントロールの識別IDを取得することが出来ます。(※この識別IDは基本的に「0」となるのでスキップしても良い処理)
親ウィンドウのハンドルとComboBoxコントロールの識別IDの取得ができたら、親ウィンドウにComboBoxコントロールの値が変更したことを表すメッセージを送信します。コードとしてはSendMessage関数を使って下記のように書きます。
Call SendMessage(hWndParent, WM_COMMAND, wParam, ByVal hWndCmbBox)
第1引数のhWndParentには対象のComboBoxコントロールの親ウィンドウのハンドル、第2引数にはコントロールから親ウィンドウへ通知するメッセージを表す「WM_COMMAND」(定数値)、第3引数のwParamには親ウィンドウへ送信するメッセージ(パラメータ)、第4引数のhWndCmbBoxには対象のComboBoxコントロールのハンドルをByVal(値渡し)でそれぞれ入力します。
第3引数のwParamは単純に送信したいメッセージをそのまま入力することはできず、コントロールの識別IDと送信したいメッセージをあわせてビット演算を行う必要があります。
wParam = (MSG * &H10000) Or (lID And &HFFFF)
上記のMSGに親ウィンドウへ送信する定数値メッセージ、lIDにコントロールID(基本は0)を入力すればwParamへ入力する値を作成することができます。
ComboBoxコントロールの値変更のメッセージは「CBN_SELCHANGE」です。ただし、このメッセージだけではうまくいかない場合もあるため、サンプルコードでは変更された値が有効であること表す「CBN_SELENDOK」を事前に送信しています。
ComboBoxコントロールの値変更 (代替案)
基本的には前項の手法でComboBoxコントロールの値を変更することができますが、画面上では値が変更されているのに内部的に値が変更されていないことがあります。そのような場合は少し強引な手法ですが、手で操作する時と同じくComboBoxコントロールに対して下記を行うことで解決します。
2. ComboBoxコントロールのプルダウンメニューを開く
3. ComboBoxコントロールの値を変更
4. ComboBoxコントロールのプルダウンメニューを閉じる
プルダウンメニューの開閉のアニメーション描画が行われるので見栄えは少し悪いですが手作業とほぼ同じ動作であるため、親ウィンドウに正常に値変更のメッセージを送ることが出来ず、内部的な値の更新が行われない場合は代替案の一つとして利用することができます。
ComboBoxコントロールのプルダウンメニューを開く(閉じる)
ComboBoxコントロールのハンドルに対して”マウスの左クリック押下のメッセージ”を送信することで、ComboBoxコントロールを左クリックしたときと同じ結果を得ることができます。このとき、クリック操作左クリックを離すメッセージメッセージの送信も必要になります。 各メッセージを送信するにはSendMessage関数を使い、下記のように書きます。
Call SendMessage(hWndCmbBox, WM_LBUTTON_DOWN, 0, 0) ‘ボタン押下
Call SendMessage(hWndCmbBox, WM_LBUTTON_UP, 0, 0) ‘ボタンを離す
第1引数のhWndCmbBoxには文字列の取得を行う対象のEditコントロールのハンドル、第2引数にはマウス左ボタンの押下/離すメッセージ「WM_LBUTTON_DOWN/WM_LBUTTON_UP」(定数値)、第3,第4引数は今回の場合では使用しないので「0」をそれぞれ入力します。
これにより指定のコンボボックスのプルダウンメニューを開くことができます。また、既に開かれているプルダウンメニューに対して上記処理を行うことで、プルダウンメニューを閉じることも可能です。サンプルコードではプルダウンメニューの開閉操作は共通して上記の処理を行っています。
関連情報
VBA×WindowsAPIまとめページ
その他のWindowsAPI関数は下記ページにまとまっているので合わせて参照下さい。
参考
Microsoft公式:ウィンドウ メッセージ (Windows とメッセージ) – Win32 apps