CUSTOMTKINTER GUIDE VOL.2
- CustomTkinterでオリジナルのメニューバー(File・表示・設定・Help)を自作する
- こんな人におすすめ
- CustomTkinterにメニューバーが無い理由(tk.Menuとの違い)
- 完成イメージとダウンロード
- ステップ1: メニューバーの土台を作る(CTkFrame+CTkButton)
- ステップ2: customtkinter ドロップダウンメニューの表示/非表示を実装する
- ステップ3: メニュー項目と「外側クリックで閉じる」を実装する
- ステップ4: 4つのメニューに対応させ、排他制御を実装する
- 完成版(sample_final.py):自分のアプリに組み込むテンプレート
- よくある質問
- まとめ
- シリーズ記事一覧
CustomTkinterでオリジナルのメニューバー(File・表示・設定・Help)を自作する
こんにちは。ニンジンです🥕
前回、CustomTkinterの基本の使い方を紹介しました。CTkLabel・CTkButton・CTkEntry・CTkFrameという基本部品と、pack/gridのレイアウトが分かると、簡単な入力フォームまではすぐに作れるようになります。
今回は一歩進んで、Windowsアプリでおなじみの「File・表示・設定・Help」のようなメニューバー(ウィンドウ上部の横並びメニュー)を、CustomTkinterで自作します。実は、CustomTkinterには標準でこの見た目のメニューバー部品が用意されていません。この記事では、その理由と、CTkFrame・CTkButtonを組み合わせて自作する方法を、段階ごとのサンプルコードとダウンロード付きで解説します。
この記事で分かること
- CustomTkinterにメニューバー専用の部品が無い理由
- CTkFrame+CTkButtonでメニューバーの土台を作る方法
- customtkinter ドロップダウンメニューの表示/非表示を切り替える実装
- メニューの外側をクリックしたら閉じる制御
- 複数メニューの排他制御(1つ開いたら他は自動的に閉じる)
- 自分のアプリにそのまま組み込める完成テンプレート(ダウンロード可能)
こんな人におすすめ
- CustomTkinterでツールを作っていて、File/設定などのメニューを追加したい人
- customtkinterのmenuやmenubarで検索してもうまく情報が見つからなかった人
- 完成品だけでなく、実装の考え方も理解したうえで自分のアプリに組み込みたい人
CustomTkinterにメニューバーが無い理由(tk.Menuとの違い)
Tkinter標準のtk.Menuを使えば、app.config(menu=menu)という書き方でOSネイティブのメニューバーを付けることができます。ただし、このtk.MenuはCustomTkinterが提供する部品ではなく、あくまでTkinterの標準機能です。そのため、見た目がOS標準のまま描画され、CustomTkinterのフラットデザインやダークモード配色にはまったく追従しません。
CustomTkinterのアプリにテーマに馴染むメニューバーを付けたい場合、CTkFrameとCTkButtonを組み合わせて自分で再現する必要があります。この記事で作るのは、まさにその「CustomTkinterのテーマに馴染む自作メニューバー」です。
完成イメージとダウンロード
先に完成形の動きを説明すると、次のような挙動になります。
- ウィンドウ上部に「File / 表示 / 設定 / Help」のボタンが横並びになっている
- ボタンをクリックすると、その下にドロップダウンでメニュー項目が表示される
- メニューの外側をクリックすると自動的に閉じる
- 別のメニューボタンをクリックすると、開いていたメニューは閉じて新しいメニューが開く(同時に2つ開かない)
この記事では、この動きを5段階のサンプルコードに分けて、1つずつ機能を追加しながら組み立てていきます。各段階のsample.pyは以下からまとめてダウンロードできます。
サンプル一式(zip)をダウンロードステップ1: メニューバーの土台を作る(CTkFrame+CTkButton)
sample1_base.pyまずは、メニューの中身は考えずに「横一列に並んだボタン」というメニューバーの土台だけを作ります。
import customtkinter as ctk
app = ctk.CTk()
app.geometry("500x350")
app.title("CustomTkinterメニューバー入門")
# メニューバー(横一列のバー)
menubar = ctk.CTkFrame(app, height=36, corner_radius=0)
menubar.pack(side="top", fill="x")
file_btn = ctk.CTkButton(menubar, text="File", width=70, fg_color="transparent", text_color="#1e1a16", hover_color="#e8c4a0", corner_radius=0)
file_btn.pack(side="left")
view_btn = ctk.CTkButton(menubar, text="表示", width=70, fg_color="transparent", text_color="#1e1a16", hover_color="#e8c4a0", corner_radius=0)
view_btn.pack(side="left")
settings_btn = ctk.CTkButton(menubar, text="設定", width=70, fg_color="transparent", text_color="#1e1a16", hover_color="#e8c4a0", corner_radius=0)
settings_btn.pack(side="left")
help_btn = ctk.CTkButton(menubar, text="Help", width=70, fg_color="transparent", text_color="#1e1a16", hover_color="#e8c4a0", corner_radius=0)
help_btn.pack(side="left")
body_label = ctk.CTkLabel(app, text="ここが本体エリアです")
body_label.pack(expand=True)
app.mainloop()ポイントは、各ボタンのfg_color="transparent"です。ボタンの背景をメニューバーと同じ色に透明化することで、「四角いボタンが並んでいる」のではなく「メニューバーに文字が並んでいる」ように見えます。hover_colorだけ薄いオレンジを指定しているので、マウスを乗せたときだけ反応が分かります。
✓ 動作確認方法(sample1_base.py)
python sample1_base.pyで起動する- File・表示・設定・Helpのボタンが横一列に並んで表示される
- ボタンにマウスを乗せると、薄いオレンジ色にハイライトされる
- ボタンをクリックしても、何も起きない(ドロップダウンは出ない) ← これが正しい動作です
この段階ではまだクリック処理(command)を割り当てていないので、「何も起きない」のがステップ1の完成形です。バグではありません。
ステップ2: customtkinter ドロップダウンメニューの表示/非表示を実装する
sample2_toggle.py次に、Fileボタンをクリックしたときに、その下にパネルが表示/非表示になる仕組みを作ります。メニューの中身(項目)はまだ空のままです。
file_menu = ctk.CTkFrame(app, corner_radius=6, border_width=1, border_color="#c05208")
ctk.CTkLabel(file_menu, text="メニュー項目がここに入ります").pack(padx=16, pady=12)
def toggle_file_menu():
if file_menu.winfo_ismapped():
file_menu.place_forget()
else:
x = file_btn.winfo_x()
y = menubar.winfo_height()
file_menu.place(x=x, y=y)
file_menu.lift()
file_btn.configure(command=toggle_file_menu)ドロップダウンの正体は、place()で位置を指定して重ねて表示するだけのCTkFrameです。pack()やgrid()ではなくplace()を使うことで、他の部品のレイアウトに影響を与えずに、好きな座標へ「浮かせて」表示できます。
winfo_ismapped(): そのウィジェットが現在画面に表示されているかどうかを調べるplace_forget():place()で表示していたウィジェットを非表示にするwinfo_x()/winfo_height(): ボタンやメニューバーの位置・サイズをピクセル単位で取得する。これを使ってドロップダウンをボタンの真下に配置しているlift(): 他のウィジェットより手前(上のレイヤー)に表示する
✓ 動作確認方法(sample2_toggle.py)
python sample2_toggle.pyで起動する- Fileをクリック → 「メニュー項目がここに入ります」というプレースホルダーが表示される
- 本体エリアの空白(外側)をクリック → 何も起きない(閉じない) ← この段階ではまだ正しい動作です
- もう一度Fileをクリック → パネルが閉じる(同じボタンの再クリックでの開閉トグルのみ実装済み)
「外側クリックで閉じる」機能は次のステップ3で追加します。この段階で外側クリックに反応しなくても不具合ではありません。
ステップ3: メニュー項目と「外側クリックで閉じる」を実装する
sample3_items.py空だったドロップダウンに、実際の項目(開く・保存・終了)を追加し、それぞれにクリック処理を割り当てます。また、メニューの外側をクリックしたときに自動で閉じる処理も加えます。
def close_file_menu():
file_menu.place_forget()
def on_open():
print("「開く」が押されました")
close_file_menu()
def on_save():
print("「保存」が押されました")
close_file_menu()
def on_exit():
app.destroy()
ctk.CTkButton(file_menu, text="開く", anchor="w", fg_color="transparent", text_color="#1e1a16", hover_color="#f0cba8", command=on_open).pack(fill="x", padx=4, pady=(4, 0))
ctk.CTkButton(file_menu, text="保存", anchor="w", fg_color="transparent", text_color="#1e1a16", hover_color="#f0cba8", command=on_save).pack(fill="x", padx=4)
ctk.CTkButton(file_menu, text="終了", anchor="w", fg_color="transparent", text_color="#1e1a16", hover_color="#f0cba8", command=on_exit).pack(fill="x", padx=4, pady=(0, 4))項目ボタンをクリックしたら、それぞれの処理を実行したあとにclose_file_menu()でメニューを閉じています。「終了」だけはapp.destroy()でウィンドウそのものを閉じる実処理にしてあります。
外側クリックで閉じる処理は、少し工夫が必要です。
file_menu_open = False # 開いているかどうかを自前で管理する
def close_file_menu():
global file_menu_open
file_menu.place_forget()
file_menu_open = False
def toggle_file_menu():
global file_menu_open
if file_menu_open:
close_file_menu()
else:
x = file_btn.winfo_x()
y = menubar.winfo_height()
file_menu.place(x=x, y=y)
file_menu.lift()
file_menu_open = True
file_btn.configure(command=toggle_file_menu)
def in_bbox(widget, event):
x1 = widget.winfo_rootx()
y1 = widget.winfo_rooty()
x2 = x1 + widget.winfo_width()
y2 = y1 + widget.winfo_height()
return x1 <= event.x_root <= x2 and y1 <= event.y_root <= y2
def on_click_outside(event):
if not file_menu_open:
return
if not in_bbox(file_menu, event) and not in_bbox(file_btn, event):
close_file_menu()
app.bind_all("<Button-1>", on_click_outside, add="+")app.bind_all("<Button-1>", ...)で、ウィンドウ内のどこをクリックしても呼ばれる処理を登録します。「開いているかどうか」の判定にfile_menu.winfo_ismapped()(Tkinterが管理する表示状態)をそのまま使いたくなりますが、place()を呼んだ直後は、実際の画面反映がまだ済んでいないタイミングでwinfo_ismapped()がFalseを返すことがあります。commandのクリック処理とbind_allの外側クリック判定はどちらも同じ1回のクリックの中でほぼ同時に走るため、このタイミング次第では「開いた直後なのに、Tkinter的にはまだ開いていないと判定されて即座に閉じられてしまう」という事態が起こり得ます。
そこで、Tkinterの内部状態に頼らず、file_menu_openという自分で管理する変数で「開いているかどうか」を判定するようにしています。place()を呼ぶのと同じタイミングで自分でTrueにしているので、タイミングのズレが起きません。
in_bbox)にすることで、ボタン自身のcommand(トグル処理)と衝突しないようにしています。
✓ 動作確認方法(sample3_items.py)
- コマンドプロンプト(またはターミナル)から
python sample3_items.pyで起動する(コンソール出力を見るため) - Fileをクリック → 「開く」「保存」「終了」の3項目が表示される
- 「開く」をクリック → コンソールに
「開く」が押されましたと表示され、メニューが閉じる - もう一度Fileを開いて「保存」をクリック → 同様にコンソール表示後、閉じる
- もう一度Fileを開いて、メニューの外側(本体エリアの空白)をクリック → 自動的に閉じる(ステップ2との違いはここ)
- 最後に「終了」をクリック → ウィンドウごと閉じる
ステップ4: 4つのメニューに対応させ、排他制御を実装する
sample4_all_menus.pyここまではFileメニュー1つだけの実装でした。最後に、表示・設定・Helpメニューにも同じ仕組みを広げつつ、「1つのメニューを開いたら、他は自動的に閉じる」排他制御を加えます。
最初は、ステップ3と同じようにmenu.winfo_ismapped()で開閉状態を判定する形で実装しました。ところが実際に動かしてみると、Fileボタンをクリックしても、メニューが一瞬たりとも表示されないという現象が起きました。エラーは出ません。ただ何も起きないように見えるのです。
原因を1つずつ処理を追って調べたところ、次のような順番で処理が実行されていることが分かりました。
- Fileボタンを押すと、まずボタン自身の
command(トグル処理)が呼ばれ、file_menu.place()でメニューが配置される - ところが、この直後の時点では
winfo_ismapped()はまだFalseを返す(Tkinter内部の表示状態への反映が追いついていない) - その直後、同じクリックに対して
app.bind_all("<Button-1>", ...)で登録した外側クリック判定処理も呼ばれる - この処理は「どのメニューも
winfo_ismapped()がTrueでない」と判定し、「開いているメニューは無い」と誤解してclose_all_menus()を呼んでしまう - 結果、開いたはずのメニューがその場で消され、画面が再描画される前に閉じてしまう
ステップ3では「どのメニューも開いていなければ何もしない(returnして終了)」という作りだったため、この問題が表面化しませんでした。ステップ4で「デフォルトでは閉じる」という設計に変えたことで、この潜在的なタイミング問題が顕在化した形です。
place()やpack()を呼んだ直後のwinfo_ismapped()は、画面への反映タイミングによっては期待通りの値を返さないことがあります。「今どの状態か」を判定する必要がある処理では、Tkinter側の内部状態を都度問い合わせるのではなく、自分のコードで状態を持って管理したほうが確実です。そこで、「今どのメニューが開いているか」をopen_menu_widgetという変数で自前管理する形に直しました。
menus = [
(file_btn, file_menu),
(view_btn, view_menu),
(settings_btn, settings_menu),
(help_btn, help_menu),
]
# 今どのメニューが開いているかを自前で管理する(winfo_ismapped()には頼らない)
open_menu_widget = None
open_btn_widget = None
def close_all_menus():
global open_menu_widget, open_btn_widget
for _, menu in menus:
menu.place_forget()
open_menu_widget = None
open_btn_widget = None
def open_menu(btn, menu):
global open_menu_widget, open_btn_widget
close_all_menus()
x = btn.winfo_x()
y = menubar.winfo_height()
menu.place(x=x, y=y)
menu.lift()
open_menu_widget = menu
open_btn_widget = btn
def make_toggle(btn, menu):
def toggle():
if open_menu_widget is menu:
close_all_menus()
else:
open_menu(btn, menu)
return toggle
file_btn.configure(command=make_toggle(file_btn, file_menu))
view_btn.configure(command=make_toggle(view_btn, view_menu))
settings_btn.configure(command=make_toggle(settings_btn, settings_menu))
help_btn.configure(command=make_toggle(help_btn, help_menu))ポイントはopen_menu()の中で、新しいメニューを表示する前に必ずclose_all_menus()を呼んでいることです。これにより、どのメニューボタンを押しても「まず全部閉じてから、押されたメニューだけ開く」という動きになり、複数のメニューが同時に開くことがなくなります。open_menu_widgetへの代入もplace()と同じタイミングで行っているので、先ほどのようなタイミングのズレは起きません。
make_toggle()は、ボタンとメニューの組み合わせごとに専用のトグル関数を作って返す関数です。File・表示・設定・Helpの4つで同じロジックを使い回すために、こういう書き方にしています。
外側クリックの判定(in_bbox)も、open_menu_widgetを見る形に書き換えます。
def on_click_outside(event):
if open_menu_widget is None:
return
if not in_bbox(open_menu_widget, event) and not in_bbox(open_btn_widget, event):
close_all_menus()
app.bind_all("<Button-1>", on_click_outside, add="+")
✓ 動作確認方法(sample4_all_menus.py)
- コマンドプロンプトから
python sample4_all_menus.pyで起動する - Fileをクリック → 「開く/保存/終了」が表示される
- Fileを開いたまま、続けて「表示」をクリック(Fileを閉じる操作をせずに直接) → Fileのメニューが自動的に閉じて、代わりに「表示」のメニューが開く ← 排他制御の一番のポイント
- 「表示」の「ダークモード」をクリック → 画面全体が実際にダーク配色に切り替わり、メニューが閉じる
- 再度「表示」を開いて「ライトモード」をクリック → 元の配色に戻る
- 「設定」→「環境設定」をクリック → コンソールに
「環境設定」が押されましたと表示されメニューが閉じる - 「Help」→「バージョン情報」をクリック → コンソールに
「バージョン情報」が押されましたと表示され閉じる - どれか1つ開いた状態で外側をクリック → 自動的に閉じる
手順3(排他制御)が今回いちばんハマった箇所です。ここが正しく動けば、他のメニューも同じ仕組みで動いています。
完成版(sample_final.py):自分のアプリに組み込むテンプレート
sample5_final.pyステップ4までの内容(open_menu_widgetで開閉状態を自前管理する排他制御を含む)を1つのファイルにまとめたものが、ダウンロードできるsample5_final.pyです。実装自体はステップ4と同じですが、ダウンロードしてすぐ使えるように、各メニュー項目のクリック処理にひと工夫を加えています。
def on_open():
print("「開く」が押されました")
# ここに「開く」を押したときの処理を書く
# 例: filedialog.askopenfilename() でファイル選択ダイアログを開く など
close_all_menus()このように、「クリックしたらコンソールにメッセージが表示される」という最低限の動作は最初から用意しつつ、実際の処理を書く場所をコメントで明示しています。ダウンロードした直後に実行してもエラーにならず、メニューがきちんと反応することを確認したうえで、自分の処理を追加していけます。
なお、「表示」メニューの「ダークモード」「ライトモード」と、「File」メニューの「終了」だけは、コメントではなく実際に動く処理(ctk.set_appearance_mode()、app.destroy())にしてあります。ダウンロードしてそのまま実行するだけで、テーマの切り替えとアプリの終了は体験できます。
✓ 動作確認方法(sample5_final.py)
- コマンドプロンプトから
python sample5_final.pyで起動する - 動作の確認手順はステップ4(sample4_all_menus.py)とまったく同じです(File→表示への切り替え、ダークモード、外側クリックで閉じるなど)
- 違いはウィンドウタイトルの表記と、「開く」「保存」「環境設定」「バージョン情報」に処理を追加するためのコメントが入っている点だけです
sample4とsample5で動作に差が出た場合は、どちらか一方だけが古いバージョンのままになっている可能性があります。両方とも最新のzipから展開し直してから比較してください。
よくある質問
QCustomTkinterにmenuのような専用ウィジェットは無いのですか?
Aありません。TkinterのMenuBar/Menu機能(tk.Menu)はそのまま使えますが、CustomTkinterのテーマとは見た目が一致しません。テーマに馴染むメニューバーが欲しい場合は、この記事のようにCTkFrame・CTkButtonで自作する必要があります。
Qplace()ではなくpack()やgrid()でドロップダウンを作ってはいけないのですか?
Apack()やgrid()は、同じ親の中にある他の部品の配置にも影響を与えてしまいます。ドロップダウンのように「他の部品の上に浮かせて、必要なときだけ重ねて表示する」用途には、独立して座標指定できるplace()が向いています。
Qメニューの項目数を増やしたい場合はどうすればいいですか?
A各メニューのCTkFrameに対して、ctk.CTkButton(...).pack(fill="x", padx=4)を追加するだけです。メニューの開閉やレイアウトの仕組みは変更する必要がありません。
Qサブメニュー(項目にカーソルを合わせるとさらにメニューが出る形式)にも対応できますか?
Aこの記事の範囲では扱っていませんが、考え方は同じです。項目のボタンに対して、さらにplace()で別のドロップダウンを表示する処理を追加すれば実現できます。階層が深くなるほど外側クリック判定が複雑になるため、まずは今回の1階層メニューで仕組みに慣れることをおすすめします。
Qなぜ「終了」以外の項目は処理が空(print文だけ)なのですか?
A「開く」「保存」などの処理はアプリごとに内容が大きく異なるためです。空のまま配布して、ダウンロードした人が自分のアプリに合わせて処理を追加できるようにしています。「終了」「テーマ切り替え」は、どんなアプリでも同じ書き方で動く処理なので、あらかじめ実装済みにしてあります。
Q開閉状態の判定にwinfo_ismapped()を使わないのはなぜですか?
Aやplace()pack()を呼んだ直後は、Tkinter内部の表示状態への反映が間に合わず、winfo_ismapped()が一瞬Falseのままになることがあるためです。この記事でも、複数メニューの排他制御を実装した際に、このタイミングのズレが原因でメニューを開いた瞬間に自分自身を閉じてしまうバグに遭遇しました。open_menu_widgetのような自前の変数で状態を管理すれば、このズレを避けられます。
まとめ
この記事では、CustomTkinterに標準搭載されていない「File・表示・設定・Help」形式のメニューバーを、CTkFrame・CTkButton・place()を組み合わせて自作する方法を、5段階に分けて紹介しました。
- CustomTkinterにはテーマに馴染むメニューバー専用部品が無いため、CTkFrame+CTkButtonで自作する
- ドロップダウンは
place()で浮かせて表示し、place_forget()で非表示にする - 外側クリックで閉じる処理は、クリック座標がメニュー・ボタンの範囲内かどうかで判定する
- 複数メニューの排他制御は、新しいメニューを開く前に必ず全メニューを閉じることで実現する
Follow me on X
本ブログの中の人「ニンジン🥕」はXやってます。新着記事はいち早くポストでお知らせしているので、見逃したくない方は @ninjin_py_vba をフォローしてみてください🥕
@ninjin_py_vba をフォローシリーズ記事一覧
電子工作・自動化ツールの製作記録をシリーズごとにまとめています。気になるテーマからお読みください。
-
FILE.01 — IoT
Raspberry Pi Pico W × GASラズパイPico W×GASで自作!「LINEで動く温湿度&スマートリモコン」完全ロードマップ
「外出中のペットの室温が心配…」そんな思いからスタートしたIoT自作プロジェクトの総集編。温湿度監視からエアコン遠隔操作まで、仕組みをゼロから作りたい方のための全5回の開発記録です。
シリーズを読む -
FILE.02 — CNC
Arduino × レーザー刻印CNCCNC自作シリーズ
高精度な加工を目指し、本格的な自作CNC製作に挑戦中。現在は基幹パーツであるオリエンタルモーターの納品を待つ「設計・準備編」を公開。ハードとソフトの両面から、理想のマシンを形にする過程をリアルタイムにお届けします。
シリーズを読む -
FILE.03 — XY軸制御
Raspberry Pi × ステッピングモーターXYテーブルシリーズ
Raspberry Piとステッパモーターを使い、ゼロから2軸制御に挑む記録。OS設定から回路設計、多軸制御のPythonコードまで、躓きやすいポイントを徹底図解。電子工作初心者が「動く感動」を味わうための実戦ガイドです。
シリーズを読む -
FILE.04 — 業務自動化
Python × Excel × CustomTkinter【Python開発記】NINJIN Mail制作記
PythonとExcelを連携させ、実務で即戦力となるメール送信ツールを開発。CustomTkinterによるUI構築や、ミスを防ぐテンプレート活用術など、現場の「痒い所に手が届く」自動化ノウハウを細部まで丁寧に解説します。
シリーズを読む -
FILE.05 — 業務自動化
Python × Excel × GUIアプリ化Invoice Maker(請求書・見積書自動作成)
手作業の請求書作成から卒業!PythonでExcelデータを読み込み一括PDF化する「Invoice_Maker」の作り方を全3回で解説。基本ロジックからGUIアプリ化まで、実務で役立つ自動化ノウハウが満載です。
シリーズを読む -
FILE.06 — 業務自動化
Access × VBA【Access VBA開発記】対応履歴管理データベース
問い合わせ・トラブル対応履歴がExcelでバラバラに散らばる悩みから、Access+VBAで検索性の高い管理データベースを自作。テンプレート配布から設計解説、Win32 API実装で味わった失敗談まで、生成AI時代の検証記録を全3回でお届けします。
シリーズを読む



コメント