1. はじめに
この記事では、Power Automate Desktopのフローを自動取得し、保存する方法を紹介します。
Power Automate Desktop 無償版(無料版)には、作成したフローを一括でエクスポート(バックアップ)する機能がありません。対象のフローを全選択・コピーして、メモ帳等に貼り付けて保存する方法が一般的ですが、サブフローの数が増えると手間も大きくなります。
なんとか自動化できないか?と調べてみたところ、Power Automate Desktopだけでは実現が難しいことが分かりました。そこでPythonを活用して、フロー内容をテキスト形式でエクスポートする方法を考案しました。
とはいえ、Pythonは初心者レベルのため、生成AI(Copilot)の力を借りながら実装しています。自力だけでは書き切れませんでしたが、工夫しながら形にできたので、参考になれば嬉しいです。
また、Pythonがインストールされていない環境でも実行できます。
2. 更新履歴
2025年9月20日
プログラム Ver1.20公開
動作を調整できるパラメータ設定を追加
2025年8月17日
プログラム Ver1.10公開
オプションに「エクスポートファイルに関数定義の開始(FUNCTION)と終了(END FUNCTION)を含める」機能を追加(12. オプション機能解説)
2025年8月10日
プログラム Ver1.00初公開
3. 動作イメージ(動画)
Power Automate Desktopのフローを一括エクスポートする、プログラムの動作イメージを紹介します。
4. 動作イメージ(概要)
フローデザイナーの上でコマンドプロンプトを開き、Pythonプログラムを実行します。 (Pythonプログラムをダブルクリックしても動作します)
また、Pythonがインストールされていない環境でも動作するよう、PyInstallerを使用して作成したWindows実行ファイル(.exe)も公開しています。
PythonプログラムからPower Automate Desktopを自動操作し、サブフローの検索および「Ctrl+A → Ctrl+C → ファイル出力」の処理を繰り返し実行します。
エクスポートされたファイルは、実行したPythonプログラムのサブフォルダ、または指定した保存先フォルダに格納されます。
この方法を活用することで、大量のサブフローの内容を一括で取得できます。手動で「ファイル作成 → フロータブ選択 → Ctrl+a → Ctrl+c → メモ帳へ Ctrl+v → 保存」より、効率的かつ正確に作業ができます。
改良やカスタマイズはご自由にどうぞ。ご自身の環境に合わせて調整してみてください。
5. Windows実行ファイル版について(.exe)
Pythonがインストールされていない環境でも実行 できるよう、本ツールの Windows実行ファイル(.exe)を PyInstaller で作成しました。 以下のリンクからZIPファイルをダウンロードしてご利用ください。
PADFlowExporter.zip(Windows EXEファイル版)
GitHubからダウンロード(約67MB)Ver 1.20
PADFlowExporter.zip(Windows EXEファイル版)
ベクターからダウンロード(約67MB)Ver 1.20
本プログラムは、すべての環境に対する動作チェックは行っておりません。また、 Power Automate Desktopの仕様変更 や 特殊なPC の場合、プログラムが正しく動作しない可能性もあります。ご使用の際は、ご自身の環境にて十分にご確認のうえご利用ください。
PAD Flow Exporter のEXEファイルのサイズが大きいのは、PyInstallerがPython本体とすべての依存ライブラリを同梱するためです。
zipファイルをダウンロードし、ファイルを解凍してください。プログラム実行ファイルは「 PADFlowExporter.exe
」です。
実行後の操作方法は、 プログラム実行 の項目をご覧ください。
6. 検証確認
- Windows 11
- Power Automate Desktop 2.59.154.25213(Release Date: 2025年8月)
- Python 3.13.7(Release Date: Aug 14,2025)
Windows実行ファイル(.exe)版を利用する場合は、Pythonは必要ありません
7. Python追加モジュール
このプログラムを実行するには、以下の Python パッケージが必要です。ご利用の環境に応じて、各パッケージをインストールしてください。
pip install pyautogui
pip install pillow
pip install opencv-python
8. 準備
8-1. プログラム作成(Python)
下記プログラムをすべてコピー後、メモ帳/テキストエディタ等で新規テキストファイルを作成し、貼り付けてください。 ファイル名は「 PADFlowExporter.py
」とします。
保存場所は、Pythonが動作する場所であればどこでも可能(※1)ですが、この解説では「C:\Users\ユーザ名(※2)\Documents\python」で作成しています。
(※1) Pythonのインストール先や、パスが通っている任意のフォルダを指します。
(※2) ‘ユーザ名’ はご自身の Windows ユーザー名に置き換えてください。
#
# PAD Flow Exporter (Ver1.20)
#
import ctypes
import sys
# Windows での DPI 対応(高 DPI の環境でアイコン検出がずれないように)
if sys.platform == "win32":
try:
# Windows 8.1 以降の DPI 認識を有効にする呼び出し
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
# 古い環境では代替 API を呼ぶ
ctypes.windll.user32.SetProcessDPIAware()
import os
import re
from ctypes import windll
from PIL import Image
import time
import datetime
import shutil
import tempfile
import threading
import subprocess
import logging
import hashlib
import atexit
import tkinter as tk
from tkinter import filedialog, scrolledtext, ttk, messagebox
import pyautogui
import pyperclip
import json
# PyAutoGUI の安全策とキー間のデフォルト待ち時間
pyautogui.FAILSAFE = True
pyautogui.PAUSE = 0.5
# コンソール出力を抑制(GUI アプリとして動かすため)
sys.stdout = open(os.devnull, "w")
sys.stderr = open(os.devnull, "w")
# 定数とデフォルト設定
SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) # スクリプト所在ディレクトリ
SUBFLOW_LIST_FILE = os.path.join(SCRIPT_PATH, "subflow_list.txt") # デフォルトのサブフローリスト保存先
ORIGINAL_IMAGE = os.path.join(SCRIPT_PATH, 'search_icon.png') # サブフローボタン認識用の画像ファイル(原寸)
LOG_FILE = "export_log.txt"
CREATE_NO_WINDOW = 0x08000000 # subprocess で explorer を開くときにウィンドウを作らないフラグ
# 動作パラメータ(デフォルト値)
ACTIVATE_WAIT = 0.3
IMAGE_SEARCH_INTERVAL = 1.0
IMAGE_CONFIDENCE = 0.8
IMAGE_SEARCH_TIMEOUT = 60
CLICK_RETRIES = 5
CLICK_INTERVAL = 1.0
INTER_FLOW_DELAY = 0.5
HOTKEY_INTERVAL = 0.1
POST_HOTKEY_SLEEP = 0.2
CLIPBOARD_POLL_INTERVAL = 0.1
CLIPBOARD_RETRIES = 5
CLIPBOARD_RETRY_INTERVAL = 0.5
KEEP_TEMP_IMAGES = False
EXPORT_TIMEOUT = 600
# 実行時に変更されるフラグやパス
INCLUDE_FUNCTION = False # FUNCTION / END FUNCTION を含めるか
SAVE_PATH = ""
TARGET_IMAGE = None # 実際にスケーリング済みで使用するテンポラリ画像ファイルパス
# ロギング初期化(最小限)
logging.basicConfig(level=logging.CRITICAL)
# 一時画像管理リスト(prepare_target_image で生成した一時ファイルを保持)
_temp_images = []
def _cleanup_temp_images():
# KEEP_TEMP_IMAGES が False の場合、終了時に一時ファイルを削除する
if KEEP_TEMP_IMAGES:
return
for p in _temp_images:
try:
os.remove(p)
except Exception:
pass
atexit.register(_cleanup_temp_images) # プログラム終了時に一時画像を掃除
# 設定ファイル
CONFIG_FILE = os.path.join(SCRIPT_PATH, "PADFlowExporter_config.json")
# 設定のデフォルト値を辞書で保持(GUI のパラメータウィンドウで使う)
_default_config = {
"ACTIVATE_WAIT": ACTIVATE_WAIT,
"IMAGE_SEARCH_INTERVAL": IMAGE_SEARCH_INTERVAL,
"IMAGE_CONFIDENCE": IMAGE_CONFIDENCE,
"IMAGE_SEARCH_TIMEOUT": IMAGE_SEARCH_TIMEOUT,
"CLICK_RETRIES": CLICK_RETRIES,
"CLICK_INTERVAL": CLICK_INTERVAL,
"INTER_FLOW_DELAY": INTER_FLOW_DELAY,
"HOTKEY_INTERVAL": HOTKEY_INTERVAL,
"POST_HOTKEY_SLEEP": POST_HOTKEY_SLEEP,
"CLIPBOARD_POLL_INTERVAL": CLIPBOARD_POLL_INTERVAL,
"CLIPBOARD_RETRIES": CLIPBOARD_RETRIES,
"CLIPBOARD_RETRY_INTERVAL": CLIPBOARD_RETRY_INTERVAL,
"KEEP_TEMP_IMAGES": KEEP_TEMP_IMAGES,
"EXPORT_TIMEOUT": EXPORT_TIMEOUT
}
# 実際に使用する設定(設定ファイルを読み込んで上書きされる)
_config = _default_config.copy()
def load_config():
# 設定ファイルが存在すれば読み込み、_config を更新する
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
# _config のキーのみを対象に設定を更新(不要なキーを無視)
_config.update({k: data.get(k, v) for k, v in _config.items()})
except Exception:
logging.exception("設定ファイルの読み込みに失敗しました")
def save_config():
# 現在の _config を JSON で保存
try:
with open(CONFIG_FILE, "w", encoding="utf-8") as f:
json.dump(_config, f, ensure_ascii=False, indent=2)
except Exception:
logging.exception("設定ファイルの保存に失敗しました")
def apply_config_globals():
# _config の内容をグローバル定数に反映するためのヘルパー
global ACTIVATE_WAIT, IMAGE_SEARCH_INTERVAL, IMAGE_CONFIDENCE, IMAGE_SEARCH_TIMEOUT
global CLICK_RETRIES, CLICK_INTERVAL, INTER_FLOW_DELAY
global HOTKEY_INTERVAL, POST_HOTKEY_SLEEP
global CLIPBOARD_POLL_INTERVAL, CLIPBOARD_RETRIES, CLIPBOARD_RETRY_INTERVAL
global KEEP_TEMP_IMAGES, EXPORT_TIMEOUT
ACTIVATE_WAIT = float(_config.get("ACTIVATE_WAIT", ACTIVATE_WAIT))
IMAGE_SEARCH_INTERVAL = float(_config.get("IMAGE_SEARCH_INTERVAL", IMAGE_SEARCH_INTERVAL))
IMAGE_CONFIDENCE = float(_config.get("IMAGE_CONFIDENCE", IMAGE_CONFIDENCE))
IMAGE_SEARCH_TIMEOUT = int(_config.get("IMAGE_SEARCH_TIMEOUT", IMAGE_SEARCH_TIMEOUT))
CLICK_RETRIES = int(_config.get("CLICK_RETRIES", CLICK_RETRIES))
CLICK_INTERVAL = float(_config.get("CLICK_INTERVAL", CLICK_INTERVAL))
INTER_FLOW_DELAY = float(_config.get("INTER_FLOW_DELAY", INTER_FLOW_DELAY))
HOTKEY_INTERVAL = float(_config.get("HOTKEY_INTERVAL", HOTKEY_INTERVAL))
POST_HOTKEY_SLEEP = float(_config.get("POST_HOTKEY_SLEEP", POST_HOTKEY_SLEEP))
CLIPBOARD_POLL_INTERVAL = float(_config.get("CLIPBOARD_POLL_INTERVAL", CLIPBOARD_POLL_INTERVAL))
CLIPBOARD_RETRIES = int(_config.get("CLIPBOARD_RETRIES", CLIPBOARD_RETRIES))
CLIPBOARD_RETRY_INTERVAL = float(_config.get("CLIPBOARD_RETRY_INTERVAL", CLIPBOARD_RETRY_INTERVAL))
KEEP_TEMP_IMAGES = bool(_config.get("KEEP_TEMP_IMAGES", KEEP_TEMP_IMAGES))
EXPORT_TIMEOUT = int(_config.get("EXPORT_TIMEOUT", EXPORT_TIMEOUT))
# 起動時に設定ファイルを読み込み、グローバルへ反映
load_config()
apply_config_globals()
def log_message(msg: str, level: str = "info"):
# ラッパー:logging のメソッドを文字列から呼び出す
getattr(logging, level)(msg)
def prepare_target_image(image_path: str) -> str:
"""
サブフロー認識用のアイコン画像を準備する。
Windows の DPI 設定に合わせてリサイズした一時画像を作成し、そのパスを返す。
返り値は一時ファイルのパス(後で削除される)。
"""
if not os.path.exists(image_path):
logging.exception("サブフロー認識画像ファイルが存在しません: %s", image_path)
raise FileNotFoundError(
f"サブフロー認識画像ファイル(search_icon.png)が存在しません\n"
f"{image_path} に保存してください"
)
# 画面 DPI を取得してスケールを算出(96dpi を基準とする)
hdc = windll.user32.GetDC(0)
dpi = windll.gdi32.GetDeviceCaps(hdc, 88)
scale = dpi / 96.0
# 画像を読み込み、一時ファイルへ保存(必要ならリサイズ)
orig = Image.open(image_path)
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as tmp:
temp_image = tmp.name
_temp_images.append(temp_image) # 終了時に削除するためリストへ追加
if scale != 1.0:
w, h = orig.size
new_size = (int(w * scale), int(h * scale))
# 高品質リサンプリングでリサイズ
resized = orig.resize(new_size, Image.LANCZOS)
resized.save(temp_image)
else:
# スケールが 1 の場合は単にコピー
shutil.copyfile(image_path, temp_image)
return temp_image
def initialize_save_path_and_logging(custom_path=None, enable_log=True):
"""
保存先フォルダとログファイルの初期化。
custom_path が指定されればそこを使用し、なければタイムスタンプフォルダを作成。
ログを有効にすると export_log.txt に出力するハンドラをセットする。
戻り値: (save_path, log_file_path_or_empty_string)
"""
if custom_path:
save_path = custom_path
else:
ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
save_path = os.path.join(SCRIPT_PATH, ts)
os.makedirs(save_path, exist_ok=True)
log_file = os.path.join(save_path, LOG_FILE)
logger = logging.getLogger()
logger.handlers.clear()
if enable_log:
handler = logging.FileHandler(log_file, encoding="utf-8")
handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.setLevel(logging.INFO)
logger.addHandler(handler)
else:
# ログを出したくない場合は重大度を CRITICAL にして出力を抑制
logger.setLevel(logging.CRITICAL)
return save_path, (log_file if enable_log else "")
def click_target_image(image_path: str):
"""
画面上で指定の画像(TARGET_IMAGE)を探し、見つかったら中央をクリックする。
見つかるまでループし、IMAGE_SEARCH_TIMEOUT 秒経過で FileNotFoundError を投げる。
ウィンドウのアクティブ化や ACTIVATE_WAIT 等の待機を行うことで安定化を試みる。
"""
start = time.time()
while True:
if time.time() - start > IMAGE_SEARCH_TIMEOUT:
raise FileNotFoundError("サブフローボタンのアイコンが見つかりません(タイムアウト)")
# まずアクティブウィンドウを取り、可能ならアクティベートしてから検索
try:
win = pyautogui.getActiveWindow()
if win:
try:
win.activate()
except Exception:
pass
time.sleep(ACTIVATE_WAIT)
except Exception:
# pyautogui.getActiveWindow が失敗する場合もあるが続行
pass
# 画像検索(pyautogui の locate を使用)
pos = pyautogui.locateCenterOnScreen(image_path, confidence=IMAGE_CONFIDENCE)
if pos:
pyautogui.click(pos)
return True
# 見つからなければ少し待って再試行
time.sleep(IMAGE_SEARCH_INTERVAL)
def get_flow_text(flow_name: str) -> str:
"""
フロー名をテキストボックスへ貼り付けて Enter、表示されたフロー内容を選択してコピーし、
クリップボードからテキストを取得して返す。
クリップボードの反映を待つループやリトライを含む。
"""
# フロー名をクリップボードへコピーして貼り付け(検索用や移動用の操作)
pyperclip.copy(flow_name)
pyautogui.hotkey('ctrl', 'v', interval=HOTKEY_INTERVAL)
time.sleep(POST_HOTKEY_SLEEP)
# クリップボードへ正しく貼られたかを簡易チェック(最大 5 秒)
start = time.time()
while time.time() - start < 5:
if pyperclip.paste() == flow_name:
break
time.sleep(CLIPBOARD_POLL_INTERVAL)
pyautogui.press('enter')
# フローのテキストを取得するために Ctrl+A / Ctrl+C を行い、クリップボードから読み取る
for i in range(CLIPBOARD_RETRIES):
pyautogui.hotkey('ctrl', 'a', interval=HOTKEY_INTERVAL); time.sleep(POST_HOTKEY_SLEEP)
pyautogui.hotkey('ctrl', 'c', interval=HOTKEY_INTERVAL); time.sleep(POST_HOTKEY_SLEEP)
text = pyperclip.paste()
if text:
return text
logging.warning(f"クリップボードが空です({i+1}/{CLIPBOARD_RETRIES})")
time.sleep(CLIPBOARD_RETRY_INTERVAL)
# リトライしても得られなければ例外
logging.exception(f"エラー: {flow_name} の取得に失敗しました")
raise ValueError(f"{flow_name} の取得に失敗しました")
def validate_hash_difference(flow_text: str, prev_hash: str, flow_name: str) -> str:
"""
取得したフローのテキストから SHA-256 ハッシュを作成し、前回のハッシュと同一であれば
同じフロー名が選択され続けている(想定外)として例外を投げる。
そうでなければ新しいハッシュを返す。
"""
current = hashlib.sha256(flow_text.encode('utf-8')).hexdigest()
if current == prev_hash:
logging.error(f"エラー: {flow_name} のサブフロー名が違う可能性があります")
raise ValueError(f"{flow_name} のサブフロー名が違う可能性があります")
return current
def save_flow_text(flow_text: str, flow_name: str, timestamp_flag: bool) -> str:
"""
取得したフローのテキストをファイルへ保存する。
INCLUDE_FUNCTION が False の場合は FUNCTION / END FUNCTION のブロックを削除して保存する。
ファイル名はフロー名をファイル名として不正文字を置換する。既存ファイルがあるか
timestamp_flag が True ならタイムスタンプを付与して別名で保存する。
戻り値は保存したパス。
"""
global SAVE_PATH
# オプションで関数定義ブロックを除去
if not INCLUDE_FUNCTION:
flow_text = re.sub(
r"^FUNCTION\s+.*?GLOBAL\s*\n", "", flow_text, flags=re.MULTILINE
)
flow_text = re.sub(r"^END FUNCTION\s*$", "", flow_text, flags=re.MULTILINE)
# ファイル名に使えない文字を置換して安全な名前にする
safe = re.sub(r'[\\/:*?"<>|]', '_', flow_name)
filename = f"{safe}.txt"
path = os.path.join(SAVE_PATH, filename)
# タイムスタンプを付与する条件(ユーザ指定か、同名ファイルが既に存在する場合)
if timestamp_flag or os.path.exists(path):
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"{safe}_{ts}.txt"
path = os.path.join(SAVE_PATH, filename)
# ファイルへ書き出し(UTF-8)
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(flow_text)
logging.info(f"Exported: {path}")
return path
def export_subflow(flow_name: str, prev_hash: str, timestamp_flag: bool) -> str:
"""
1 つのサブフローをエクスポートする一連の処理。
サブフロー用ボタンをクリックし(click_target_image)、フロー内容を取得して
ハッシュチェック、保存まで行う。新しいハッシュを返す。
"""
# 小さな待機を入れて GUI の遷移が安定するのを待つ
time.sleep(0.5)
click_target_image(TARGET_IMAGE)
time.sleep(INTER_FLOW_DELAY)
text = get_flow_text(flow_name)
new_hash = validate_hash_difference(text, prev_hash, flow_name)
save_flow_text(text, flow_name, timestamp_flag)
return new_hash
def load_subflow_list() -> str:
"""
スクリプトフォルダ内の subflow_list.txt を読み込み、
デフォルトで GUI のテキストエリアに表示するために使用する。
読み込み失敗時は空文字を返す。
"""
try:
if os.path.exists(SUBFLOW_LIST_FILE):
return open(SUBFLOW_LIST_FILE, "r", encoding="utf-8").read()
except Exception:
logging.exception("エラー: サブフローリスト読み込みに失敗しました")
return ""
def save_subflow_list(content: str) -> None:
"""
ユーザが入力したサブフロー名リストを subflow_list.txt に保存する。
次回起動時に復元される。
"""
try:
with open(SUBFLOW_LIST_FILE, "w", encoding="utf-8") as f:
f.write(content)
except Exception:
logging.exception("エラー: サブフローリストに書き込み失敗しました")
# GUI クラス定義
class PADExporterGUI:
"""
Tkinter を使った GUI。ユーザはサブフロー名のリストを入力し、保存先やオプションを指定して
エクスポートを実行できる。エクスポートは別スレッドで実行され、GUI はスレッドセーフな
更新メソッド(root.after を使ったもの)を提供する。
"""
def __init__(self, root: tk.Tk):
self.root = root
self.root.title("PAD Flow Exporter")
self.root.minsize(600, 550)
self.root.rowconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
# 保存先フォルダと状態
self.save_folder = None
self.manual_folder_selected = False
self.cancel_requested = False # ユーザがキャンセル要求をしたかどうか
# メニューバー(パラメータ設定を開ける)
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
menu_main = tk.Menu(menubar, tearoff=0)
menu_main.add_command(label="パラメータ", command=self.open_param_window)
menubar.add_cascade(label="設定", menu=menu_main)
# メインフレームのレイアウト
frame = tk.Frame(root)
frame.grid(sticky="nsew", padx=10, pady=10)
frame.rowconfigure(9, weight=0)
frame.rowconfigure(10, weight=1)
frame.columnconfigure(0, weight=1)
frame.columnconfigure(1, weight=1)
# ヘッダとバージョン表示
tk.Label(frame, text="エクスポートするフローを入力してください").grid(row=0, column=0, sticky="w")
self.version_label = tk.Label(frame, text="Ver 1.20", fg="gray")
self.version_label.grid(row=0, column=1, sticky="e")
# サブフロー名入力欄(複数行)
self.flow_input = scrolledtext.ScrolledText(frame, height=6)
self.flow_input.grid(row=1, column=0, columnspan=2, sticky="nsew")
self.flow_input.insert(tk.END, load_subflow_list()) # 起動時に以前保存したリストを読み込む
# ボタン群(エクスポート開始 / キャンセル)
button_frame = tk.Frame(frame)
button_frame.grid(row=2, column=0, columnspan=2, sticky="w", pady=5)
self.export_button = tk.Button(
button_frame, text="エクスポートを実行", width=20, command=self.start_export
)
self.export_button.pack(side="left", padx=(0, 10))
self.cancel_button = tk.Button(
button_frame, text="エクスポートをキャンセル", width=20, command=self.request_cancel
)
self.cancel_button.pack(side="left")
self.cancel_button.config(state="disabled")
# 保存先選択ボタン
self.folder_button = tk.Button(
frame, text="保存先フォルダを選択", width=20, command=self.select_folder
)
self.folder_button.grid(row=3, column=0, sticky="w", pady=(5, 0))
# 保存先表示ラベル
self.save_path_label = tk.Label(
frame,
text="保存先: 未選択(プログラム実行フォルダ内のサブフォルダに保存されます)",
anchor="w"
)
self.save_path_label.grid(row=4, column=0, columnspan=2, sticky="w", pady=(5, 0))
# オプションチェックボックス
self.timestamp_var = tk.BooleanVar(value=True)
self.timestamp_checkbox = tk.Checkbutton(
frame,
text="エクスポートファイル名に作成日時を追加する(例: フロー名_年月日_時分秒.txt)",
variable=self.timestamp_var
)
self.timestamp_checkbox.grid(row=5, column=0, columnspan=2, sticky="w")
self.log_enabled_var = tk.BooleanVar(value=False)
self.log_checkbox = tk.Checkbutton(
frame,
text="エクスポート処理のログを export_log.txt に記録する",
variable=self.log_enabled_var
)
self.log_checkbox.grid(row=6, column=0, columnspan=2, sticky="w")
self.include_function_var = tk.BooleanVar(value=False)
self.include_function_checkbox = tk.Checkbutton(
frame,
text="エクスポートファイルに関数定義の開始(FUNCTION)と終了(END FUNCTION)を含める",
variable=self.include_function_var
)
self.include_function_checkbox.grid(row=7, column=0, columnspan=2, sticky="w")
# メッセージ表示エリア(ログのように扱う)
tk.Label(frame, text="メッセージ").grid(row=8, column=0, columnspan=2, sticky="w", pady=(2, 0))
self.message_box = scrolledtext.ScrolledText(frame, height=12, state="disabled")
self.message_box.grid(row=9, column=0, columnspan=2, sticky="nsew")
# 進捗バー
progress_frame = tk.Frame(frame)
progress_frame.grid(row=11, column=0, columnspan=2, pady=(5, 0), sticky="ew")
progress_frame.grid_columnconfigure(0, weight=1)
self.progress_bar = ttk.Progressbar(progress_frame, orient="horizontal", mode="determinate")
self.progress_bar.grid(row=0, column=0, columnspan=2, sticky="ew")
self.progress_bar["maximum"] = 100
# フォルダを開くボタン、終了ボタン
self.folder_open_button = tk.Button(
frame, text="保存先フォルダを開く", width=20, command=self.open_save_folder
)
self.folder_open_button.grid(row=12, column=0, sticky="w", pady=5)
self.exit_button = tk.Button(frame, text="終了", width=20, command=self.root.quit)
self.exit_button.grid(row=12, column=1, sticky="e", pady=5)
def set_widgets_state(self, st: str):
# 一括でウィジェットの有効/無効を切り替えるヘルパー
for w in (
self.export_button,
self.folder_button,
self.timestamp_checkbox,
self.log_checkbox,
self.include_function_checkbox,
self.folder_open_button
):
w.config(state=st)
def log_gui_message(self, msg: str):
# GUI のメッセージエリアに追記する。呼び出し側が main スレッドかどうかに依存しない。
self.message_box.configure(state="normal")
self.message_box.insert(tk.END, msg + "\n")
self.message_box.configure(state="disabled")
self.message_box.see(tk.END)
def thread_safe_log(self, msg: str):
# 別スレッドからでも安全にログを表示するために root.after を使う
self.root.after(0, lambda m=msg: self.log_gui_message(m))
def thread_safe_progress(self, value: int):
# 別スレッドから進捗バーを更新するためのラッパー
self.root.after(0, lambda v=value: self.progress_bar.configure(value=v))
def select_folder(self):
# 保存先フォルダをユーザに選ばせるダイアログ
f = filedialog.askdirectory(title="保存先フォルダを選択")
if f:
self.save_folder = f
self.manual_folder_selected = True
self.save_path_label.config(text=f"保存先: {f}")
def open_save_folder(self):
# 保存先フォルダをエクスプローラで開く
folder = (self.save_folder if self.manual_folder_selected else SAVE_PATH or SCRIPT_PATH)
if folder and os.path.exists(folder):
subprocess.Popen(['explorer', os.path.normpath(folder)], creationflags=CREATE_NO_WINDOW)
else:
self.log_gui_message("エラー: 保存先が見つかりません")
def request_cancel(self):
# 実行中の処理に対してキャンセル要求を立てる(スレッド内でこれをチェックする)
self.cancel_requested = True
self.log_gui_message("キャンセルを受け付けました")
def start_export(self):
"""
エクスポート開始ボタンが押されたときの処理。
入力チェックを行い、確認ダイアログののち 10 秒後に別スレッドで実行を開始する。
"""
# メッセージ領域をクリア
self.message_box.configure(state="normal")
self.message_box.delete("1.0", tk.END)
self.message_box.configure(state="disabled")
# 実行前の確認(ユーザに PAD の画面が表示されているか確認)
if not messagebox.askyesno(
"確認",
"エクスポート対象のフローデザイナー画面と、左上の「サブフロー」ボタンは表示されていますか?"
):
return
# サブフロー名リストを取得し、空行は無視
names = [
ln.strip()
for ln in self.flow_input.get("1.0", tk.END).splitlines()
if ln.strip()
]
if not names:
self.log_gui_message("エラー: フロー名が入力されていません")
return
# 大文字小文字を区別しない重複チェック
norm = [n.lower() for n in names]
dup = [n for n in set(norm) if norm.count(n) > 1]
if dup:
self.log_gui_message(f"エラー: フロー名が重複しています: {', '.join(dup)}")
return
# 関数を含めるかのフラグを更新
global INCLUDE_FUNCTION
INCLUDE_FUNCTION = self.include_function_var.get()
# 実行準備状態にする(GUI を無効化し、キャンセルボタンを有効化)
self.cancel_requested = False
self.set_widgets_state("disabled")
self.cancel_button.config(state="normal")
self.log_gui_message(
"10秒後に処理が開始されます。正しく動作させるため、キーボードやマウスの操作は控えてください\n"
)
# 10秒後に別スレッドで実行(ユーザがウィンドウをアクティブにして準備できるよう猶予)
self.root.after(
10000,
lambda: threading.Thread(
target=self.execute_export, args=(names,), daemon=True
).start()
)
def execute_export(self, names):
"""
実際のエクスポート処理(別スレッドで実行される)。
1 件ずつ export_subflow を呼び、進捗更新・ログ出力・タイムアウト・キャンセル処理を行う。
"""
try:
global TARGET_IMAGE, SAVE_PATH
# 設定をグローバルへ適用(GUI 上で変更された可能性があるため)
apply_config_globals()
# 画像を DPI に合わせて準備し、保存先とログの初期化
TARGET_IMAGE = prepare_target_image(ORIGINAL_IMAGE)
SAVE_PATH, _ = initialize_save_path_and_logging(
self.save_folder if self.manual_folder_selected else None,
self.log_enabled_var.get()
)
# ユーザが手動で保存先を選択していたら GUI に反映
if self.manual_folder_selected:
self.root.after(
0, lambda: self.save_path_label.config(text=f"保存先: {SAVE_PATH}")
)
total = len(names)
prev = ""
ts_flag = self.timestamp_var.get()
self.thread_safe_progress(0)
start_time = time.time()
for idx, nm in enumerate(names, start=1):
# キャンセル確認
if self.cancel_requested:
self.thread_safe_log("\nエクスポートがキャンセルされました")
return
# 全体タイムアウトチェック
if time.time() - start_time > EXPORT_TIMEOUT:
raise TimeoutError("全体の処理がタイムアウトしました")
# 個別フローのエクスポート(ボタンのクリック → テキスト取得 → 保存)
prev = export_subflow(nm, prev, ts_flag)
self.thread_safe_log(f"{nm} をエクスポート完了")
self.thread_safe_progress(int(idx / total * 100))
else:
# for ループを正常に完了した場合の処理
self.thread_safe_log("\nエクスポートは正常に終了しました")
self.thread_safe_progress(100)
# ウィンドウを一瞬最前面にしてユーザーの注意を喚起
self.root.after(0, lambda: self.root.attributes("-topmost", True))
self.root.after(0, lambda: self.root.update())
self.root.after(0, lambda: self.root.attributes("-topmost", False))
# 実行したサブフロー一覧を保存して次回に復元できるようにする
save_subflow_list("\n".join(names))
except TimeoutError as te:
logging.exception(f"タイムアウト: {te}")
self.thread_safe_log(f"タイムアウト: {te}")
return
except Exception as e:
# 例外発生時はログと GUI メッセージに出す
logging.exception(f"エラー: {e}")
if str(e):
self.thread_safe_log(f"エラー: {e}")
else:
# 例外メッセージが空の場合は想定される原因をメッセージ化
self.thread_safe_log(
"エラー: フローデザイナー画面にて、左上に表示される「サブフロー」ボタンが隠れている、"
"または表示されていない可能性があります"
)
return
finally:
# 終了時に GUI の状態を戻す
self.root.after(0, lambda: self.set_widgets_state("normal"))
self.root.after(0, lambda: self.cancel_button.config(state="disabled"))
self.cancel_requested = False
def open_param_window(self):
"""
パラメータ設定ウィンドウを開く。
_config の現在値を表示し、ユーザが編集して OK を押すと保存して反映する。
"""
win = tk.Toplevel(self.root)
win.title("パラメータ設定")
win.resizable(False, False)
win.grab_set() # モーダルにする
# 設定フィールドの定義(ラベル, キー, 型変換関数)
fields = [
("ウィンドウ有効化後の待機(秒)", "ACTIVATE_WAIT", float),
("画像検索間隔(秒)", "IMAGE_SEARCH_INTERVAL", float),
("画像検索の信頼度(0-1)", "IMAGE_CONFIDENCE", float),
("画像検索タイムアウト(秒)", "IMAGE_SEARCH_TIMEOUT", int),
("クリックリトライ回数", "CLICK_RETRIES", int),
("クリック間隔(秒)", "CLICK_INTERVAL", float),
("フロー間の追加待機(秒)", "INTER_FLOW_DELAY", float),
("ホットキー間隔(秒)", "HOTKEY_INTERVAL", float),
("ホットキー直後の待機(秒)", "POST_HOTKEY_SLEEP", float),
("クリップボード反映ポーリング間隔(秒)", "CLIPBOARD_POLL_INTERVAL", float),
("クリップボードリトライ回数", "CLIPBOARD_RETRIES", int),
("クリップボードリトライ間隔(秒)", "CLIPBOARD_RETRY_INTERVAL", float),
("一時画像を保持する(True/False)", "KEEP_TEMP_IMAGES", lambda v: v.lower() in ("1","true","yes")),
("全体処理タイムアウト(秒)", "EXPORT_TIMEOUT", int),
]
entries = {}
frm = ttk.Frame(win, padding=10)
frm.grid(row=0, column=0, sticky="nsew")
# 各フィールドの UI を作成し、現在値を埋める
for i, (label, key, caster) in enumerate(fields):
ttk.Label(frm, text=label).grid(row=i, column=0, sticky="w", padx=(0, 8), pady=4)
ent = ttk.Entry(frm, width=25)
val = _config.get(key, _default_config.get(key, ""))
ent.insert(0, str(val))
ent.grid(row=i, column=1, sticky="w", pady=4)
entries[key] = (ent, caster)
btns = ttk.Frame(frm)
btns.grid(row=len(fields), column=0, columnspan=2, pady=(10, 0), sticky="e")
def on_ok():
# OK ボタンが押されたら入力を検証して _config を更新、保存して適用
for label, key, caster in fields:
ent, caster_fn = entries[key]
val = ent.get().strip()
try:
_config[key] = caster_fn(val)
except Exception:
messagebox.showerror("入力エラー", f"{label} の値が不正です: {val}")
return
save_config()
apply_config_globals()
win.destroy()
def on_reset():
# デフォルトにリセット(UI にデフォルト値を再表示)
for _, key, _t in fields:
ent, _c = entries[key]
ent.delete(0, tk.END)
ent.insert(0, str(_default_config.get(key, "")))
# ボタン(デフォルト / OK / キャンセル)
ttk.Button(btns, text="デフォルト", command=on_reset).pack(side="left", padx=5)
ttk.Button(btns, text="OK", command=on_ok).pack(side="left", padx=5)
ttk.Button(btns, text="キャンセル", command=win.destroy).pack(side="left", padx=5)
# エントリポイント
if __name__ == "__main__":
root = tk.Tk()
app = PADExporterGUI(root)
root.mainloop()
8-2. ボタンイメージ配置
エクスポート作業では、各フローのタブを選択する必要があります。本プログラムは検索機能を使って各タブを選択します。
まず、①「サブフローを検索する」機能を表示するには、②「サブフロー」ボタンのクリックが必要です。 ただし、現時点(2025年8月)では②「サブフロー」ボタンに割り当てられたショートカットキーが存在しない?ため、Pythonの画像認識を用いてボタンを検出します。
「サブフロー」ボタンを検出するには、事前にその画像を準備し、実行時にPythonプログラムで認識できるようにしておく必要があります。
画像は自身でキャプチャするか、以下のサンプル画像ボタンを利用してください。
<ボタンサンプル>
search_icon.png ダウンロード
ボタン画像のダウンロード手順(Google Chromeの場合)
- 上記リンクをクリックして画像を表示します
- 画像上で右クリック → 「名前を付けて画像を保存」を選択
- 保存先は
PADFlowExporter.py
と同じフォルダにしてください - ファイル名は
search_icon.png
に変更して保存します
8-3. (任意)保存するフローリストファイルの作成
subflow_list.txt
は、エクスポート実行時に自動で作成されます。そのため、事前にファイルを用意・配置する必要はありません(任意です)。
保存したいPower Automate Desktopのフローを記載するファイルを用意します。
-
PADFlowExporter.py
と同じフォルダに、新規ファイルsubflow_list.txt
を作成します - このファイルには、 保存したい Power Automate Desktop のフロー名を1行ずつ記載 してください
サブフローが無い場合は、Mainのみ記載してください
9. プログラム実行
9-1-1. Python版(.pyファイル実行)
PADFlowExporter.py
と 同じフォルダ に、以下のファイルが揃っていることを確認してください:
- PADFlowExporter.py
- search_icon.png
- subflow_list.txt (任意:自動的に作成されます)
コマンドプロンプトから起動する場合は、以下のコマンドを入力します。
C:\Users\ユーザ名\Documents\python>py PADFlowExporter.py
または、エクスプローラー上でファイルを ダブルクリック して実行することもできます。
9-1-2. Windows実行ファイル版(.exeファイル実行)
ダウンロードした実行ファイルを解凍し、PADFlowExporter.exe
を実行してください。
環境によっては「Microsoft Defender SmartScreen」の警告画面が表示される場合がありますが、これは一般的なセキュリティ機能によるものです。内容をご確認のうえ、問題がないと判断できる場合は「実行」ボタンを押して先に進んでください。
9-2. エクスポートするフローの表示
エクスポートしたいフローデザイナーを開きます。画面上に「サブフロー」ボタン(赤枠で囲まれた部分)が表示されるように、ウィンドウの位置やサイズを調整してください。 ウィンドウを最大化する必要はありませんが、 サブフローのボタンが確実に見える状態 であることが重要です。
10. プログラム実行(エクスポート実行)
エクスポートするフロー名を「テキストボックス」に入力してください。
「保存先フォルダを選択」ボタンを押し、エクスポートファイルの保存先を選択します。未選択の場合は、プログラム実行フォルダ内のサブフォルダに保存されます。
フローデザイナーのサブフローボタンが見える状態 で「エクスポートを実行」ボタンを押してください。
確認画面が表示されます。各種確認後、「はい」ボタンを押してください。
実行結果はメッセージに表示されます。完了後「保存先フォルダを開く」ボタン押します。
保存ファイルはテキストファイルとして出力されます。ファイル名はデフォルトで「フロー名_年月日_時分秒.txt」ですが、オプションで変更も可能です。
11. エクスポートファイルを戻すには(インポート)
本プログラムには インポート機能は搭載されていません が、手動で復元する方法をご紹介します。あらかじめ、エクスポートされたファイルをご用意ください。
ファイル名は「フロー名+年月日_時分秒」という構成になります。(設定によっては、ファイル名が「フロー名」のみの場合もあります)
Power Automate Desktop を起動し、「新しいフローの作成」をクリックして新しいフローを作成します。フロー名は任意ですが、後から識別しやすいような名前にしておくと便利です。
エクスポートしたファイルを全選択し、コピーを行います。下記画像は、mainをコピーしているイメージです。
mainフロー画面の上で「 右クリック→貼り付け 」でインポートします。
各アクションが表示されれば、コピーは成功です。ただしこの段階では、サブフローの内容はまだコピーされていないため、フロー実行時にエラーが発生する可能性があります。
サブフローのインポートも同様の手順で行います。サブフロー名は、 エクスポート時のファイル名と一致させる 必要があります。その後は同様に、コピーと貼り付けの操作を繰り返します。
12. オプション機能解説
12-1. エクスポートファイルに関数定義の開始(FUNCTION)と終了(END FUNCTION)を含める
チェックを入れると、エクスポートされたファイルの先頭に「FUNCTION」、末尾に「END FUNCTION」が付きます。 Power Automate Desktop のバックエンドでは、マクロ記述用の内部スクリプト言語として Robin が使われています。Robin は手続き型の構文を持ち、「FUNCTION 〜 END FUNCTION」は関数定義の構文ブロックを表します。この設定を有効にすると、関数定義部分も含めてすべてをバックアップします。現在のPower Automate Desktopでは使用する機会は多くないかもしれません。なお、 この方法でバックアップしたファイルは、通常とはインポート手順が異なるため注意してください。 デフォルトでは、無効にしてあります。
下記、左側がオプション無効。右側がオプション有効の場合。それぞれのエクスポートファイルの中身を比較しました。
12-2. 動作を調整できるパラメータ設定
使用しているPCの環境や構成に応じて動作を調整するためのパラメータです。
通常は変更する必要はありません。[メニュー]→[設定]→[パラメータ]から設定できます。
・ウィンドウ有効化後の待機(ACTIVATE_WAIT)
目的: 対象ウィンドウをアクティブ化して描画とフォーカスが安定するのを待つ
効果: 短すぎるとクリックやキー送信が無効になる。長すぎると全体が遅くなる
デフォルト: 0.3(秒)
推奨値: 0.1 から 1.0 秒。リモート環境や重いアプリでは長めに
・画像検索間隔(IMAGE_SEARCH_INTERVAL)
目的: 画面上で目標画像を繰り返し探す間隔を決める
効果: 短いと検出が速いが CPU 負荷と誤検出が増える。長いと待ちが増えるが安定する
デフォルト: 1.0(秒)
推奨値: 0.5 から 2.0 秒。検出失敗が多ければ間隔を長めに
・画像検索の信頼度(IMAGE_CONFIDENCE)
目的: 画像マッチングで許容する類似度の閾値を設定する
効果: 値を上げると誤検出が減り下げると微妙な差異を許容する
デフォルト: 0.8(0~1)
推奨値: 0.7 から 0.95。高DPIや微妙なスケール差がある場合は下げる
・画像検索タイムアウト(IMAGE_SEARCH_TIMEOUT)
目的: 画像が見つからない場合に検索を打ち切るまでの最大時間を設定する
効果: 長くすると待ち続けて遅延が発生する。短すぎると一時的な遅延で失敗になる
デフォルト: 60(秒)
推奨値: 30 から 120 秒。ネットワークやアプリの応答が遅い環境では長めに
・クリックリトライ回数(CLICK_RETRIES)
目的: クリック後に期待する反応が無いときに再試行する回数を指定する
効果: 再試行で一時的なUI不具合を回避できるが多すぎると冗長
デフォルト: 5(回)
推奨値: 1 から 5 回。安定しないUIでは増やす
・クリック間隔(CLICK_INTERVAL)
目的: 連続クリックやリトライ間の待ち時間を設定する
効果: 短すぎるとUIが処理しきれず失敗する。長すぎると遅延
デフォルト: 1.0(秒)
推奨値: 0.2 から 1.5 秒。アプリの応答速度に合わせる
・フロー間の追加待機(INTER_FLOW_DELAY)
目的: サブフロー切替や画面遷移後に余裕を持たせる待ち時間を設定する
効果: 次操作の画面更新やデータロードが完了するのを確保する
デフォルト: 0.5(秒)
推奨値: 0.2 から 2.0 秒。複雑な遷移は長めに
・ホットキー間隔(HOTKEY_INTERVAL)
目的: 複数キーを順に送信する際のキー間ウェイトを決める
効果: 短すぎるとキー組み合わせが誤認される。長すぎると遅くなる
デフォルト: 0.1(秒)
推奨値: 0.02 から 0.1 秒。通常はデフォルトで十分
・ホットキー直後の待機(POST_HOTKEY_SLEEP)
目的: ホットキー送信直後にアプリ側で処理が反映されるのを待つ短時間を設定する
効果: クリップボードや選択状態の反映を安定させる
デフォルト: 0.2(秒)
推奨値: 0.1 から 0.5 秒。コピーや貼り付けの反映が遅い環境では増やす
・クリップボード反映ポーリング間隔(CLIPBOARD_POLL_INTERVAL)
目的: クリップボードの内容が更新されたかを確認する頻度を決める
効果: 短いと検出が速いがCPU負荷が上がる。長いと遅延が増える
デフォルト: 0.1(秒)
推奨値: 0.05 から 0.2 秒。安定性と速度のバランスで調整
・クリップボードリトライ回数(CLIPBOARD_POLL_INTERVAL)
目的: コピー結果が空のときに再取得を試みる回数を指定する
効果: アプリやOSのタイミングずれを吸収できる。多すぎると無駄な待機が増える
デフォルト: 5(回)
推奨値: 3 から 10 回。コピーに失敗しやすければ増やす
・クリップボードリトライ間隔(CLIPBOARD_RETRY_INTERVAL)
目的: クリップボード再試行の間隔を設定する
効果: 短くすると素早く復旧を確認できるが過度にCPUを使う。長くすると対応が遅い
デフォルト: 0.5(秒)
推奨値: 0.1 から 1.0 秒。環境に応じて調整
・一時画像を保持する(KEEP_TEMP_IMAGES)
目的: DPI補正後などに生成する一時画像ファイルを処理後に削除するか保持するかを決める
効果: True にするとデバッグが容易だがディスクを消費する。False にすると自動でクリーンアップされる
デフォルト: False(True/False)
推奨値: 本番は False デバッグ時は True
・全体処理タイムアウト(EXPORT_TIMEOUT)
目的: 全フローの開始から終了までの許容最大時間を設定する
効果: これを超えると強制終了して異常を報告する。長くするとエラー検出が遅れる
デフォルト: 600(秒)
推奨値: 処理件数と平均処理時間の合計に余裕を見て設定する。例えば 10 分から 30 分
13. エラー処理
13-1. フロー名が間違っていた場合
フローデザイナーの「サブフローを検索する」機能を使ってフローを抽出していますが、この検索は完全一致ではなく部分一致で動作します。そのため、検索条件によっては意図しないフロー名が一致してしまい、誤ってエクスポートされるケースも考えられます。
<例>フローデザイナーでの名称: 01-InetServiceCheck_Main
テキストボックスのフロー名 | エラー内容 | エクスポート結果 | エラー検出方法 |
---|---|---|---|
01-InetServiceCheck_Main | エラーなし | 成功 | ー |
01-InetServiceCheck_Maina | 1文字多い | 実行時にエラー | プログラムに具備 |
01-InetServiceCheck_Mai | 1文字少ない | 成功 | 未実施 |
本プログラムでは最低限のエラー処理を実装していますが、すべての予期せぬケースに対応できるわけではありません。万が一、想定外のエラーが発生した際はご容赦いただけますと幸いです。
14. さいごに
私はPython初心者で、普段はPower Automate Desktopを使って自動化プログラムを作成しています。サブフローの数が多くなると、テキストへのエクスポート作業も増えてしまい、かなりの時間がかかっていました。「もっと効率よくできないか?」と思い立ち、今回のプログラムを作ることにしました。
改良やカスタマイズはご自由にどうぞ。ご自身の環境に合わせて調整してみてください。