ダイアログのボタンをプログラムでクリックする方法。
ダイアログ上のボタンのウィンドウハンドル(HWND)を取得し、そのHWNDに対してボタンクリックのメッセージを送信すればよい。
|
|
VC++に付属しているspy++を使って、ダイアログのボタンのコントロールIDを調べることが出来る。
コントロールIDとは、ダイアログを自作したことのある人なら分かると思うけど、IDOKとかIDCANCELとかの、プログラマーがコントロール(ボタンやエディットボックス)に付けたIDのこと。
ただ、Windowsが表示しているダイアログの場合、バージョンによってIDが変わることもあるので汎用的に使えるとは限らない。
例えばInternetExplorerの「ファイルのダウンロード」ダイアログの「保存(S)」ボタンの場合、ネットで検索すると0x1144(&H1144)という人がいたが、自分の環境(WindowsXP
SP2、IE6SP2)では0x1148(4424)だった。
あと、Windowsのダイアログの「キャンセル」ボタンは大抵IDCANCELだと思われる(値は2。winuser.hで定義されている)。
Win32APIの以下の関数等を使って、目的のダイアログのHWNDを取得する。
関数名 | 概要 |
EnumWindows() | 表示されているウィンドウのHWNDを全て列挙する。 これらに対してウィンドウクラス名やタイトルを取得して判定し、目的のウィンドウ(ダイアログ)を探す。 |
GetWindowText() | ウィンドウのHWNDからタイトル(キャプション)を取得する。 |
GetClassName() | ウィンドウのHWNDからウィンドウクラス名を取得する。 |
FindWindow() | ウィンドウクラス名やタイトルを指定して、合致するウィンドウ(のHWND)を取得する。 |
ダイアログのHWNDが分かっていれば、Win32APIのEnumChildWindows()で子ウィンドウ(のHWND)を列挙できる。
その中にボタンも含まれているので、GetWindowText()等を使って目的のボタンを特定する。
static BOOL CALLBACK FindSaveButton(
HWND hwnd, // ウィンドウハンドル
LPARAM lParam // アプリケーション定義の値(今回はボタンのHWNDを返す為のポインター)
)
{
TCHAR tbuf[1024];
::GetWindowText(hwnd, tbuf, sizeof(tbuf)); //表示されているテキストを取得
if (lstrcmp(tbuf, _T("保存(&S)")) == 0) {
HWND *ret = reinterpret_cast<HWND*>(lParam);
*ret = hwnd;
return FALSE; //探索終了
}
return TRUE; //探索続行
}
//ダイアログのHWNDから「保存」ボタンのHWNDを取得する
HWND GetSaveButton(HWND hdlg)
{
HWND hbtn = NULL;
::EnumChildWindows(hdlg, FindSaveButton, reinterpret_cast<LPARAM>(&hbtn));
return hbtn;
}
※hdlgはダイアログのHWND
ボタンのHWNDからコントロールIDを調べるには、GetDlgCtrlID()を使用する。
int id = ::GetDlgCtrlID(hbtn);
ボタンのコントロールID(とダイアログのHWND)からHWNDを取得するには、GetDlgItem()を使用する。
HWND hbtn = ::GetDlgItem(hdlg, id);
※hdlgはダイアログのHWND、hbtnはボタンのHWND、idはボタンのコントロールID
試した結果、ボタンをクリックするにはとりあえず3通りの方法がある。Bが一番シンプルで楽かな?
方式 | 例 | |
---|---|---|
A | マウスのボタンを押し、離すメッセージを送信する。 クリック位置のクライアント座標をLPARAMにちゃんと指定すれば、手動でクリックする場合に一番近づく。 |
::PostMessage(hbtn, WM_LBUTTONDOWN, MK_LBUTTON, 0); ::PostMessage(hbtn, WM_LBUTTONUP, 0, 0); |
B | ボタンコントロール専用のクリックメッセージを送信する。 これを送ると、裏ではAのメッセージが送信される。 |
::PostMessage(hbtn, BM_CLICK, 0, 0);BMは、ボタン コントロール メッセージ 。 |
C | ボタンの処理を実行するコマンドを送信する。 AでもBでも、最終的にはこのメッセージが送信されている。 |
::PostMessage(hdlg, WM_COMMAND, MAKEWPARAM(id, BN_CLICKED), (LPARAM)hbtn);BNは、ボタン通知(Button Notification)。 MAKEWPARAM(id,flag) は、((flag<<16)|id) とほぼ同等。そしてBN_CLICKEDは0なので、WPARAMにはidのみを指定しても結果は同じだが、それは正しいプログラミングとは言えないだろう。 |
※hdlgはダイアログのHWND、hbtnはボタンのHWND、idはボタンのコントロールID
いずれの方法でも大丈夫だと思うけど、AorBと共にCを組み合わせては駄目。
AorBによってCと同等のWM_COMMANDがダイアログに対して送られるので、同じコマンドが2つ行ってしまう。
IEの「ファイルのダウンロード」で試したとき、次の処理のダイアログが2つ出た(爆)
本来なら1つだけしか出ないはずなのに^^;
さて、ちょっと(かなり。だいぶ)苦労したのが、ボタンによって押せたり押せなかったりしたこと。
IDOKやIDCANCELといったボタンは問題なく押せたのだが、IEの「ファイルのダウンロード」の「保存(S)」ボタンや「開く(O)」ボタンが押せないことがあった。具体的には、押したいボタンにフォーカスは移動するのだが、処理が実行されなかった。
同じダイアログの「キャンセル」ボタンは動作したので、コントロールIDの値の大きさが関係しているのかなーと想像している。IDOKは1、IDCANCELは2、「開く」はうちでは0x1147、「保存」は0x1148だから。本当はもっと本質的な違いがあるんだろうけど。
とりあえず手動でボタンを押した場合とプログラムでメッセージを送信した場合とのイベントの処理のされ方をspy++で表示して見比べた結果、WM_SETFOCUSの戻り値が異なっているのを発見。(プログラムで送信して上手くいっていないケースでは、戻り値がNULLになっていた)
確かにプログラムで送るときはフォーカスを特に気にしてなかった。というか、ダイアログが最前面に出ていない状態でボタンを押したかった。
手動でクリックする場合は当然ダイアログが最前面に来ていたが、プログラムからの場合はそれを実行させるVisualStudioが最前面に来ていたし。
そういう訳で試しにプログラム内でSetFocus(hdlg)してやってから上述のボタンクリック送信を行ったところ、見事に押せた!
どうしてフォーカスが関係あるのかは不思議だけど…。なお、SetForegroundWindow()やSetActiveWindow()は実行していないので、最前面かどうかは無関係なようだ。
ちなみに、SetFocus()してから実際にフォーカスが移るまで多少時間がかかる模様。自分の環境では400ミリ秒ほどスリープしないと駄目だった。
スリープするのではなくフォーカスが移ったかどうかを確認する方法は不明。SetFocus()の前後の自前のGetFocus()は、どちらもNULLだったし!まぁ、GetFocus()は現在のスレッドに対するフォーカスのあるウィンドウを取得するものらしいので、他所のダイアログのは取れないのかも。