PR

CustomTkinterでPythonツールをGUIアプリ化する方法|filedialogでファイル選択・クラスで設計【第3回】

Python

Python業務自動化 Invoice Maker 第3回

スポンサーリンク

はじめに:CustomTkinterでPythonスクリプトをGUIアプリ化する

こんにちは、VBA・Python業務自動化ラボ(py-vbalab.com)のニンジンです🥕

この記事では、第1回・第2回で作った「Excel請求書を自動でPDF化するPythonスクリプト」を、CustomTkinterを使って、ターミナルなしでマウス操作できるデスクトップGUIアプリに作り替えます。filedialogでのファイル選択、CTkEntry.get() で開始行・終了行を受け取る方法、class を使ったアプリ設計まで、実際に動くコードで解説します。

これまでの連載で、Excelからのデータ読み取り・PDF化・複数データのループ処理という「裏側のロジック」は完成しました。ただ、現場で事務スタッフに「Pythonのコードを開いて for i in range(15, 50): に書き換えてください」とお願いするのは非現実的です。コードを誤って壊すリスクもあります。そこで最終回は、誰でも直感的に操作できる操作画面(GUI)を作り、社内の誰もが使えるツールに仕上げます。

この記事で分かること

  • PythonスクリプトをCustomTkinterでデスクトップGUIアプリに作り替える手順
  • filedialog.askopenfilename() でExcelファイルを選ばせる方法(パス直打ちのミス防止)
  • CTkEntry.get() で画面入力(開始行・終了行)をコードに渡す方法
  • class InvoiceApp(ctk.CTk) というクラスを使った本格的なアプリ設計
  • 完成後に立ちはだかる「他人のPCで動かない」問題と、その対処の方向性
完成したInvoice MakerのGUI画面。ファイル選択ボタンと読み込み設定の入力欄が並んでいる

▲ この記事で作るGUIアプリの完成イメージ

「コードを書く時間がない」「社内のPC全部にPythonを入れるのは無理」という方向けに、本記事で作るツールの完全版(環境構築不要のフルパッケージ版)【Invoice Maker (NINJIN EDITION)】をnoteで頒布しています。

👉 noteでチェックする
スポンサーリンク

CustomTkinterによるGUI開発の事前準備

CTk CustomTkinter · 基礎編 CustomTkinterとは?インストールから基本の使い方まで この記事はCustomTkinterでツールをGUI化する実践編です。pipでの導入、CTkLabel・CTkButton・CTkEntry・CTkFrameの配置、pack/grid配置の違いなど、基礎からじっくり知りたい方は、まずこちらの入門記事からどうぞ。 基礎編を読む

準備1:CustomTkinterのインストール

今回は、Python標準のGUIライブラリ「Tkinter」を、フラットデザイン・ダークモード対応に拡張した CustomTkinter を使います。コマンドプロンプト(またはターミナル)で以下を実行します。

pip install customtkinter
コマンドプロンプトでpip install customtkinterを実行してインストールが完了した画面
▲ pip install customtkinter の実行結果

準備2:テストに必要なサンプルExcelファイル(受注管理表・請求書雛形)

お手持ちの管理表と書類フォーマットを使うか、下記のサンプルをダウンロードしてください(第1回・第2回と同じファイルです)。

⬇ 受注管理表_Sample.xlsx ⬇ 請求書_Sample.xlsx

準備3:Pythonコードを動かす環境の準備

同一フォルダ内に、次の3ファイルを置きます。

  • テストコード(本記事のコード)
  • 受注管理表(Excel)
  • 請求書雛形(Excel)

次のセクションのPythonコードをVS Codeなどのエディターにコピー&ペーストして実行してください。

VS Codeで作業フォルダを開き、テストコードとExcelファイルを同じ場所に置いた状態

▲ 同一フォルダにコードとExcelファイルを配置

VS Codeでフォルダを開き、コードとExcelファイル2つを同じ階層に置いておくとパスの指定が楽です。


スポンサーリンク

GUI版の完成コード:CustomTkinterで請求書自動作成ツールを作る

こちらが完成形のコードです。第2回で作ったPDF化ロジックを、ボタンを押したときに呼ばれる run_process という関数(メソッド)に閉じ込め、画面の入力欄と連動させています。1行ずつコメントを付けてあります。

⬇ テストコード一式をダウンロード(Invoice_Maker_test03.zip)

# invoice_maker_test03.py
# invoice_maker_test02.pyをベースに、GUIで操作できるようにcustomtkinterを使用して拡張したコードです。
# GUIでファイル選択やシート名、行範囲、列・セルの紐付けを入力できるようにしています。

import openpyxl
import win32com.client
import customtkinter as ctk
from tkinter import filedialog
import os

#GUI版のコードは別ファイルで作成します。
class InvoiceApp(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("NINJIN Invoice Maker TEST")
        self.geometry("600x650")

        # --- ファイル選択セクション ---
        self.label_file = ctk.CTkLabel(self, text="【1】ファイルの選択", font=("BIZ UDゴシック", 16, "bold"))
        self.label_file.pack(pady=5)

        self.kanri_path = ctk.StringVar(value="未選択")
        ctk.CTkButton(self, text="受注管理表を選択", command=self.select_kanri).pack(pady=5)
        ctk.CTkLabel(self, textvariable=self.kanri_path).pack()

        self.hina_path = ctk.StringVar(value="未選択")
        ctk.CTkButton(self, text="請求書雛形を選択", command=self.select_hina).pack(pady=5)
        ctk.CTkLabel(self, textvariable=self.hina_path).pack()

        # --- 範囲・シート設定セクション ---
        self.label_setting = ctk.CTkLabel(self, text="【2】読み込み設定", font=("BIZ UDゴシック", 16, "bold"))
        self.label_setting.pack(pady=5)

        # シート名と行範囲(横に並べる)
        self.frame_labels = ctk.CTkFrame(self)
        self.frame_labels.pack(pady=(10, 0)) # 上に余白
        
        # 表題は self.frame_labels の中に入れる
        self.lbl_kanri = ctk.CTkLabel(self.frame_labels, text="管理表シート", width=120)
        self.lbl_kanri.pack(side="left", padx=5)

        self.lbl_hina = ctk.CTkLabel(self.frame_labels, text="請求書シート", width=120)
        self.lbl_hina.pack(side="left", padx=5)

        self.lbl_start = ctk.CTkLabel(self.frame_labels, text="開始行", width=60)
        self.lbl_start.pack(side="left", padx=5)

        self.lbl_end = ctk.CTkLabel(self.frame_labels, text="終了行", width=60)
        self.lbl_end.pack(side="left", padx=5)
        # --- 【2】入力欄(Entry)用のフレーム ---
        self.frame_entries = ctk.CTkFrame(self)
        self.frame_entries.pack(pady=(0, 10)) # 下に余白
        
        # 入力欄は self.frame_entries の中に入れる
        self.entry_kanri_sheet = ctk.CTkEntry(self.frame_entries, width=120)
        self.entry_kanri_sheet.insert(0, "Sheet1")
        self.entry_kanri_sheet.pack(side="left", padx=5)
        
        self.entry_hina_sheet = ctk.CTkEntry(self.frame_entries, width=120)
        self.entry_hina_sheet.insert(0, "Sheet1")
        self.entry_hina_sheet.pack(side="left", padx=5)

        self.entry_start = ctk.CTkEntry(self.frame_entries, width=60)
        self.entry_start.insert(0, "4")
        self.entry_start.pack(side="left", padx=5)

        self.entry_end = ctk.CTkEntry(self.frame_entries, width=60)
        self.entry_end.insert(0, "9")
        self.entry_end.pack(side="left", padx=5)
        
        # --- 受注Noの列を入力するセクション ---
        self.label_mapping = ctk.CTkLabel(self, text="【3】受注Noの列を入力(作成されるPDFのファイル名に使用)", font=("BIZ UDゴシック", 16, "bold"))
        self.label_mapping.pack(pady=10)
        
        self.frame_order = ctk.CTkFrame(self)
        self.frame_order.pack( padx=20, fill="x")
        
        self.order_entry = ctk.CTkEntry(self.frame_order, width=60)
        self.order_entry.pack(padx=5)
        self.order_entry.insert(0, "A")  # デフォルトはA列

        # --- 列・セル指定セクション ---
        self.label_mapping = ctk.CTkLabel(self, text="【4】列・セルの紐付け_左側:管理表の列/右側:請求書雛形のセル", font=("BIZ UDゴシック", 16, "bold"))
        self.label_mapping.pack(pady=10)

        # グリッド状に配置
        self.frame_map = ctk.CTkFrame(self)
        self.frame_map.pack(pady=5, padx=20, fill="x")

        # 設定項目(項目名, 管理表の列, 雛形のセル)
        # 「self.変数名」で受けることで、クラス全体の「持ち物」になる
        self.e_col_company, self.e_cell_company = self.create_map_row(self.frame_map, "会社名", "C", "A5", 0)
        self.e_col_product, self.e_cell_product = self.create_map_row(self.frame_map, "商品名", "F", "B18", 1)
        self.e_col_count,   self.e_cell_count   = self.create_map_row(self.frame_map, "数量",   "G", "J18", 2)
        self.e_col_amount,  self.e_cell_amount  = self.create_map_row(self.frame_map, "単価",   "H", "L18", 3)
        
        # --- 実行ボタン ---
        self.btn_run = ctk.CTkButton(self, text="PDF作成開始", fg_color="green", hover_color="darkgreen", command=self.run_process)
        self.btn_run.pack(pady=30)

    def create_map_row(self, master, label_text, col_val, cell_val, row):
        ctk.CTkLabel(master, text=label_text).grid(row=row, column=0, padx=10, pady=5)
        
        # 管理表の列用
        e_col = ctk.CTkEntry(master, width=50)
        e_col.insert(0, col_val)
        e_col.grid(row=row, column=1, padx=5, pady=5)
        
        # 雛形のセル用
        e_cell = ctk.CTkEntry(master, width=80)
        e_cell.insert(0, cell_val)
        e_cell.grid(row=row, column=2, padx=5, pady=5)
        
        # 【重要】作った2つのEntryをセットにして外に返す(return)
        return e_col, e_cell

    def select_kanri(self):
        path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")])
        if path: self.kanri_path.set(path)

    def select_hina(self):
        path = filedialog.askopenfilename(filetypes=[("Excel files", "*.xlsx")])
        if path: self.hina_path.set(path)

    def run_process(self):
        # ここに以前作成したPDF作成ロジックを入れ込む
        # 1. 設定
        KANRI_PATH = self.kanri_path.get()   # GUIで選択された受注管理表のパスを取得
        HINAGATA_PATH = self.hina_path.get()    # GUIで選択された請求書の雛形となるExcelファイルのパスを取得
        kanri_sheet_name = self.entry_kanri_sheet.get()     # GUIから受注管理表のシート名を取得
        hina_sheet_name = self.entry_hina_sheet.get()       # GUIから請求書雛形のシート名を取得
        start_row = int(self.entry_start.get()) # GUIから開始行を取得
        end_row = int(self.entry_end.get())     # GUIから終了行を取得
        col_order=self.order_entry.get()    # 受注番号の列を取得
        col_company=self.e_col_company.get()  # 会社名の列を取得
        cell_company=self.e_cell_company.get() # 会社名のセルを取得
        col_product=self.e_col_product.get()  # 商品名の列を取得
        cell_product=self.e_cell_product.get() # 商品名のセルを取得
        col_count=self.e_col_count.get()    # 数量の列を取得
        cell_count=self.e_cell_count.get()   # 数量のセルを取得
        col_amount=self.e_col_amount.get()   # 単価の列を取得
        cell_amount=self.e_cell_amount.get()  # 単価のセルを取得
        
        # --- 追加:ファイルが選択されているかチェック ---
        if KANRI_PATH == "未選択" or HINAGATA_PATH == "未選択":
            print("エラー:ファイルを選択してください")
            return # ここで関数を終了させる
        # ----------------------------------------------

        # Excelアプリケーションの起動(ループの外で行うと高速です)
        excel = win32com.client.Dispatch("Excel.Application")   # DispatchでExcelアプリケーションを起動
        excel.Visible = False          # Excelのウィンドウを表示しない
        excel.DisplayAlerts = False    # 警告ダイアログを表示しない
        excel.ScreenUpdating = False   # 画面更新を停止(より徹底する場合)

        try:    # エラー発生時もExcelを確実に終了させるためにtryブロックを使用
            wb_kanri = openpyxl.load_workbook(KANRI_PATH, data_only=True)   # KANRI_PATHを開く data_only=Trueで数式の結果を値で取得
            ws_kanri = wb_kanri[kanri_sheet_name]   # GUIから受注管理表のシート名を取得してシートを指定
            
            # 2. for文でstart_row行目からend_row行目まで繰り返す
            for i in range(start_row, end_row + 1):
                try:    # for文の中でエラーが起きても次の行に進むように、ここでもtryブロックを使用
                    # 行番号(i)を使って各データを取得
                    order_id     = ws_kanri[f"{col_order}{i}"].value # 受注番号 .valueでセルの値を取得
                    company_name = ws_kanri[f"{col_company}{i}"].value # 会社名 
                    s_name       = ws_kanri[f"{col_product}{i}"].value # 商品名
                    count        = ws_kanri[f"{col_count}{i}"].value # 数量
                    amount       = ws_kanri[f"{col_amount}{i}"].value # 単価

                    print(f"--- 処理中({i}行目): {company_name} 様 ---")

                    # 3. 雛形へ書き込み
                    wb_hina = openpyxl.load_workbook(HINAGATA_PATH) #HINAGATA_PATHを開く
                    ws_hina = wb_hina[hina_sheet_name] # シート名を指定してシートを取得
                    ws_hina[f"{cell_company}"] = company_name    #HINAGATA_PATHの指定されたセルに会社名を書き込み
                    ws_hina[f"{cell_product}"] = s_name     #HINAGATA_PATHの指定されたセルに商品名を書き込み
                    ws_hina[f"{cell_amount}"] = amount     #HINAGATA_PATHの指定されたセルに金額を書き込み
                    ws_hina[f"{cell_count}"] = count      #HINAGATA_PATHの指定されたセルに数量を書き込み

                    # 一時ファイルのパス(会社名を含めて重複を避ける)
                    temp_excel = os.path.abspath(f"temp_{order_id}.xlsx")   #abspathで絶対パスを取得 order_idを使って一時ファイルの名前を個別化する
                    wb_hina.save(temp_excel)    # 保存してからPDF化する必要があるため、一時的なExcelファイルを作成

                    # 4. PDF化処理
                    output_pdf = os.path.abspath(f"請求書_{order_id}_{company_name}.pdf")   # 出力するPDFのパスを作成(会社名と受注番号を含めて重複を避ける)
                    doc = excel.Workbooks.Open(temp_excel)  # Openで先ほど保存した一時的なExcelファイル(temp.xlsx)を開く
                    doc.ExportAsFixedFormat(0, output_pdf)  # ExportAsFixedFormatでPDF形式で保存。第1引数は0でPDF、第2引数は保存先のパス
                    doc.Close(False)    # Closeでtemp.xlsxを閉じる。引数は変更を保存するかどうか(Falseで保存しない)

                    # 一時ファイルの削除
                    os.remove(temp_excel)   # 一時的なExcelファイルを削除
                    print(f"成功: {output_pdf}")

                except Exception as e:  # エラーが発生しても次の行に進むための例外処理
                    print(f"【失敗】 {i}行目の処理でエラー: {e}")
                finally:
                    # ループ内での後片付け(必要に応じて)
                    pass    # 今回は特に後片付けはないですが、ここでExcelの設定をリセットすることもできます

        finally:
            # 5. Excelを完全に終了
            excel.Quit()
            print("すべての処理が完了しました。")

if __name__ == "__main__":
    app = InvoiceApp()
    app.mainloop()

画面の操作方法とテスト結果の確認

  • コードを実行し、GUIウィンドウが立ち上がれば成功です
  • 「受注管理表を選択」「請求書雛形を選択」でExcelファイルを選びます
  • シート名・開始行・終了行・列/セルの対応を入力します
  • 「PDF作成開始」ボタンを押します
  • VS Codeのターミナルに「6件分の処理完了」が表示され、同じフォルダに6件分のPDFができていれば成功です
GUIアプリで6件分の請求書PDFが生成され、ターミナルに処理完了ログが表示された画面

▲ 6件分のPDFが生成され、ターミナルに完了ログが出た状態

ターミナルに「成功: 請求書_…pdf」が件数分並び、「すべての処理が完了しました。」と出れば完了です。フォルダにPDFができているか確認しましょう。


スポンサーリンク

現場で役立つGUIアプリ(デスクトップアプリ)開発 3つのコツ

① filedialogによる直感的なファイル選択(パス入力ミスの防止)

パスを直接キーボードで打つのはミスのもとです。from tkinter import filedialog と書き、filedialog.askopenfilename() を使うと、Windows標準の「ファイルを開く」ダイアログを呼び出せます。選ばれたパスは StringVar を通じて画面のラベルに即座に反映させます。filedialog の使い方(前回開いたフォルダを記憶する実装など)は、tkinterのfiledialogでExcelファイルを選択する方法で詳しく解説しています。

② .get() を使った画面入力値(開始行・終了行)のプログラムへの引き渡し

GUIを作る最大のメリットがここです。ユーザーが入力欄(CTkEntry)に入れた「開始行」「終了行」を self.entry_start.get() で取得し(文字列で返るので int() で数値化)、そのまま for 文の range() に渡します。コードを一切書き換えずに、処理する範囲を自由に変更できるようになります。

③ class(クラス)を使った本格的なアプリケーション設計

変数がごちゃごちゃにならないよう、class InvoiceApp(ctk.CTk): というクラスでアプリ全体を設計しています。self.entry_start のようにウィジェットをクラスの持ち物にしておくと、どのメソッドからでも参照でき、コードが整理されます。クラスの書き方に不安があれば、先に基礎編(CustomTkinter入門)を読んでおくとスムーズです。


スポンサーリンク

まとめ:ツールは完成、しかし「他人のPCで動かない」最後の壁

全3回の「Invoice Maker」制作記、Excelの裏側操作から始まり、最後はデスクトップアプリまで到達しました。これで請求書のPDF化は、マウス操作数回で終わります。

ただし、このツールを自分以外のPC(他の社員のパソコン)で動かそうとすると、最後の壁が立ちはだかります。相手のPCにもPythonとライブラリをインストールしないといけないことです。非エンジニアに環境構築を頼むのは骨が折れます。

この壁を越える方法としては、PyInstallerなどでコードを .exe にまとめて「ダブルクリックで起動する普通のソフト」にする、というアプローチがあります(本記事では詳しくは触れていません)。

PR

Pythonがインストールされていないパソコンでも、ダブルクリックで起動できる実行ファイル(.exe)化済みの完全版パッケージ「NINJIN Invoice Maker」をnoteで頒布しています。すぐに使える設定項目つき、ソースコードなしで動きます。

👉 noteで確認する

次に読む記事


スポンサーリンク

よくある質問(CustomTkinter GUI化のつまずき)

QCustomTkinterのウィンドウは出るのに「PDF作成開始」を押しても何も起きません

AまずVS Codeのターミナルにエラーが出ていないか確認します。「エラー:ファイルを選択してください」なら、受注管理表と請求書雛形を両方選んでください。ModuleNotFoundError: No module named ‘win32com’ なら pip install pywin32 を実行します。win32comはExcel本体を操作するため、Excelがインストールされていないパソコンでは動きません。

Qfiledialogをimportしてもファイル選択ダイアログが出ません

Aimport文は from tkinter import filedialog と書きます(customtkinter側にはfiledialogはありません)。filedialog.askopenfilename() の戻り値(パス文字列)を変数で受けて使います。ダイアログでキャンセルすると空文字が返るので、その分岐(if path: …)も入れておくと安全です。

QEntryに入れた開始行・終了行が反映されません

Aself.entry_start.get() は文字列を返すので、int() で数値に変換してから range() に渡します。この記事のコードでは start_row = int(self.entry_start.get()) としています。入力欄が空だと int() が ValueError になるため、デフォルト値を insert(0, “4”) のように入れておきます。

Qclass InvoiceApp(ctk.CTk): のクラス設計が難しいです。関数だけで作れませんか?

A小さなツールなら関数だけでも作れます。ただしウィジェットが増えると、関数間での変数の受け渡しが煩雑になります。クラスにして self.entry_start のように持たせると、どのメソッドからでも参照でき、コードが整理されます。CTkの基本に不安があれば、先に基礎編(CustomTkinter入門)を読むと理解しやすいです。

Q作ったツールを他の人のパソコンで使ってもらうにはどうすればいいですか?

A相手のパソコンにPythonとライブラリ(openpyxl・pywin32・customtkinter)をインストールする必要があります。それが難しい場合は、PyInstallerなどでコードを .exe にまとめる方法があります。環境構築不要の配布パッケージは著者のnoteで頒布しています。


ニンジン🥕アイコン

Follow me on X

本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕

@ninjin_py_vba をフォロー

この記事が役に立ったら、シェアしてもらえると嬉しいです🥕

Xでポスト はてブ LINE Pocket

PROJECT LOG — SERIES INDEX
スポンサーリンク

シリーズ記事一覧

電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。

超音波距離センサーHC-SR04をRaspberry Pi Picoで使う|仕組み・配線・精度検証
.uds1-block { position: relative; overflow: hidden; border-radius: 12px; border: 1px solid #f0cba8; background: linear-g…
今週の注目ガジェット〜09/07週〜|電子工作・自作向け新製品まとめ(TickrCast/Radxa Linkr ほか)
ニンジンのガジェット通信 09/07週。ESP32-P4の自作気象モニター、極小IP-KVM「Radxa Linkr」、ESP32のDINレール型PLC、LTE開発キットなど、電子工作で使えそうな新製品5つを解説。
サーボモータの角度をタクトスイッチで可変|OLED(SSD1306)に現在角度をリアルタイム表示
Raspberry Pi Pico 2W×MG996RにタクトスイッチとOLED(SSD1306)を追加。ボタンで角度を可変しながら現在角度をリアルタイム表示するMicroPython実装を、framebufの文字拡大やサーボの遊び対策まで実機ベースで解説します。
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす:PWM制御とキャリブレーション実践
MG996RサーボモーターをRaspberry Pi Pico 2Wで動かす方法を解説。公称180度のはずが実測198度だった個体差の原因究明から、ホーンのズレ補正、タクトスイッチでの角度制御まで実機検証の様子を紹介します。
Tapo P110Mの消費電力をExcelレポート化する【年次集計・電気代試算・セキュリティ判断編】
FastAPI+openpyxlでTapo P110Mの消費電力をExcelレポート化。15秒間隔の生ログを日次集計する設計や、31円/kWhでの電気代試算、外部公開を見送った理由まで解説します。

コメント