NeetEch

Windowsの自動化にWinAppDriverを使ってみた

こんにちは,ぎいとです.

タイトルの通り,WinAppDriverを使ってUI操作を自動化してみたのでそのやり方を勝手に解説します.とは言っても自分なりに解釈していることを書いているので間違っている可能性は全然あります.

細かい事は無視して動かせればいいやくらいのモチベーションの方に役立つかも.細かいことが気になるのであればGitHubのドキュメントなりを参照してください.GitHubのissuesは詰まった時に参考になりました.

英語のドキュメントを日本語に訳した,英語のテストを日本語に訳しただけの記事はそこ彼処にありますのでそちらも見ておくといいかも.

この記事ではExcelを例に自動化の手順を解説しますが,Excelに限らず様々なアプリ,ソフトも同様の手順で自動化できると思います.

準備編

動作環境

バージョンは古すぎなければそこまで気にしなくていいと思います.
Pythonの他にもJava,C#,Rubyなどが利用可能なようです.筆者はPythonが楽なのでここではPythonを使用.

エディタはなんでもいいです.メモ帳でも大丈夫ですがまぁこだわりがなければVScodeでいいでしょう.
それからWindowsの設定で開発者用モードをONにしておきます.

それぞれの実行ファイルのパスを一応載せておきます.環境により少し違ったりするので探してみてください.作業中はショートカットを作成しておくと楽かも.

Pythonを使用する場合は,pipから必要なモジュールをインストールしておきます.その他の言語を利用する場合にも適宜対応してください.

pip install robotframework-appiumlibrary

今回の解説例ではExcel 2013を使用.もちろん2013でなくてもいいですし,Excelではなくメモ帳でもなんでもいいです.サンプルはExcelじゃないと動きませんが,手順どおりならどのアプリでもなんとかなるハズ.

ウィンドウの構成とinspect.exe

自動化を作成するには次の手順を踏みます.

  1. inspect.exeで操作したいウィンドウのコンポーネントを調べる
  2. 1をもとに実際に操作するスクリプトを作成

ウィンドウはコンポーネント(部品)によって構成されています.代表的なのは文章を入力・編集できる「テキストエディタ」,クリックすると処理が行われる「ボタン」などでしょうか.

ウィンドウを構成するあらゆるものがコンポーネントです.ウィンドウもコンポーネントです.たぶん

自動化を行うには,どのコンポーネントに対して何をするのかが重要になります.コンポーネントを特定して操作するわけです.

そしてコンポーネントを特定するにはそのコンポーネントがどこにあるのか知る必要があります.これを教えてくれるのがinspect.exeです.

例を見てみましょう.


Excelとinspect.exeを開いてみます.inspect.exeは常に前面に配置されるので画面が見づらくなります.必要ないときは最小化しておきましょう.

説明の都合上,今回使用するExcelブックをワーキングディレクトリにautotest.xlsxとしてあらかじめ作成しておきます.

inspect.exeのツリー

まずは左側のツリーを見てください.
デスクトップ1(ルートウィンドウ)の下に,タスクバー,開いたExcel,名前が空×6(状況により個数変動),Program Managerというウィンドウが確認できます.

この画面から,それぞれのウィンドウが様々な部品を持っていることが確認できます.これらの部品がさらに部品を持っていることも確認してみてください.

このようにコンポーネントは階層構造をとっています.また,名前が空(“”)のウィンドウであったとしても,必ずしも中身がないウィンドウであるとは限りません.

もう少し具体的に見てみましょう.

Excelの左上にあるファイルボタンがどのようなコンポーネントなのかを見るには,そこにカーソルを合わせればOKです.カーソルを探したい部品に当てると,inspect.exeが自動でその情報を教えてくれます.

ボタンにカーソルを合わせる

どうやらファイルボタンは,
“デスクトップ 1″ウィンドウ” → autotest.xlsx-Execl”ウィンドウ → 2番目の””ウィンドウ → ”Ribbon”ツールバー → ”Ribbon”ウィンドウ → ””ウィンドウ → ””ウィンドウ枠 → ”Ribbon”ウィンドウ枠
と辿った先にあるようです.

このようにして,まずは操作したいコンポーネントがどこにあるのかを知っておきましょう

実際に動かしてみる

Excelの操作例

自動化の基本は特定して操作です.特定してから何度も操作をしたり,特定してさらに特定したりします.

操作したい部品の場所を調べたら,これをプログラム上で特定するわけですが,説明がややこしくなりそうなのでまずは見てください.

from appium import webdriver
from selenium.webdriver.common.keys import Keys
import time

# ------ アプリの起動 ------
desired_caps={}

# 絶対パス,または相対パスでautotest.xlsxを指定
# 相対パスによる指定
import os
desired_caps['app'] = os.getcwd() + '\\autotest.xlsx'
# 絶対パスによる指定
# desired_caps['app']='C:\\User\\YOURNAME\\autotest.xlsx'

driver = webdriver.Remote(
    command_executor = 'http://127.0.0.1:4723',
    desired_capabilities = desired_caps
)
time.sleep(2)

# ------Excelウィンドウ(ClassName: "XLMAIN")を特定------
xl_root = driver.find_element_by_class_name("XLMAIN")

# ------表に入力してみる------
xl_root.send_keys( "This" + Keys.ENTER )
xl_root.send_keys( "is a" + Keys.ENTER )
xl_root.send_keys( "PEN!!" + Keys.ENTER )
time.sleep(2)

# ------ショートカットキーを入力------
for _ in range(3):
    # 元に戻す
    xl_root.send_keys( Keys.CONTROL + "z" )
time.sleep(2)

# ------ ↓ ↓ → ↑ ← と動かす------
xl_root.send_keys(Keys.DOWN + Keys.DOWN + Keys.RIGHT + Keys.UP + Keys.LEFT)
time.sleep(1)

# ------Name: "Ribbon"となっているコンポーネントを特定------
ribbon = xl_root.find_element_by_name("Ribbon")

# ------Name: "Ribbon"コンポーネントから,
#             AutomationId: "FileTabButton"となっているコンポーネントを特定------
file_button = ribbon.find_element_by_accessibility_id("FileTabButton")

# ------クリック操作を実行------
file_button.click()
time.sleep(1)

# ------Excelウィンドウに対してキー操作: ↓ ↓ ↓ ENTER ------
for _ in range(3):
    # Keys.DOWN + Keys.DOWN + Keys.DOWN では早すぎる可能性が高い
    xl_root.send_keys(Keys.DOWN)
    time.sleep(1)
# Excelの表示がわかりづらいですが,上書き保存が選択されているはずです
time.sleep(2)
xl_root.send_keys(Keys.ENTER)
time.sleep(2)

# ------アプリを終了------
driver.quit()

ところどころにあるtime.sleepは,処理が早すぎることで発生しうる問題を回避するためのもの.あと人間にとって見やすいように.

実行するにはまずWinAppDriver.exeを実行し起動.コマンドプロンプトから,

python driver_sumple.py

と入力すればOK.いちいちWinAppDriverを起動するところからやるのが面倒な場合は,おわりにのバッチファイルを用意してください.

では詳しく見ていきましょう.

アプリの起動

Pythonでは辞書を使って起動するアプリについて記述します.”app”に起動するアプリケーションの実行ファイルパスを指定.コマンドプロンプトでの操作に近い印象です.パスが通っていれば実行ファイル名だけでも大丈夫なようです.下にメモ帳の例を載せておきます.

desired_caps["app"] = "C:\\Windows\\System32\\notepad.exe"
# C:\Windows\System32 にはパスが通っているので,以下のようにしても同様
#desired_caps["app"] = "notepad.exe"

# 引数を渡したい場合は次のようにする
desired_caps["appArguments"] = "C:\\Users\\YOURNAME\\memo.txt"

# アプリを起動
# コマンドプロンプトに対して,
# >C:\\Windows\\System32\\notepad.exe C:\\Users\\YOURNAME\\memo.txt
# と入力したのと同じようにアプリを起動する
driver = webdriver.Remote(
    command_executor = 'http://127.0.0.1:4723',
    desired_capabilities = desired_caps
)

※ Excelの例では起動するアプリにExcelの実行ファイルではなく,autotest.xlsxというExcelファイルを渡しています.Windowsのコマンドプロンプトでは,コマンドにファイル名を指定すると,拡張子から適切なアプリを判別し,そのアプリからファイルを起動してくれるようです.

アプリを起動したくない場合にはdesired_caps[“app”]=”Root”とします.これが便利なケースは,既に開いているウィンドウに対する処理や,画面下に表示されているタスクバーに対する処理を行う場合でしょうか.

Excelウィンドウを特定

“autotest.xlsx -Excel”ウィンドウを操作の対象とするためにまずはこのウィンドウを特定します.

事前にinspect.exeのツリーから”autotest.xlsx -Excel”ウィンドウを選択し,ExcelウィンドウはClassNameが”XLMAIN”となっていることを調べておきます.この情報をもとに,

xl_root = driver.find_element_by_class_name("XLMAIN")

を記述.ドライバにClassName: “XLMAIN” となっている部品を探してもらい,これをxl_rootに代入しています.

ClassName以外からでもエレメントを見つけることが可能です.よく使いそうなのは,NameとAccessibilityId.詳しくはこちら

Excelブックが2つ以上開かれている状態などの,同じ階層に同じClassNameが存在している場合には,ClassNameで区別ができないため,ClassName以外で特定する必要があります.

なおこの記事でコンポーネント,部品と呼んでいるものはエレメントと同じです.

表に入力してみる

xl_root.send_keys( str ) は,xl_rootに対して引数に与えられた文字列を入力します.

このような’ウィンドウに対するキー入力’は,そのウィンドウにフォーカスした状態で入力したキーと同じ動作をするようです.

例を挙げます.メモ帳を開いてみましょう.

メモ帳にフォーカスし,”hoge”と入力した場合,「メモ帳のウィンドウ」ではなく,「メモ帳のウィンドウ内のどこかにあるテキストエディタ」に文字が入力されるのが正確な表現でしょう.

メモ帳のウィンドウとそのテキストエディタ

よってスクリプトで厳密に書くのであれば,

nptf = [メモ帳のテキストエディタを特定]
nptf.send_keys("hoge")

のように,メモ帳のウィンドウからテキストエディタを特定し,send_keysを呼び出すこととなるわけです.ところが実際にはメモ帳ウィンドウに対する入力で問題なくテキストエディタに入力されます

どうやら,文字列ならテキストエディタへ入力,ショートカットキーであれば適切な処理を行うなど,勝手にうまく対応してくれるようです.これはユーザがキーボードから入力する際の挙動と同じだと思っていいでしょう.

Excelの例では,Bookを開いた時に選択されているセルに対して文字列の入力が行われます.必ずしもA1が選択されているわけではないことに注意です.

ショートカットキーを入力

表に入力してみると同様にExcelのルートウィンドウに対するキーの入力ですが,セルには入力されません.Excelにあらかじめ設定されている処理が行われているのが確認できます.

事前にVBAでマクロを作成し,ショートカットキーを設定しておけば複雑な処理も可能.Excelに関してはこのような使い方がいいだろうと思いますが,VBAを扱うにもそれなりに勉強が必要かもしれません.

↓ ↓ → ↑ ← と動かす

フォーカスするセルを動かしてみます.さりげなく最初にフォーカスしているセルの1つ下のセルにフォーカスを当て,その後の上書き保存に続きます.

Excelファイルはフォーカスしているセルも保存しているようです.よって,このプログラムを実行するたびに,”This”,”is a”,”PEN!!” のそれぞれが入力されるセルは下に落ちていきます.

これを防ぐには,inspect.exeを用いてセルA1を特定し,それに対する入力に変更する必要がありますが少し注意が必要です.

新しい.xlsxファイルを開いてinspect.exeで調べてみるとわかるのですが,表のコンポーネントである”グリッド”以下にセルは存在していない可能性があります.
どうやら[セルA1のコンポーネント]は,セルA1に何かが入力されて初めて作成されるようです.つまり,A1に対する入力であることを明示するには,A1に既にデータが入力されている必要があります.

よってこのような操作を行いたい場合には,ショートカットキーを入力で触れたマクロを用いた方が遥かに簡単かと考えられます.

Name: “Ribbon”となっているコンポーネントを特定
Name: “Ribbon”コンポーネントから,AutomationId: “FileTabButton”となっているコンポーネントを特定

Excelのルートウィンドウを特定した時と同様に,さらに下の階層に存在するコンポーネントを特定します.Excelのルートウィンドウ(xl_root)を起点に探索を行うため,仮にxl_rootの外側に全く同じコンポーネントが存在していたとしても,特定が可能です.

実はツリーの”Ribbon”ツールバー,”Ribbon”ウィンドウ,”Ribbon”ウィンドウ枠はすべてName: “Ribbon”となっています.よって,

ribbon = xl_root.find_element_by_name("Ribbon")

において,ribbonにどのコンポーネントが代入されるのかはわかりません.筆者は調べてもいません

しかしfind_element関数はディレクトリ内を再起的に探索してくれるため,ribbonにどのコンポーネントが代入されていたとしても,FileTabButtonを見つける事が可能です.

特定したいコンポーネントが唯一定まるのであれば,階層が離れている事は問題になりません.唯一に定まらない場合に限り,階層を順に追っていく必要が出てきます.

クリック操作を実行

click()でクリック操作です.ここではfile_buttonにExcel左上のファイルボタンが特定されているので,これをクリックしています.

click()とsend_keys()が基本的な使い方かと思いますが,その他のことは調べてみてください.

Excelウィンドウに対してキー操作: ↓ ↓ ↓ ENTER

コンポーネントは変数に代入されているので,他のコンポーネントに対する処理を行った後でも当然のように扱えます.

ファイルボタンをクリックしたことで,Excelルートウィンドウの中身は変化している可能性がありますが,Excelルートウィンドウ自体は変化していません.よってここでもキー操作を賢く自動で処理してくれます.

切り替わった画面のコンポーネントをわざわざ特定する必要がないので便利ですね.

コメントとしても書きましたが,上書き保存のボタンが選択されているハズです.もちろんinspect.exeから調べて特定することも可能でしょう.

アプリを終了

アプリを終了してウィンドウを閉じます.Excelのルートウィンドウであるxl_rootではなく,driverに対する処理です.

これを指定しない場合には,driver_sumple.py終了後もウィンドウが残り続けます.普通に操作も可能です.
もしかしたらWinAppDriverとの接続が残ってしまう可能性もあるので,とりあえず閉じておくのが良さそう(調べてない).

WinAppDriverを終了するわけではないので,そちらは別途終了させる必要があります.

おわりに

今回はWinAppDriverの自動化について実験の結果と,筆者のように詳しくない人でもとりあえず使えるレベルまでできるだけ丁寧に勝手に解説しました.もう一度いいますが,筆者は詳しくない人なので結構間違っている可能性高いです.

いつものとおり説明が長くパッとしませんが誰かの役に立てたなら幸いです.

Excelは割と使っている人も多いのではないかと思い記事にしてみましたが,そもそもWinAppDriverの記事が多くないところを考えると需要はないのかもしれない

一応おまけでバッチファイルをおいておきます.WinAppDriverの起動から閉じるところまでやってくれる筈です.

@echo off
cd /d %~dp0

REM パスを指定
SET wadpath="C:\Program Files (x86)\Windows Application Driver\WinAppDriver.exe"

REM WinAppDriverを起動
START "wad" %wadpath%

REM 実行
python driver_sumple.py

REM WinAppDriverを終了
TASKKILL /FI "IMAGENAME eq WinAppDriver.exe"

最後のTASKKILLはちょっと無理やり感も漂う.強制終了なので不具合が起きたら怖い.これ以外の方法がよくわからないので何かいい方法ご存知の方,コメントお待ちしております.

特定の時間に実行させるにはタスクスケジューラを使うのが一般的なようです.ここではそこまでは書きません.

久しぶりの更新でしたがいかがだったでしょうか.筆者は普段,Windowsに触る機会があまりなく,Windowsでそれっぽいことをしたのは今回が初めてだったので新鮮と言えば新鮮でした.

正直なところ,ついでにVBAやらバッチファイルについてもやったのでWindowsが少し嫌いになるくらいには疲れました.しばらくは触りたくないです.

まぁでも自動でPCが動くの見るのって楽しいですよね.

そんなこんなでコメントお待ちしております〜

参考文献

参考にさせていただいきました.ありがとうございます.

Follow me!