CUSTOMTKINTER GUIDE VOL.4
- CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)
- こんな人におすすめ
- CTk App Shellでできること
- ダウンロードと起動(pip install -r requirements.txt)
- フォルダ構成と、画面を増やす手順
- サイドバーのアイコンを、ライト/ダークで自動で切り替える(CTkImage)
- ライト/ダークの切替と、色の決め方
- ダッシュボードはgridの12列で組む(UIの大きさ125%でも崩れない)
- 作業画面に自分の処理をつなぐ(別スレッドで実行)
- テーマに合うメッセージボックス4種(messagebox)
- Windows実機で動かして見つかった4つの罠
- customtkinterのバージョンを固定している理由
- よくある質問
- まとめ
CustomTkinterのテンプレートを無料配布|サイドバー付きのモダンなダークUI(GitHub公開)
こんにちは。ニンジンです🥕
CustomTkinterで、サイドバー付きのアプリ画面をすぐに作れるテンプレート「CTk App Shell」を作りました。GitHubで無料公開しています。まずは、動いているところを見てください。
Pythonで作った処理に画面を付けたいとき、毎回ゼロからレイアウトを組むのはけっこう大変です。このテンプレートは、サイドバー、ダッシュボード、作業画面、設定画面、メッセージボックスまで一通りそろえてあるので、中身を差し替えるだけで「それっぽいツール」になります。
この記事で分かること
- CTk App Shellでできること(3つの画面とメッセージボックス)
- ダウンロードと起動の方法(
pip install -r requirements.txt) - サイドバーのアイコンを、ライト/ダークに合わせて自動で切り替える方法(CTkImage)
- gridで組んだダッシュボードを、UIの大きさ125%でも崩さないコツ
- 作業画面に自分の処理をつなぐ方法(別スレッドで実行)
- Windows実機で動かして見つかった4つの罠と、その直し方
こんな人におすすめ
- CustomTkinterの基本は分かったので、見た目の整ったアプリを作ってみたい人
- Pythonの自動化スクリプトに、人に渡せる画面を付けたい人
- AIに書かせたCustomTkinterのコードが、思ったような見た目にならずに困っている人
ダウンロードと起動(pip install -r requirements.txt)
- GitHubのページで「Code」→「Download ZIP」を押して、一式をダウンロードする(gitが使える人は
git cloneでも大丈夫です)git clone https://github.com/kurotyy-svg/ctk-app-shell.git - zipを展開する(アイコン画像などを一緒に読み込むので、zipの中から直接実行しないでください)
- 展開したフォルダの中で、コマンドプロンプト(またはPowerShell)を開く
- 次の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にしています。
ダッシュボードはgridの12列で組む(UIの大きさ125%でも崩れない)
設定の「UIの大きさ」を125%にすると、ウィンドウの大きさはそのままで、中の部品だけが1.25倍になります。最初の版では、この状態で数字のカードが窮屈になり、数字が見切れていました。
直したところは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で解説しています。
作業画面に自分の処理をつなぐ(別スレッドで実行)
- 「参照」でフォルダを選ぶと、パスが「入力フォルダ」の欄に入ります
- 「実行」を押すと、確認ダイアログ(はい/いいえ)が出ます。フォルダを選んでいないと警告が出ます
- 「はい」を押すと
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)
標準の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: タイトルバーとタスクバーに、標準のアイコンが出る
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をほかのアプリと重ならない名前に変えてください。
罠4: ライト/ダークを切り替えた直後に画面を移ると、一瞬まっ暗になる
最初の版は、表示しない画面を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%の見切れ・ダイアログの見切れ・タイトルバーとタスクバーの標準アイコン・切替時のまっ暗)は対処済み
ぜひ、自分のPythonツールに画面を付けるときの土台として使ってみてください。
CustomTkinter Series
CustomTkinterシリーズ
部品の作り方を1つずつ学ぶ3回と、それを全部まとめたテンプレートの4本です。
部品の作り方を学ぶ
VOL.1CustomTkinterのインストールと使い方|最初のウィンドウから入力フォームまでコード付き解説
VOL.2CustomTkinterでオリジナルのメニューバー(File・表示・設定・Help)を自作する
VOL.3CustomTkinterでオリジナルのMessageBox(確認・警告・エラー)を自作する
全部入りのテンプレート
Follow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォロー


コメント