0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Power Automate Desktopのサブフローを一括バックアップ(エクスポート)する方法

Last updated at Posted at 2025-09-06

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のフローを一括エクスポートする、プログラムの動作イメージを紹介します。

PADFlowExporter_Animation.gif

4. 動作イメージ(概要)

エクスポート対象のフローデザイナーを開きます。
qiita-PADFlowExporter-01.png

フローデザイナーの上でコマンドプロンプトを開き、Pythonプログラムを実行します。 (Pythonプログラムをダブルクリックしても動作します)
qiita-PADFlowExporter-02.png

また、Pythonがインストールされていない環境でも動作するよう、PyInstallerを使用して作成したWindows実行ファイル(.exe)も公開しています。
qiita-PADFlowExporter-03.png

PythonプログラムからPower Automate Desktopを自動操作し、サブフローの検索および「Ctrl+A → Ctrl+C → ファイル出力」の処理を繰り返し実行します。
qiita-PADFlowExporter-05.png

エクスポートされたファイルは、実行したPythonプログラムのサブフォルダ、または指定した保存先フォルダに格納されます。
qiita-PADFlowExporter-06.png

この方法を活用することで、大量のサブフローの内容を一括で取得できます。手動で「ファイル作成 → フロータブ選択 → 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 」です。
実行後の操作方法は、 プログラム実行 の項目をご覧ください。

qiita-PADFlowExporter-07.png

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 ユーザー名に置き換えてください。

PADFlowExporter.py
#
# 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. ボタンイメージ配置

エクスポート作業では、各フローのタブを選択する必要があります。本プログラムは検索機能を使って各タブを選択します。

qiita-PADFlowExporter-08.png

まず、①「サブフローを検索する」機能を表示するには、②「サブフロー」ボタンのクリックが必要です。 ただし、現時点(2025年8月)では②「サブフロー」ボタンに割り当てられたショートカットキーが存在しない?ため、Pythonの画像認識を用いてボタンを検出します。

「サブフロー」ボタンを検出するには、事前にその画像を準備し、実行時にPythonプログラムで認識できるようにしておく必要があります。

画像は自身でキャプチャするか、以下のサンプル画像ボタンを利用してください。

<ボタンサンプル>
search_icon.png
search_icon.png ダウンロード

ボタン画像のダウンロード手順(Google Chromeの場合)

  1. 上記リンクをクリックして画像を表示します
  2. 画像上で右クリック → 「名前を付けて画像を保存」を選択
  3. 保存先は PADFlowExporter.py と同じフォルダにしてください
  4. ファイル名は search_icon.png に変更して保存します

8-3. (任意)保存するフローリストファイルの作成

subflow_list.txt は、エクスポート実行時に自動で作成されます。そのため、事前にファイルを用意・配置する必要はありません(任意です)。

保存したいPower Automate Desktopのフローを記載するファイルを用意します。

  1. PADFlowExporter.py と同じフォルダに、新規ファイル subflow_list.txt を作成します
  2. このファイルには、 保存したい Power Automate Desktop のフロー名を1行ずつ記載 してください

サブフローが無い場合は、Mainのみ記載してください

subflow_list.txt の記載例
qiita-PADFlowExporter-09.png

赤枠の部分のフロー名を記載します
qiita-PADFlowExporter-10.png

9. プログラム実行

9-1-1. Python版(.pyファイル実行)

PADFlowExporter.py同じフォルダ に、以下のファイルが揃っていることを確認してください:

  • PADFlowExporter.py
  • search_icon.png
  • subflow_list.txt (任意:自動的に作成されます)

qiita-PADFlowExporter-11.png

コマンドプロンプトから起動する場合は、以下のコマンドを入力します。

C:\Users\ユーザ名\Documents\python>py PADFlowExporter.py

または、エクスプローラー上でファイルを ダブルクリック して実行することもできます。

9-1-2. Windows実行ファイル版(.exeファイル実行)

ダウンロードした実行ファイルを解凍し、PADFlowExporter.exe を実行してください。

qiita-PADFlowExporter-14.png

環境によっては「Microsoft Defender SmartScreen」の警告画面が表示される場合がありますが、これは一般的なセキュリティ機能によるものです。内容をご確認のうえ、問題がないと判断できる場合は「実行」ボタンを押して先に進んでください。
qiita-PADFlowExporter-22.png

9-2. エクスポートするフローの表示

エクスポートしたいフローデザイナーを開きます。画面上に「サブフロー」ボタン(赤枠で囲まれた部分)が表示されるように、ウィンドウの位置やサイズを調整してください。 ウィンドウを最大化する必要はありませんが、 サブフローのボタンが確実に見える状態 であることが重要です。

qiita-PADFlowExporter-13.png

10. プログラム実行(エクスポート実行)

エクスポートするフロー名を「テキストボックス」に入力してください。
qiita-PADFlowExporter-15.png

「保存先フォルダを選択」ボタンを押し、エクスポートファイルの保存先を選択します。未選択の場合は、プログラム実行フォルダ内のサブフォルダに保存されます。
qiita-PADFlowExporter-16.png

必要であれば各オプションを選択します。
qiita-PADFlowExporter-17.png

フローデザイナーのサブフローボタンが見える状態 で「エクスポートを実行」ボタンを押してください。
qiita-PADFlowExporter-18.png

確認画面が表示されます。各種確認後、「はい」ボタンを押してください。
qiita-PADFlowExporter-19.png

実行結果はメッセージに表示されます。完了後「保存先フォルダを開く」ボタン押します。
qiita-PADFlowExporter-20.png

保存ファイルはテキストファイルとして出力されます。ファイル名はデフォルトで「フロー名_年月日_時分秒.txt」ですが、オプションで変更も可能です。
qiita-PADFlowExporter-21.png

11. エクスポートファイルを戻すには(インポート)

本プログラムには インポート機能は搭載されていません が、手動で復元する方法をご紹介します。あらかじめ、エクスポートされたファイルをご用意ください。
ファイル名は「フロー名+年月日_時分秒」という構成になります。(設定によっては、ファイル名が「フロー名」のみの場合もあります)
qiita-PADFlowExporter-23-2.png

Power Automate Desktop を起動し、「新しいフローの作成」をクリックして新しいフローを作成します。フロー名は任意ですが、後から識別しやすいような名前にしておくと便利です。
qiita-PADFlowExporter-24.png

エクスポートしたファイルを全選択し、コピーを行います。下記画像は、mainをコピーしているイメージです。
qiita-PADFlowExporter-25.png

mainフロー画面の上で「 右クリック→貼り付け 」でインポートします。
qiita-PADFlowExporter-26.png

各アクションが表示されれば、コピーは成功です。ただしこの段階では、サブフローの内容はまだコピーされていないため、フロー実行時にエラーが発生する可能性があります。
qiita-PADFlowExporter-27.png

サブフローのインポートも同様の手順で行います。サブフロー名は、 エクスポート時のファイル名と一致させる 必要があります。その後は同様に、コピーと貼り付けの操作を繰り返します。
qiita-PADFlowExporter-28.png

12. オプション機能解説

12-1. エクスポートファイルに関数定義の開始(FUNCTION)と終了(END FUNCTION)を含める

qiita-PADFlowExporter-29.png
チェックを入れると、エクスポートされたファイルの先頭に「FUNCTION」、末尾に「END FUNCTION」が付きます。 Power Automate Desktop のバックエンドでは、マクロ記述用の内部スクリプト言語として Robin が使われています。Robin は手続き型の構文を持ち、「FUNCTION 〜 END FUNCTION」は関数定義の構文ブロックを表します。この設定を有効にすると、関数定義部分も含めてすべてをバックアップします。現在のPower Automate Desktopでは使用する機会は多くないかもしれません。なお、 この方法でバックアップしたファイルは、通常とはインポート手順が異なるため注意してください。 デフォルトでは、無効にしてあります。

下記、左側がオプション無効。右側がオプション有効の場合。それぞれのエクスポートファイルの中身を比較しました。
qiita-PADFlowExporter-30.png

13. エラー処理

13-1. フロー名が間違っていた場合

フローデザイナーの「サブフローを検索する」機能を使ってフローを抽出していますが、この検索は完全一致ではなく部分一致で動作します。そのため、検索条件によっては意図しないフロー名が一致してしまい、誤ってエクスポートされるケースも考えられます。

<例>フローデザイナーでの名称: 01-InetServiceCheck_Main

テキストボックスのフロー名 エラー内容 エクスポート結果 エラー検出方法
01-InetServiceCheck_Main エラーなし 成功
01-InetServiceCheck_Maina 1文字多い 実行時にエラー プログラムに具備
01-InetServiceCheck_Mai 1文字少ない 成功 未実施

本プログラムでは最低限のエラー処理を実装していますが、すべての予期せぬケースに対応できるわけではありません。万が一、想定外のエラーが発生した際はご容赦いただけますと幸いです。

14. さいごに

私はPython初心者で、普段はPower Automate Desktopを使って自動化プログラムを作成しています。サブフローの数が多くなると、テキストへのエクスポート作業も増えてしまい、かなりの時間がかかっていました。「もっと効率よくできないか?」と思い立ち、今回のプログラムを作ることにしました。

改良やカスタマイズはご自由にどうぞ。ご自身の環境に合わせて調整してみてください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?