S-JIS[2007-10-31] 変更履歴

VC++ ダイアログボタンクリック

ダイアログのボタンをプログラムでクリックする方法。


概要

ダイアログ上のボタンのウィンドウハンドル(HWND)を取得し、そのHWNDに対してボタンクリックのメッセージを送信すればよい。

  1. ダイアログのボタンのIDを調べておく
  2. ダイアログのHWNDを取得する。
  3. IDからボタンのHWNDを取得する。
  4. ボタンをクリックするメッセージを送信する。
  1. ダイアログのHWNDを取得する。
  2. ボタン上のテキスト等からボタンのHWNDを取得する。
  3. (ボタンのHWNDからIDを取得する。)
  4. ボタンをクリックするメッセージを送信する。

コントロールIDの事前調査

VC++に付属しているspy++を使って、ダイアログのボタンのコントロールIDを調べることが出来る。
コントロールIDとは、ダイアログを自作したことのある人なら分かると思うけど、IDOKとかIDCANCELとかの、プログラマーがコントロール(ボタンやエディットボックス)に付けたIDのこと。

  1. 目的のダイアログを表示する。
  2. スパイを実行する。
    VC++2005の場合、「スタート」→「プログラム(P)」→「Microsoft Visual C++ Standard Edition」→「スパイ++」
  3. メニューバーの「検索(E)」→「ウィンドウ検索(F)」(あるいはツールバーのウィンドウ検索ボタン)で「ウィンドウ検索」ダイアログを開く。
  4. ファインダーツールの丸十字マークをドラッグして目的のダイアログのボタン上に持ってゆき、マウスを離す。
  5. (「ウィンドウ検索」ダイアログの下部にあるラジオボタンで「プロパティ(P)」を選択する。)
  6. 「ウィンドウ検索」ダイアログのOKボタンを押す。すると「ウィンドウプロパティ」ダイアログに切り替わる。
  7. 「一般」タブの「コントロールID」に表示されているのが目的のコントロールID(十六進数表示)。

ただ、Windowsが表示しているダイアログの場合、バージョンによってIDが変わることもあるので汎用的に使えるとは限らない。
例えばInternetExplorerの「ファイルのダウンロード」ダイアログの「保存(S)」ボタンの場合、ネットで検索すると0x1144(&H1144)という人がいたが、自分の環境(WindowsXP SP2、IE6SP2)では0x1148(4424)だった。

あと、Windowsのダイアログの「キャンセル」ボタンは大抵IDCANCELだと思われる(値は2。winuser.hで定義されている)。


ダイアログのHWNDの取得

Win32APIの以下の関数等を使って、目的のダイアログのHWNDを取得する。

関数名 概要
EnumWindows() 表示されているウィンドウのHWNDを全て列挙する。
これらに対してウィンドウクラス名やタイトルを取得して判定し、目的のウィンドウ(ダイアログ)を探す。
GetWindowText() ウィンドウのHWNDからタイトル(キャプション)を取得する。
GetClassName() ウィンドウのHWNDからウィンドウクラス名を取得する。
FindWindow() ウィンドウクラス名やタイトルを指定して、合致するウィンドウ(のHWND)を取得する。

ボタンの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の相互取得

ボタンの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()は現在のスレッドに対するフォーカスのあるウィンドウを取得するものらしいので、他所のダイアログのは取れないのかも。


VC++ページへ戻る / 技術メモへ戻る
メールの送信先:ひしだま