所得税の達人をUI Automation APIで自動化する
正直な感想
達人シリーズの自動化は確かに魅力的だ。毎年の確定申告作業で同じ操作を繰り返すのは苦痛だし、自動化できれば素晴らしい。
でも実際やってみると……時間かかりすぎ。
Claude Codeで対話しながらコードを書いたり、生成AIでアイデアを形にしたり、そういう「未来」を体験している今この瞬間に、Win32 APIの
WM_KEYDOWNがどうとか、MFCのカスタム描画でUIAutomationが効かないとか、time.sleep(1.5)じゃないと安定しないとか……こういう泥臭い作業をしている自分がいる。なんだろう、この気持ち。
2026年にもなって、ウィンドウハンドルを取得してPostMessageしてる。AIがコードを書いてくれる時代に、座標クリックを回避するためにショートカットファイルのパスをハードコードしてる。
税理士として、この作業の費用対効果も考えてしまう。自分でやる分にはまだいいが、スタッフにやらせるとなると時給が発生する。そもそも「自動化できないUIを人間が操作する」という発想自体がイケてない。
もっと言えば、税務ソフトのUIをRPAで自動操作するというアプローチ自体が間違っているのでは。コードが書けるなら、e-Taxに直接XMLを作成してデプロイしたほうが早いのではないか。財務諸表データや別表を最初からXMLで生成する。税務ソフトのボタンをクリックする代わりに、データそのものを作る。
たぶん、アプローチがこっちじゃない。
継続的に取り組みたい気持ちはある。でも、費用対効果を考えると……トホホ。
結論
PythonのuiautomationライブラリでWindows UI Automation APIを使い、「所得税の達人」の事業者選択操作を自動化できた。ポイントは以下の3つ。
- 所得税の達人はショートカットから直接起動 - 達人CubeのUIはカスタム描画でUIAutomationで検出できないため、座標クリックに依存せず直接起動する
- ダイアログ検出のタイミング調整 -
time.sleep(1.5)程度の待機が必要 - ダブルクリック+確認ダイアログ対応 - OKボタンが検出できない場合はダブルクリックで代替
背景
達人Cubeは税務申告ソフトで、毎年の確定申告作業で繰り返し操作が必要になる。Claude Codeと連携してRPAスクリプトを開発し、作業効率を上げたい。
セットアップ
UV環境の構築
cd C:\Users\numbe\Git_repo\RPA_TATSUJIN
uv init --name rpa-tatsujin
uv add uiautomation mss pillow
プロジェクト構造
RPA_TATSUJIN/
├── pyproject.toml
├── open_jigyosha.py # 事業者を開くスクリプト
├── utils/
│ └── logger.py # ログ・スクリーンショット機能
└── memo/
└── 2026-01-08/ # 実行ログ・スクリーンショット
試行錯誤の経緯
1. 達人CubeのUI要素が検出できない問題
最初は達人Cubeから「所得税」ボタンをクリックして所得税の達人を起動しようとした。

しかし、pywinautoとuiautomationの両方で調査した結果、達人CubeはMFCのカスタム描画UIを使っており、要素に名前が設定されていなかった。
# pywinautoで調査した結果
# 全ての要素が Afx:00400000:8 クラスで、Name属性が空
解決策: 座標クリックに依存せず、所得税の達人をスタートメニューのショートカットから直接起動する。
SHOTOKUZEI_LNK = r"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\達人シリーズ\所得税の達人(令和06年分版).lnk"
def launch_shotokuzei():
subprocess.Popen(["cmd", "/c", "start", "", SHOTOKUZEI_LNK], shell=True)
# 起動を待つ
for i in range(30):
time.sleep(1)
shotokuzei = auto.WindowControl(searchDepth=1, SubName="所得税の達人")
if shotokuzei.Exists(0):
return shotokuzei
return None
2. ダイアログ検出のタイミング問題
「開く」ボタンをクリックした後、ダイアログの検出に失敗することがあった。
# 1秒では不足
time.sleep(1)
dialog = shotokuzei.WindowControl(Name="開く")
# → Exists=False
# 1.5秒なら安定
time.sleep(1.5)
dialog = shotokuzei.WindowControl(Name="開く")
# → Exists=True
3. OKボタンが見つからない問題
ダイアログのOKボタンがButtonControl(Name="OK")で検出できない場合があった。
解決策: ダブルクリックで開く方法を代替として実装。
ok_btn = dialog.ButtonControl(searchDepth=5, Name="OK")
if ok_btn.Exists(2):
ok_btn.Click()
else:
# ダブルクリックで開く
target_item.DoubleClick()
4. 確認ダイアログへの対応
ダブルクリック後に確認ダイアログが表示される。
「選択されたデータおよび下の明細について、データを開きます。よろしいですか?」というダイアログのOKをクリックする処理を追加。
5. スクリーンショット機能(Claude Codeとの連携)
各ステップでスクリーンショットを撮る機能を実装した。
def screenshot(name: str = "") -> Path:
"""スクリーンショットを撮ってimages_時刻ディレクトリに保存"""
with mss.mss() as sct:
# 座標が最も左のモニターを選択(4Kモニター3台環境)
leftmost = min(range(1, len(sct.monitors)),
key=lambda i: sct.monitors[i]['left'])
monitor = sct.monitors[leftmost]
sct_img = sct.grab(monitor)
mss.tools.to_png(sct_img.rgb, sct_img.size, output=str(filepath))
return filepath
ディレクトリ構造:
memo/
└── 2026-01-08/
├── 11_18_43.txt # ログファイル
├── images_11_18_43/ # スクリーンショット(セッション1)
│ ├── 01_start.png
│ ├── 02_after_open_click.png
│ └── 03_error_no_dialog.png
└── images_11_25_00/ # スクリーンショット(セッション2)
├── 01_start.png
└── 02_complete.png
なぜスクリーンショットを撮るのか
Claude Codeと連携して再帰的にRPAスクリプトを開発するため。
開発フロー:
- Claude Codeがスクリプトを作成・実行
- 各ステップでスクリーンショットを撮影し、memoディレクトリに保存
- エラーや想定外の挙動が発生したら停止
- Claude Codeがスクリーンショットを読み込み、「なぜ止まったか」を確認
- 次に押すべきボタンや修正すべきコードを特定
- スクリプトを修正して再実行
- 成功するまで2〜6を繰り返す
メリット:
- 人間がログを読んでClaude Codeに伝える手間が省ける
- スクリーンショットから視覚的にUI状態を把握できる
- 「ダイアログが見つからない」→「実際には表示されている」といった乖離をすぐ発見できる
- 試行錯誤のログが自動的に残る
最終的なコード
def open_jigyosha(target_code: str = "9999"):
"""指定した個人コードの事業者を開く"""
# 所得税の達人ウィンドウを取得(なければ起動)
shotokuzei = auto.WindowControl(searchDepth=1, SubName="所得税の達人")
if not shotokuzei.Exists(3):
shotokuzei = launch_shotokuzei()
if not shotokuzei:
return False
# ツールバーの「開く」ボタンをクリック
open_btn = shotokuzei.ButtonControl(Name="事業者データの選択を行います。")
open_btn.Click()
time.sleep(1.5)
# ダイアログ取得
dialog = shotokuzei.WindowControl(Name="開く")
list_ctrl = dialog.ListControl()
# 個人コードで検索してクリック
target_item = list_ctrl.ListItemControl(Name=target_code)
target_item.Click()
# OKボタンまたはダブルクリック
ok_btn = dialog.ButtonControl(searchDepth=5, Name="OK")
if ok_btn.Exists(2):
ok_btn.Click()
else:
target_item.DoubleClick()
# 確認ダイアログのOKをクリック
confirm_dialog = shotokuzei.WindowControl(SubName="所得税の達人")
if confirm_dialog.Exists(2):
confirm_ok = confirm_dialog.ButtonControl(Name="OK")
if confirm_ok.Exists(1):
confirm_ok.Click()
return True
結果
事業者「9999」が正常に開かれ、業務メニューが表示された。

仮想クリックの実装
背景
通常のClick()メソッドはマウスカーソルを実際に動かすため、自動化中に他の作業ができない。マウスを動かさずに操作する「仮想クリック」を実装した。
実装した方法
import ctypes
# Win32 API定数
WM_KEYDOWN = 0x0100
WM_KEYUP = 0x0101
VK_RETURN = 0x0D
def send_enter_to_window(hwnd):
"""ウィンドウにEnterキーを直接送信(フォーカス不要)"""
ctypes.windll.user32.PostMessageW(hwnd, WM_KEYDOWN, VK_RETURN, 0)
time.sleep(0.05)
ctypes.windll.user32.PostMessageW(hwnd, WM_KEYUP, VK_RETURN, 0)
def virtual_select(element):
"""仮想選択 - マウスを動かさずに選択"""
pattern = element.GetSelectionItemPattern()
if pattern:
pattern.Select()
return True
return False
動作する方法
| 方法 | 動作 | マウス移動 |
|---|---|---|
SelectionItemPattern.Select() | ○ | なし |
PostMessage(WM_KEYDOWN) | △(アプリ依存) | なし |
SetFocus() + SendKeys() | ○ | フォーカス奪われる |
Click() | ○ | カーソル移動する |
動作しなかった方法
InvokePattern.Invoke()→ COMエラー (-2147220992)LegacyIAccessiblePattern.DoDefaultAction()→ COMエラーBM_CLICKメッセージ → 反応なし
マウス/フォーカスが奪われる問題(Windows仕様)
調査結果
結論:アプリケーション依存であり、完全なバックグラウンド操作は難しい
Windowsの仕様
- WM_KEYDOWNの設計: 「キーボードフォーカスを持つウィンドウに送られる」のがWindowsの基本設計
- PostMessageの動作はアプリ依存:
- Firefox、Acrobatでは動作する
- Word、Notepadでは動作しない
- 「受信側アプリがどう入力を受け取るかによる」
- 参考: LearnCodeByGaming
- pywinautoの制限:
click()(WM_*メッセージ) はマウスを動かさないが、アプリによっては動作しないclick_input()は実際にマウスを動かすset_focus()はSetForegroundWindowを使うため、フォーカス移動は避けられない- 参考: pywinauto Issue #1096
達人の場合
達人は古いWin32アプリケーションで、UIAutomationパターンが完全にサポートされていない:
| パターン | 動作 |
|---|---|
SelectionItemPattern.Select() | ○ 動作する |
InvokePattern.Invoke() | × COMエラー |
PostMessage(WM_KEYDOWN) | △ 一部動作しない |
代替案
- 別のデスクトップで実行: 仮想デスクトップやRDPセッションで自動化を走らせる
- ヘッドレス実行: ロックされたPCでは制限がある
- 諦めてフォーカスを許容: 自動化中は別の作業をしない
参考リンク
- Microsoft Learn - WM_KEYDOWN
- pywinauto Issue #1096 - No active desktop required
- pywinauto Issue #1153 - Click() not working for background window
- AutoHotkey - Sending click to inactive window
- LearnCodeByGaming - Send inputs to multiple windows
今後の課題
- 業務メニューからの操作自動化
- 複数事業者の連続処理
- エラーハンドリングの強化
- フォーカスが奪われる問題: Windowsの仕様上、完全なバックグラウンド操作は難しい
参考
- uiautomation GitHub
- Windows UI Automation APIはMicrosoftが提供するアクセシビリティAPI