• #python
  • #rpa
  • #windows
  • #uiautomation
  • #達人cube
開発完了

所得税の達人を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つ。

  1. 所得税の達人はショートカットから直接起動 - 達人CubeのUIはカスタム描画でUIAutomationで検出できないため、座標クリックに依存せず直接起動する
  2. ダイアログ検出のタイミング調整 - time.sleep(1.5)程度の待機が必要
  3. ダブルクリック+確認ダイアログ対応 - 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から「所得税」ボタンをクリックして所得税の達人を起動しようとした。

達人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スクリプトを開発するため。

開発フロー:

  1. Claude Codeがスクリプトを作成・実行
  2. 各ステップでスクリーンショットを撮影し、memoディレクトリに保存
  3. エラーや想定外の挙動が発生したら停止
  4. Claude Codeがスクリーンショットを読み込み、「なぜ止まったか」を確認
  5. 次に押すべきボタンや修正すべきコードを特定
  6. スクリプトを修正して再実行
  7. 成功するまで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の仕様

  1. WM_KEYDOWNの設計: 「キーボードフォーカスを持つウィンドウに送られる」のがWindowsの基本設計
  2. PostMessageの動作はアプリ依存:
    • Firefox、Acrobatでは動作する
    • Word、Notepadでは動作しない
    • 「受信側アプリがどう入力を受け取るかによる」
    • 参考: LearnCodeByGaming
  3. pywinautoの制限:
    • click() (WM_*メッセージ) はマウスを動かさないが、アプリによっては動作しない
    • click_input() は実際にマウスを動かす
    • set_focus()SetForegroundWindow を使うため、フォーカス移動は避けられない
    • 参考: pywinauto Issue #1096

達人の場合

達人は古いWin32アプリケーションで、UIAutomationパターンが完全にサポートされていない:

パターン動作
SelectionItemPattern.Select()○ 動作する
InvokePattern.Invoke()× COMエラー
PostMessage(WM_KEYDOWN)△ 一部動作しない

代替案

  1. 別のデスクトップで実行: 仮想デスクトップやRDPセッションで自動化を走らせる
  2. ヘッドレス実行: ロックされたPCでは制限がある
  3. 諦めてフォーカスを許容: 自動化中は別の作業をしない

参考リンク

今後の課題

  • 業務メニューからの操作自動化
  • 複数事業者の連続処理
  • エラーハンドリングの強化
  • フォーカスが奪われる問題: Windowsの仕様上、完全なバックグラウンド操作は難しい

参考

  • uiautomation GitHub
  • Windows UI Automation APIはMicrosoftが提供するアクセシビリティAPI