1. はじめに
この記事では、Power Automate Desktopのフローを自動取得し、保存する方法を紹介します。
Power Automate Desktop 無償版(無料版)には、作成したフローを一括でエクスポート(バックアップ)する機能がありません。対象のフローを全選択・コピーして、メモ帳等に貼り付けて保存する方法が一般的ですが、サブフローの数が増えると手間も大きくなります。
なんとか自動化できないか?と調べてみたところ、Power Automate Desktopだけでは実現が難しいことが分かりました。そこでPythonを活用して、フロー内容をテキスト形式でエクスポートする方法を考案しました。
とはいえ、Pythonは初心者レベルのため、生成AI(Copilot)の力を借りながら実装しています。自力だけでは書き切れませんでしたが、工夫しながら形にできたので、参考になれば嬉しいです。
また、Pythonがインストールされていない環境でも実行できます。
2. 更新履歴
- 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 → 保存」より、効率的かつ正確に作業ができます。
本プログラムは、下記の環境で動作確認しています。ただし、すべての環境に対する動作保証は行っておりません。また、Power Automate Desktopの仕様変更により、プログラムが正しく動作しない可能性もあります。ご使用の際は、ご自身の環境にて十分にご確認のうえご利用ください。
改良やカスタマイズはご自由にどうぞ。ご自身の環境に合わせて調整してみてください。
5. Windows実行ファイル版について(.exe)
Pythonがインストールされていない環境でも実行 できるよう、本ツールの Windows実行ファイル(.exe)を PyInstaller で作成しました。 以下のリンクからZIPファイルをダウンロードしてご利用ください。
PADFlowExporter.zip(Windows EXEファイル版)
GitHubからダウンロード(約67MB)
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.10)
#
import ctypes # C言語API呼び出し用モジュールを読み込み
import sys # プラットフォーム判定や標準出力操作に使用
# Windowsの場合、高DPI設定を有効化して文字やUIのにじみを防止
if sys.platform == "win32":
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2) # Windows 8.1以降のDPI認識設定
except Exception:
ctypes.windll.user32.SetProcessDPIAware() # レガシーOS用のDPI認識設定
import os # ファイル/ディレクトリ操作用
import re # 正規表現操作用
from ctypes import windll # Windows API呼び出し用にwindllをインポート
from PIL import Image # 画像読み書き・変換用ライブラリ
import time # 時間待ちや経過時間測定用
import datetime # タイムスタンプ生成用
import shutil # ファイルコピー用
import tempfile # 一時ファイル作成用
import threading # マルチスレッド実行用
import subprocess # 外部プロセス起動用(Explorer起動など)
import logging # ログ出力用
import hashlib # SHA256ハッシュ計算用
import atexit # プログラム終了時クリーンアップ登録用
import tkinter as tk # GUI構築用
from tkinter import filedialog, scrolledtext, ttk, messagebox # 各種Tkウィジェット
import pyautogui # 画面操作(クリック/キー入力)自動化用
import pyperclip # クリップボード読み書き用
# 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起動フラグ(非表示)
CLICK_RETRIES = 5 # クリックリトライ回数
CLICK_INTERVAL = 1.0 # クリック間隔(秒)
CLIPBOARD_RETRIES = 5 # クリップボードリトライ回数
CLIPBOARD_RETRY_INTERVAL = 0.5 # クリップボードリトライ間隔
EXPORT_TIMEOUT = 600 # 全体処理タイムアウト(秒)
INCLUDE_FUNCTION = False # FUNCTION~END FUNCTIONを残すか(GUIで切替)
SAVE_PATH = "" # エクスポート先フォルダ(実行時設定)
TARGET_IMAGE = None # DPI補正後の検索アイコンパス
# デフォルトは重大なログのみ表示
logging.basicConfig(level=logging.CRITICAL)
# 一時画像ファイルのパスを保持し、終了時に削除
_temp_images = []
def _cleanup_temp_images():
"""プログラム終了時に一時画像を削除"""
for p in _temp_images:
try:
os.remove(p) # ファイル削除
except Exception:
pass # 削除失敗は無視
atexit.register(_cleanup_temp_images) # プログラム終了時クリーンアップ登録
# ロギング用ラッパー --------------------------------------------------
def log_message(msg: str, level: str = "info"):
"""汎用ログ出力(ファイル/コンソール共通)"""
getattr(logging, level)(msg)
# 画像準備 ------------------------------------------------------------
def prepare_target_image(image_path: str) -> str:
"""
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:
shutil.copyfile(image_path, temp_image) # DPI変更不要ならコピー
return temp_image
# 保存先初期化&ログ設定 ----------------------------------------------
def initialize_save_path_and_logging(custom_path=None, enable_log=True):
"""
保存先フォルダとログ(export_log.txt)を初期化
custom_path: ユーザー指定フォルダ
enable_log: ログ出力有無
"""
# 保存先を決定(指定なければtimestampフォルダ)
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) # FileHandlerを追加
else:
logger.setLevel(logging.CRITICAL)
return save_path, (log_file if enable_log else "")
# 画面上アイコンクリック ------------------------------------------------
def click_target_image(image_path: str):
"""
指定アイコンを画面上から探してクリック
見つからなければFileNotFoundError
"""
for _ in range(CLICK_RETRIES):
try:
win = pyautogui.getActiveWindow() # 現アクティブウィンドウ取得
if win:
win.activate() # アクティブ化
time.sleep(0.3)
except Exception:
pass
pos = pyautogui.locateCenterOnScreen(image_path, confidence=0.8)
if pos:
pyautogui.click(pos) # 発見時にクリック
return True
time.sleep(CLICK_INTERVAL)
raise FileNotFoundError("サブフローボタンのアイコンが見つかりません")
# フローテキスト取得 ----------------------------------------------------
def get_flow_text(flow_name: str) -> str:
"""
フロー名を入力→Enter→クリップボード読み取りでテキストを取得
"""
pyperclip.copy(flow_name) # クリップボードにコピー
pyautogui.hotkey('ctrl', 'v') # 貼り付け
start = time.time()
while time.time() - start < 5: # 貼り付け完了待ち
if pyperclip.paste() == flow_name:
break
time.sleep(0.1)
pyautogui.press('enter') # Enterキーで実行
for i in range(CLIPBOARD_RETRIES): # テキスト取得リトライ
pyautogui.hotkey('ctrl', 'a'); time.sleep(0.2)
pyautogui.hotkey('ctrl', 'c'); time.sleep(0.2)
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:
"""
SHA256で前回と同一テキストか判定し、同一ならエラー
"""
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:
"""
テキストを安全なファイル名で保存し、フルパスを返却
"""
global SAVE_PATH
if not INCLUDE_FUNCTION: # 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)
with open(path, "w", encoding="utf-8", newline="") as f:
f.write(flow_text) # ファイル書き込み
logging.info(f"Exported: {path}")
return path
# サブフロー1件エクスポートまとめ ----------------------------------------
def export_subflow(flow_name: str, prev_hash: str, timestamp_flag: bool) -> str:
"""
- アイコンクリック
- テキスト取得
- ハッシュ検証
- ファイル保存
"""
time.sleep(0.5) # Click直後の余裕時間
click_target_image(TARGET_IMAGE) # サブフローボタンクリック
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:
"""前回実行時に入力したサブフロー名をファイルから読み込む"""
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:
"""実行後のサブフロー名リストをファイルに上書き保存"""
try:
with open(SUBFLOW_LIST_FILE, "w", encoding="utf-8") as f:
f.write(content)
except Exception:
logging.exception("エラー: サブフローリストに書き込み失敗しました")
# GUIクラス定義 ----------------------------------------------------------
class PADExporterGUI:
"""Tkinterベースの一括エクスポートGUI"""
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 # キャンセル要求フラグ
# メインフレーム生成・配置
frame = tk.Frame(root)
frame.grid(sticky="nsew", padx=10, pady=10)
frame.rowconfigure(8, weight=0)
frame.rowconfigure(9, 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.10", 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=10, state="disabled")
self.message_box.grid(row=9, column=0, columnspan=2, sticky="nsew")
progress_frame = tk.Frame(frame)
progress_frame.grid(row=10, 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=11, column=0, sticky="w", pady=5)
self.exit_button = tk.Button(frame, text="終了", width=20, command=self.root.quit)
self.exit_button.grid(row=11, column=1, sticky="e", pady=5)
# 以下、GUI動作サポート用の小メソッド群 -------------------------------
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上のメッセージ欄に追記"""
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):
"""別スレッドからGUIへの安全なログ送信"""
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):
"""エクスポート完了後に保存先をExplorerで開く"""
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):
"""「エクスポートを実行」ボタン押下時の前処理"""
# メッセージ欄をクリア
self.message_box.configure(state="normal")
self.message_box.delete("1.0", tk.END)
self.message_box.configure(state="disabled")
# 実行確認ダイアログ
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
# FUNCTION定義含有フラグ更新
global INCLUDE_FUNCTION
INCLUDE_FUNCTION = self.include_function_var.get()
# ボタン無効化&キャンセル有効化
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):
"""バックグラウンドスレッドでのサブフロー一括エクスポート処理"""
try:
global TARGET_IMAGE, SAVE_PATH
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()
) # 保存先&ロギング設定
if self.manual_folder_selected:
# GUI表示も更新
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:
# 全件正常終了時の演出
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:
logging.exception(f"エラー: {e}")
if str(e):
self.thread_safe_log(f"エラー: {e}")
else:
self.thread_safe_log(
"エラー: フローデザイナー画面にて、左上に表示される「サブフロー」ボタンが隠れている、"
"または表示されていない可能性があります"
)
return
finally:
# 終了時にウィジェット状態を戻す
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 # フラグリセット
# エントリポイント ------------------------------------------------------
if __name__ == "__main__":
root = tk.Tk() # Tkルート生成
app = PADExporterGUI(root) # GUIアプリ生成
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では使用する機会は多くないかもしれません。なお、 この方法でバックアップしたファイルは、通常とはインポート手順が異なるため注意してください。 デフォルトでは、無効にしてあります。
下記、左側がオプション無効。右側がオプション有効の場合。それぞれのエクスポートファイルの中身を比較しました。
13. エラー処理
13-1. フロー名が間違っていた場合
フローデザイナーの「サブフローを検索する」機能を使ってフローを抽出していますが、この検索は完全一致ではなく部分一致で動作します。そのため、検索条件によっては意図しないフロー名が一致してしまい、誤ってエクスポートされるケースも考えられます。
<例>フローデザイナーでの名称: 01-InetServiceCheck_Main
テキストボックスのフロー名 | エラー内容 | エクスポート結果 | エラー検出方法 |
---|---|---|---|
01-InetServiceCheck_Main | エラーなし | 成功 | ー |
01-InetServiceCheck_Maina | 1文字多い | 実行時にエラー | プログラムに具備 |
01-InetServiceCheck_Mai | 1文字少ない | 成功 | 未実施 |
本プログラムでは最低限のエラー処理を実装していますが、すべての予期せぬケースに対応できるわけではありません。万が一、想定外のエラーが発生した際はご容赦いただけますと幸いです。
14. さいごに
私はPython初心者で、普段はPower Automate Desktopを使って自動化プログラムを作成しています。サブフローの数が多くなると、テキストへのエクスポート作業も増えてしまい、かなりの時間がかかっていました。「もっと効率よくできないか?」と思い立ち、今回のプログラムを作ることにしました。
改良やカスタマイズはご自由にどうぞ。ご自身の環境に合わせて調整してみてください。