【VBA×WindowsAPI】メッセージボックスのボタンの文字列を変更する
VBAでメッセージボックスの「OK」や「キャンセル」ボタンの文字列を変更したいと思ったことはあるでしょうか。UserFormを使えばボタンの文字列や位置を調整できるため、ほとんどの場合はそんな状況に出くわさないと思いますが、一応WindowsAPIを使うことでメッセージボックスの文字列を変更することができます。かなり専門的で高度な処理を行いますが、コピペで再現自体は可能です。
ちなみに「キャンセル」を「Cancel」、「はい」を「Yes」のように日本語OSで英語OSの再現をしたいという場合は、本ページの内容でも実現することはできますがMessageBoxEx関数でロケールIDを指示する手法のほうが簡単かつ処理が単純なのでそちらをおすすめします。
メッセージボックスのボタンの文字列を変更
WindowsAPIを使うことで、上画像のようにMsgBox関数で表示されるメッセージボックスの文字列を変更することでができます。内部的にはあくまでも用意されているボタンはそのままで、描画されているテキストを書き換えているだけの処理となっています。(※戻り値のvbOKやvbCancelもそのまま)
メッセージボックスはモーダルウィンドウであるため、MsgBox関数を呼び出した後ではFindWindow関数などでアクセスすることができず、処理の対象であるウィンドウのハンドルを取得することができません。この問題を解決するためには、特定の処理の途中に別の処理を割り込ませる「フック」と呼ばれる技術を使います。このフック処理を使うことにより、メッセージボックスウィンドウが表示されるよりも前に「ボタンの文字列を変更させる」という処理を割り込ませることができるようになります。
VBAでメッセージボックスのボタンの文字列を変更するには下記のWindows APIを利用します。
それぞれ関数のより詳細な使い方の解説は各関数のリンクページを参照下さい。
UnhookWindowsHookEx関数 :フックプロシージャを削除する (フックを解除する)
CallNextHookEx関数 :フック処理の途中で次のフックにメッセージを渡す
GetCurrentThreadId関数 :実行中のスレッドIDを取得する
GetClassName関数 :ウィンドウのクラス名を取得する
SetDlgItemText関数 :指定コントロールの文字列を変更する
「そもそもWindows APIって何?」という方はコチラ(メインページ)も併せて参照下さい。
サンプルコード
メッセージボックスのボタンの文字列を変更するためのサンプルコードは下記の通りです。main関数を実行することで、ボタンの表記を変えたメッセージボックスを表示することができます。
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 |
Option Explicit Private Declare PtrSafe Function SetWindowsHookEx Lib "user32" Alias "SetWindowsHookExA" (ByVal idHook As Long, ByVal lpfn As LongPtr, ByVal hmod As LongPtr, ByVal dwThreadId As Long) As LongPtr Private Declare PtrSafe Function UnhookWindowsHookEx Lib "user32" (ByVal hHook As LongPtr) As Long Private Declare PtrSafe Function CallNextHookEx Lib "user32" (ByVal hHook As LongPtr, ByVal nCode As Long, ByVal wParam As LongPtr, lParam As Any) As LongPtr Private Declare PtrSafe Function GetCurrentThreadId Lib "kernel32" () As Long Private Declare PtrSafe Function GetClassName Lib "user32" Alias "GetClassNameA" (ByVal hwnd As LongPtr, ByVal lpClassName As String, ByVal nMaxCount As Long) As Long Private Declare PtrSafe Function SetDlgItemText Lib "user32" Alias "SetDlgItemTextA" (ByVal hDlg As LongPtr, ByVal nIDDlgItem As Long, ByVal lpString As String) As Long 'フック処理用定数 Private Const WH_CBT As Long = 5 Private Const HCBT_ACTIVATE As Long = 5 'ボタンのコントロールID定数 Private Const IDOK As Long = 1 '[OK]ボタン Private Const IDCANCEL As Long = 2 '[キャンセル]ボタン 'Private Const IDABORT As Long = 3 '[中止]ボタン 'Private Const IDRETRY As Long = 4 '[再試行]ボタン 'Private Const IDIGNORE As Long = 5 '[無視]ボタン 'Private Const IDYES As Long = 6 '[はい]ボタン 'Private Const IDNO As Long = 7 '[いいえ]ボタン Private hHook As LongPtr 'フックプロシージャハンドル '------------------------------------------------------------------------------ ' メイン処理 '------------------------------------------------------------------------------ Sub main() Dim lRet As Long 'フック処理設定 hHook = SetWindowsHookEx(WH_CBT, AddressOf CBTProc, 0&, GetCurrentThreadId) 'メッセージボックス表示(フック処理適用) lRet = MsgBox("フック処理を使ってメッセージボックスの", vbOKCancel + vbInformation) 'ボタンの文字列は変わってもボタンの種類は元のまま If lRet = vbOK Then Debug.Print "[OK]ボタンがクリック" ElseIf lRet = vbCancel Then Debug.Print "[キャンセル]ボタンがクリック" End If End Sub '------------------------------------------------------------------------------ ' CBTProcコールバック関数 '------------------------------------------------------------------------------ Public Function CBTProc(ByVal nCode As Long, ByVal wParam As LongPtr, ByVal lParam As Long) As Long Dim sClassName As String * 256 Dim lRet As Long 'ウィンドウがアクティブ化された時 If nCode = HCBT_ACTIVATE Then 'クラス名取得 (wParamはフック対象のウィンドウのハンドル) lRet = GetClassName(wParam, sClassName, Len(sClassName)) 'フックするウィンドウがメッセージボックス(ダイアログ)かを判定 If Left(sClassName, lRet) = "#32770" Then 'メッセージのテキスト変更 Call SetDlgItemText(wParam, IDOK, "ボタンの文字を") Call SetDlgItemText(wParam, IDCANCEL, "変更している") 'フック解除 Call UnhookWindowsHookEx(hHook) End If End If '次のフックへ処理を渡して処理を継続させる Call CallNextHookEx(hHook, nCode, wParam, lParam) End Function |
CBTProcコールバック関数は本来、コンパイルなどされて処理が中断されることはない部分の処理であるため、ブレークポイントを置いたり、デバッグエラーなどで処理が止まるとアプリケーション(Excel等)が強制終了されます。CBTProcコールバック関数内にある変数の中身が知りたい場合は「Debug.Print」などを使って処理を止めないようにして確認する必要があるので注意が必要です。
コード解説
CBTProcコールバック関数の定義
CBTProcコールバック関数は、ウィンドウ操作やユーザーインターフェースの制御を監視/フックするための関数で、特定のウィンドウイベント(ウィンドウの作成やサイズ変更など)に応じて指定の処理を行わせることができます。関数名に決まりはありませんが引数は下記の通りの設定が必要です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
'------------------------------------------------------------------------------ ' CBTProcコールバック関数 '------------------------------------------------------------------------------ Public Function CBTProc(ByVal nCode As Long, ByVal wParam As LongPtr, ByVal lParam As Long) As Long 'ウィンドウがアクティブ化された時 (wParam=対象ウィンドウのハンドル) If nCode = HCBT_ACTIVATE Then '何らかの処理 '条件に応じてフック解除 Call UnhookWindowsHookEx(hHook) End If '次のフックへ処理を渡して処理を継続させる Call CallNextHookEx(hHook, nCode, wParam, lParam) End Function |
ウィンドウイベントは引数のnCodeから受け取ることのできるコードによって判別することができます。ウィンドウがアクティブ化されたときは定数「HCBT_ACTIVATE」、ウィンドウが閉じられるときは定数「HCBT_DESTROYWND 」というように、CBTProcコールバック関数はウィンドウの状況に応じて決められたコードを常に受け取ることができるため、条件に応じて発火するイベントを追加することができます。(コードの種類はMicrosoft公式ページ参照)
ウィンドウイベントが発火した際には引数wParamから対象のウィンドウのハンドルを取得することができます。これにより、ウィンドウが画面に表示されるよりも前に、このウィンドウに対して何らかの処理(サンプルコードではボタン文字列の変更)を行うことができるようになります。
フック処理は終了しないと、CBTProcコールバック関数が常に呼び出され続けてしまいます。UnhookWindowsHookEx関数を使うことでフックの解除ができるため、基本的に実現したい処理が終わったタイミングで速やかに解除することが鉄則です。(アプリ再起動でもフックは解除される)
CBTProcコールバック関数の設定
CBTProcコールバック関数の定義ができたら、SetWindowsHookEx関数を使ってフック処理として登録します。フック処理にもいくつかの種類(キーボードやマウスの入力監視等)がありますが、ウィンドウの状態のイベントとしてはCBTを利用します。(その他の種類はMicrosoft公式ページ参照)
hHook = SetWindowsHookEx(WH_CBT, AddressOf CBTProc, 0&, ThreadId)
SetWindowsHookEx関数でCBTのフック処理を行うには上記のように記載します。ThreadIdには監視する対象のスレッドIDを入力します。VBAに紐づくウィンドウをフックしたい場合は基本的にGetCurrentThreadId関数で取得される値を入力すれば問題ないです。それ以外の引数は上記のような決まった値となります。これにより戻り値としてフックプロシージャへのハンドルを取得することができます。フック解除時はこのハンドルを使用するので保持しておく必要があります。
フック処理の登録時のスレッドIDにより監視する範囲の指定を行うことができます。このフックの監視範囲は可能な限り小さくする必要があります。というのも、フック範囲をすべて(0&)にしてしまうと、VBAとは関係ないところで動いている処理で表示されたウィンドウもフックの対象となってしまいます。無駄な処理が走ってしまうことはもちろんですが、意図しない処理が勝手に行われてしまう可能性があるので注意が必要です。
ウィンドウのフック
上記の設定で基本的なフック処理の実装は完了です。これによりSetWindowsHookEx関数が実行された後、UnhookWindowsHookEx関数が実行されるまではCBTProcコールバック関数が呼び出され続けます。つまり、SetWindowsHookEx関数の実行後に新たなウィンドウであるメッセージボックスを表示すれば、メッセージボックス表示時の処理をフックすることができます。これはメッセージボックスに限らずUserFormやファイル選択ダイアログなどのウィンドウでも同じです。
本来はウィンドウが表示されてからFindWindow関数などでウィンドウハンドルを取得し、その後にウィンドウのボタンの文字列を変更する必要があります。しかしこの場合、1度ウィンドウが画面上に表示されてしまっているため、ボタンの文字列が切り替わる瞬間が見えてしまいます。フック処理を使うことで、ウィンドウが画面上に表示する直前にボタンの文字列を切り替える処理を割り込ませることができるため(いわゆる裏側で)この問題を解決することができるというわけです。
関連情報
VBA×WindowsAPIまとめページ
その他のWindowsAPI関数は下記ページにまとまっているので合わせて参照下さい。
参考
Microsoft公式:フックの概要 – Win32 apps