windows-app-automation
Windows ネイティブアプリを自動化するときは、Python + pywinauto で自動化スクリプトを作成する。
対応エージェント環境
| 環境 | 実行場所 | winauto コマンド |
|---|---|---|
| Claude Code (Windows) | PowerShell / CMD ターミナル | winauto または python winauto.py |
| GitHub Copilot (VS Code) | VS Code 統合ターミナル(PowerShell) | winauto または python winauto.py |
| Kiro (AWS IDE) | Kiro 統合ターミナル(PowerShell) | winauto または python winauto.py |
| WSL (WSL2 端末) | bash/zsh from VS Code / Kiro / Windows Terminal | winauto(ラッパー経由で Windows Python を呼ぶ) |
注意: pywinauto は Windows 専用。WSL から呼ぶ場合は Windows 側 Python で実行される。
セットアップ(初回のみ)
Windows ネイティブ(Claude Code / Copilot / Kiro 共通)
# 依存ライブラリと winauto コマンドのインストール
python tools/winauto/install.py
# インストール確認(doctor が pywinauto・デスクトップ到達性・ロック状態を確認する)
winauto doctor
winauto apps
WSL 端末から使う場合
# Windows 側 Python を自動検出してインストール
python tools/winauto/install.py
# インストール確認(新しい端末で)。doctor は WSL→Windows のブリッジを
# 端から端まで確認する: interop → ラッパー → Windows Python → デスクトップ到達性
winauto doctor
winauto apps
winauto doctor は WSL 側でも Windows 側でも動く唯一のコマンドで、WSL から実行すると
ラッパー越しに Windows 側の doctor を呼び出して所見を合流させる。--output json で機械可読。
問題があれば終了コード 1 を返すので、セットアップスクリプトからも判定に使える。
インストーラーが Windows 側 Python を見つけられない場合:
# Windows Python のパスを確認
cmd.exe /c where python
# 手動で Windows 側に pip インストール
cmd.exe /c python -m pip install pywinauto Pillow pywin32 comtypes
依存ライブラリのみインストール(既存 Python 環境に追記)
pip install pywinauto>=0.6.9 Pillow>=9.0.0 pywin32>=306 comtypes>=1.4.0
環境別の実行方法
Claude Code / GitHub Copilot / Kiro(Windows ターミナル)
これらはすべて Windows 上で動作するため、同じコマンドが使える。
# winauto CLI(インストール済みなら直接呼べる)
winauto apps
winauto tree --app notepad
winauto click "name:=OK" --app notepad
# またはリポジトリから直接
python tools/winauto/winauto.py apps
python tools/winauto/winauto.py tree --app notepad
# ヘルパースクリプトも同様
python .github/skills/windows-app-automation/scripts/element_inspector.py --list
Copilot(VS Code)固有の注意点:
- VS Code のターミナルが PowerShell の場合、
pythonコマンドが正しく通るか確認する python --versionで Python 3.9 以上が表示されることを確認する
Kiro IDE 固有の注意点:
- Kiro の統合ターミナルから実行する(PowerShell)
- Kiro エージェントは直接
!winauto tree --app notepadのようにシェルコマンドを呼べる - スキルの自動化スクリプトを Kiro に書かせた後、Kiro のターミナルで実行する
WSL 端末(VS Code Remote / Kiro WSL / Windows Terminal)
# winauto ラッパー経由(インストール済みなら直接呼べる)
winauto apps
winauto tree --app notepad
# パスは WSL パス形式で指定可能(ラッパーが wslpath 変換を行う)
winauto screenshot --app notepad --output /tmp/screenshot.png
# スクリプトを WSL から実行(Windows Python が呼ばれる)
winauto run my_automation.py
# ヘルパースクリプトは Windows Python 経由で実行
cmd.exe /c python .github/skills/windows-app-automation/scripts/element_inspector.py --list
WSL 固有の注意点:
- ラッパーは Windows の
python.exeを直接 exec する(cmd.exeを挟まない)。cmd.exe経由だと WSL の cwd に対して「CMD does not support UNC paths as current directories」を吐き、出力を読むエージェントを惑わせるため - 引数のパス変換はラッパーが行うが、変換対象は限定されている。変換されるのは
screenshot/codegen/recordの--outputの値、runのスクリプト位置引数、 および/で始まる絶対パスだけ。セレクタ(name:=OK)やtypeに渡す入力テキストは 変換しない(同名のファイルが cwd にあるだけで壊れるのを避けるため) winauto runに渡すスクリプトの中身のパスは Windows パス(C:/...)で書く ——変換されるのはコマンドライン引数だけで、スクリプト本文は解釈しないWINAUTO_NO_PATH_CONV=1で変換を完全に無効化できる- WSL ターミナルに出力は返ってくるが、GUI 操作の対象は Windows デスクトップ上のウィンドウ
kiro-cli から呼び出す場合
kiro-cli は WSL 上で動作する AI エージェント CLI。--trust-all-tools を付けると winauto コマンドを
自律的に呼び出してスクリプトを生成・実行できる。詳細は references/kiro-cli-usage.md を参照。
基本パターン:
# タスク記述に「winauto CLI が使える」と明示するとエージェントが活用する
kiro-cli chat --no-interactive --trust-all-tools \
"Notepad を起動してテキストを入力してスクリーンショットを /tmp/sc.png に保存して。winauto CLI が使える。"
自動実行フロー:
kiro-cli --trust-all-tools
├─ winauto apps ← 起動中アプリを偵察
├─ winauto tree --app <name> ← UI 要素を偵察
├─ [Python スクリプト生成] ← pywinauto スクリプトをファイルに書く
├─ winauto run script.py ← 実行(Windows Python が動く)
└─ winauto screenshot ← 結果を画像で確認
kiro-cli 固有の注意点:
--trust-all-toolsがないと winauto などの外部コマンド実行が承認待ちになる- GUI 操作は Windows デスクトップ側で発生するため stdout に状況は出ない。
winauto screenshotで確認 - kiro-cli が生成したスクリプト内の Windows パスは
C:/...形式にする
並列実行とデスクトップ排他
Windows デスクトップは 1 セッションに 1 つしかない共有排他資源。フォーカスとマウスカーソルは
1 組しかないため、複数プロセスが同時に set_focus() / click_input() を撃つと互いの操作を
奪い合って壊れる。agent-flow を --workers 3 のように並列で回すと即座にこれが起きる。
winauto は入力・フォーカスを奪うコマンドをファイルロックで直列化する(既定で有効)。
| コマンド | |
|---|---|
| ロックを取る | launch click type select keys screenshot run inspect codegen |
| ロックを取らない | apps tree get-text wait doctor record |
読み取り専用をロック対象から外してあるのは、長い wait がロックを占有して他の発行を
止めてしまわないようにするため。
WSL からの呼び出しもラッパー経由で Windows Python に収束するので、ロック 1 本で
「WSL 発」と「Windows ネイティブ発」の双方が直列化される。分散実行(agent-flow --git)では
PC ごとにデスクトップが別なので、PC 単位でロックが閉じるこの構造が意味論的にも正しい。
winauto --lock-timeout 600 click "name:=OK" --app myapp # ロック待ちの上限(既定 300 秒・0 で無限)
winauto --no-lock screenshot --app myapp # 排他を外す(並列 GUI 操作は壊れる)
WINAUTO_LOCK_TIMEOUT=600 winauto click ... # 環境変数でも指定可
ロック待ちの告知と待ちタイムアウトは stderr に出る(stdout はエージェントが読む結果なので汚さない)。
誰が握っているかは winauto doctor の lock 行で分かる。
agent-flow から使うときの注意:
- GUI ノードは Windows デスクトップのある PC でしか実行できない。分散時は GUI 対応 PC だけが
participateするバス/ブランチに分ける agent_timeout(既定 600 秒)は GUI タスクには短いことがある。ロック待ち時間も含まれる点に注意- 画面ロック中・RDP 切断中・未ログオンのセッションでは GUI 操作もスクリーンショットも失敗する。
winauto doctorのdesktop行が warn を出す
利用可能な補助スクリプト
scripts/element_inspector.py— UIツリーの探索・セレクタの特定(最初に必ず実行)scripts/app_launcher.py— アプリ起動・待機・コマンド実行・終了を一括管理tools/winauto/winauto.py— Playwright 風の統合 CLI(inspect/click/type/select/screenshot/codegen/record)
最初に --help を実行して利用方法を確認する。必要になるまでスクリプト本体は読まない。
進め方の判断フロー
依頼内容 → 対象アプリは何か?
├─ Win32 (MFC / VCL / Delphi / 古いC++) → backend=win32 を優先
├─ WPF / UWP / WinForms / Qt → backend=uia を使う
└─ 不明 → uia から試し、失敗なら win32
アプリは起動済みか?
├─ No → Application.start(app_path) で起動
└─ Yes → Application.connect(title_re / process) でアタッチ
要素が見つかるか? → 必ず先に element_inspector.py で探索する
├─ auto_id あり → child_window(auto_id="...") ← 最優先
├─ name + type → child_window(title="...", control_type="...")
└─ class あり → child_window(class_name="...") ← Win32 向け
ワークフロー: 調査 → 生成 → 実行
Step 1: 要素ツリーを探索する
# アプリ一覧を確認
python scripts/element_inspector.py --list
# UIツリーを探索(depth は 3〜5 が目安)
python scripts/element_inspector.py --app notepad --depth 4
# 特定の子要素から探索
python scripts/element_inspector.py --app notepad --selector "control:=Document"
# JSON で出力(大きなツリーの解析に便利)
python scripts/element_inspector.py --app notepad --json > tree.json
winauto CLI でも同等の操作が可能:
python tools/winauto/winauto.py apps
python tools/winauto/winauto.py tree --app notepad --depth 4
python tools/winauto/winauto.py inspect --app notepad # 対話REPL
Step 2: スクリプトを生成する
# codegen でテンプレートを生成(アプリを起動してツリーを取得)
python tools/winauto/winauto.py codegen notepad.exe --output test_notepad.py
# 生成されたスクリプトを確認・編集して完成させる
または、手動で直接スクリプトを書く(Step 1 で取得したセレクタを使う)。
人にやって見せてもらう(record) — 手順が言葉で書き取りにくいときは、操作を記録する。
winauto record --app 勤怠管理 --output events.jsonl
# 人がアプリを操作する → Ctrl+C で止める
1 行 1 JSON(JSONL)で、押した要素・入力欄の確定値・選択・チェック・ウィンドウの
切り替わりが残る。打鍵そのものは記録しない——低レベルキーボードフックは
デスクトップ全体のキーロガーになり、対象アプリ以外へ打ったパスワードまで拾うため。
入力欄へ打った文字は value として、キーボードから開いたメニューは invoke として
残るので、実務上の穴は「UI に対応物の無いショートカット」だけである(必要なら
{"event":"keys",...} の行を手で足す)。
記録の 1 行はそのまま打てる argv ではなく、何が起きたかの事実である。再現するときは
launch → winauto launch、window → その画面を winauto wait で待つ、
click / toggle → winauto click、value → winauto type、
select → winauto select <セレクタ> <項目名> と読み替える。
agent-dashboard の「手順を組み立てる → 操作を記録する」に貼り付けると、工程列に変換される。
Step 3: 実行する
# 単体実行
python my_automation.py
# app_launcher.py 経由(アプリ起動・終了を自動管理)
python scripts/app_launcher.py --app notepad.exe -- python my_automation.py
# winauto CLI でワンライナー実行
python tools/winauto/winauto.py click "name:=OK" --app notepad
pywinauto 基本パターン
アプリ起動・接続
from pywinauto import Application
BACKEND = "uia" # Win32 アプリなら "win32"
# 起動
app = Application(backend=BACKEND).start("notepad.exe")
app = Application(backend=BACKEND).start(r"C:\MyApp\app.exe --arg1 value")
# 実行中のアプリにアタッチ
app = Application(backend=BACKEND).connect(title_re=".*Notepad.*")
app = Application(backend=BACKEND).connect(process=12345) # PID
app = Application(backend=BACKEND).connect(path="notepad.exe")
# トップウィンドウを取得
win = app.top_window()
win = app.window(title_re=".*Notepad.*")
win.wait("ready", timeout=10)
要素の検索
# auto_id(最も安定)
btn = win.child_window(auto_id="btnSubmit")
# タイトル + コントロールタイプ
btn = win.child_window(title="OK", control_type="Button")
# クラス名(Win32 向け)
edit = win.child_window(class_name="Edit")
# インデックス(最終手段)
first_edit = win.child_window(control_type="Edit", found_index=0)
# チェーン(階層的な指定)
ok_btn = win.child_window(auto_id="mainPanel") \
.child_window(control_type="Button", title="OK")
# 存在確認
if btn.exists(timeout=3):
btn.click_input()
# 状態待機
btn.wait("enabled", timeout=10)
btn.wait("exists,visible", timeout=10)
操作
from pywinauto.keyboard import send_keys
# クリック(確実な方法)
btn.click_input()
# テキスト入力(高速、IME 非経由)
edit.set_text("Hello World")
# キーストローク入力(IME・特殊キー対応)
edit.type_keys("Hello World", with_spaces=True)
edit.type_keys("{CTRL}a{DEL}") # Ctrl+A → Delete
# ウィンドウ全体にキー送信
send_keys("^s") # Ctrl+S
send_keys("%{F4}") # Alt+F4
send_keys("{ENTER}")
# テキスト取得
text = edit.window_text()
all_texts = [c.window_text() for c in win.children()]
# メニュー操作
win.menu_select("File->Save As")
win.menu_select("Edit->Find->Find Next")
# スクロール
list_box.scroll("down", "page")
list_box.scroll("up", "line", count=3)
# ドラッグ&ドロップ
src.drag_mouse_input(dst)
スクリーンショット
# ウィンドウ全体
win.set_focus()
img = win.capture_as_image()
img.save("/tmp/screenshot.png")
# 要素のみ
elem = win.child_window(auto_id="mainPanel")
img = elem.capture_as_image()
img.save("/tmp/element.png")
ダイアログ処理
# ダイアログが開くまで待機
dlg = app.window(title_re=".*Save As.*")
dlg.wait("ready", timeout=10)
# ファイルパスを入力
filename_field = dlg.child_window(auto_id="1001") # 標準ファイルダイアログ
if not filename_field.exists(timeout=2):
filename_field = dlg.child_window(class_name="Edit") # フォールバック
filename_field.set_text(r"C:\output\result.txt")
# ボタンクリック
dlg.child_window(title="Save", control_type="Button").click_input()
# 上書き確認ダイアログ
confirm = app.window(title_re=".*Confirm.*|.*Replace.*")
if confirm.exists(timeout=2):
confirm.child_window(title="Yes", control_type="Button").click_input()
コントロールタイプ別 Tips
リストボックス / コンボボックス
# ListBox: アイテム選択
lb = win.child_window(control_type="ListBox")
lb.select("Item Name") # 名前で選択
lb.get_item(0).click_input() # インデックスで選択
# ComboBox: ドロップダウン
cb = win.child_window(control_type="ComboBox")
cb.select("Option 1") # 項目を選択
items = cb.item_texts() # 全項目取得
# ListBox のアイテムを全取得
items = [item.window_text() for item in lb.children()]
ツリービュー
tv = win.child_window(control_type="Tree")
# ルートアイテム取得
roots = tv.children(control_type="TreeItem")
# 子アイテム展開
root = roots[0]
root.expand()
children = root.children(control_type="TreeItem")
# パスで選択
tv.get_item(r"\Root\Child\Grandchild").select()
チェックボックス / ラジオボタン
cb = win.child_window(title="Enable feature", control_type="CheckBox")
cb.check() # チェック ON
cb.uncheck() # チェック OFF
is_checked = cb.get_toggle_state() == 1
rb = win.child_window(title="Option A", control_type="RadioButton")
rb.select()
タブコントロール
tab = win.child_window(control_type="Tab")
tab.select("Settings") # タブ名で選択
tab.select(1) # インデックスで選択
よくある落とし穴と対処法
| 症状 | 原因 | 対処 |
|---|---|---|
ElementNotFoundError |
セレクタが間違い / UIが未描画 | element_inspector.py で再確認; wait() を追加 |
| 操作が失敗・無応答 | 要素が disabled / focus なし | wait("enabled") → set_focus() → click_input() |
| テキスト入力が文字化け | IME / Unicode 問題 | type_keys() の代わりに set_text() を使う |
click() が効かない |
座標クリックが必要 | click_input() を使う(より低レベル) |
| ダイアログが検出できない | タイトルの一致パターンが違う | title_re=".*キーワード.*" で部分一致 |
backend=uia で要素が見えない |
アプリが UIA 非対応 | backend=win32 に切り替える |
| 高速実行で要素見つからない | UI描画遅延 | win.wait("ready") / elem.wait("exists") を使う |
| 管理者権限アプリを操作できない | UAC 分離 | スクリプト自体を管理者権限で実行 |
WSL: pywinauto が import できない |
Linux Python に入っている | Windows Python で実行する: cmd.exe /c python script.py |
WSL: winauto コマンドが見つからない |
インストール未完 / PATH 未設定 | python tools/winauto/install.py を再実行; source ~/.bashrc |
WSL: winauto apps が空 |
WSL から Windows デスクトップが見えない | winauto doctor で interop / ラッパー / デスクトップ到達性を切り分ける |
| 操作が数分止まったまま | 別プロセスが GUI ロックを保持している | winauto doctor の lock 行で保持者を確認。並列度を下げるか --lock-timeout を延ばす |
GUI ロックを … 秒以内に取得できませんでした |
先行の GUI 操作が長い/異常終了 | --lock-timeout 0(無限待ち)か、保持者プロセスを終了させる |
| スクリーンショットが真っ黒/ウィンドウが 0 個 | 画面ロック中・RDP 切断中・未ログオン | セッションをアンロックした状態を保つ。winauto doctor の desktop 行で検知できる |
Copilot: ターミナルで python が見つからない |
PATH に Python が未登録 | VS Code の Python インタープリタ設定を確認; where python でパスを確認 |
| Kiro: エージェントが winauto を実行できない | Kiro がシェルコマンドをブロック | Kiro の設定で trustTools: true を確認; 手動でターミナル実行 |
ベストプラクティス
- 必ず先に explorer で探索する:
element_inspector.py --app <name>でセレクタを確認してからスクリプトを書く auto_idを最優先にする: 開発者が設定した AutomationID は最も安定したセレクタwait()を省略しない:click_input()の前に必ずwait("enabled")かwait("exists")を入れるclick_input()を使う:click()より確実。マウスイベントを直接送信するset_text()を使う:type_keys()は IME 問題が起きやすい。値を確定させるだけならset_text()が確実app_launcher.pyでライフサイクル管理: アプリの起動・終了をスクリプト内に書かず、app_launcher.pyに任せる- スクリーンショットで状態確認: エラー時は
capture_as_image().save()で状態を記録する
参照
references/selector-syntax.md— セレクタ構文の詳細リファレンスexamples/notepad_automation.py— Notepad 自動化の完全サンプルexamples/file_dialog_handling.py— ファイルダイアログ操作パターン集tools/winauto/winauto.py— winauto CLI(--helpで各コマンド確認)- pywinauto Documentation
- Windows UI Automation