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段階です。
- 文字列でなく番号で選ぶ(
select(1)で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切断で止まる条件は、書く前に実行環境を決めて、規約に反映しておく項目です。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。