Claude Media
Claude Codeでpywinautoを書く — 要素の特定と待機を固めるWindows自動操作

Claude Codeでpywinautoを書く — 要素の特定と待機を固めるWindows自動操作

APIのないWindows業務アプリをpywinautoで動かすとき、バックエンド選択、要素の特定、待機の書き方をClaude Codeに固定させる手順です。CLAUDE.mdの規約例と、RDPで止まる条件も示します。

APIもCSV出力も用意されていない社内のWindowsアプリを、Pythonから操作したい。pywinautoはそのための定番ライブラリで、Claude Codeに書かせるのにも向いています。ただし放っておくと、画面座標のクリックや time.sleep だらけのスクリプトになりがちです。壊れにくくするには、バックエンドの選択、要素の特定、待機の3点を先に決めてClaudeに渡します。

この記事では、その3点をCLAUDE.mdの規約と最初の依頼文に落とす手順を示します。ホットキー中心の自動化はAutoHotkey v2のスクリプトを書く手順に任せ、ここでは画面のコントロールを名前で掴む方式に絞ります。

前提と準備

Windows上のPython環境が必要です。pywinautoは pip install pywinauto(condaなら conda install -c conda-forge pywinauto)で入ります。入ったかどうかは、メモ帳を起動する次の2行で確かめられます。

from pywinauto.application import Application
app = Application(backend="uia").start("notepad.exe")

メモ帳が立ち上がれば準備完了です。対象の業務アプリを動かすPCで、Claude Codeも使える状態にしておきます。

最初に決めるのはwin32かuiaか

pywinautoの入口は Application(backend="...") です。バックエンドは2種類あり、対象アプリの作りで選びます。

backend対象
win32(既定)対象MFC、VB6、VCL、シンプルな構成のWinForms、古い業務アプリ全般
uia対象WinForms、WPF、ストアアプリ、Qt5、ブラウザ

ここを間違えると、後の要素特定がすべて空振りします。Claudeに「いい感じに選んで」と任せず、人間が調べて結論を渡す部分です。

調べ方は検査ツールで決まります。Visual Studioに付属するSpy++ ですべてのコントロールが見えるなら win32。Windows SDKのInspect.exeをUIAモードにして、Spy++ より多くのコントロールが見えるなら uia です。どちらの検査ツールでも見えないコントロールは、mouse と keyboard モジュールによる座標とキー送信が最後の手段になります。

古いVB6資産の業務アプリなら win32 が当たる可能性が高い領域です。そのコードを読み解く側の作業はVB6のコードを仕様書に起こす手順で扱っています。

要素の特定はprint_control_identifiersの出力を渡して決める

Claudeはあなたの手元のアプリの画面を知りません。要素の名前を推測させると、存在しない OKButton が並びます。代わりに、実機で画面の構造を文字として書き出し、それを材料に渡します。

pywinautoの print_control_identifiers() は、ウィンドウ配下のコントロールを木構造で出力します。出力には各コントロールの child_window(...) 形式の記述があり、より詳しいウィンドウ指定はこの出力からそのままコピーできます。バックエンド uia の公式例では、次のような行が並びます。

child_window(title="Windows NT Properties", control_type="Window")
   | child_window(title="OK", auto_id="1", control_type="Button")
   | child_window(title="Cancel", auto_id="2", control_type="Button")
   | child_window(title="Folder name:", auto_id="13156", control_type="Edit")

実際のアプリでは、次のような小さなスクリプトで出力をファイルに残します。

from pywinauto import Application
 
app = Application(backend="uia").connect(title_re=".*受注管理.*")
dlg = app.window(title_re=".*受注管理.*")
dlg.print_control_identifiers()

print_control_identifiers の出力先は標準出力なので、実行時にリダイレクトして controls.txt に保存します(python dump.py > controls.txt)。この controls.txt をClaudeに読ませ、「このファイルにある child_window の記述だけを使ってスクリプトを書く」と指示します。

識別の優先順位も決めておくと安定します。出力例では、多くのコントロールに auto_id と control_type が付いています。これらを含む指定を優先し、表示名(title)だけの指定は文言変更で外れる前提で扱います。Button3 のような番号付きの名前は、control typeと番号で自動的に付く名前なので、画面の項目が増減すると指す先がずれます。

属性アクセスの「ゆるい一致」を切る

pywinautoは app.UntitledNotepad のような属性アクセスを、近い名前に寄せるbest matchで解決します。タイプミスに強い反面、間違った名前でも黙って別のコントロールを掴む余地があります。allow_magic_lookup=False を渡すと、存在しない属性へのアクセスがすぐ例外になります。

app = Application(backend="uia", allow_magic_lookup=False)

Claudeの書いたコードは、この設定のうえで window(...) と child_window(...) を明示する形にそろえさせると、誤った指定が実行直後に表面化します。

待機は暗黙の待ちと明示のwaitを使い分ける

業務アプリの自動化が壊れる原因の多くは待機です。Claudeに何も言わないと、time.sleep(3) を挟む書き方が出やすくなります。pywinautoには、これを置き換える仕組みが揃っています。

ウィンドウやコントロールの解決は、成功するまで既定のタイムアウトで再試行されます。ダイアログを開く操作の直後に、そのダイアログのコントロールを参照すれば、現れるまで待ってくれます。この暗黙の待機が、sleep を書かなくてよい理由です。既定のタイムアウトは5秒です。

それを超える処理には、明示の待機を使います。

待ちたい状況書き方
ウィンドウが表示されるまで書き方dlg.wait('visible')
ダイアログが消えるまで書き方dlg.wait_not('visible')
大きなファイルの読み込み完了まで書き方app.window(title='data.txt - Notepad').wait('ready', timeout=30)
画面は応答するが裏で処理が続く書き方app.wait_cpu_usage_lower(threshold=5)

wait_cpu_usage_lower は、コントロールがすべて存在するのに処理中という、遅延初期化型のアプリ向けです。これはアプリ全体のプロセスにだけ使え、個別のウィンドウには使えません。

任意の条件を待ちたいときは timings モジュールの wait_until や wait_until_passes を使います。例では always_wait_until_passes(4, 2) のデコレーターで、2秒おきに最大4秒、条件が通るまで関数を再実行します。

動作全体の速度は Timings.fast()(各待ちを半分)と Timings.slow()(倍)で一括調整できます。待機の調整をコード各所の sleep に分散させず、この1か所にまとめるようClaudeに指示します。

CLAUDE.mdに書く規約

ここまでの決めごとを、毎回の依頼で繰り返さずに済むようCLAUDE.mdに置きます。次は一例です。

# pywinauto 規約
- 対象アプリは backend="uia"(Inspect.exe で確認済み)
- Application / Desktop は allow_magic_lookup=False で作る
- 要素は controls.txt にある child_window の記述だけを使う。無い要素は推測せず質問する
- 優先順位は auto_id + control_type > title。Button3 のような番号名は使わない
- time.sleep は禁止。wait / wait_not / wait_cpu_usage_lower を使う
- 座標指定の click_input は、UIA で見えない要素の最後の手段に限る
- 日本語の画面名は item access か window(title_re=...) で指定する

最後の行は、日本語UIの業務アプリで効きます。Pythonは非ASCIIの識別子を使えないため、英語以外の画面では app['ダイアログ名']['コントロール名'] のitem accessか、window(title_re=...) の明示呼び出しで指定します。app.受注一覧 のような属性アクセスをClaudeが書いてしまうと動きません。

規約を短く保つ考え方は、Claude Codeのベストプラクティスにも沿っています。コードを読めば分かることは書かず、Claudeが推測で間違えることだけを書く、という方針です。

最初の依頼は材料と完了条件をセットで渡す

依頼文の例です。材料(controls.txt)と、完了とみなす条件を同時に渡します。

受注管理アプリで「検索」ボタンを押し、結果一覧の1行目の受注番号を
orders.csv に追記する pywinauto スクリプトを書いてください。
 
- 使える要素は controls.txt にあるものだけ
- 検索後の一覧表示は wait で待つ。sleep は使わない
- 要素が見つからないときは ElementNotFoundError の内容をそのまま表示して終了
- 書き終えたら python -m py_compile で構文だけ確認し、
  実機での実行が必要な箇所を一覧にして報告する

合否が返ってくる確認手段をClaudeに渡すと、Claudeが出力を読んで直し、再実行までを自分で回せます。GUI自動化では画面を直接見せられないため、確かめられる範囲は構文チェックと、実機で実行した人間のログに限られます。そこで「実機で確認が要る箇所を報告させる」ことを完了条件に含めています。

実機でスクリプトが失敗したら、トレースバックと、そのときの print_control_identifiers() の出力を貼り直して修正を依頼します。コントロールの名前が画面の状態で変わるアプリでは、ダイアログごとに controls.txt を分けると、指示の精度が上がります。

ブラウザの自動化で待機とロケータをどう置き換えるかはSeleniumのテストを移行する手順にまとめています。pywinautoが受け持つのは、ブラウザの外側にあるデスクトップアプリです。

名前が取れないコントロールの扱い

ownerdraw(自前描画)のリストボックスのように、見た目は普通でも文字列を返さないコントロールがあります。この場合、texts() が空文字の一覧を返し、select("項目名") がIndexErrorで失敗します。回避策は2段階です。

  1. 文字列でなく番号で選ぶ(select(1) で2番目の項目)
  2. それも効かなければ、キー送信で選ぶ(type_keys("{HOME}{DOWN 2}{ENTER}") で3番目)

業務アプリは、この手の独自コントロールを含むことが珍しくありません。Claudeに「texts() が空なら番号選択にフォールバックする」と書かせるより、先に実機で texts() の結果を確かめ、取れない項目を controls.txt の注記に残しておくほうが確実です。選択した項目の文字列が隣の編集ボックスに出るアプリなら、そちらから読み取る手もあります。

画面を持たない実行環境では止まる

スクリプトが手元で動いても、夜間の定時実行にした途端に失敗することがあります。夜間の定時実行で止まる条件は次の3つです。

  • リモートデスクトップ(RDP)のウィンドウを最小化すると、リモート側に操作できるデスクトップがなくなる
  • RDPを切断すると、デスクトップがロックされる
  • ロックされた状態では、click_input など _input で終わるマウス操作、set_focus、type_keys が動かない

ロック中でも使える入力方法は、set_edit_text(uia と win32 の両方、主に編集ボックス向け)と、win32 の send_chars / send_keystrokes です。CIのエージェントは、サービスとして動かすとGUIを作れず、SSH経由でも動かないため、通常のアプリケーションとして起動したエージェントだけが使えます。

この制約は、Claudeに書かせる段階で効いてきます。定時実行が前提なら、CLAUDE.mdに「ロック中に動かす可能性があるため click_input と type_keys を避け、set_edit_text など _input で終わらない方法を優先する」と加えておくと、最初から条件に合った書き方になります。常時ログインした専用PCで動かすなら、この制約は気にしなくて済みます。

ブラウザやアプリごとの個別条件

uia でブラウザを操作する場合、Chromeを --force-renderer-accessibility フラグ付きで起動する必要があります。また、カスタムのプロパティやコントロールはcomtypesの制約でサポートされません。どのアプリでもこの2点は、依頼の前に確認する項目です。

失敗ログと画面の変更をClaude Codeに渡して直す

スクリプトが固まったあとの保守も、Claudeに任せられます。Claude Codeは claude -p で非対話実行でき、標準入力のテキストを渡せます。たとえば、失敗ログを渡して原因の見当をつけさせる使い方です。

python run_orders.py 2> error.log \
  || cat error.log \
  | claude -p "このエラーの原因を、controls.txt と照らして説明して"

アプリの更新で画面が変わった場合は、新しい controls.txt を作り直し、旧版との差分を見せて「変わった要素に合わせて修正して」と依頼します。要素の特定を auto_id 中心にしておくと、文言だけが変わった更新では修正が要らないこともあります。

まとめ

pywinautoをClaude Codeに書かせる作業の核心は、推測させないことです。バックエンドは検査ツールで人間が決め、要素は print_control_identifiers() の出力から選ばせ、待機は sleep でなく wait 系に限る。この3つをCLAUDE.mdに書けば、画面を見ないClaudeでも、壊れにくいスクリプトの土台を作れます。

動作の最終確認は実機でしかできません。ロック画面やRDP切断で止まる条件は、書く前に実行環境を決めて、規約に反映しておく項目です。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/common-workflows
  2. code.claude.com/docs/en/best-practices
  3. pywinauto.readthedocs.io/en/latest
  4. pywinauto.readthedocs.io/en/latest/getting_started.html
  5. pywinauto.readthedocs.io/en/latest/wait_long_operations.html
  6. pywinauto.readthedocs.io/en/latest/remote_execution.html
この記事を共有:XはてブLinkedIn