UiAutomation
WindowsのPythonでuiautomationを使う機会がありましたので 諸々の事を備忘録としてまとめておきます。 ネットサーチでは意外と「試してみました」的なものしかなく実用的に使うのには苦労しましたので 役に立てればと思います。
uiautomationを使う前に、"本当にGUIツールを操作する必要が有りますか?"ということをもう一度考えてください。
システムが非力だった頃、プログラムはCUI(コマンドベース)で動かすのが普通で、 フィルタプログラミングやCSVをプログラムに食わせて処理をすることが主流でした。 しかし、人間が操作するにはGUIの方が直感的で人に優しい、という事でGUI全盛の時代となりました。 それを、人間の変わりにuiautomation等を使ったRPA(Robotic Process Automation)でやろうという動きになっています。
人→GUIプログラム(処理) から 自動化プログラム→GUIプログラム(処理)という構造の変化です。 この場合、わざわざGUIを使用して動かすのは無駄な手間です。 こういう場合にはAPIを利用できないか、ローカルのシステム内でのAPIでも良いし、Web-APIでも良いので やりたい事へのAPIを用意できないか考えてみてください。 また、システムにコマンドラインでのツールが無いか調べてみるのも有効です。 そうすれば無駄な動きをさせることなくプログラムからやりたい処理を実行できます。 フィルタプログラミングのように処理のパーツを作り込んで、連携させてやりたい事を実現させる方が 少ない資源でキビキビ動くと思います。
RPA系の自動化の場合、人間の目を通さないことによるデータの範囲や値の誤入力、 微妙な漢字間違いやスペル間違いでの多重登録、 等々、人間ならアレレ?と気付くことに対応できるのかの注意も必要です。
GUI操作の良い点は下記のようなことが考えられます。
人が判断出来やすい文字列等でデータや選択肢が表示される
レジストリやDB等に記録されるデータはその値が何を表しているのか分かりにくい事が多々有ります。 単なる数値だったりして直接操作するには何を入れるべきか悩むことになります。 その点、GUIでは人が理解できる表示になっているので間違いにくいといえます。
ひとつの選択/クリックで複雑な処理を実行できる
GUIでのひとつの選択が複数個所に反映されたり、DBの複数のテーブルを書き換えたりする場合も有ります。 逆に言うと何をやっているのか分かってないとスクリプト等で直接操作するのは難しくなります。
このような場合、既存のGUIを操作するほうが安全確実に早く実装できると考えられます。
Web系の自動化には スクリプト言語 + Selenium を使うのが良いと思います。
自戒の念を込めて、老婆心ながら自動化に無駄に翻弄されないで下さい。
Pythonでuiautomation使用時の注意事項
使い方以前の問題で色々と引っかかった点をまず列挙します。
2021/1/27時点 windows 10 64bit 2004(20H1)環境です。
pythonのuiautomationモジュールはWindowsのuiautomation機能を pythonから使えるようにラップしたものです。全てのWindows側の機能を使えるように 作られているわけでは有りません。
Pythonのバージョン
uiautomationを使う場合、他のマシンにも展開することが多いと思います。 この時、Pythonの入っていないマシンにexe化して展開することになりますが 2021/01/27 時点でpyinstallerでexe化する場合、Pythonのver 3.9.xでは 実行時にエラーが出て動きませんでした。エラーを取るのも方法ですが、 ver 3.8.6に落とすと問題なく実行できました。 各種モジュールの関係だと思われますが、ver 3.8系にしておく方が 問題に会わなくてすみます。
Symantecとpyinstaller
pyinstallerを使う場合、Symantecのvirus対策ソフトが入っていると exe化したプログラムがマルウエアとして検出され、削除されます。 64bit版ならOKという情報がありましたが、64bit版でも発生しました。
Symantecをご利用の場合、pyinstallerの独自ビルド等が必要との情報が 有りましたので、トライしてみてください。 私は展開先がESETでしたのでトライしていません。
動的なプログラムに注意
最近のプログラムではuiの対象となる画面構成(=オブジェクト構造)が 動的なものが多く、生成された後でないとuiautomationでは検出できません。
また、画面に表示されていないものはクリックできません。 その為、クリックするにはメニューや画面をスクロールしたり、折りたたまれているものを 展開してから操作する必要が有りますので注意が必要です。
インストール
通常のモジュールのインストールと同様にpipでインストール可能です。
uiautomation以外に必要なモジュールが有れば同様にインストールして下さい。
pip install uiautomation
WindowsのSDKからinspectプログラムもインストールしておくと、オブジェクトの構造を 調べることが出来ますのでインストールすることをお勧めします。
利用しているWindowsのSDKはMicrosoftからダウンロード可能です。 "Windows SDK inspect"辺りで検索するとインストール方法が見つかります。
インストールテスト
上手くインストールされているか下記のコードを実行してみてください。 スタートボタンを押したときの状態に成ればOKです。
日本語環境Windowsを仮定していますので他の言語環境だとName="スタート"を変更する必要が有るでしょう。 ソースの文字コードはUTF-8です。
このドキュメントのコードは以下の環境で確認しています。
windows7 64bit
Python 3.6.5
uiautomation==2.0.10
このドキュメント内では下記のようにuiautomationをインポートしuiで参照します。
import uiautomation as ui
import uiautomation as ui s = ui.ButtonControl(searchDepth=2,Name="スタート") s.GetInvokePattern().Invoke()
スタートボタンオブジェクトを取り出し実行(Invoke)させています。
タスクバーを自動的に隠す設定にしてInvokeをClickに変えてみるとスタートメニューが出ません。
import uiautomation as ui s = ui.ButtonControl(searchDepth=2,Name="スタート") s.Click()
1秒待ってもう一度クリックするか、カーソル位置を指定してからクリックすると出ます。
この辺りが動的な動きによる動作の違いの例です。 タスクバーを自動的に隠す設定にしていなければクリックでも動作します。 出来るだけクリックは使わずinvokeにする方が問題が少ないでしょう。
import uiautomation as ui import time s = ui.ButtonControl(searchDepth=2,Name="スタート") s.Click() time.sleep(1) s.Click()
import uiautomation as ui s = ui.ButtonControl(searchDepth=2,Name="スタート") s.MoveCursorToInnerPos() s.Click()
操作するオブジェクトを調べる
まずは自動化したいプログラムのオブジェクト構造を調べることから始まります。 inspectが使えるなら起動しておくとuiオブジェクトの構造が表示されます。 ただし、表示形式は省略された記述となっているものも有り、使い方を推測して使う必要が有ります。
なかには、.NETやCOMの仕組みに従っていなくてオブジェクト構造をinspectでは調べられないプログラムもあります。 その場合には、pythonのuiautomationで操作することは難しくなります。 パーツ画像での画面上の場所を探索する機能のある他のツール等を使用するほうが良いでしょう。
SDKのinspectを使えない場合にはuiautomation自体でオブジェクト構造をダンプすることも出来ます。
inspectで見たい部分が上手く表示できない場合などは自分で調べた方が確実かもしれません。
このドキュメントでは調査用の関数の例を提供します。
下記のソースのinspectはpythonのモジュールです。SDKのものでは有りません。
import uiautomation as ui import inspect def dump_obj(obj): print('----Inspect-----------------------------------------------------') if obj == None: print('---Obj not exist ----') else: try: print('Obj = ',obj) for x in inspect.getmembers(obj): print(x[0],':',x[1]) except Exception as e: print(e) print('---------------------------------------------------------') s = ui.ButtonControl(searchDepth=2,Name="スタート") dump_obj(s)
次に、オブジェクトの階層をuiautomationの機能でダンプするプログラムの例です。 depthで何レベル下まで表示するかをコントロールできます。
検索に使える形式でダンプしていますのでこれをコピペして貼り付ければオブジェクトを指定できます。
import uiautomation as ui def dump_structure(obj,level,depth=5): if obj == None: return ct=obj.ControlTypeName name=obj.Name print('{level}:{margin}{ct}(Name="{name}")'.format(level=level,margin=' '*level,ct=ct,name=name)) if level < depth: objs = obj.GetChildren() for x in objs: dump_structure(x,level+1,depth) w = ui.WindowControl() while(w): dump_structure(w,1,2) w = w.GetNextSiblingControl()
プログラムウインドウやオブジェクトの並びや上下関係は選択の状況で動的に変わります。 何番目のオブジェクトという指定方法は危険だと思われます。
プログラムウインドウは選択されたプログラムが上にくるようです。 Windowsの画面管理をOSの作り手の視点で考えると、画面上のオブジェクトのzオーダー順(画面上の重なりの順)に 並んでいると想像できます。画面中の位置を上から順に調べていけば簡単に対象のウインドウや コントロールを探すことができるようにしてあると想像できます。
言い換えると選択されたウインドウやポップアップ等で表示されるものは、上側に作られます。
タスクバーが一番上のPaneControlで、一番下にデスクトップのPaneControl(Name="Program Manager")が有り その間に起動されたプログラム類が重なり順に配置されているようです。 タスクバーは表示をマウスカーソルの位置で隠したり表示させたりする必要が有るので一番上なんでしょう。
下記のようにすると全体の構造が表示できます。
w = ui.GetRootControl() dump_structure(w,1,3)
uiautomationの機能を調べる
pythonのインストールディレクトリ\Lib\site-packages\uiautomation の下にソースがあります。
まずpythonのインストールディレクトリの下のuiautomationをdoxygenにかけて ドキュメント を作成します。 doxygenの使い方については検索してください。 オプションの注意点は下記です。
Wizard/Mode extraction mode : All Entries Wizard/Mode language : Optimize for Java or C# output Wizard/Output : HTMLだけで良いと思います。 : with navigation panelとwith search function にチェックを入れます。
作業用に別ディレクトリを作成し、そこにソースをコピーして作業するのが安全だと思います。
MicrosoftのAPIをpythonのuiautomationモジュールが呼び出しています。 pythonから使えるのはこのuiautomationモジュールで定義されたものとなります。
Microsoftの情報はこの辺りです。
https://docs.microsoft.com/ja-jp/dotnet/framework/ui-automation/ui-automation-fundamentals
https://docs.microsoft.com/en-us/windows/desktop/api/uiautomationclient
doxygenをrunさせると uiautomation ドキュメント が作成されますので必要に応じて参照してください。
ウインドウプログラムの基本構造
自分でGUIプログラムを作成したことが有る人は分かっていると思いますが、 作ったことの無い人もいると思いますので軽く説明します。
GUIプログラムは一番もととなるシステムウインドウの下に、 各プログラムがひとつ又は複数のウインドウとして作成されます。
マウスカーソルイベントやキーボードイベントは、まずシステムで受け取られ、 ショートカットなどの判定をされた後、プログラムに渡されるべきものは、 選択されているウインドウに渡されます。
各ウィンドウ内には、GUIパーツが配置されていて、イベントがその構造に応じて 順次渡されていきます。
GUIパーツはコントロール(Control)と呼ばれています。例えばボタン(ButtonControl)、 文字を入力するエディット(EditControl)。ウインドウ(WindowControl)もコントロールです。
コントロールはオブジェクトの種類を表します。
コントロールは色々な機能パターン(Pattern)を持っています。
doxygenで出来たドキュメントでButtonControlを見ると、
ボタンコントロールは独自に3つの機能パターンを持っています。
また基底クラスから継承した様々なメンバー関数が使えます。
uiautomationでの操作は種別(コントロール)オブジェクトを取り出し、
更にコントロールが持っている機能(パターン)オブジェクトを取り出し、
パターンオブジェクトの持つ機能を呼び出すという流れです。
uiautomation.ButtonControl ExpandCollapsePattern GetExpandCollapsePattern (self) InvokePattern GetInvokePattern (self) TogglePattern GetTogglePattern (self) 基底クラス uiautomation.Control
ここで大切なのはキーイベントやマウスイベントはウインドウの重なりや、 Controlの選択された状態で渡される経路が変わるということです。 ロバスト(変化に強い)なプログラムにするには出来るだけマウスイベントや キーイベントを使わず、invokeやsetvalue等の直接Controlに作用するものを 使うほうが良いでしょう。しかし、InvokePatternを実装していないClickだけしか出来ない ケースも有るようです。その場合にはClickで対応するしかないようです。
対象プログラムの起動
操作対象のプログラムは直接プログラムを起動するのが問題が少ないでしょう。 デスクトップのプログラムアイコンをダブルクリックしても実行できますが、 アイコンの上に他のウインドウが重なっていると、そのウインドウをダブルクリックしてしまいます。
起動されたプログラムはトップ(最上位)に表示されます。
起動方法の例をメモ帳プログラムで示してみます。
import subprocess proc = subprocess.Popen('cmd /c "notepad.exe"')
uiautomation 良くある例題1
メモ帳で新規テキストファイルを作成して保存するまでの例を挙げてみます。 途中に構造ダンプを挟んでいますが、オブジェクトを見つけたいところでダンプすると ダンプされた中から対象項目をコピペしていくだけで作成できます。 実装が終わったらダンプ部分は消す事になります。
import uiautomation as ui import inspect import time import subprocess ### dump structure ### def dump_structure(obj,level,depth=5): if obj == None: return ct=obj.ControlTypeName name=obj.Name print('{level}:{margin}{ct}(Name="{name}")'.format(level=level,margin=' '*level,ct=ct,name=name)) if level < depth: objs = obj.GetChildren() for x in objs: dump_structure(x,level+1,depth) proc = subprocess.Popen('cmd /c "notepad.exe"') #time.sleep(1) w = ui.WindowControl(Name='無題 - メモ帳') w.EditControl().GetValuePattern().SetValue('abc') w.MenuBarControl(Name='アプリケーション').MenuItemControl(Name='ファイル(F)').Click() w.MenuItemControl(Name="メモ帳の終了(X)").Click() dump_structure(w,0,5) w.ButtonControl(Name="保存する(S)").Click() dump_structure(w,0,5) s = ui.WindowControl(Name='無題 - メモ帳').WindowControl(Name='名前を付けて保存') dump_structure(s,0,5) s.EditControl(Name='ファイル名:').GetValuePattern().SetValue('a.txt') s.ButtonControl(Name="保存(S)").Click() dump_structure(s,0,5) ui.SetGlobalSearchTimeout(1.0) try: s.WindowControl(Name="名前を付けて保存の確認").ButtonControl(Name="はい(Y)").Click() except Exception as e: pass ui.SetGlobalSearchTimeout(10.0) dump_structure(s,0,5)
uiautomationのプログラムはそれぞれのオブジェクトの持つ機能を順番に'.'で繋いでいけば良く、 ui.xxxControl(Name='yyy')の様な形でコントロールを探せますので、 まずトップレベルのプログラムウインドウで絞り、階層を順に降りていけば目的のオブジェクトに 到達できます。必ずしも順番に辿る必要は無く途中は飛ばして目的のコントロールを指定しても OKです。ただし、同名のコントロールには注意してください。順に探していくパス上に 目的のコントロールより前に同じコントロールで同名のものがあるとそちらが選ばれます。 経路が一意に成るように途中のオブジェクトを指定して目的のコントロールまでを指定します。
メモ帳のファイル書き出しでは同名のファイルが既に有る場合には上書きの確認ダイアログが出ますが、出て無くてもtryでエラーを 引っ掛けて抜けています。タイムアウト時間がデフォルトで10秒と長いのでSetGlobalSearchTimeout(1.0)で タイムアウト時間を1秒に縮めています。念のため終わったらデフォルトの10秒に戻しておきましょう。
名前の指定には正規表現も使えます。
ボタンなどで良くある同じオブジェクトでも状態によって表示されている名前が変わる場合などは
正規表現が使えると便利です。
w = ui.WindowControl(RegexName=r'.*メモ帳') # 'メモ帳'で終わるウインドウ名を指定
uiautomation 例題1 をコマンドで
例題1をコマンド実現すると下記のようになります。
メモ帳を持ち出すのが本当に必要か、、、
> > echo abc > a.txt >
下記のような、テキスト系の処理は各種スクリプト言語で処理するのが楽だと思います。
* テキストの自動生成
* テキストの置換
* 定義ファイルからターゲットの生成(コンパイル、自動登録)
* CSVファイルをDBやExcel等に登録または加工
例題1の改造(使えるパターンを調べる)
第一段階としては人間の操作通りにクリックすることで動かすのが分かりやすいですが クリックはマウスの動きや、画面の重なりに左右されるのでClick()を取り除いてみます。
プログラムのui構造を見て、操作したいコントロールがどのようなパターンを持っているか調べ パターンを取り出して、操作します。今回の場合ClickはInvokePatternのInvokeで置き換えられます。 各コントロールがInvokePatternを持っていることは下記のdump_supported_patterns関数のようなものを 作成すれば知ることが出来ます。
import uiautomation as ui import inspect import time import subprocess def dump_supported_patterns(obj): if obj == None: return print('{ct}(Name="{name}")'.format(ct=obj.ControlTypeName,name=obj.Name)) for x in ui.PatternIdNames: if obj.GetPattern(x): print('-->','Get'+ui.PatternIdNames[x]+'()') proc = subprocess.Popen('cmd /c "notepad.exe"') w = ui.WindowControl(Name='無題 - メモ帳') w.EditControl().GetValuePattern().SetValue('abc') dump_supported_patterns(w.MenuBarControl(Name='アプリケーション').MenuItemControl(Name='ファイル(F)')) w.MenuBarControl(Name='アプリケーション').MenuItemControl(Name='ファイル(F)').GetInvokePattern().Invoke() dump_supported_patterns(w.MenuItemControl(Name="メモ帳の終了(X)")) w.MenuItemControl(Name="メモ帳の終了(X)").GetInvokePattern().Invoke() dump_supported_patterns(w.ButtonControl(Name="保存する(S)")) w.ButtonControl(Name="保存する(S)").GetInvokePattern().Invoke() s = ui.WindowControl(Name='無題 - メモ帳').WindowControl(Name='名前を付けて保存') dump_supported_patterns(s.EditControl(Name='ファイル名:')) s.EditControl(Name='ファイル名:').GetValuePattern().SetValue('a.txt') dump_supported_patterns(s.ButtonControl(Name="保存(S)")) s.ButtonControl(Name="保存(S)").GetInvokePattern().Invoke() ui.SetGlobalSearchTimeout(1.0) try: dump_supported_patterns(s.WindowControl(Name="名前を付けて保存の確認").ButtonControl(Name="はい(Y)")) s.WindowControl(Name="名前を付けて保存の確認").ButtonControl(Name="はい(Y)").GetInvokePattern().Invoke() except Exception as e: pass ui.SetGlobalSearchTimeout(10.0)
uiautomation.pyを見ると分かりますが、それぞれPythonの辞書(id: 名前)データとして下記に一覧があります。
Controlの種類 : ControlTypeNames Patternの種類 : PatternIdNames Propertyの種類 : PropertyIdNamesパターンはこの一覧から取り出したPatternIdでGetPattern(id)を呼び出して パターンオブジェクトが返ってきたらそのパターンを実装していると分かります。 例題のソースでは'Get'を付けた形で表示し、コピペでソースに貼り付けやすくしています。
注意!! uiautomationがGetPattern(id)で取り出せるパターンを 必ずしもGetXxxxxPattern()として実装していない場合があります。 GetXxxxPattern()でエラーが出た場合にはそのままでは使えません。 例えばEditControl等では、その下のScrollBarControlを使ってください。
pythonのインストールディレクトリ\Lib\site-packages\uiautomation の下にソースがあるので 自分で追加することは可能です。
あるいは GetPattern(ui.PatternId.オブジェクトが持っているパターン名)で取り出して 使うことが可能です。
例として「メモ帳」のEditControlはScrollPatternを持っていますがuiautomationでは GerScrollPaterrn()での取り出しは実装されていません。 でもEditControlオブジェクトからGetPattern(ui.PatternId.ScrollPattern)で取り出してやれば 使えることは確認しました。
パターンと同様にプロパティも何を持っているかは下記のような関数を作ると取り出せます。 こちらは持っていないプロパティを取り出そうとすると例外が発生するので、tryで括っています。
def dump_supported_propeties(obj): if obj == None: return print('{ct}(Name="{name}")'.format(ct=obj.ControlTypeName,name=obj.Name)) for x in ui.PropertyIdNames: try: v = obj.GetPropertyValue(x) except: v=None if v: print('...',ui.PropertyIdNames[x],'=',v)
これでほぼUIの操作に必要な道具立ては揃いました。 後は各パターンの持つ機能を調べる事になります。
May the source be with you.
uiautomationのドキュメント
を参照しながらソースを見るのが一番確実です。
よく使いそうな機能
ウインドウの操作等で良く使いそうな機能をまとめて置きます。
Click()を無くす
クリックを使わずに動かす時に使う機能
GetInvokePattern().Invoke()
InvokePatternを持っていればこれでクリックの代わりになります。
obj.GetInvokePattern().Invoke()
GetSelectionItemPattern().Select()
メニューやリスト等でSelectionItemPatternを持っていればこれで選択できます。
SelectionItemPatternをドキュメントで調べるとIsSelected()が有り
選択されているかも調べられます。
obj.GetSelectionItemPattern().Select()
GetWindowPattern().Close()
SetWindowVisualState(ui.WindowVisualState.Maximized)
ウインドウのクローズ、最大化、最小化等はWindowPatternの関数呼び出しでできます。
WindowVisualStateクラスに Normal = 0、Maximized = 1、Minimized = 2 と定義されています。
proc = subprocess.Popen('cmd /c "notepad.exe"') w = ui.WindowControl(Name='無題 - メモ帳') p = w.GetWindowPattern() p.SetWindowVisualState(ui.WindowVisualState.Maximized) # ウインドウの最大化 time.sleep(2) p.Close()
画面に見えるようにする
GetScrollItemPattern().ScrollIntoView()
ScrollItemPattern を持っていれば ScrollIntoViewで自動的にスクロールして見えるようにしてくれます。
obj.GetScrollItemPattern().ScrollIntoView()
インストールしたuiautomationではButton等にこの機能がインプリメントされていません。 見える所に移動したいオブジェクトがあれば、 inspectでIsScrollItemPatternAvailableがtrueであれば使えますので、インストールしたソースの uiautomation.pyを検索しListItemControlの中のGetScrollItemPatternの部分を 必要なコントロールの下にコピーすると使えるようになります。
自分でスクロールするするには、ScrollPattern を持っている上位コントロールを使って
ScrollまたはSetScrollPercentでスクロールします。
ScrollではScrollAmount クラスに定義されている
LargeDecrement=0、LargeIncrement=3、NoAmount=2、SmallDecrement=1、SmallIncrement=4
を使ってLargeはPage Up/Downキー、Smallは矢印キー分のスクロールを行ないます。
SetScrollPercentはパーセント位置(float 0から100)を指定します。
水平方向と垂直方向の指定が可能です。スクロールさせないときは-1を指定します。
全体を順にスクロールするならSetScrollPercentで指定するのが楽そうです。
必ずしも指定したパーセント位置に移動するわけではなく
スクロールの移動単位に応じた、その近辺の移動可能な位置へスクロールします。
# 水平移動なし、垂直 PageUp obj.GetScrollPattern().Scroll(ui.ScrollAmount.NoAmount, ui.ScrollAmount.LargeDecrement) # 水平移動なし、垂直50%位置へ obj.GetScrollPattern().SetScrollPercent(-1, 50.0) obj.GetScrollPattern().SetScrollPercent(ui.ScrollPattern.NoScrollValue, 50.0)
スクロールバーでスクロールする。
ScrollBarControlではGetRangeValuePattern()で取り出した
RangeValuePatternのSetValue(value)にパーセント位置を指定することでスクロールできます。
垂直用と水平用は別々に有ります。必ずしも指定したパーセント位置に移動するわけではなく
スクロールの移動単位に応じた、その近辺の移動可能な位置へスクロールします。
s=obj.ScrollBarControl(Name="垂直") r = s.GetRangeValuePattern() r.SetValue(30.0) # 30%位置付近にスクロール
画面上に見えているかを確認する。
Controlクラスにはbool型のIsOffscreenプロパティが有りますので
その派生クラスでもこれが使えます。
obj.IsOffscreen # True or False
スクロールしたら画面内に表示されているかこれで調べられます。 IsOffscreenですので見えてないときTrueです。Falseの時、画面内に表示されてます。
値を設定する
toggle switch コントロール
obj.GetTogglePattern().Toggle()
slider コントロール
v = 設定位置
obj.GetRangeValuePattern().SetValue(float(v))
radio button コントロール
obj.GetSelectionItemPattern().Select()
combo box コントロール
v = 選択する名前
x = Select(itemName =v)
check box コントロール
v = 設定値
if obj.GetTogglePattern().ToggleState != int(v): obj.GetTogglePattern().Toggle()
edit コントロール
v = 文字列
obj.GetValuePattern().Value() # 値取り出し obj.GetValuePattern().SetValue(v) # 値設定
button コントロール
obj.GetInvokePattern().Invoke()
対象プログラムの実装方法によっては別途考慮しなければならない事も有ります。 典型的な方法のひとつとして参考にして下さい。
最後の手 Clik & SendKey(s)
オブジェクトがuiautomationから上手く見えないときには最終手段のClickとSendKey(s)等で 乗り切ることになります。この場合、画面の解像度や対象ウインドウのサイズによって 表示方法が変化する場合が多いので、できるだけ同じ条件になるように対象を最大化したり ウンンドウサイズを指定したりするのが安全です。
また、操作時は必ず画面上のトップ(他に隠れず)に見えるようにする必要が有ります。
uiautomationで使える機能は ドキュメントを参照 して下さい。
名前を見れば機能がだいたい想像がつくと思います。
マイクロソフトのペイントでお絵かきする例を下記に挙げてみます。 テストはWindows7でやっていますのでWindows10では上手く行かないかもしれません。 調整をお願いします。
他の例と同じ部分は省いていますので、importや参照関数類は他の例を引用してください。
def multi_line(base,points): p1 = p2 = [] for point in points: if len(p1)==0: p1 = point else: p2 = point ui.DragDrop(base[0]+p1[0],base[1]+p1[1],base[0]+p2[0],base[1]+p2[1],2) print(p1,p2) p1 = p2 def main(): proc = subprocess.Popen('cmd /c "mspaint.exe"') w = ui.GetRootControl().WindowControl(searchDepth=2,RegexName=r'.*ペイント') w.GetWindowPattern().SetWindowVisualState(ui.WindowVisualState.Maximized) dump_structure(w,1,5) p_area = w.GetFirstChildControl() while(p_area.BoundingRectangle.bottom - p_area.BoundingRectangle.top < 300): p_area = p_area.GetNextSiblingControl() print(p_area.BoundingRectangle) x = p_area.BoundingRectangle.left +10 y = p_area.BoundingRectangle.top +10 multi_line([x,y],[[50,50],[100,50],[100,80]]) ui.Click(318,70) ## Text button ui.Click(x+10,y) ui.SendKeys('Test Paint') ui.Click(x,y) ## 確定させる ui.Click(930,60) ## 緑ボタン ui.Click(446,104) ## 星ボタン ui.DragDrop(x+150,y,x+200,y+50) ui.Click(x,y) ## 確定させる
mspaint.exeを起動してから、そのWindowを見つけます。 ここではメニュー等の表示の変化を起こさないように最大化しています。 画面の横幅が1024pixcel未満の場合には上部のリボンの表示が変わりボタンの位置がずれます。
mspaintはuiautomationからはPaneControlを幾つか持っているように見えますが その並びは条件によって変化するようです。その為、ここでは縦のサイズが300pixel以上の PaneControlを描画領域として検出しています。
ControlクラスにはBoundingRectangleという属性が有り画面上の領域情報(Rect class)を持っています。
BoundingRectangle.top : 上のy位置
BoundingRectangle.left : 左のx位置
BoundingRectangle.bottom : 下のy位置
BoundingRectangle.right : 右のx位置
これを取り出して描画領域やクリック領域の参考にします。
完全に固定なら、対象ウインドウのキャプチャを取って画像エディタで位置を調べることもできます。
この場合、ウィンドウ内の位置ですので、ウインドウの領域の[left,top]と加算してください。
ui.Click()で指定する位置はデスクトップ上のピクセル単位での絶対位置です。
ウインドウ内の相対位置では有りません。
後は実際に操作するのと同じようにクリックとキー入力を繰り返します。 この例では分かりやすくするため保存等の処理を省いています。
ひとまずここまでで公開します。
2021/2/16 : First version
2021/2/21 : 全体の構造表示をGetRootControl()に変更、SetGlobalSearchTimeout導入
2021/3/02 : 関数のソースをリンクに追加
2021/3/28 : 最終手段のClick & SendKey(s)追加、ui_lib.py(構造ダンプに位置情報追加)
2022/6/14 : GetScrollItemPatternの追加方法を加筆