【VBA×WindowsAPI】ComboBoxコントロールを操作する

ComboBoxコントロールは、ユーザーが選択できるリストを表示するためのコントロールです。本来、Excel VBAだけでは操作不能なExcel以外のアプリケーションのComboBoxコントロールですが、Windows APIと組み合わせることでプログラムとして操作することが可能になります。これにより、ユーザーが手入力している作業をExcel外の領域でも自動化することができます。

本ページではそんなComboBoxコントロールに対して、VBAで値の入力と現在の値を取得する方法を解説していきます。コントロールの種類の調べ方をはじめ、ボタンやテキストボックス等のその他コントロールの自動化は下記メインページを参照下さい。

ComboBoxコントロールを操作

ComboBoxコントロールはクリックすることでプルダウンメニューが表示されるコントロールのことです。メモ帳の[ページ設定]ウィンドウの用紙サイズや給紙方法のコントロールや、[名前を付けて保存]ウィンドウの文字コード選択のコントロールなどがこれにあたります。

本ページではComboBoxコントロールに対して「指定の値に変更する方法」と「現在の値を取得する方法」の2つの方法を解説します。コンボボックスの操作はRPA操作においてもアプリケーションの設定変更時に必須の処理となるため重要な要素の1つとなります。

VBAで別アプリケーションのComboBoxコントロールを操作するには下記のWindows APIを利用します。それぞれ関数のより詳細な使い方の解説は各関数のリンクページを参照下さい。

icon-check-square FindWindow関数           :指定のウィンドウのハンドルを取得する
icon-check-square FindWindowEx関数       :指定のウィンドウ内にある子ウィンドウのハンドルを取得する
icon-check-square GetParent関数               :指定のウィンドウの親ウィンドウのハンドルを取得する
icon-check-square GetWindowLongPtr関数:指定のウィンドウの情報を取得する
icon-check-square SendMessage関数         :指定のウィンドウにメッセージを送信する

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

サンプルコード

指定のComboBoxコントロールに対して値を変更、取得するサンプルコードは下記の通りです。
メモ帳の[名前を付けて保存]ウィンドウを開いた状態で下記コードを実行すると、SetValueToComboBox関数でComboBoxコントロールの値を変更GetValueFromComboBox関数で現在のComboBoxコントロールの値(文字列)を取得することができます。

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コントロールの現在の項目の値を取得するには下記の手順を行います。

1. 操作対象のComboBoxコントロールのハンドルを取得する
2. 現在選択されている項目のインデックスを取得
3. 項目の文字列を受け取る用のバッファを用意
4. インデックスとバッファを使って文字列を取得

 
少し回りくどい処理に感じますが、WindowsAPIは文字列を取得する際にその文字列を格納だけのメモリを確保する必要があるため、文字数の取得とそれに関わるインデックス取得の処理が発生します。
 

icon-edit 現在選択されている項目のインデックスを取得

ComboBoxコントロールのハンドルに対して”現在の項目インデックスを取得するためのメッセージ”を送信することで、そのComboBoxコントロールの現在の項目インデックスを取得することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。

icon-code 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型にしておく必要があります。
  

icon-edit 項目の文字列を受け取る用のバッファを用意

ComboBoxの値(文字列)を取得するには、その値を受け取る用のバッファ(メモリ)を確保しておく必要があります。バッファの確保とは、たとえば"Text"という文字列を受け取る際にその文字数の4文字分の値を格納できるメモリをあらかじめ確保しておくことをいいます。

バッファを確保するにはそもそも文字数が何文字なのかを予め確認しておく必要があります。
ComboBoxコントロールのハンドルに対して”項目の文字数を取得するためのメッセージ”と合わせて項目インデックスを送信することで、そのComboBoxコントロールの指定項目の文字数を取得することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。

icon-code 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引数で指定した文字数分の空文字列を作成することができます。

icon-code バッファ(固定長文字列)の確保 

Dim sBufAs String
sBuf= String(CLng(lLen), vbNullChar)   
'※lLenはLong型に変換

これにより、コンボボックスの現在値の文字数と同じだけのメモリを確保した変数sBufを用意することができます。最終的に取得したコンボボックスの値はこの変数sBuf内に格納されます。
   

icon-edit インデックスとバッファを使って文字列を取得

取得する項目のインデックスと文字数分のバッファが用意出来たら実際に文字列を取得します。
ComboBoxコントロールのハンドルに対して"文字列を取得するためのメッセージ"と合わせて対象の項目インデックスと値を受け取るバッファを送信することで、対象の文字列することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。

icon-code 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コントロールの項目を指定の値に変更には下記の手順を行います。

1. 操作対象のComboBoxコントロールのハンドルを取得する
2. ComboBoxコントロールの値を変更
3. ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信する

 
ComboBoxコントロールの値を変更するためメッセージである「CB_SETCURSEL」や「CB_SELECTSTRING」は送信するだけでコンボボックスの値を変更することができますが、画面上の表示が切り替わるだけで内部的な値の変更はされません

これはComboBoxコントロールの値変更イベントを親のウィンドウが持っている(VBAのUserFormと同じ)ためで、値を内部的にも変更するにはコントロールの値を変更したということを明示的に親ウィンドウに伝えて値変更イベント(CBN_SELCHANGE)を行わせる必要があります。
 

icon-edit ComboBoxコントロールの値を変更

ComboBoxコントロールのハンドルに対して”現在の値を変更するためのメッセージ”と合わせてを送信することで、そのComboBoxコントロールの値を指定の値に変更することができます。メッセージの送信はSendMessage関数を使い、下記のように書きます。

icon-code SendMessage関数 

Call SendMessage(hWndCmbBox, CB_SELECTSTRING, 0, ByVal sText)

第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数にはコントロールの値を変更するメッセージ「CB_SELECTSTRING」(定数値)、第3引数は今回の場合では使用しないので「0」、第4引数のsTextには指定する項目の文字列をByVal(値渡し)でそれぞれ入力します。

これにより、sTextを含む項目がコンボボックスの値として設定されます。このとき、sTextを含む項目が複数存在する場合はインデックスの最も小さい項目が値となり、sTextを含む項目が1つも存在しない場合は値の変更は行われません。
 

icon-edit ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信する

ComboBoxコントロールの親ウィンドウに値変更のメッセージを送信するには、その親ウィンドウのハンドルをあらかじめ取得しておく必要があります。親ウィンドウのハンドルを取得するにはGetParent関数を使い下記のように書きます。使い方としては引数にハンドルを入力するだけです。

icon-code GetParent関数 

hWndParent = GetParent(hWndCmbBox)

 
次にComboBoxコントロールの識別IDを取得します。
識別IDを取得するにはGetWindowLongPtr関数を使い下記のように書きます。

icon-code GetWindowLongPtr関数 

lID = GetWindowLongPtr(hWndCmbBox, GWL_ID)

第1引数のhWndCmbBoxには対象のComboBoxコントロールのハンドル、第2引数には識別IDを取得することを表す定数値「GWL_ID」を入力することで、指定のComboBoxコントロールの識別IDを取得することが出来ます。(※この識別IDは基本的に「0」となるのでスキップしても良い処理)

親ウィンドウのハンドルとComboBoxコントロールの識別IDの取得ができたら、親ウィンドウにComboBoxコントロールの値が変更したことを表すメッセージを送信します。コードとしてはSendMessage関数を使って下記のように書きます。

icon-code SendMessage関数 

Call SendMessage(hWndParent, WM_COMMAND, wParam, ByVal hWndCmbBox)

第1引数のhWndParentには対象のComboBoxコントロールの親ウィンドウのハンドル、第2引数にはコントロールから親ウィンドウへ通知するメッセージを表す「WM_COMMAND」(定数値)、第3引数のwParamには親ウィンドウへ送信するメッセージ(パラメータ)、第4引数のhWndCmbBoxには対象のComboBoxコントロールのハンドルをByVal(値渡し)でそれぞれ入力します。

第3引数のwParamは単純に送信したいメッセージをそのまま入力することはできず、コントロールの識別IDと送信したいメッセージをあわせてビット演算を行う必要があります。

icon-code wParamの作成 

wParam = (MSG * &H10000) Or (lID And &HFFFF)

上記のMSGに親ウィンドウへ送信する定数値メッセージ、lIDにコントロールID(基本は0)を入力すればwParamへ入力する値を作成することができます。

ComboBoxコントロールの値変更のメッセージは「CBN_SELCHANGE」です。ただし、このメッセージだけではうまくいかない場合もあるため、サンプルコードでは変更された値が有効であること表す「CBN_SELENDOK」を事前に送信しています。

 

ComboBoxコントロールの値変更 (代替案)

基本的には前項の手法でComboBoxコントロールの値を変更することができますが、画面上では値が変更されているのに内部的に値が変更されていないことがあります。そのような場合は少し強引な手法ですが、手で操作する時と同じくComboBoxコントロールに対して下記を行うことで解決します。

1. 操作対象のComboBoxコントロールのハンドルを取得する
2. ComboBoxコントロールのプルダウンメニューを開く
3. ComboBoxコントロールの値を変更
4. ComboBoxコントロールのプルダウンメニューを閉じる

 
プルダウンメニューの開閉のアニメーション描画が行われるので見栄えは少し悪いですが手作業とほぼ同じ動作であるため、親ウィンドウに正常に値変更のメッセージを送ることが出来ず、内部的な値の更新が行われない場合は代替案の一つとして利用することができます。
 

icon-edit ComboBoxコントロールのプルダウンメニューを開く(閉じる)

ComboBoxコントロールのハンドルに対して"マウスの左クリック押下のメッセージ"を送信することで、ComboBoxコントロールを左クリックしたときと同じ結果を得ることができます。このとき、クリック操作左クリックを離すメッセージメッセージの送信も必要になります。 各メッセージを送信するにはSendMessage関数を使い、下記のように書きます。

icon-code 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」をそれぞれ入力します。

これにより指定のコンボボックスのプルダウンメニューを開くことができます。また、既に開かれているプルダウンメニューに対して上記処理を行うことで、プルダウンメニューを閉じることも可能です。サンプルコードではプルダウンメニューの開閉操作は共通して上記の処理を行っています。

関連情報

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

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

icon-share-square 参考

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

2024年3月26日Excel,RPA,VBA,Windows API