WindowsアプリもPythonで自動化したい|pywinautoで「待ってから操作」を試してみた

Pythonでブラウザを自動操作するとき、最近はPlaywrightを使うことが増えました。

Playwrightを使っていて便利だと感じるのが、単純に数秒待つのではなく、画面や要素が操作できる状態になるまで待ってから次へ進めることです。

では、Windowsのデスクトップアプリでも同じようなことはできないのでしょうか。

以前はPyAutoGUIなどを使って、

Python
import time
time.sleep(3)
Python

のように、「とりあえず3秒待つ」という処理を書くことがありました。

もちろんこれでも動く場面はあります。

ただ、パソコンの状態によって2秒で起動することもあれば、5秒かかることもあります。

それなら、

「3秒経ったら操作する」のではなく、「操作できる状態になったら次へ進む」

ほうが安心です。

そこで今回試してみたのが、WindowsアプリをPythonから操作できる pywinauto です。

今回はWindowsのメモ帳を使って、

  • メモ帳を起動する
  • 操作できる状態になるまで待つ
  • 本文の入力欄を調べる
  • 日本語を自動入力する
  • 入力できたことまで確認する

ところまで試してみます。

目次

pywinautoとは

pywinautoは、WindowsのGUIアプリケーションをPythonから操作するためのライブラリです。

ボタンや入力欄など、WindowsアプリのUI要素を取得して操作できます。

今回特に試してみたかったのが、wait() を使った待機処理です。

例えば、

Python
window.wait("ready", timeout=30)

とすると、単純に30秒停止するわけではありません。

ウィンドウが操作可能な状態になるまで待ち、準備ができればその時点ですぐ次の処理へ進みます。

timeout=30 は、あくまで待機する上限時間です。

固定時間待つ time.sleep() とは考え方が少し違います。

今回の環境を作る

今回は学習用として、次のフォルダを作りました。

PowerShell
C:\dev\pywinauto-study

Python標準の仮想環境を使います。

PowerShellで次のように実行しました。

Bash
cd C:\dev
mkdir pywinauto-study
cd pywinauto-study

python -m venv .venv
.\.venv\Scripts\Activate.ps1

python -m pip install pywinauto

今回はpywinauto自体を試すことが目的なので、環境構築もできるだけシンプルにしています。

まずメモ帳を起動して待ってみる

最初は、メモ帳を起動して、操作できる状態になるまで待つだけのコードを作りました。

Python
from subprocess import Popen

from pywinauto import Desktop


def main() -> None:
    Popen(["notepad.exe"])

    window = Desktop(backend="uia").window(
        class_name="Notepad",
        active_only=True
    )

    window.wait("ready", timeout=30)

    print("メモ帳のウィンドウが操作可能になりました。")


if __name__ == "__main__":
    main()

ここでポイントになるのが、

Python
window.wait("ready", timeout=30)

です。

ready は、ウィンドウが表示され、操作可能な状態になるまで待つために使っています。

30秒間ずっと停止するわけではなく、条件を満たせばすぐ次へ進みます。

最初はうまくいかなかった

最初は、起動したアプリケーションからそのままメモ帳のウィンドウを探していました。

Python
Application(backend="uia").start("notepad.exe")

ところが、

PowerShell
pywinauto.timings.TimeoutError: timed out

となり、wait() がタイムアウトしました。

調べてみると、この環境では、メモ帳を起動したプロセスと、実際にウィンドウを持っているプロセスを同じものとして扱えないケースがありました。


そこで、起動処理とウィンドウ検索を分けることにしました。

Python
Popen(["notepad.exe"])

でメモ帳を起動し、

Python
Desktop(backend="uia")

からデスクトップ上のウィンドウを探します。

この方法に変更すると、メモ帳のウィンドウを取得できるようになりました。

こうした「実際に試して初めて分かる違い」も、GUI自動化では重要そうです。

本文の入力欄はどうやって見つける?

pywinautoでは、いきなり操作コードを書くより、まず対象アプリのUI要素を調べるのが大切です。
今回は、メモ帳の本文入力欄をどう指定するかを調べます。

そこで使ったのが、print_control_identifiers() です。
pywinautoから見えているUI要素を一覧表示できます。

実際にメモ帳のUI構造を調べてみました。

print_control_identifiers() でメモ帳のUI要素を確認

出力の中に、次のような部分がありました。

Python
Document - 'pywinautoで自動入力しています'

さらにDocument要素の情報を確認すると、

Python
control_type='Document'
class_name='RichEditD2DPT'

となっていました。

そこで本文欄を、次のように指定できます。

Python
editor = window.child_window(
    control_type="Document",
    class_name="RichEditD2DPT"
)

今回はこの環境のメモ帳で RichEditD2DPT になっていました。

Windowsやメモ帳のバージョンによってUI構造が異なる可能性があるため、固定値として覚えるより、print_control_identifiers() を使って実際の構造を確認するのが良さそうです。

本文入力欄も操作可能になるまで待つ

本文欄が見つかったら、ここでもすぐに操作せず、準備が整うまで待ちます。

Python
editor.wait("ready", timeout=30)

メモ帳のウィンドウだけでなく、実際に操作する本文欄についても待機しています。

今回やりたかった、

「時間を決めて待つのではなく、状態を待つ」

という考え方です。

日本語はSetValue()で入力した

最初は type_keys() を使えばよいのかなと思いました。

ただし、type_keys() はキー入力を送る方法なので、日本語の場合はIMEなどの状態にも影響されます。

今回は本文欄がUI AutomationのValueパターンに対応していたため、文字列を直接設定する方法を使いました。

Python
value = editor.wrapper_object().iface_value

text = "pywinautoで自動入力しています"

value.SetValue(text)

これで、メモ帳へ日本語を直接入力できます。

pywinautoからメモ帳へ日本語を自動入力

実際にメモ帳へ、

PowerShell
pywinautoで自動入力しています

と入力されました。

キーボード入力を再現するのではなく、入力欄の値そのものを設定しているのが今回のポイントです。

入力した後も「確認できるまで待つ」

さらに今回は、「入力命令を出して終わり」にはしませんでした。

入力した内容を読み戻して、期待した文字列と一致するまで待ちます。

Python
from pywinauto.timings import wait_until

wait_until(
    10,
    0.2,
    lambda: value.CurrentValue == text
)

このコードでは、最大10秒の間、0.2秒ごとに値を確認しています。

期待した文字列になれば、その時点ですぐ終了します。

つまり、

Python
window.wait("ready", timeout=30)

は、操作する前の待機。

そして、

Python
wait_until(
    10,
    0.2,
    lambda: value.CurrentValue == text
)

は、操作した後の確認待機です。

この2つを組み合わせることで、「たぶん起動しただろう」 「たぶん入力できただろう」ではなく、状態を確認しながら処理を進められます。

メモ帳の本文を読み戻し、入力成功を確認

PowerShellには、

PowerShell
入力確認成功: pywinautoで自動入力しています

と表示されました。

完成したコード

今回の最終的なコードです。

Python
"""メモ帳と本文欄の準備を待ち、日本語の文字列を自動入力する。"""

from subprocess import Popen

from pywinauto import Desktop
from pywinauto.timings import wait_until


def main() -> None:
    # 起動用プロセスとウィンドウを持つプロセスが異なる場合にも対応する。
    Popen(["notepad.exe"])

    # 複数のメモ帳が開いていても、アクティブなウィンドウを対象にする。
    window = Desktop(backend="uia").window(
        class_name="Notepad",
        active_only=True
    )

    # ウィンドウが操作可能になるまで待つ。
    window.wait("ready", timeout=30)

    # 以降は同じウィンドウを対象にする。
    window = Desktop(backend="uia").window(
        handle=window.wrapper_object().handle
    )

    # この環境のメモ帳の本文欄は Document / RichEditD2DPT。
    editor = window.child_window(
        control_type="Document",
        class_name="RichEditD2DPT"
    )

    editor.wait("ready", timeout=30)

    value = editor.wrapper_object().iface_value

    if value.CurrentIsReadOnly:
        raise RuntimeError("本文入力欄が読み取り専用です。")

    if value.CurrentValue:
        raise RuntimeError(
            "既存の本文を保護するため、空のタブで再実行してください。"
        )

    # IMEを経由せずUnicode文字列を直接設定する。
    text = "pywinautoで自動入力しています"
    value.SetValue(text)

    # 入力結果が反映されるまで確認する。
    wait_until(
        10,
        0.2,
        lambda: value.CurrentValue == text
    )

    print(f"入力確認成功: {value.CurrentValue}")


if __name__ == "__main__":
    main()

time.sleep()を使わずに操作できた

今回pywinautoを試してみて、一番面白かったのはやはり待機処理でした。

例えば、

Python
time.sleep(3)

なら、対象アプリの状態に関係なく必ず3秒待ちます。

一方、

Python
window.wait("ready", timeout=30)

なら、

操作できる状態になったら先へ進む

という書き方ができます。

さらに wait_until() を使えば、

操作した結果が期待した状態になるまで待つ

こともできます。

ブラウザ自動化でPlaywrightを使ったときにも感じましたが、自動化では単に操作するだけでなく、

「いつ操作するか」

がかなり重要です。

pywinautoはWindows業務アプリの自動化にも使えそう

今回はメモ帳というシンプルなアプリでしたが、pywinautoではWindowsアプリのボタンや入力欄などを取得して操作できます。

そのため、

  • 社内のWindows業務アプリ
  • 古いデスクトップソフト
  • Excelとは別の専用アプリ
  • ブラウザではない入力作業

など、自動化しにくかった作業にも使える可能性があります。

もちろん、対象アプリによってUI構造は違います。

今回も、最初はウィンドウをうまく取得できませんでした。

それでも、

Python
print_control_identifiers()

などを使ってUI構造を調べながら進められることが分かりました。

まとめ

今回はpywinautoを使って、Windowsのメモ帳をPythonから操作してみました。

試した内容は、

  • メモ帳を起動する
  • wait() で操作可能になるまで待つ
  • print_control_identifiers() でUI要素を調べる
  • 本文入力欄を特定する
  • SetValue() で日本語を入力する
  • wait_until() で入力結果まで確認する

という流れです。

特に印象に残ったのは、固定時間待つのではなく、状態を待てること。

GUI自動化では、ここが安定性に大きく影響しそうです。

Playwrightでブラウザ自動化を試してきましたが、Windowsのデスクトップアプリではpywinautoという選択肢もかなり面白そうです。

次は、メモ帳よりもう少し実際の業務に近いWindowsアプリでも試してみたいと思います。

目次