PR

CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)

Python

CUSTOMTKINTER GUIDE VOL.4

スポンサーリンク

CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)

こんにちは。ニンジンです🥕

CustomTkinterで、サイドバー付きのアプリ画面をすぐに作れるテンプレート「CTk App Shell」を作りました。GitHubで無料公開しています。まずは、動いているところを見てください。

▲ ダッシュボード → 作業の実行 → 設定 → ライト/ダーク切替(Windows 11で録画)
GitHubでCTk App Shellをダウンロード

Pythonで作った処理に画面を付けたいとき、毎回ゼロからレイアウトを組むのはけっこう大変です。このテンプレートは、サイドバー、ダッシュボード、作業画面、設定画面、メッセージボックスまで一通りそろえてあるので、中身を差し替えるだけで「それっぽいツール」になります。

この記事で分かること

  • CTk App Shellでできること(3つの画面とメッセージボックス)
  • ダウンロードと起動の方法(pip install -r requirements.txt)
  • サイドバーのアイコンを、ライト/ダークに合わせて自動で切り替える方法(CTkImage)
  • gridで組んだダッシュボードを、UIの大きさ125%でも崩さないコツ
  • 作業画面に自分の処理をつなぐ方法(別スレッドで実行)
  • Windows実機で動かして見つかった4つの罠と、その直し方
スポンサーリンク

こんな人におすすめ

  • CustomTkinterの基本は分かったので、見た目の整ったアプリを作ってみたい人
  • Pythonの自動化スクリプトに、人に渡せる画面を付けたい人
  • AIに書かせたCustomTkinterのコードが、思ったような見た目にならずに困っている人

スポンサーリンク

CTk App Shellでできること

画面は3つで、左のサイドバーで切り替えます。

画面できること
ダッシュボード数字のカード4枚、最近のアクティビティ、使用状況のバー(見本データ)
作業フォルダを選んで「実行」。確認ダイアログ → ログと進捗バー → 完了ダイアログ
設定ダーク/ライト/システムの切替、UIの大きさ(90〜125%)、スイッチ(見本)
CTk App Shellのダッシュボード(ダークモード)
▲ ダッシュボード(ダークモード)
作業画面で実行が終わったところ
▲ 作業画面。実行が終わるとログと進捗バーがここまで進む
設定画面(ライトモード)
▲ 設定画面(ライトモード)
ポイント:作業画面の「実行」はデモです。ログと進捗バーが進むだけで、ファイルの読み書きはしません。自分の処理につなぐ方法は、後半の「作業画面に自分の処理をつなぐ」で説明します。

スポンサーリンク

ダウンロードと起動(pip install -r requirements.txt)

  1. GitHubのページで「Code」→「Download ZIP」を押して、一式をダウンロードする(gitが使える人はgit cloneでも大丈夫です)
    git clone https://github.com/kurotyy-svg/ctk-app-shell.git
  2. zipを展開する(アイコン画像などを一緒に読み込むので、zipの中から直接実行しないでください)
  3. 展開したフォルダの中で、コマンドプロンプト(またはPowerShell)を開く
  4. 次の2行を実行する
    pip install -r requirements.txt
    python main.py

requirements.txtの中身はこれだけです。customtkinterは6.0.0に固定しています(理由は後半で書きます)。

requirements.txt

customtkinter==6.0.0
pillow>=10.0

動作を確認した環境

  • Windows 11
  • Python 3.13
  • customtkinter 6.0.0 / Pillow 12.3.0

Macでは確認していません。


スポンサーリンク

フォルダ構成と、画面を増やす手順

main.py              アプリ本体(画面の切替とサイドバー)
theme.py             色・フォント・アイコンの設定(見た目はここを編集)
components/          sidebar.py / card.py / dialog.py
views/               dashboard.py / work.py / settings.py (base.py が共通の土台)
assets/icons/        アイコンPNG(ライト用・ダーク用)
assets/app.ico       ウィンドウ左上・タスクバー用のアイコン(Windows)
assets/icons_src/    アイコンの元SVGとライセンス
tools/build_icons.py SVG から PNG を作り直すスクリプト(任意)
tools/record_demo.py GIF・動画の撮影用に、アプリを自動で操作するスクリプト(任意)

見た目を変えたいときはtheme.py、画面を増やしたいときはviews/とmain.py、という分け方にしています。

画面を増やす手順

サイドバーの項目は、main.pyのNAV_ITEMSで決めています。

main.py

NAV_ITEMS = [
    ("dashboard", "ダッシュボード", "layout-dashboard"),
    ("work", "作業", "terminal"),
    ("settings", "設定", "settings"),
]
  • views/に、BaseViewを継承したクラスを作る(views/work.pyが参考になります)
  • NAV_ITEMSに(キー, 表示名, アイコン名)を1行足す
  • main.pyのself.viewsに、同じキーで1行足す

スポンサーリンク

サイドバーのアイコンを、ライト/ダークで自動で切り替える(CTkImage)

▲ 選択中のボタンは背景がオレンジになり、アイコンもオレンジの線に変わる

アイコンは、Lucide(ISCライセンス)のSVGをPNGに書き出して同梱しています。1つのアイコンにつき、次の3種類を、それぞれライト用とダーク用で用意しています(1つにつき6枚)。

  • 通常
  • 選択中(オレンジの線)
  • オレンジのボタンの上用(ボタンの文字と同じ色の線)

読み込みは、theme.pyのicon()にまとめています。

theme.py

def icon(name: str, size: int = 22, variant: str = "") -> ctk.CTkImage:
    """
    assets/icons/ の PNG から CTkImage を作る。
    light_image(ライトモード用)と dark_image(ダークモード用)を渡しておくと、
    set_appearance_mode() の切り替えに合わせて自動で画像も切り替わる。

    variant: "" = 通常 / "accent" = オレンジ線 / "onaccent" = オレンジのボタン上用
    """
    key = (name, size, variant)
    if key not in _icon_cache:
        stem = f"{name}_{variant}" if variant else name
        _icon_cache[key] = ctk.CTkImage(
            light_image=Image.open(ICON_DIR / f"{stem}_light.png"),
            dark_image=Image.open(ICON_DIR / f"{stem}_dark.png"),
            size=(size, size),
        )
    return _icon_cache[key]

CTkImageにlight_imageとdark_imageの両方を渡しておくと、set_appearance_mode()でモードを切り替えたとき、画像も自動で切り替わります。customtkinter 6.0.0で試すと、モードを切り替えたときに画像も自動で切り替わりました(上の動画でも、サイドバーのアイコンの色が変わっています)。

アイコンのサイズは「96pxで書き出して、22pxで表示」

PNGは96pxで書き出し、画面にはsize=(22, 22)のように小さく表示しています。Windowsの表示スケールを上げたときに、アイコンをボケにくくするためです。


スポンサーリンク

ライト/ダークの切替と、色の決め方

▲ サイドバー下のボタンで、ダーク → ライト → ダーク

サイドバー下の「ライト/ダーク切替」か、設定画面で切り替えられます。

色はすべてtheme.pyに、(ライト用, ダーク用)のタプルで書いています。CustomTkinterは、このタプルをfg_colorなどに渡すと、モードに合わせて自動で色を切り替えてくれます。

theme.py

BG_BASE = ("#f6efe7", "#16130f")       # 画面全体の背景
BG_SIDEBAR = ("#efe3d5", "#100e0b")    # サイドバーの背景
BG_CARD = ("#ffffff", "#211c17")       # カードの背景
BG_INPUT = ("#fbf6f0", "#2a231c")      # 入力欄の背景
BORDER = ("#e6d5c3", "#33291f")        # カードの枠線

TEXT = ("#2b221a", "#f1e8de")          # 本文
TEXT_MUTED = ("#8a7a6a", "#a39484")    # 補足テキスト

ACCENT = ("#c05208", "#ff8a3d")        # アクセント(ブログのオレンジ)
ACCENT_HOVER = ("#a84606", "#ff9f5e")
ON_ACCENT = ("#ffffff", "#1a1209")     # アクセント色の上に載せる文字色

アクセント色は、このブログのオレンジ(#c05208)をライト用にして、ダーク用は暗い背景の上で映えるように明るくした#ff8a3dにしています。

注意:切り替えた瞬間、0.2〜0.3秒ほど画面がちらつきます(Windows実機の録画で確認)。全部の部品の色を描き直すためです。

スポンサーリンク

ダッシュボードはgridの12列で組む(UIの大きさ125%でも崩れない)

▲ 設定の「UIの大きさ」を125%にしてから、ダッシュボードを開いたところ

設定の「UIの大きさ」を125%にすると、ウィンドウの大きさはそのままで、中の部品だけが1.25倍になります。最初の版では、この状態で数字のカードが窮屈になり、数字が見切れていました。

125%で数字が見切れたダッシュボード
▲ 修正前(125%)。「1,28」「35」のように数字が欠けている
125%でも数字が収まったダッシュボード
▲ 修正後(125%)。数字もラベルも収まっている

直したところは2つです。

  • カードのアイコンを1行目だけに置いて、数字がカードの幅いっぱいを使えるようにした
  • ダッシュボード全体を12列のgridにして、下段を7列+5列に分けた

views/dashboard.py

# 12列の等幅グリッド。uniform を同じ名前にそろえると、列幅が均等になる。
# KPIカードは3列ぶん×4枚、下段は 7列(アクティビティ)+5列(使用状況) に分ける
for col in range(12):
    body.grid_columnconfigure(col, weight=1, uniform="col")
body.grid_rowconfigure(1, weight=1)

# --- 1行目: KPIカード ---
for col, (title, value, note, icon_name, color) in enumerate(STATS):
    card = StatCard(body, title, value, note, icon_name, note_color=color)
    card.grid(row=0, column=col * 3, columnspan=3, sticky="nsew",
              padx=(0 if col == 0 else 8, 0 if col == 3 else 8), pady=(0, 16))

grid_columnconfigureのuniformに同じ名前を付けると、その列は同じ幅にそろいます。KPIカードは3列ぶんずつ(columnspan=3)使うので、4枚が等幅で並びます。gridの基本はVol.1で解説しています。

注意:125%から100%に戻した瞬間は、描き直しの途中のコマが0.3秒ほど写ります(録画で確認)。

スポンサーリンク

作業画面に自分の処理をつなぐ(別スレッドで実行)

▲ フォルダを入れて「実行」→ 確認 → ログと進捗バー → 完了
  • 「参照」でフォルダを選ぶと、パスが「入力フォルダ」の欄に入ります
  • 「実行」を押すと、確認ダイアログ(はい/いいえ)が出ます。フォルダを選んでいないと警告が出ます
  • 「はい」を押すとrun_job()が別スレッドで動き、ログと進捗バーが進みます
  • 終わると完了ダイアログが出ます

自分のツールにするときは、views/work.pyのrun_job()の中身だけを差し替えます。いまの中身(デモ)はこうなっています。

views/work.py

def run_job(folder: str, output_name: str, mode: str, log, progress) -> None:
    """
    ★ここを自分の処理に差し替えます★

    「実行」ボタンを押して確認ダイアログで「はい」を選ぶと、この関数が別スレッドで呼ばれます。
    時間のかかる処理を書いても、画面は固まりません。

      folder       : 「参照」で選んだフォルダ
      output_name  : 「出力ファイル名」の欄の文字
      mode         : 「処理モード」で選んだ項目
      log("文字")   : ログ欄に1行追加する
      progress(0.5): 進捗バーを更新する(0.0 〜 1.0)

    注意: この中から CTkLabel などの画面の部品を直接さわらないでください
    (必ず log() / progress() を通します)。エラーは raise すれば、エラーダイアログが出ます。

    いまはデモです。フォルダの中身は見ませんし、ファイルも作りません。
    """
    steps = ["ファイルを読み込み中", "データを整形中", "集計中", "結果を書き出し中"]
    for i, step in enumerate(steps, start=1):
        log(f"({i}/{len(steps)}) {step}…")
        time.sleep(0.5)  # ← 本物の処理に置き換える
        progress(i / len(steps))
    log("完了(デモ): 実際のファイル処理は行っていません。")

書き換え例: フォルダ内の .xlsx を1つずつ処理する

from pathlib import Path


def run_job(folder, output_name, mode, log, progress):
    files = list(Path(folder).glob("*.xlsx"))
    if not files:
        raise ValueError("フォルダに .xlsx ファイルがありません。")
    log(f"{len(files)} 件のファイルを処理します")
    for i, f in enumerate(files, start=1):
        log(f"処理中: {f.name}")
        # ここに1ファイルぶんの処理を書く
        progress(i / len(files))
    log("完了")

Windows 11の実機で、.xlsxが3つ入ったフォルダと、空のフォルダの両方で動作を確認しました。

ファイルが1つもないときはraiseしています。run_job()の中でエラーをraiseすると、ログに「エラー: …」と出て、エラーダイアログが表示されます。

ポイント:run_job()は別スレッドで動くので、この中からCTkLabelなどの部品を直接さわらないでください。画面の更新はlog()とprogress()を通して行います。内部では、別スレッドがqueueに積んだ内容を、画面側がafter()で100ミリ秒ごとに受け取っています。

スポンサーリンク

テーマに合うメッセージボックス4種(messagebox)

▲ 情報・警告・エラー・はい/いいえの4種類
4種類のメッセージボックス
▲ 左上から 情報・警告・エラー・はい/いいえ

標準のtkinter.messageboxはOSの見た目のまま表示されて、CustomTkinterのダークテーマにはなじみません。そこで、テーマに合わせたダイアログをcomponents/dialog.pyに用意しています。作り方の詳しい解説はVol.3にまとめています。

使い方は、作業画面の「実行」ボタンの処理がそのまま例になります。ask_yes_no()は「はい」でTrueを返すので、ifでそのまま分岐できます。

views/work.py

def _on_run(self):
    if self._running:
        return
    folder = self.folder_entry.get().strip()
    if not folder:
        show_warning(self.winfo_toplevel(), "入力フォルダを選択してください。")
        return
    # はい/いいえの戻り値(bool)で、処理を進めるか止めるかを分岐する
    if not ask_yes_no(self.winfo_toplevel(), f"次のフォルダを処理します。\n{folder}\n\n実行しますか?"):
        self._log("キャンセルされました。")
        return
    self._start_job(folder)

スポンサーリンク

Windows実機で動かして見つかった4つの罠

このテンプレートは、Linuxの仮想画面で動作を確認してから、Windows 11の実機で確かめました。実機で動かして初めて気づいたことが4つあったので、まとめておきます。

  • 罠1: UIの大きさ125%で、数字が見切れる
  • 罠2: 確認ダイアログのボタンが、下で見切れる
  • 罠3: タイトルバーとタスクバーに、標準のアイコンが出る
  • 罠4: ライト/ダークを切り替えた直後に画面を移ると、一瞬まっ暗になる

罠1: UIの大きさ125%で、数字が見切れる

ダッシュボードの節で書いたとおりです。ウィンドウの大きさは変わらず部品だけが大きくなるので、横に4枚並べたカードが窮屈になっていました。

罠2: 確認ダイアログのボタンが、下で見切れる

ダイアログの修正前と修正後
▲ 左が修正前、右が修正後(修正前の画像はフォルダのパスをぼかしています)

最初はダイアログの高さを200pxに固定していました。Linuxの仮想画面では収まっていたのですが、Windowsでは文字の行の高さがLinuxより大きいためか、4行のメッセージだとボタンが入りきりませんでした。いまは高さを固定せず、幅だけを見えない枠で確保して、高さは中身に合わせて自動で決まるようにしています。

components/dialog.py

# 幅だけ最小値を決める細い見えない枠。高さは中身(メッセージの行数)に合わせて自動で伸びる。
# (高さを固定すると、パスが長いメッセージでボタンが見切れる)
ctk.CTkFrame(self, width=_W, height=1, fg_color="transparent").pack()

罠3: タイトルバーとタスクバーに、標準のアイコンが出る

タイトルバーのアイコンの変更前と変更後
▲ タイトルバー左上を2倍に拡大

CustomTkinterは、ウィンドウを作ってから約200ミリ秒後に、標準のアイコンを設定します。ただし、自分で先にiconbitmap()を呼んでおくと、そちらが優先されます(customtkinter 6.0.0のソースで確認しました)。そこで、ウィンドウを作った直後に自分のアイコン(assets/app.ico)を設定しています。

theme.py

def apply_window_icon(window) -> None:
    """
    CustomTkinter 標準の青いアイコンを、このアプリのアイコンに置き換える(Windows用)。

    CustomTkinter は、ウィンドウを作った約200ミリ秒後に標準アイコンを設定します。
    ただし iconbitmap() を自分で先に呼んでおくと、そちらが優先されます。
    なので、ウィンドウの __init__ の中でこの関数を呼びます。
    Windows以外では .ico が使えないため、何もしません。
    """
    if sys.platform != "win32" or not APP_ICO.exists():
        return
    try:
        window.iconbitmap(str(APP_ICO))
    except Exception:
        pass

メイン画面とダイアログの両方で、タイトルバーが人参のアイコンに変わることを実機で確認しました。

ところが、タスクバーのほうはPythonのアイコンのままでした。python main.pyで起動したアプリは、Windowsからはpython.exeの一部として扱われるためです。そこで、アプリ専用のID(AppUserModelID)を付けて、別のアプリとして扱ってもらうようにしました。

theme.py

APP_ID = "py-vbalab.ctk-app-shell"  # 自分のアプリにするときは、ほかと重ならない名前に変える


def set_windows_app_id() -> None:
    """
    タスクバーに、Pythonのアイコンではなくこのアプリのアイコンを出す(Windows用)。

    python main.py で起動すると、Windowsはこのアプリを python.exe の一部として扱うため、
    タスクバーにはPythonのアイコンが出ます。アプリ専用の ID(AppUserModelID)を付けると、
    別のアプリとして扱われ、ウィンドウのアイコン(app.ico)がタスクバーにも出ます。
    最初のウィンドウを作る「前」に呼ぶ必要があります。Windows以外では何もしません。
    """
    if sys.platform != "win32":
        return
    try:
        import ctypes

        ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(APP_ID)
    except Exception:
        pass

このIDは、最初のウィンドウを作る「前」に設定する必要があります。そのため、App()を作る直前に呼んでいます。

main.py

if __name__ == "__main__":
    theme.set_windows_app_id()  # タスクバーのアイコンを app.ico にする(ウィンドウを作る前に呼ぶ)
    app = App()
    app.mainloop()

Windows 11の実機で、タスクバーのアイコンも人参に変わることを確認しました。テンプレートを自分のアプリにするときは、APP_IDをほかのアプリと重ならない名前に変えてください。

タスクバーのアイコンの変更前と変更後
▲ タスクバーのアイコンを3倍に拡大。左がPythonのアイコン、右が人参のアイコン

罠4: ライト/ダークを切り替えた直後に画面を移ると、一瞬まっ暗になる

画面を移った直後にまっ暗になった様子
▲ 修正前の録画から。サイドバーだけが描かれ、右側が約0.6秒まっ暗

最初の版は、表示しない画面をgrid_remove()で隠していました。この方式だと、テーマを切り替えたあと、隠れていた画面を表示した瞬間にまとめて描き直しが起きるようで、右側が約0.6秒まっ暗になりました。いまは全部の画面を同じ場所に重ねて置いておき、表示したい画面をtkraise()で一番上に出す方式にしています。

main.py(__init__ の中)

for view in self.views.values():
    view.grid(row=0, column=0, sticky="nsew")
self.show_view("dashboard")

main.py

def show_view(self, key: str):
    """指定した画面を一番上に出し、サイドバーの選択表示も合わせる。"""
    self.views[key].tkraise()
    self.sidebar.set_active(key)

重ねておけば、裏の画面もテーマを切り替えた時点で描き直されるので、まっ暗になることはなくなりました。録画で見ると、画面の切り替えはほとんどが0.1秒以内で、描き替え途中のコマが写るときも0.2秒ほどです。


スポンサーリンク

customtkinterのバージョンを固定している理由

このブログでは、AIが書いたコードが、ライブラリのバージョンアップでエラーも出さずに静かに壊れる話を何度か書いてきました。テンプレートの見た目は、余白や色の細かい指定の積み重ねです。描き方が少し変わっただけでも、エラーは出ないまま見た目だけが崩れることがあります。

動作を確かめたのは、customtkinter 6.0.0だけです。ほかのバージョンで同じ見た目になるかは確かめていません。そのため、requirements.txtで6.0.0に固定しています。

バージョンを上げるときは、3つの画面とダイアログを一通り開いて、見た目を確認してから使ってください。


スポンサーリンク

よくある質問

Q商用のツールに使ってもいいですか?

A商用でも使えます。ライセンスは次のとおりです。
・テンプレート本体: MIT
・アイコン: ISC(Lucide)
・アイコンの一部: MIT(Feather由来)
使うときは、テンプレートのLICENSEファイルを一緒に残してください(著作権表示と許諾の文が入っています)。アイコンを使うときは、assets/icons_src/LICENSE-lucide.txtも残してください。

Qアイコンを追加するには?

Aassets/icons_src/にSVGを入れて、pip install -r tools/requirements-dev.txtのあとpython tools/build_icons.pyを実行すると、PNGが作られます。SVGの描画には resvg-py を使っているので、pipだけで動きます(Windows 11で確認)。最初は cairosvg を使っていましたが、Windowsでは Cairo 本体が入っていないため、OSError: no library called "cairo-2" was foundで止まりました。PNGは同梱済みなので、アイコンを足さないならこの手順は要りません。

QMacやLinuxでも動きますか?

ALinuxは仮想画面で、3つの画面とダイアログが動くことを確認しました。Macは確認していません。タイトルバーとタスクバーのアイコン差し替えは、Windowsだけの処理です。

Q記事の動画はどうやって撮ったのですか?

Atools/record_demo.py という撮影用スクリプトでアプリを自動で操作して、Windowsの Snipping Tool で録画しました。画面の切り替えやボタンの処理を呼び出して動かしているので、マウスで1回ずつクリックしたものではありません。


スポンサーリンク

まとめ

CustomTkinterで、サイドバー付きのアプリ画面をすぐ作れるテンプレート「CTk App Shell」を紹介しました。

  • サイドバー・ダッシュボード・作業画面・設定画面・メッセージボックスがそろっている
  • 見た目はtheme.py、処理はviews/work.pyのrun_job()を差し替えるだけ
  • アイコンは CTkImage にライト用とダーク用を渡して、自動で切り替える
  • Windows実機で見つかった罠(125%の見切れ・ダイアログの見切れ・タイトルバーとタスクバーの標準アイコン・切替時のまっ暗)は対処済み
GitHubでCTk App Shellをダウンロード

ぜひ、自分のPythonツールに画面を付けるときの土台として使ってみてください。


CustomTkinter Series

CustomTkinterシリーズ

部品の作り方を1つずつ学ぶ3回と、それを全部まとめたテンプレートの4本です。

部品の作り方を学ぶ

全部入りのテンプレート

CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)
VOL.4 テンプレートGitHubで無料配布

CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)

Vol.1〜3で作った部品を、サイドバー・ダッシュボード・設定画面と一緒に1つのアプリにまとめました。中身を差し替えるだけで、自分のPythonツールになります。

この記事です

ニンジン🥕アイコン

Follow me on X

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

@ninjin_py_vba をフォロー

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

Xでポスト はてブ LINE Pocket
まとめハブ
Python
スポンサーリンク

コメント