メモ帳をPythonで操作する コントロールの調査方法

プログラミング

概要

pythonでの自動化の方法は、色々ありますがpywinautoの使い方を説明します。

uiautomationと言う仕組みを使ってアプリケーションのコントロールを取得して
操作します。

この方法では、操作できないアプリケーションも少なくないため、他の方法と併用することをおすすめします。

今回は、pywinautoでuiautomationを使ってメモ帳を操作してテキストを保存します。

準備

  • python環境
  • pywinauto
    pip install pywinauto
  • inspect.exe
    Windows SDK に入っています。
    https://developer.microsoft.com/ja-jp/windows/downloads/sdk-archive/
    ダウンロードしてインストールしたら。C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64
    上のようなフォルダに保存されます。
    フォルダ名が数値になっているところは、バージョン番号なので環境によって異なっている場合があります。

inspect.exeの使い方

設定

inspect.exeを起動するとこのような画面になります。

左のウィンドウがinspect.exeで右側のウィンドウがメモ帳です。

inspect.exeは、アプリケーションの中のボタンやメニューなどの部品のIDや名前を取得するツールです。

これで取得した部品(以後、コントロールと呼びます)のIDや名前から、pywinautoを使ってアプリケーションを操作します。

まず、見やすくするためにinspect.exeの
“Options” -> “Settings…”を開いてください。

そうするとこのような画面が開きます。
左ペインで選択するのがinspect.exe本体で表示する項目です。
多すぎると見づらいので上から4つ選択しました。
右のペインは、ToolTipで表示する項目です。
同様に設定しました。

使い方

inspect.exeのツールバーで上の画像のこの二つを選択して薄水色にしてください。

この状態で調べたいアプリケーションのコントロールにマウスを置くと少し間をおいて、
コントロールが黄色の四角形で囲まれます。

黄色い四角で囲まれたコントロールの情報がinspect.exeで表示されます。

メモ帳の編集メニューを調べてみます。

inspect.exeに
Name:”編集(E)”
LocalizedControlType:”メニュー項目”
とあります。

この2つが必要になります。

他にも使える項目はあるのですが、まずは安定して使えるこの2項目を押さえておきましょう。

困ったこと

inspect.exeで得られる情報とpywinautoのクラス名が異なる場合があります。

例えば、inspect.exeで「Edit」と表示されるコントロールを、pywinautoではclass_name='Edit'で指定する必要があります。

このような差異に慣れるまでは、pythonコードを併用して確認することをおすすめします。

調査用コード

使用方法

inspect.exeが使いづらいので調査用のコードを作りました。

上の画像のような画面になります。

def main():
    target_string = ".* - メモ帳" # <--- ここを書き換えてください
    app = Application(backend="uia").connect(title_re=target_string)
    main_window = app.window(title_re=target_string
    (以下略)

コード中の “def main():” に続くコードの “target_string” を調べたいアプリケーションのタイトルバーの文字列に合わせて変更して下さ。

ツールの画面下方の検索窓に調べたい文字列入れるとヒットした項目が展開され赤色表示されます。

コード

from pywinauto import Application
from PyQt6.QtWidgets import (
    QApplication, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
    QWidget, QLineEdit, QPushButton, QHBoxLayout
)
from PyQt6.QtGui import QColor


def extract_control_info(control):
    """
    コントロール情報を再帰的に取得する関数
    """
    element_info = control.element_info  # element_infoを取得
    control_info = {
        "title": control.window_text(),
        "class_name": element_info.class_name,
        "name": element_info.name,
        "control_type": element_info.control_type,
        "automation_id": element_info.automation_id,
        "children": []
    }
    try:
        for child in control.children():
            control_info["children"].append(extract_control_info(child))
    except Exception as e:
        control_info["children"].append({"error": str(e)})

    return control_info


def populate_tree_recursive(parent_item, control_info):
    """
    QTreeWidgetにコントロール情報を再帰的に追加する関数
    """
    for key, value in control_info.items():
        if key == "children":
            for child in value:
                child_item = QTreeWidgetItem(["Child", child.get("name", "Unknown")])
                parent_item.addChild(child_item)
                populate_tree_recursive(child_item, child)
        else:
            detail_item = QTreeWidgetItem([key, str(value)])
            parent_item.addChild(detail_item)


def populate_tree(tree_widget, control_info):
    """
    QTreeWidget全体を作成する関数
    """
    root_item = QTreeWidgetItem([control_info["name"], control_info["class_name"]])
    tree_widget.addTopLevelItem(root_item)
    populate_tree_recursive(root_item, control_info)


def clear_highlight_and_collapse(tree_widget):
    """
    ツリー全体を閉じ、すべての項目のハイライトをクリアする
    """
    def reset_and_collapse(item):
        for i in range(item.columnCount()):
            item.setBackground(i, QColor("white"))
        item.setExpanded(False)  # 項目を閉じる
        for j in range(item.childCount()):
            reset_and_collapse(item.child(j))

    for i in range(tree_widget.topLevelItemCount()):
        reset_and_collapse(tree_widget.topLevelItem(i))


def search_and_highlight(tree_widget, query):
    """
    検索ボックスの入力に基づいて一致する項目を赤くして自動的に展開する
    """
    def highlight_and_expand(item):
        found = False
        for i in range(item.columnCount()):
            if query.lower() in item.text(i).lower():
                item.setBackground(i, QColor("red"))
                found = True

        # 子アイテムを再帰的に処理
        for j in range(item.childCount()):
            if highlight_and_expand(item.child(j)):
                found = True

        # ヒットした項目のみ展開
        item.setExpanded(found)
        return found

    # すべての項目に対して再帰的に検索
    for i in range(tree_widget.topLevelItemCount()):
        highlight_and_expand(tree_widget.topLevelItem(i))


def main():
    #######################################################
    # ここを書き換えてください
    #######################################################
    # 起動中のメモ帳に接続
    target_string = ".* - メモ帳" # <--- ここを書き換えてください
    app = Application(backend="uia").connect(title_re=target_string)
    main_window = app.window(title_re=target_string)

    # コントロール情報を取得
    control_info = extract_control_info(main_window)

    # PyQt6アプリケーションのセットアップ
    qt_app = QApplication([])

    # メインウィンドウ
    window = QWidget()
    layout = QVBoxLayout()
    window.setLayout(layout)

    # QTreeWidget
    tree_widget = QTreeWidget()
    tree_widget.setHeaderLabels(["Property", "Value"])
    layout.addWidget(tree_widget)

    # 列幅を設定
    tree_widget.setColumnWidth(0, 300)
    tree_widget.setColumnWidth(1, 500)

    # ツリーにデータを追加
    populate_tree(tree_widget, control_info)

    # 検索ボックスと検索ボタン
    search_layout = QHBoxLayout()
    search_box = QLineEdit()
    search_button = QPushButton("Search")
    search_layout.addWidget(search_box)
    search_layout.addWidget(search_button)
    layout.addLayout(search_layout)

    # 検索ボタンのクリック時に検索を実行
    def on_search():
        clear_highlight_and_collapse(tree_widget)  # 前回のハイライトと展開をクリア
        search_and_highlight(tree_widget, search_box.text())  # 新しい検索を実行

    search_button.clicked.connect(on_search)

    # メインウィンドウの表示
    window.setWindowTitle("Pywinauto Control Viewer with Search")
    window.resize(800, 600)
    window.show()

    # PyQtアプリケーションの実行
    qt_app.exec()


if __name__ == "__main__":
    main()

 

もう一つの調査用コード

from pywinauto import Application


def print_all_controls(element, depth=0):
    """
    再帰的にすべてのコントロール情報を表示する
    :param element: 現在のコントロール要素
    :param depth: 現在の深さ(インデント用)
    """
    indent = ". " * depth  # インデント用スペース
    try:
        # 各コントロールの基本情報を表示
        print(f"{indent}- Control: {element.window_text()}")
        print(f"{indent}  Title: {element.element_info.name}")
        print(f"{indent}  Class Name: {element.element_info.class_name}")
        print(f"{indent}  Control Type: {element.element_info.control_type}")
        print(f"{indent}  Automation ID: {element.element_info.automation_id}")
    except Exception as e:
        print(f"{indent}  Error: {e}")

    # 子要素を再帰的に探索
    try:
        for child in element.children():
            print_all_controls(child, depth + 1)
    except Exception as e:
        print(f"{indent}  Failed to retrieve children: {e}")


# メモ帳に接続
target_string = ".* - メモ帳"  # <--- ここを変更してください
app = Application(backend="uia").connect(title_re=target_string)

# メインウィンドウを取得
main_window = app.window(title_re=target_string)

# コントロール構造を再帰的に取得
print("すべてのコントロールを取得:")
print_all_controls(main_window)

 

こちらは、調査内容は同じですがテキストで表示します。
こちらのほうが検索しやすい場合もあるので載せておきます。

こちらも使うときにコードの “target_string” を書き換えてください。

メモ帳を操作する

やりたいこと

メモ帳の簡単な操作をします。

  1. メモ帳を起動します
  2. テキスト領域にテキストを書き込みます
  3. “Ctrl+S”で上書き保存のダイアログを開きます
  4. ファイル名を書き込みます
  5. 保存ボタンを押して保存します
  6. メモ帳をしゅうりょうします

このような操作をすることを目的にします。

なお、同等の操作は複数あります。
例えば”Ctrl+S”は、メニューから上書き保存を選択しても同じ動作になります。

調査する

メモ帳を起動します。

ウィンドウのタイトル

タイトルバーをよく見てタイトルを確認します。
「無題 – メモ帳」です。
半角スペースや半角”-“も正確に確認してください。

テキスト領域

先ほど紹介したコードで調べたところ、
class_name : Edit
control_type : Edit
となっています。

ファイル保存ダイアログ

一度、調査用のツールを終了した後、メモ帳を”Ctrl+S”を押してファイル保存用のダイアログを表示させます。
それからツールを開きなおします。

これは、ダイアログが開いていない状態では情報を取得できないからです。

情報を取得しなおしたら下の検索用の入力欄に”名前を付けて保存”といれて”Search”ボタンをクリックしてください。

ダイアログの情報が
name : 名前を付けて保存
control_type : Window
として確認できました。

ファイル名の書き込み

ツールの検索欄に “ファイル名” と入れて検索します。

そうすると3つのコントロールが赤色に表示されます。
その中で control_type を見てみると TextとComboBoxとEditがあるのが分かります。

今回は、下の画面の様にメモ帳の名前を付けて保存ダイアログの”ファイル名”の入力欄にファイル名を書き込みたいので control_type が “Edit” であるコントロールに書き込めば良いのだと推測できます。

保存ボタン

押したいボタンには、”保存(S)”と表示されています。
この文字を検索します。

そうすると

title : 保存(S)
control_type : Button

が見つかりました。

これで、必要な情報は収集できました。

 コード

from pywinauto import Application
import time

# アプリケーションを起動
app = Application(backend="uia").start("notepad.exe")

# メインウィンドウを取得
main_window = app.window(title_re=".* - メモ帳")

# テキスト編集エリアを取得
#edit = main_window.child_window(title="テキスト エディター", control_type="Edit")
#edit = main_window.child_window(title="テキスト エディター", auto_id="15")
edit = main_window.child_window(class_name="Edit")
edit.type_keys("Pywinautoでメモ帳を操作してみました!")

# 名前を付けて保存ダイアログを開く (ショートカットキー: Ctrl + S)
main_window.type_keys("^s")
time.sleep(1)  # ダイアログが開くのを待機

# 保存ダイアログを取得
save_dialog = main_window.child_window(title="名前を付けて保存", control_type="Window")

# ファイル名を入力
file_name_edit = save_dialog.child_window(class_name="Edit")
file_name_edit.type_keys("test_file.txt")

# 保存ボタンをクリック
save_button = save_dialog.child_window(title="保存(S)", control_type="Button")
save_button.click()

# アプリケーションを閉じる
main_window.close()

 

コード解説

main_window の取得

main_window = app.window(title_re=".* - メモ帳")

タイトルバーに「無題 – メモ帳」と表示されているのでそれを使ってmain_windowを取得しています。

“.*”は、正規表現であらゆる文字列がマッチします。

編集エリアへの書き込み

# テキスト編集エリアを取得
#edit = main_window.child_window(title="テキスト エディター", control_type="Edit")
#edit = main_window.child_window(title="テキスト エディター", auto_id="15")
edit = main_window.child_window(class_name="Edit")
edit.type_keys("Pywinautoでメモ帳を操作してみました!")

class_name を使って編集領域を取得して、
文字列を入力しました。

コメントアウトしてありますが
タイトルとコントロールタイプ や タイトルとオートメイトIDでも編集領域を取得することができます。
ぜひ、試してみてください。

ファイル保存ダイアログを開く

# 名前を付けて保存ダイアログを開く (ショートカットキー: Ctrl + S)
main_window.type_keys("^s")
time.sleep(1)  # ダイアログが開くのを待機

“Ctrl+S”キーを押します。
その後、ダイアログが完全に開くのを待機します。

ダイアログの取得

main_windowをダイアログに変えて取得しなおします。

タイトルとコントロールタイプを使って取得しています。

# 保存ダイアログを取得
save_dialog = main_window.child_window(title="名前を付けて保存", control_type="Window")

ファイル名の書き込み

クラス名を元にコントロールを取得して、書き込みます。

保存ボタンを押す

タイトルとコントロールタイプからボタンを取得し、

save_button.click()

としてボタンを押します。

メモ帳の終了

main_window.close()

アプリケーションを終了します。

まとめ

pywinauto から uiautomaiton を使って、アプリケーションを操作するための基礎を説明しました。

特に pywinauto を使うときに大切なのはコントロール(部品)の取得で
そのためのツールを自作を含め3種類紹介しました。

uiautomationは、すべてのアプリケーションで動作するわけではないため、適用範囲は限られますが、対応するアプリケーションでは非常に便利です。
ぜひ試してみてください。

また、自動化で面白い話題がありましたら記事にしますのでお楽しみに。

注意点

  • pywinautoはUI Automation Frameworkを利用するため、アプリケーションの種類によっては期待通りに動作しない場合があります。
  • 操作対象アプリケーションのセキュリティ設定やバージョンにより、動作しない場合があります。

Commnts

タイトルとURLをコピーしました