Python で他のアプリをコントロールする
VBA で Win32API を利用し、クラス名やキャプション名からのウィンドウハンドルを取得して、SendMessage や PostMessage を駆使して TextBox に文字を放り投げたり、ComboBox を選択したり、Button をクリックしたりという事 (以下①) もやりました。
VBA で、UIAutomation を利用し、エレメントを取得して操ったりする事 (以下②) もありました。
(なんで IsInvokePatternAvailable が False なんだよ!って怒ったりね)
でも、最近は「Python でお願い」されることが増えました。
以前、win32gui と言うのを使って①を行った事はあるんです。
win32gui だと、①の方法を、VBA に近い記述で組めるけど、②の機能はない(と思う)。
という訳で色々探してたら、pywinauto と言うのが、①にも②にも対応してるっぽい。
(PyAutoGUI というのもあったけど、②に対応しているかどうかわからないので今回は未確認)
余談ですが、win32guiのインストールは、pip install win32gui
じゃなくてpip install pywin32
pywinauto の良い所
pywinauto のドキュメントは英語なんですよ・・・
https://pywinauto.readthedocs.io/en/latest/contents.html
ワタシィ、エイゴォ、ワッカリマセェ〜ン、タメィゴゥ
しかし、①も②も対応なんて、使わない手はない。
しかも、VBA で組むと全然コードが違う①と②が、ほぼ同じ記述の仕方で組めるように Wrap されてる!
ちなみに、pip install pywinauto
を眺めてると分かりますが、win32gui も使用している様です。
pywinauto で、意外と見つけにくかった情報
pywinauto で、よく見るやり方
from pywinauto import Desktop, Application
app = Desktop(backend='win32')
win = app.window(class_name='Notepad', top_level_only=True) #メモ帳アプリのウィンドウを取得
ed_elem = win.child_window(class_name='Edit') #メモ帳内のエディット領域を取得
ed_elem.set_edit_text(u'OK!') #OK!を書き込む
app = Desktop(backend='win32')
のbackend='win32'
の部分が、①の方法で取得しますよ、という意味です。
app = Desktop(backend='uia')
と記載すれば、②の方法で取得する事になります。
前述の通り、どちらで取得してもその後のコードは似たようになるので、どちらを選んでも良い様に思えますが、②の方法でしか取得できないコントロールがあるので、注意してください。
この辺は、Spy++ や、inspect を使って確認する事になります。
(inspect で確認できるのに、UIAutomation で取得できないモノも、ちらほら・・・)
余談ですが、
win = app.window(title_re='B.*', class_name_re='N.*', top_level_only=True)
title_re
やclass_name_re
を使う事で、。正規表現によるマッチが出来ます。
とあるコントロールの次コントロールが取得したい
ComboBox などは、キャプションが無く、class_name でしか取得できない事が多いですが、ComboBox が複数ある場合、「このコントロールの次の ComboBox が取りたい!」という事も往々にしてあると思います。
from pywinauto import Desktop, Application
app = Desktop(backend='win32')
win = app.window(title_re='BT.*', class_name='#32770', top_level_only=True) #ウィンドウを取得
flg_combo = False
for obj_ctrl in win.descendants():
if obj_ctrl.element_info.class_name == 'Static' and obj_ctrl.element_info.name == 'フォーマット(&F)':
flg_combo = True
elif flg_combo and obj_ctrl.element_info.class_name == 'ComboBox':
obj_ctrl.select('JPEG')
break
当該コントロールの子コントロールは、[当該コントロール].descendants()
で取得できます。
それを for ループで回せばOK。
今回躓いたのは、クラス名やキャプション名を取得する手法。
上記を見てもらえばわかると思いますが、
-
[当該オブジェクト].element_info.name
でキャプション名 -
[当該オブジェクト].element_info.class_name
でクラス名
が取得できます。
調べるのが面倒なので、覚書
-
[当該オブジェクト].print_control_identifiers()
で当該オブジェクトとその子オブジェクトの情報を取得 -
[当該オブジェクト].TypeKeys('{ENTER}')
で当該オブジェクトにキー操作(この例ではENTERキー押下)を行う - 普通にテキストボックス等に文字を入力する場合は
[当該オブジェクト].set_edit_text(u'OK!')
で良い