最近、「CustomTkinter」が注目されて、話題になっていますね!どうやら、「CustomTkinter」を使うと、PythonだけでWebアプリ顔負けのモダンなデスクトップGUIが作れるようです!

そこで今回は、Python環境で「CustomTkinter」の画面遷移・テーマ管理・非同期処理といった設計パターンの実装を行ってみました!コピペで動く最小サンプル付きですので、ぜひ皆さんも記事を読んで試してみてください!

この記事で分かること

  • CustomTkinterへの移行コストと、Tkinterとの見た目の差
  • 画面遷移・テーマ切替・非同期処理の実装パターン(実コード付き)
  • 「動くけど汚い」GUIコードを設計から改善する具体的な方法

Tkinter → CustomTkinterに移行する価値

PythonでGUIを作ろうとしたとき、まずTkinterにたどり着いた方は多いはずです。ただ、「動くけどダサい…」という壁に一度はぶつかったことはありませんか?CustomTkinterはそのTkinterを拡張したライブラリで、追加のフレームワークを覚えずにモダンなUIへ脱皮できます。

CustomTkinterでできること

  • ダークモード/ライトモードのワンライン切替
  • 角丸ボタン・スライダー・スイッチなどのモダンウィジェット
  • 既存のtkinter構文をほぼそのまま流用した移行

ここで、「そもそもTkinterと何が違うの?」という疑問が出てくると思います。

大きな差としては、「ctk.set_appearance_mode() 1行でアプリ全体のテーマを切替」が可能ということです。これにより、ウィジェットを1つずつ再描画する手間なしに全体のデザインを即時変更することが出来ます。

今話題の、「ダークモード対応GUI」というやつですね!

比較すると、以下のようになります。

項目TkinterCustomTkinter
デザインOS依存の古いUIモダンなフラットデザイン
ダークモード手動実装が必要標準サポート(1行)
移行コスト—import変更+接頭辞追加のみ

「ダークモード対応GUIって難しそう…」と感じるかもしれませんが、この記事を読めば初心者の方でも大丈夫!順を追ってCustomTkinterの設計パターン全体まで解説します。

見た目の差をコードで比較

Tkinterでボタンを作る場合は tk.Button(root, text="送信") と書きます。CustomTkinterへの移行は ctk.CTkButton(root, text="送信") と接頭辞を変えるだけ。つまり、既存コードの tk. を ctk.CTk に置き換えるだけで大部分は動きます。見た目はそれだけで劇的に改善されます。

移行コスト:変更が必要なコードの範囲

実際の移行作業で変更が必要なのは主に3点です。①インポート文を import customtkinter as ctk に変更、②ルートウィンドウを ctk.CTk() に変更、③ウィジェットのクラス名を CTk〇〇 形式に変更。たとえば10画面・200行規模のアプリでも、正規表現の一括置換で1時間以内に移行できます。

レイアウト3パターンの使い分け

CustomTkinterはTkinterのレイアウトマネージャーをそのまま使います。つまり pack・grid・place の3つです。「どれを使えばいいか分からない」で詰まったことはありませんか?それぞれに明確な得意分野があります。

pack(縦積み)が向くケース

packは要素を上から順番に積み重ねるシンプルなマネージャーです。設定画面・ログ表示・チャットUIのように、縦一列に要素を並べる場面に最適です。たとえばサイドバーやステータスバーのような「幅いっぱいに広げたい要素」には fill="x", expand=True を組み合わせると一発で決まります。

grid(表形式)が向くケース

gridは行と列で位置を指定する方式で、フォームやダッシュボードに向いています。たとえばラベル+入力欄を2列で並べるとき、packで無理やり実装するより grid(row=0, col=0) で一発指定できます。複数のwidgetを整列させたいときはgrid一択と覚えておきましょう。

place(絶対座標)を使うべき場面

placeはピクセル単位で配置を指定します。ウィンドウサイズが固定のツール・オーバーレイ表示・アイコンを重ねるような場面で活躍します。ただし、ウィンドウのリサイズに追従しないため、固定サイズのダイアログ等に限定して使うのがベストプラクティスです。

画面遷移の設計パターン

「画面を切り替えたいけど、Toplevelを使うと子ウィンドウが増えて収集がつかない…」という経験はありませんか?CustomTkinterでは、Frameをスタックして見せ方を切り替える方法が定番パターンです。

Frameスタック方式(シンプルアプリ向け)

画面数が3〜5枚程度の小規模アプリに向いた方法です。すべてのFrameを事前に生成しておき、表示したいFrameだけ tkraise() で前面に持ってきます。つまり、ページを「重ねた紙の束」としてイメージすると分かりやすいです。コードがシンプルで、画面遷移のバグも起きにくい構成です。

ページ管理クラス方式(大規模アプリ向け)

画面が6枚以上になる、またはページ間でデータを受け渡したい場合は、PageManagerクラスを作って管理する方式が効果的です。各ページをクラスとして定義し、PageManagerが辞書でインスタンスを管理します。たとえば manager.show("Dashboard") のように名前で呼び出せるため、保守性が大幅に上がります。

実装コード比較

以下に両方の「動く最小サンプル」を示します。まずFrameスタック方式から。

# frame_stack.py
# Frameスタック方式:画面切り替えの最小サンプル
import customtkinter as ctk

class PageA(ctk.CTkFrame):
    def __init__(self, master, controller):
        super().__init__(master)
        ctk.CTkLabel(self, text="ページA").pack(pady=20)
        ctk.CTkButton(self, text="ページBへ",
                      command=lambda: controller.show(PageB)).pack()

class PageB(ctk.CTkFrame):
    def __init__(self, master, controller):
        super().__init__(master)
        ctk.CTkLabel(self, text="ページB").pack(pady=20)
        ctk.CTkButton(self, text="ページAへ",
                      command=lambda: controller.show(PageA)).pack()

class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Frame Stack Demo")
        self.geometry("400x300")
        self._frames = {}
        for F in (PageA, PageB):
            frame = F(self, self)
            self._frames[F] = frame
            frame.place(relwidth=1, relheight=1)
        self.show(PageA)

    def show(self, page_class):
        self._frames[page_class].tkraise()

if __name__ == "__main__":
    App().mainloop()

カラーテーマ管理を1ファイルで完結させる

「ダーク/ライトを切り替えたら一部のウィジェットだけ色が変わらない」というバグ、経験したことはありませんか?テーマ設定を各ファイルに散らばらせているのが原因です。1ファイルに集約するだけで劇的に管理しやすくなります。

テーマ定数ファイルの設計

カラーコードを直接ウィジェットに書くのではなく、theme.py という定数ファイルにまとめて管理します。たとえば PRIMARY = "#1f6aa5" のように定義しておけば、デザイン変更時は1ファイルだけ編集すれば済みます。つまり「デザインの単一責任原則」を守る構成です。

ダーク/ライトをボタン1つで切り替える実装

# theme_toggle.py
# ダーク/ライト切替の最小サンプル
import customtkinter as ctk
import json, os

SETTINGS_FILE = "settings.json"

def load_mode():
    if os.path.exists(SETTINGS_FILE):
        return json.load(open(SETTINGS_FILE)).get("mode", "dark")
    return "dark"

def save_mode(mode):
    json.dump({"mode": mode}, open(SETTINGS_FILE, "w"))

class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Theme Toggle Demo")
        self.geometry("400x200")
        self._mode = load_mode()
        ctk.set_appearance_mode(self._mode)
        self._btn = ctk.CTkButton(self, text="テーマ切替",
                                  command=self.toggle)
        self._btn.pack(pady=60)

    def toggle(self):
        self._mode = "light" if self._mode == "dark" else "dark"
        ctk.set_appearance_mode(self._mode)
        save_mode(self._mode)

if __name__ == "__main__":
    App().mainloop()

ユーザー設定をJSONで永続化する

上記サンプルの save_mode() がその実装です。アプリ終了後も設定を保持するために、settings.json に書き出します。次回起動時に load_mode() で読み込めば、ユーザーが選んだテーマが復元されます。JSONはPython標準ライブラリで扱えるため、外部依存なしに実装できるのがメリットです。

「重いGUI」を改善する非同期処理パターン

ボタンを押したらアプリがフリーズした…という経験はありませんか?これはGUIの根本的な仕組みに起因する問題で、設計で解決できます。

なぜGUIがフリーズするのか(メインスレッドの仕組み)

GUIアプリは「イベントループ」という仕組みで動いています。つまり、メインスレッドがボタン押下・描画・キーボード入力をすべて順番に処理しているのです。ここに時間のかかる処理(API通信・ファイル読み込み等)を置くと、処理が終わるまでイベントループが止まり、画面がフリーズして見えます。

threadingで解決する方法(実装例)

解決策はシンプルです。重い処理を別スレッドに逃がします。

# threading_sample.py
# 重い処理をスレッドに逃がしてフリーズを防ぐ最小サンプル
import customtkinter as ctk
import threading, time

class App(ctk.CTk):
    def __init__(self):
        super().__init__()
        self.title("Threading Demo")
        self.geometry("400x200")
        self._label = ctk.CTkLabel(self, text="待機中")
        self._label.pack(pady=30)
        ctk.CTkButton(self, text="重い処理を実行",
                      command=self.start_task).pack()

    def start_task(self):
        # daemonスレッドでバックグラウンド実行
        t = threading.Thread(target=self.heavy_task, daemon=True)
        t.start()

    def heavy_task(self):
        self._label.configure(text="処理中…")
        time.sleep(3)  # 重い処理の代わり(API通信等)
        # GUIの更新はafter()経由でメインスレッドに戻す
        self.after(0, lambda: self._label.configure(text="完了!"))

if __name__ == "__main__":
    App().mainloop()

asyncioを使う場合の注意点

asyncioはTkinterのイベントループと共存させるのが難しく、初心者には罠が多いパターンです。asyncioを使いたい場合は asyncio.run() をスレッド内で呼び出すか、asynctkinter のような専用ライブラリを介するのが現実的です。たとえばWebSocket通信を扱う場合でも、まずはthreadingで試すのが最も安全な入り口です。

Webビルダーで設計してからコードに落とす開発フロー

正直に言うと、私もはじめはコードだけで画面を設計しようとして「書いては実行、書いては実行」を繰り返す非効率な時間を過ごしていました。そこで試したのが、ビジュアルで画面を設計してからコードに落とすフローです。

ビジュアルで画面設計 → コード生成の流れ

FigmaやExcalidrawで画面ワイヤーフレームをざっくり描いてから、そのレイアウトをCustomTkinterのgridに落とし込みます。「左カラム:ナビゲーション (col=0)、右カラム:メインコンテンツ (col=1)」のように対応表を作るだけで、コーディング中に迷う時間がゼロになります。ChatGPTやGeminiにワイヤーフレームの説明を投げてスケルトンコードを生成させるのも、最近の自分の定番フローです。

手書きコードとの併用方法

生成AIで作ったスケルトンは「構造だけ合っていれば十分」と割り切るのがコツです。ウィジェットの種類・イベント処理・スレッド管理は手書きで上書きします。たとえば生成コードの tk.Button を ctk.CTkButton に置き換えるだけでモダンUIに変わります。「設計はAI、ロジックは自分」という役割分担が、開発速度と品質を両立させる最速ルートです。

まとめ:CustomTkinterで脱・入門コードを果たそう

今回は、CustomTkinterの画面遷移・テーマ管理・非同期処理という3つの設計パターン実装に挑戦してみました。

標準のTkinterも便利ですが、「CustomTkinter」は更に便利で、移行コストがほぼゼロなのにプロ並みのUIが手に入るため、これは使わないのはもったいない!と感じました。

導入も簡単ですので、みなさんも今回の記事を参考に、ぜひ「CustomTkinter」を活用してみてください!