ちまたで話題のRPA(Robotic Process Automation)をpythonで自作することで、業務効率化はもちろん、必要に応じて内製でカスタマイズできるシステム作成を目指します。
今回は使えそうなライブラリ PyWin32 の検証をしていきます。
-- 実行環境 --
Windows 10 64ビット版(1903)、python 3.7.4
PyWin32
Windows 特化のライブラリでWindows APIを使うことが出来るようになります。非常に様々な処理が可能になりますが、Windows APIが別言語で構成されている関係か、呼び出しには通常のpythonとは違った注意点などが色々あり取り扱いが難しい印象です。
RPAとして使えそうな関数などいくつか紹介・試してみたいと思います。
ツール
PyWin32を使う前に、ターゲットのウィンドウのクラスやハンドルの取得・確認に、Microsoft Visual Studio に同梱されているツール Microsoft Spy++ を使用したのでご紹介します。 GUIでの操作ではなく、ウィンドウやコントロールのハンドルを取得して処理を行う場合にはこの手のツールは必須じゃないかと思います。
下の画像はコントロールパネルのシステムのプロパティ、詳細設定タブを表示した状態をSpy++で調べた時の状態です。ウィンドウの階層構造なども分かり非常に重宝しました。

PyWin32 の検証
試す操作
コントロールパネルのシステムのプロパティから「詳細設定」タブの操作をしてみたいと思います。

ウィンドウハンドル取得
ウィンドウのタイトルからウィンドウハンドルを取得します。今回はウィンドウタイトルをキーに検索を行います。
import win32gui
target_class = 0
target_title = "システムのプロパティ"
hWnd = win32gui.FindWindow( target_class, target_title)
print( hex(hWnd))
実行結果

PyWin32に含まれているwin32guiの関数FindWindowを使用し、ターゲットウィンドウのハンドルを取得しています。ウィンドウタイトルだけでなくクラス名検索したい場合にはクラス名にクラス名を指定してください。
システムのプロパティの場合は target_class = “#32770″ になります。クラス名などはSpy++で調べることが可能です。
ボタンのハンドル取得
上記のプロパティ画面内の「環境変数」ボタンのハンドルを取得してみたいと思います。コントロールの一致条件は「テキスト」「コントロールID」「クラス名」としています。
import ctypes
import win32gui
import win32con
def main():
# 対象定義
target_window_class = 0
target_window_title = "システムのプロパティ"
target_control_id = 0
target_control_class = 0
target_control_title = "環境変数(&N)..."
# 親ウィンドウハンドル取得
hWnd = win32gui.FindWindow( target_window_class, target_window_title)
print( "親ウィンドウのハンドル", hex(hWnd))
# ウィンドウ内のコントロール検索
hWnd_child = get_hwnd_search_control( hWnd, target_control_id, target_control_class, target_control_title)
print( "ターゲットのハンドル", hex(hWnd_child))
return 0
def get_hwnd_search_control(hWnd, target_control_id, target_control_class, target_control_title):
try:
htarget = 0
if hWnd != 0 :
# チェック情報取得
cid = win32gui.GetDlgCtrlID( hWnd)
cls = win32gui.GetClassName( hWnd)
# ターゲットテキスト取得
length = win32gui.SendMessage(hWnd, win32con.WM_GETTEXTLENGTH, 0, 0)
buff = ctypes.create_unicode_buffer(length + 1)
win32gui.SendMessage( hWnd, win32con.WM_GETTEXT, length+1, buff)
# チェック
if win32gui.IsWindowVisible(hWnd) :
if target_control_id == 0 or target_control_id == cid:
if target_control_class == 0 or target_control_class == cls :
if target_control_title == "" or target_control_title == buff.value:
# ターゲット確認
htarget = hWnd
# 渡されたハンドルがNGの場合、その子およびその階層をチェック
if htarget == 0 :
hChild = win32gui.GetWindow( hWnd, win32con.GW_CHILD)
while (hChild):
htmp = get_hwnd_search_control( hChild, target_control_id, target_control_class, target_control_title)
if htmp != 0 :
htarget = htmp
break
hChild = win32gui.GetWindow( hChild, win32con.GW_HWNDNEXT)
else:
htarget = 0
return htarget
except Exception as err:
# 取得失敗
print("【ERROR】get_hwnd_search_control【ERROR】")
print (err)
return 0
if __name__ == '__main__':
main()
実行結果

ちゃんとターゲットのボタンのハンドルを取得できていることが出来ました。 流れとしては親ウィンドウハンドルを取得して、そのウィンドウの子を再起で全チェックといった流れです。
思ったより長くなってしまいました…. もっと楽な方法(というかライブラリ)がありそうな気がします。PyWin32を使う前提ではありましたが、もっと簡単な方法ありましたらコメント頂けると嬉しいです。
ボタンクリック
取得したハンドルに対して、クリックイベントを送ります。
import ctypes
import win32gui
import win32con
def main():
# 対象定義
target_window_class = 0
target_window_title = "システムのプロパティ"
target_control_id = 0
target_control_class = 0
target_control_title = "環境変数(&N)..."
# 親ウィンドウハンドル取得
hWnd = win32gui.FindWindow( target_window_class, target_window_title)
print( "親ウィンドウのハンドル", hex(hWnd))
# ウィンドウ内のコントロール検索
hWnd_child = get_hwnd_search_control( hWnd, target_control_id, target_control_class, target_control_title)
print( "ターゲットのハンドル", hex(hWnd_child))
result = win32gui.PostMessage( hWnd_child, win32con.BM_CLICK, 0, 0)
return 0
def get_hwnd_search_control(hWnd, target_control_id, target_control_class, target_control_title):
***割愛(上のサンプルと同じ)***
if __name__ == '__main__':
main()
win32gui.PostMessage を使ってターゲットウィンドウに対してBM_CLICKを送っています。
今回使用した画面はサンプルとしてはあまりよくなかったですね。クリックするには管理者権限が必要でした。実行するときは管理者実行してみて下さい。
実行すると環境変数の設定画面が開けたと思います。
まとめ
多少難易度が上がりましたが、対象となるコントロールに対して 直接クリックメッセージを送るため画像検索よりは精度が高いと思われますが、色々なケースを試したところ今回紹介した流れで処理するには採用しづらいことが分かりました。
下記にNGを3つほど上げさせてもらいましたが、現時点では私のスキルの問題で解決策が見えていません。まず実装してみるという点から複雑になる方式は避け、機能追加時に検討していきたいと思っています。
NGの理由1
アプリケーションや使用されているコントロールなどにより、同じロジックが使えないものが多々ある。(例えば、最後のクリックサンプルでPostMessage → SendMessage にしたり、引数のパラメーターを修正しないと動作しないものがありました)
ただ使用しているだけだと全く分かりませんが、アプリなどによりタブやコンボボックスなど使用しているコントロールが違い、ウィンドウの階層構造やクリック時や選択時の処理内容などが違うためだと思われます。単純にクリックだけでも都度動作確認し、動作しない場合はSpy++などを使用して望む処理を行ったときにどのようなメッセージをどこに送っているのか調査が必要そうです。そしてコントロールが千差万別で最初の定義ファイルを作成する敷居が非常に高いように思われます。
NGの理由2
NG1にも該当する話ですが、デスクトップアプリは今回紹介した方法で操作可能ですが、ブラウザなどは今回の方法では操作できません。別ライブラリの Selenium などを使えば自動化は可能ですが、デスクトップアプリはこっちのコマンド、ブラウザはこっちのコマンドといった形で実装していくと、どんどん複雑になるので初期に目標としたシンプルから逸脱するので出来ればこれは避けたい。
NGの理由 3
一部ウィンドウの中がSpy++では全く確認できないアプリがありました。また、特定コントロール内の詳細が見えないものもチラホラと(タブコントロールは見えるがその中のコントロールが見えない等)。残念ながら私の知識では解決できませんでした。(下図はJREのコンパネですが、ウィンドウは見えるんですが、中のコントロールはSpy++では確認できていません。Java系がダメなんでしょうか。。。)

見て頂いてありがとうございました。次回もPyWin32で使えそうな関数などを検証してみたいと思います。
コメント