色々なシーンで見かける以下の様な「ファイル選択ダイアログ」ですが、これを自動操作できれば便利だと思いませんか?
今回はこのダイアログを自動操作して、指定したファイルを選択させる処理を紹介致します。
ウィンドウとウィンドウハンドルについて
ダイアログの操作にあたって、まずは「ウィンドウ」というものが何かを理解する必要があります。
Windows上で動作している様々なアプリケーションは「ウィンドウ」と「コントロール」の組み合わせで構成されています。Microsoftでは以下の様に紹介されています。
Windows では、さまざまな形式のウィンドウが表示されます。実際、画面に表示されているかどうかにかかわらず、ほとんどの要素はウィンドウ内に存在します。最も一般的なアプリケーションのウィンドウなど、画面上で区切られた長方形の領域も 1 つのウィンドウ形式です。すべての種類のコントロールがウィンドウというわけではありませんが、リスト ボックス、スクロール バーなど、フォーム上にあるコントロールもウィンドウである場合があります。デスクトップに表示されるアイコン、およびデスクトップ自体もウィンドウです。
今回操作するダイアログも、もちろん「ウィンドウ」と「コントロール」で構成されています。
ではWindows上でこれら「ウィンドウ」と「コントロール」はどの様に管理されているでしょうか。
実は全ての項目には整数の”識別番号”が割り振られており、その番号によって管理されているのです。この識別番号は”ハンドル”と呼ばれます。
例えば先ほどのファイル選択ダイアログの場合ですと、
※番号は全て例です。また、ウィンドウやコントロールも大まかに色を付けているだけで、実際はもっと細かく分かれています。
この様に複数のウィンドウとコントロールで構成されており、それぞれハンドルの値は異なります。例えば上図においてファイル名の設定に関する操作を行いたい場合は、「78845678」というハンドルを使用することになります。(※ファイル名の入力ボックスに割り当てられているハンドル値)
ダイアログのハンドルを取得する
とりあえずは「ダイアログを操作するにはハンドルの値が必要」ということを覚えてもらえれば良いです。ではこれから実際にハンドルの値を取得してみましょう。
「え?わざわざ取得しなくてもさっき”78845678”って言ってたじゃん。これを使えばいいんでしょ?」
と思うかもしれませんが、ハンドルの値というのは固定で割り振られている訳ではありません。
ウィンドウが作られた段階でハンドルが割り当てられますが、ウィンドウが閉じられるとそのハンドルは解放されます。再度ウィンドウが作成された場合はまた新たなハンドル値が割り振られますので、最初と同じハンドルが割り当てられるとは限らないのです。
ハンドルを取得するには「FindWindow」と呼ばれる関数を使用します。
HWND FindWindow( LPCTSTR lpClassName, // クラス名 LPCTSTR lpWindowName // ウィンドウ名 );
・クラス名は「#32770」を指定します。この値はダイアログのクラス名となり、基本的にこの名前は固定です。
・ウィンドウ名は「アップロードするファイルの選択」を指定します。
この部分がウィンドウ名です。
これらをあわせて以下の様に記述します。
'----------------- API宣言部分 ----------------- Function FindWindow Lib "USER32" Alias "FindWindowA" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr '----------------- 処理部分 ----------------- Sub Test() Dim hWindow As Long hWindow = FindWindow("#32770", "アップロードするファイルの選択")
※使用する場合はAPI宣言が必要になります。
※環境によってはFunctionの前に”Declare PtrSafe”等の記述が必要になります。
上記のコードを実行することで取得できた値が、そのファイル選択ダイアログのハンドルです。
コントロールのハンドルを取得する
ファイルを選択して決定させるためには、”ファイル名入力ボックス”と”開くボタン”を操作する必要があります。こちらのコントロールについてもハンドルを取得しましょう。
ハンドルを取得するには「FindWindowEx」関数を使用します。
HWND FindWindowEx( HWND hwndParent, // 親ウィンドウのハンドル HWND hwndChildAfter, // 子ウィンドウのハンドル LPCTSTR lpszClass, // クラス名 LPCTSTR lpszWindow // ウィンドウ名 );
・親ウィンドウのハンドルは先ほど取得したダイアログのハンドルを指定します。
・子ウィンドウのハンドルは無し。
・入力ボックスのクラス名は「Edit」。ウィンドウ名は無し。
・ボタンのクラス名は「Button」。ウィンドウ名は「開く(&O)」となります。
今回クラス名の調べ方については省きます。
最初は「FindWindow」という関数を使い、今度は「FindWindowEx」という関数が出てきました。用途の違いは以下の通りです。
■FindWindow:トップレベルウィンドウ(親を持たないウィンドウ)を探す場合に使用
■FindWindowEx:子ウィンドウやコントロールを探す場合に使用
今回は親となるダイアログに含まれるコントロール要素を取得するため、FindWindowExを使用するという訳です。
それでは上記の内容をもとに以下の様に記述します。
'----------------- API宣言部分 ----------------- Function FindWindowEx Lib "user32.dll" _ Alias "FindWindowExA" ( _ ByVal hWndParent As Long, _ ByVal hwndChildAfter As Long, _ ByVal lpszClass As String, _ ByVal lpszWindow As String) As Long Function FindWindow Lib "USER32" Alias "FindWindowA" _ (ByVal lpClassName As String, ByVal lpWindowName As String) As LongPtr '----------------- 処理部分 ----------------- Sub Test() Dim hInputBox As Long Dim hButton As Long Dim hWindow As Long hWindow = FindWindow("#32770", "アップロードするファイルの選択") hInputBox = FindWindowEx(hWindow, 0&, "ComboBoxEx32", "") hInputBox = FindWindowEx(hInputBox, 0&, "ComboBox", "") hInputBox = FindWindowEx(hInputBox, 0&, "Edit", "") hButton = FindWindowEx(hWindow, 0&, "Button", "開く(&O)")
実はファイル名入力ボックスについては幾つかのコントロールの入れ子になっているので、「ダイアログ」の中にある「ComboBoxEx32」の、更に中にある「ComboBox」の・・・といった様に順番に掘り下げていく必要があります。
先ほどのダイアログのハンドルを取得する処理と組み合わせると、”入力ボックス”、”ボタン”のハンドルもそれぞれ取得できることを確認できるはずです。
ブレークポイントなど設定して、値が取得できていることを確認してみましょう。
メッセージを送信する
ではこれで必要なデータ(ハンドル)は全て揃いました。
後はダイアログに対して情報(ファイル名など)を送信するだけですね。
情報の送信には「SendMessage」関数を使用します。
LRESULT SendMessage( HWND hWnd, // 送信先ウィンドウのハンドル UINT Msg, // メッセージ WPARAM wParam, // メッセージの最初のパラメータ LPARAM lParam // メッセージの 2 番目のパラメータ );
・送信先ウィンドウのハンドルは先ほど取得したハンドルを指定します。
入力ボックスは”hInputBox”、ボタンは”hButton”の値を指定します。
・Msgには実行したいアクションを指定します。
・wParam、lParamは文字通りパラメータの指定です。送信したいファイルパスなどをこちらに指定します。
それでは上記の内容をもとに以下の様に記述します。
'----------------- API宣言部分 ----------------- Function SendMessage Lib "user32.dll" Alias "SendMessageA" _ (ByVal hWnd As Long, ByVal Msg As Long, _ ByVal wParam As Long, ByVal lParam As Any) As Long '----------------- 処理部分 ----------------- '~~~~~~~~ '途中の処理は割愛 '~~~~~~~~ 'ファイルパス入力' Call SendMessage(hInputBox, &HC, 0, "C:\test.jpg") 'ボタン押下' Call SendMessage(hButton, &H6, 1, 0&) 'ボタンをアクティブにする Call SendMessage(hButton, &HF5, 0, 0&) 'ボタンをクリックする
このコードをハンドルを取得する処理と組み合わせて実行すると、『Cドライブ直下にある「test.jpg」というファイルを選択して、開くボタンを押す』という処理が行われます。
実際にファイルを置いて動作を確認してみましょう。一つ一つの動作を確認するためにも、ステップインで一行ずつ処理させながら変数の中身を見ていくと分かりやすいと思います。
ところで、「SendMessage()」というたった1つの関数で文字列を貼り付けたりクリックしたりといった操作ができるのは不思議だと思いませんか?
SendMessageにどの様な動きをさせるかというのは、2つ目のMsg引数次第なんです。
「&HC」とすれば文字列送信。
「&H6」でアクティブ化。
「&HF5」でクリック。
他にも色々な操作が可能です。興味のある方はぜひ調べてみて下さい。
今回はクラス名固定のダイアログなので簡単に操作できましたが、応用すれば更に複雑なものも操作可能です。Outlookなどのソフトであったり、普段会社で使用している業務システムであったり・・・。
最後に
いかがでしたでしょうか。処理は思うように動きましたか?
「一行ずつ処理させた場合はうまくいくけど、通しで実行するとうまくいかない」という場合は、処理の間にSleepを入れてみるなどして処理に余裕を持たせてみましょう。Sleepだと確実性に欠けるという場合は、DoLoopと組み合わせて「ハンドルの値が取れるまでループさせる」といった処理を加えるとより安定すると思います。
今回使用した”SendMessage”は、ダイアログ操作以外にも色々なことに使用することができます。今度はまた別の用途で紹介してみますね。
ではまた!
コメント
いろいろ調べてこちらへたどり着きました。
すごく参考になり無事にツールが作れそうです。
大変感謝しております。ありがとうございました!
参考になった様で良かったです!
他にも分からない処理などがありましたら遠慮無く質問して下さいね!
内容次第では記事にして詳しく解説させて頂きます。
‘ファイルパス入力’
SendMessage(hInputBox, &HC, 0, “C:\test.jpg”)
の部分が実行不可能で
‘ファイルパス入力’
Call SendMessage(hInputBox, &HC, 0, “C:\test.jpg”)
にしたら実行できました
ご指摘頂きありがとうございます!
修正させて頂きます。
この記事は大変わかりやすく、非常に参考になりました。感謝申し上げます。
一点教えてください。”アップロードするファイルの選択”を、Microsoft IME 辞書ツール起動時の”テキスト ファイルからの登録”に置き換えて試行中ですが、うまくいきません。
記述中の「ダイアログのハンドルを取得する」については、そのまま流用できると考えておりますが、「コントロールのハンドルを取得する」「メッセージを送信する」で変わってしまう値はございますでしょうか?
ご質問頂いた件についてこちらで確認してみましたが、記事内の処理のままで「ファイルパスの入力」から「開くボタンの押下」まで問題無くできた事を確認致しました。
どのあたりの処理までが出来て、どの部分の処理に失敗したかをお教え頂ければ何かアドバイスできるかもしれません。
こちらの記事のお陰で業務用マクロが作れそうです!あまりこの手の記事が見当たらずほんとうに助かりました!すみませんが、もしお時間ございましたら2つほどご教授ください。
1つめです。
sendmessageの箇所で、ボタンをアクティブにするという処理が一文あるのですが、こちらは必要なのでしょうか?単にその下のボタンをクリックする処理さえあれば良さそうな気がしております(実際に試してみても問題がないような気が致します)。
2つめです。
記事中にも記載があるように、どうも待ち時間や繰り返し処理を入れていないせいか、5%くらいの確率でエラーになってしまいます。対応として、Sleepだと確実性に欠けるというのはまさにおっしゃるとおりですので、できればDoLoop(かIf)を使いたいです。ウィンドウ等の取得成否については、「Do Until hWindow 0」 というような感じで良いと思うのですが、「hInputboxに適切に値が代入できていたら(開くボタンをクリックする)」という条件はどういうふうに指定できますでしょうか?変なことを言うなと思われるかもしれませんが、タイムラグがあるせいか稀にフルパスの入力に失敗しているようです。
すみません、もしよろしかったら教えてください!
1>
確かにアクティブを外しても動作しましたね(^^;
アクティブにしないと正常に押せない場合もあったのですが、環境に依存するのかもしれません。
問題無く動作するのであればアクティブ処理は外しても問題ありません。
2>
私の場合は、「開く」ボタンを押した後に『ダイアログが消えるまでDo~Loopで待機する』という処理を入れています。
【一定時間経過してもダイアログが残っている場合】
-パスのセットに失敗していると判断して処理の先頭に戻ってやりなおす
【一定時間以内にダイアログが消えた場合】
-ループを抜けて次の処理へ
という感じですね。
他には値をセットする際のSendMessageの第二引数に「&HC」を指定していますが、ここを「&HD」にすることで入力ボックスの内容を取得することもできます。
それで見ても良いんですが、じゃあ値をセットしてからどの位待って取得すれば良いのかとか、判定を誤って2回送信してしまった場合はとかを考えるとちょっと面倒かなと。
こんな感じでやり方は色々とありますので、お好きな方法で挑戦してみて下さい。
アドバイス大変ありがとうございました。アドバイスどおり、ダイアログが消えたかどうかをLoopUntilで使ってみて、正しく動作しております。
取り急ぎ課題は解決したのですが、1つお伺いしたいことがございます。Sendmessage関数の引数について先程も教えていただいてありがたかったのですが、Sendmessage関数の解説のようなものはどういうところから情報収集しておられるのでしょうか?
例えば、第四引数の「0&」の「&」とはなんぞや?など。ググってみても、「0」が使われていることが多いようなんですが、じゃあ使える引数として何どのようなののがあって、違いは何があって、というような解説資料がなかなかヒット致しませんでした。ご教授いただいた「&HC」も適切なものがヒットせず…。自習するにあたって参考にされているようなものがございましたら教えていただければ幸いです!
うーん・・・私も何かの処理を実装したい時にその都度調べているだけですので、ご紹介できる様な資料やページは無いですね。
お力になれずすみません。
早々のご回答に感謝申し上げます。お世話になります。
”テキスト ファイルからの登録”のダイアログを表示させた状態で実行すると、上手く登録できることが確認できます。ところがダイアログを表示させていない状態だと登録ができません。
このプログラムはもしかして、予めダイアログを表示させていることを前提にしたものなのでしょうか?もしそうであれば、見当違いの質問であった旨、何卒ご容赦ください。
そうですね。こちらに掲載している内容は、『ダイアログが既に開かれている状態』ということが前提になります。
ですので、ダイアログを開くまでの処理は別途記述しなければいけません。
IME辞書を変更するといった処理であれば、VBAで実装するよりもバッチでレジストリを操作したり、何らかのツールを用いた方が良いかと思います。
お世話になっております。お知恵をお貸し頂きありがとうございました。ダイアログを開くまでの処理は、VBSでsendkeyを使って処理いたしました。
ご面倒をお掛けして申し訳ございませんが、別途質問させてください。
現状はこのVBAプログラムで問題ないのですが、後々ファイル名が変わることを想定し、ワイルドカードでも起動できるようにしておきたいと考えております。”D:\jisho\*.txt”を事例として、どのように追記したらよいでしょうか?或いはワイルドカードは使えないのでしょうか?
「ワイルドカードでも起動できるようにしたい」という内容は良く分かりませんが、単純にファイル名が変わる可能性があるという事であれば、その辞書ファイルが格納されているフォルダを予めプログラム上に持っておいて、ファイル名はプログラム実行時に読み取る様にすれば良いのでは無いでしょうか。
何に対してワイルドカードを使いたいのでしょうか?
ご返答ありがとうございます。本当に助かります。
現状はファイル名がa.txtなのですが、b.txtでも動くようにしたいと考え、ワイルドカード(*.txt)で構築したいと考えております。具体的には以下のように記述しましたが、a.txtを拾ってくれず、エラーとなってしまいます。
Sub Test()
Dim hInputBox As Long
Dim hButton As Long
Dim hWindow As Long
Dim strPattern As String, strFile As String
hWindow = FindWindow(“#32770”, “テキスト ファイルからの登録”)
hInputBox = FindWindowEx(hWindow, 0&, “ComboBoxEx32”, “”)
hInputBox = FindWindowEx(hInputBox, 0&, “ComboBox”, “”)
hInputBox = FindWindowEx(hInputBox, 0&, “Edit”, “”)
hButton = FindWindowEx(hWindow, 0&, “Button”, “開く(&O)”)
strPattern = “D:\jisho\*.txt”
strFile = Dir(strPattern)
Call SendMessage(hInputBox, &HC, 0, strFile)
Call SendMessage(hButton, &H6, 1, 0&)
Call SendMessage(hButton, &HF5, 0, 0&)
End Sub
どこが悪いのかご教授頂けると幸いです。
セットするパスのファイル名にワイルドカードは使用できません。
「*.txt」とファイル名を指定しても、ダイアログ側はどのファイルを読み込めばいいのか分かりませんよ。
ですので、「D:\jisho」というフォルダの中にあるファイル名を実行の度に読み込む様にして下さい。
Dir関数で検索すれば方法は出てくると思います。
出来ました!ありがとうございました!
初めまして、この記事を読ませていただきました。大変わかりやすく書かれていて、全くの初心者である私もある程度のことまでは理解できそうです!
一つ質問させていただきたいのですが、私の所でプログラムを動かしたところ、
hButton = FindWindowEx(hWindow, 0&, “Button”, “開く(&O)”)
のところのハンドル値が0になってしまったのですが、これちゃんとハンドル値が取得できていないということですよね?自分の画像データが、この記事にあるとおりに開けなくて困っています。何卒よろしくお願いします。
最後のボタンのハンドルを取得する所で失敗しているのですね。
確認ですが「hWindow」、「hInputBox」についてはそれぞれハンドル値が取得できているという事で宜しいでしょうか?
また、「アップロードするファイルの選択」というウィンドウは表示されたままとなっていますか?
hInputBoxの値が取得できているのであればhWindowの値も間違いという事ですので、考えられる原因としては「開く(&O)」の部分の指定が誤っている可能性位なのですが・・・
返事が遅れてしまい申し訳ございません。あのあと試行錯誤の結果、上記のエラーは解決できました。そして、無事に自動化のプログラミングを組むことにも成功しました(自動出品のプログラムです)。この記事に巡り会わなかったら、ここまでたどり着けませんでした。本当にレインさんには感謝しています。ありがとうございました。
無事解決した様で良かったです!
しかし全くの初心者と仰っていましたが・・・この短期間でまさか自動出品プログラムまで完成させられるとは凄いですね!
習得が速くてうらやましい限りです。(^^;
初めまして。
必要になり探していたところ、ドンピシャの記事で助かります。
早速ですが、ファイルの場所以外は殆ど同じにしておりますが、実行してもファイル選択のダイアログが出てきて、ファイル名に何も入力されずに止まったままです(対象ファイルはC直下で指定していますが、ダイアログはデスクトップになっているのが気にはなります)。
別の方もハンドルが0になっていたようですが、私もステップ実行で調べようとしましたが、制御がダイアログに取られてそもそもVBEが触れない状況です。
コントロールボックスの入れ子の説明箇所が各人の実行内容によって変わってくるのかと思いつつも、先へ進めない状況です。
ご教示頂ければ幸いです。
テスト環境としてのサイトはfirestrageです。流れは「サイトをIEで開きファイルを選択してアップロードして終わり」までです。
その内容でしたら以下の記事の方法で解決できると思いますので、ぜひ試してみて下さい。
https://rabbitfoot.xyz/vba-settimeout/
有り難うございます。
Vba上でのJavascriptの扱いが分からずもがきましたが、とりあえずダイアログのハンドルを取得するとこまでは、出来ました。