次は GUI には欠かせないメニューの作り方を説明します。CustomTkinter には標準のメニューバー・ウィジェット(Tkinter の Menu に相当するもの)は存在しません。Tkinter の Menu を流用することもできますが、CustomTkinter のテーマは適用されません。外部ライブラリ CTkMenuBar を導入すると、CustomTkinter にモダンなメニューバーを追加することができます。
CTkMenuBar は次のコマンドでインストールすることができます。
(.venv) $ pip install CTkMenuBar
M.Hiroi の環境では CTkMenuBar-0.9 がインストールされました。
今回は CTkMenuBar を使って Windows でも標準になっている「メニューバー」という方法を説明します。
メニューバーを作る場合、CTkMenuBar() でメニューバーのオブジェクトを生成します。主なオプションを以下に示します。
master を使わないで、第 1 引数に root を渡してもかまいません。プログラムは次のようになります。
リスト : メニューバーの設定
import customtkinter as tk
from CTkMenuBar import CTkMenuBar, CustomDropdownMenu
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# メニューバー
menu_bar = CTkMenuBar(root) # master = root としてもよい
これでメインウィンドウにメニューバーが設定されました。
あとはこのメニューバーに具体的なメニューを追加します。メニューを設定するメソッドが add_cascade() です。メニューバー上に表示される「ファイル」や「編集」といったボタンの設定です。主なオプションを以下に示します。
たとえば、リバーシのような対戦ゲームのメニューを考えてみましょう。最低限必要となるメニューは、ゲームの開始、先手と後手の選択、コンピュータの強さの設定、などでしょうか。最初の 2 つはメニュー Games で設定し、強さはメニュー Level で選択することにします。この場合、次のように Games と Level をメニューバーに追加します。
リスト : メニューバーにメニューを登録
games = menu_bar.add_cascade("Games", font = my_font)
level = menu_bar.add_cascade("Level", font = my_font)
メニューバーのメニューをクリックすると、ドロップダウンリストが表示されます。これを定義するのが CustomDropdownMenu() です。主なオプションを以下に示します。
CustomDropdownMenu には以下に示すメソッドが用意されています。
たとえば、Games に項目 Start, First (先手), Second (後手), Exit を追加するには、次のようにプログラムします。
リスト : ドロップダウンメニューの設定
games_menu = CustomDropdownMenu(widget = games)
games_menu.add_option(option="Start", command=lambda: print("Start clicked"))
games_menu.add_separator()
games_menu.add_option(option="First", command=lambda: print("First clicked"))
games_menu.add_option(option="Second", command=lambda: print("Second clicked"))
games_menu.add_separator()
games_menu.add_option(option="Exit", command=root.quit)
CustomDropdownMenu() でドロップダウンメニューを作り、それを games に結びつけます。あとは、add_option() で必要な項目を追加するだけです。これで Games をクリックすると、Start, First, Second, Exit という 4 つの項目が現れます。
Games をクリックした時の動作
次はメニュー Level の設定です。メニュー Level に項目 Level 1, 2, 3 を追加するには次のようにプログラムします。
リスト : ドロップダウンメニューの設定 (2)
level_menu = CustomDropdownMenu(widget = level)
level_menu.add_option(option="Level 1", command=lambda: print("Level 1 clicked"))
level_menu.add_option(option="Level 2", command=lambda: print("Level 2 clicked"))
level_menu.add_option(option="Level 3", command=lambda: print("Level 3 clicked"))
Level をクリックした時の動作
ゲームの中身は空ですが、このように簡単にメニューを設定することができます。
ドロップダウンリストから項目を選択するだけでよければ、CustomTkinter の標準ウィジェット CTkOptionMenu を使うと便利です。主なオプションを以下に示します。
簡単な使用例と実行結果を示します。
リスト : CTkOptionMenu の使用例
import customtkinter as ctk
root = ctk.CTk()
root.geometry("200x100")
def optionmenu_callback(choice):
print("Selected:", choice)
# CTkOptionMenuを作成
optionmenu = ctk.CTkOptionMenu(root, values=["Option 1", "Option 2", "Option 3"],
command=optionmenu_callback)
optionmenu.pack()
root.mainloop()
メニューをクリックしたときの動作
リスト : メニューのサンプルプログラム
import customtkinter as ctk
from CTkMenuBar import CTkMenuBar, CustomDropdownMenu
root = ctk.CTk()
root.geometry("200x200")
my_font = ('Noto Sans CJK JP', 14)
# メニューバー
menu_bar = CTkMenuBar(root) # master = root としてもよい
games = menu_bar.add_cascade("Games", font = my_font)
level = menu_bar.add_cascade("Level", font = my_font)
# ドロップダウンメニューの設定
games_menu = CustomDropdownMenu(widget = games)
games_menu.add_option(option="Start", command=lambda: print("Start clicked"))
games_menu.add_separator()
games_menu.add_option(option="First", command=lambda: print("First clicked"))
games_menu.add_option(option="Second", command=lambda: print("Second clicked"))
games_menu.add_separator()
games_menu.add_option(option="Exit", command=root.quit)
level_menu = CustomDropdownMenu(widget = level)
level_menu.add_option(option="Level 1", command=lambda: print("Level 1 clicked"))
level_menu.add_option(option="Level 2", command=lambda: print("Level 2 clicked"))
level_menu.add_option(option="Level 3", command=lambda: print("Level 3 clicked"))
root.mainloop()
今までの例題は、マウスで操作するものばかりでした。今度はキーボードからの入力を受け付けるウィジェットを説明します。
エントリーウィジェットは 1 行の文字列を入力、または編集することができます。例題として、数式を入力して計算する calc.py を作ります。これはとても簡単に作ることができます。まずエントリーから説明しましょう。
CustomTkinter の場合、エントリーオブジェクトは CTkEntry() で生成します。よく使うオプションは textvariable です。エントリーで入力されたデータは指定したオブジェクトに格納されます。また、オブジェクトの値が変更されると、エントリーの内容も変更されます。面白いオプションが show です。これはパスワードのように画面に見えてはいけない文字列を打ち込むときに使います。たとえば、show = '*' とすれば、入力された文字は * として表示されます。
メソッドは cget(), configure() のほかに、文字列の取得、挿入、削除、カーソルの移動、カット & ペースト、スクロールなど、たくさん用意されていますが、文字列の入力だけならば、それらを使う機会はあまりないでしょう。
それではプログラムを作りましょう。データの入力が完了したらボタンを押してもらってもいいのですが、データはキーボードから入力するのですから、マウスよりもキーボードで操作した方がいいでしょう。リターンキーの入力で式を計算するようにします。キー入力もイベントのひとつですからバインディングを設定することができます。
プログラムは次のようになります。
リスト : 式入力電卓
import customtkinter as ctk
from math import *
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# 式を格納するオブジェクト
buffer = ctk.StringVar()
buffer.set("")
# 計算
def calc(event):
if buffer.get():
value = eval(buffer.get())
buffer.set(str(value))
# エントリー
e = ctk.CTkEntry(root, textvariable = buffer, width = 200, font = my_font)
e.pack(padx = 4, pady = 4)
e.focus_set()
# バインディング
e.bind('<Return>', calc)
root.mainloop()
計算は関数 eval() を使えば簡単です。eval() は与えられた文字列を Python の式として評価します。とても簡単なプログラムですが、モジュール math をインポートしておけば、sin(), cos(), tan() などの関数を呼び出すことができるので、簡単な関数電卓としても使うことができます。
一般の GUI アプリケーションの場合、キー入力はアクティブになっているウィンドウに渡されます。Tk (Tkinter, CustomTkinter) では、これを「フォーカスウィンドウ (focus window)」といいます。フォーカスウィンドウは、マウスの操作によって変更することができますが、メソッド focus_set() によってプログラムで設定することができます。
widget.focus_set()
e.focus_set() でフォーカスウィンドウをエントリーに設定します。これですぐに式を入力することができます。
数式を入力する
リターンキーで計算する
このプログラムのポイントはメソッド bind() です。
widget.bind(eventsequence, callback)
すでにバインドされているコールバック関数がある場合、新しい関数に差し替えられます。callback を省略すると、そのイベントにバインドされている関数が返されます。引数を省略すると、そのウィジェットにバインドされているすべてのイベントをタプルに格納して返します。
それから、bind() で設定されたコールバック関数が呼び出される場合、引数にはイベントを表すオブジェクト(クラス Event のインスタンス)が渡されます。このオブジェクトからイベントの詳細情報を取得することができます。
イベントの指定は次のような構文を持っています。
<modifier-modifier-type-detail>
type は GUI 環境上で発生するイベントタイプを表します。ユーザーが操作するときに発生する主なイベントタイプには次のようなものがあります。
| Key, KeyPress | キーが押された |
| KeyRelease | キーが離された |
| Button, ButtonPress | マウスのボタンが押された |
| ButtonRelease | マウスのボタンが離された |
| Motion | マウスの移動 |
| Enter | マウスカーソルがウィンドウの中に入った |
| Leave | マウスカーソルがウィンドウから出た |
このほかにも、ウィンドウが破棄されたときに発生するイベントなど、様々なイベントタイプがあります。
マウスとキーのイベントには、ボタンやキーの種類を detail で指定します。マウスでは左ボタンが 1 となります。キーの種類は名前で指定します。英数字はその文字がそのまま名前となります。このほかに、改行キーに対する Return、バックスペースキーに対する BackSpace などがあります。
detail を指定する場合は type を省略することができます。ただし、<1> という指定は <KeyPress-1> ではなく <Button-1> となるので注意してください。また、通常の英数字の場合、<> も省略することができます。つまり、<KeyPress-a> は a と書くことができます。それから、<KeyPress> のように detail を省略すると、種類によらずキーが押されたときにバインドされたコマンドが実行されます。
イベントタイプの前にはモディファイア (modifier) をつけることができます。たとえば、<Control-d> はコントロールキーと d キーを同時に押したときのイベントを表します。主なモディファイアを次に示します。
| Control | Ctrl キーを押しながらの入力 |
| Shift | Shift キーを押しながらの入力 |
| Alt | Alt キーを押しながらの入力 |
| Button1, B1 | マウスの左ボタンを押しながらの入力 |
| Button3, B3 | マウスの右ボタンを押しながらの入力 |
| Double | ダブルクリック |
| Triple | トリプルクリック |
Tk の出身地である X Window は 3 ボタンマウスを使うので、Button2 は右ボタンではなく中ボタンとなります。たとえば、左ボタンのダブルクリックに対応するイベントは <Double-1> となります。また、イベントタイプは複数個指定することができます。たとえば、<Escape>a はEsc キーが押されたあとで a キーを押したイベントに対応します。
Tkinter の場合、イベントの詳細情報はイベントオブジェクトのインスタンス変数にセットされてコールバック関数に渡されます。たとえば、次の表に示す情報が格納されています。
| 変数名 | データ |
|---|---|
| num | マウスボタンの番号 |
| x,y | マウスカーソルの座標 |
| time | イベントの発生時刻 |
| char | キーに対応する文字 |
| keysym | キーに対応する名前 |
これ以外にも、いろいろな情報がありますので、詳細は Tkinter のマニュアルや Module Docs をお読みください。たとえば、次のプログラムを実行すると、キーに対応する名前を表示することができます。
リスト : キーの名前を表示
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 16)
buffer = ctk.StringVar()
buffer.set('')
# キーの表示
def print_key(event):
key = event.keysym
buffer.set('push key is {}'.format(key))
# ラベルの設定
ctk.CTkLabel(root, text = '*** push any key ***', font = my_font).pack(pady = 2)
a = ctk.CTkLabel(root, textvariable = buffer, font = my_font)
a.pack(pady = 2)
a.bind('<Any-KeyPress>', print_key)
a.focus_set()
root.mainloop()
関数 print_key() にはイベントオブジェクト event が渡されるので、event.keysym で押されたキーの名前を求めることができます。実際に試してみると、F1 や F2 キーには F1, F2 という名前が割り当てられていることがわかります。
左側の Alt キーを押したときの動作
フレームは、複数のウィジェットをひとまとめにする入れ物として使われるウィジェットです。単純なウィジェットですが、ジオメトリマネージャと組み合わせることで、複雑なウィジェットの配置にも簡単に対応することができます。
フレームウィジェットは CTkFrame() で生成します。主なオプションを以下に示します。
オプションを指定する必要はほとんどないと思います。
フレームにウィジェットを配置する方法はいままでと同じですが、もうひとつ、ジオメトリマネージャのオプション in (in_) を使う方法があります。in は Python のキーワードなので CustomTkinter や Tkinter では in_ を使います。簡単な例題として、ボタンを 3 つ横に並べて、その下にボタンを縦に 3 つ配置するプログラムを作ってみます。
これは Grid マネージャを使ってもプログラムできますが、ふたつのフレームと Pack マネージャを用いることで簡単に実現できます。横に並べるボタンをフレーム f0 に配置し、縦に並べるボタンをフレーム f1 に配置します。次のリストを見てください。
リスト : フレームにボタンを配置する
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# フレームの生成
f0 = ctk.CTkFrame(root)
f1 = ctk.CTkFrame(root)
# f0 にボタンを配置する
ctk.CTkButton(f0, text = 'button 00', font = my_font).pack(side = 'left', padx = 2, pady = 2)
ctk.CTkButton(f0, text = 'button 01', font = my_font).pack(side = 'left', padx = 2, pady = 2)
ctk.CTkButton(f0, text = 'button 02', font = my_font).pack(side = 'left', padx = 2, pady = 2)
# f1 にボタンを配置する
ctk.CTkButton(root, text = 'button 10', font = my_font).pack(in_ = f1, fill = 'both', padx = 2, pady = 2)
ctk.CTkButton(root, text = 'button 11', font = my_font).pack(in_ = f1, fill = 'both', padx = 2, pady = 2)
ctk.CTkButton(root, text = 'button 12', font = my_font).pack(in_ = f1, fill = 'both', padx = 2, pady = 2)
# フレームの配置
f0.pack()
f1.pack(fill = 'both')
root.mainloop()
フレーム f0 にはいままでと同じ方法でボタンを配置します。このボタンを pack するとフレーム f0 に配置されます。この段階ではフレームにボタンを詰め込んだだけなので、まだウィンドウには表示されません。メインウィンドウにフレームを配置しないとボタンは表示されないのです。
フレーム f1 には、in_ オプションを使ってボタンを配置します。この場合、ボタンはメインウィンドウ root から生成しますが、pack の in_ オプションによりフレーム f1 に配置されます。なお、in_ オプションは grid() でも使用することができます。
最後にフレーム f0 と f1 を pack() で配置します。これでボタンが表示されます。それから、縦に配置したボタンを引き伸ばすため、fill オプションを指定します。この場合、ボタンをフレームに配置するときと、フレームをウィンドウに配置するときの 2 か所で指定する必要があります。片方だけではボタンを引き伸ばすことはできません。ご注意くださいませ。
フレームを使ってボタンを配置する
CTkScrollableFrame は「スクロールバー付きのフレーム」ウィジェットです。通常の Tkinter ではフレームをスクロールさせるために Scrollbar と組み合わせる複雑な実装が必要ですが、このウィジェットなら簡単に作成することができます。
主なオプションを以下に示します。
基本的な使い方は簡単です。ウィジェットを作成し、その中に他のパーツ (ラベルやボタンなど) を配置するだけでスクロール可能になります。
簡単な使用例と実行結果を示します。
リスト : スクロールバー付きフレームの使用例
import customtkinter as ctk
root = ctk.CTk()
# スクロール可能なフレームを作成
scrollable_frame = ctk.CTkScrollableFrame(root, width=100, height=200)
scrollable_frame.pack(padx=2, pady=2)
# フレーム内に大量のラベルを追加
for i in range(20):
label = ctk.CTkLabel(scrollable_frame, text= f"Item {i}" , font=('Noto Sans CJK JP', 14))
label.pack(pady=5)
root.mainloop()
初期状態
最後尾にスクロールした状態
今回はリストボックスというウィジェットを説明します。リストボックスは複数の文字列を表示し、ユーザーはその中からひとつ以上の文字列を選ぶことができます。Tkinter には Listbox ウィジェットがありますが、CustomTkinter にはありません。外部ライブラリ CTkListbox を導入すると、CustomTkinter にモダンなリストボックスを追加することができます。
CTkListbox は次のコマンドでインストールすることができます。
(.venv) $ pip install CTkListBox
M.Hiroi の環境では CTkListbox-1.5 がインストールされました。
今回は CTkListbox を使って、calc.pl で入力した計算式をリストボックスに格納しておいて、必要なときに取り出せるように改造してみましょう。
リストボックスのオブジェクトは CTkListbox() で生成します。基本的な使い方は簡単で、メソッド insert() で項目を追加し、command オプションで項目を選択した時の動作を指定します。項目が多くてウィンドウに収まらない場合、右側にスクロールバーが自動的に設置されるので、Tcl/Tk や Tkinter のリストボックスよりも簡単に使用することができます。
CTkListbox で重要なオプションが multiple_selection です。値が False (規定値) の場合、項目を 1 つだけ選択できます。別の項目をクリックすると前の選択は解除されます。True に設定すると、複数の項目を個別にクリックして選択・解除できます (トグル形式)。
データの挿入、削除、取得は次のメソッドで行います。
このほかにもいろいろなメソッドがあります。詳細は CTkListbox のドキュメントをお読みください。
位置の指定には次の方法があります。
簡単な使用例を示します。
リスト : リストボックスの使用例
import customtkinter as ctk
from CTkListbox import *
def show_value(selected_option):
print(selected_option)
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 16)
lb = CTkListbox(root, font = my_font, command=show_value)
lb.pack(padx=10, pady=10)
for n in range(8):
lb.insert('end', f'option {n}')
root.mainloop()
option 0 を選択
option 7 を選択
それでは calc.py を修正しましょう。最初に、CTkEntry と CTkListbox のオブジェクト (ウィジェット) を生成します。
リスト : ウィジェットの生成
import customtkinter as ctk
from CTkListbox import *
from math import *
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# 式を格納するオブジェクト
buffer = ctk.StringVar()
buffer.set("")
# Entry の生成
e = ctk.CTkEntry(root, width = 200, font = my_font, textvariable = buffer)
# Listbox の生成
lb = CTkListbox(root, width = 200, height = 300, font = my_font)
オプションでフォントと width / height を指定するだけなので、とくに難しいところはないと思います。
次はバインディングを設定します。リストボックスからデータを選ぶ処理ですが、ダブルクリックしてもらうことにします。プログラムは次のようになります。
リスト : バインディングの設定
# 計算
def calc(event):
expr = buffer.get()
lb.insert('end', expr)
lb.see(lb.size() - 1) # 'end' は機能しない
value = eval(expr)
buffer.set(str(value))
e.icursor(0)
# 式の取り出し
def get_expr(event):
x = lb.curselection()
buffer.set(lb.get(x))
lb.deactivate(x)
e.focus_set()
# バインディング
e.bind('<Return>', calc)
lb.bind('<Double-1>', get_expr)
エントリーではリターンキーが入力されると関数 calc() が実行されます。calc() では式をリストボックスに代入し、計算結果を buffer にセットします。これが逆になると、答えをリストボックスに代入することになります。
それから、メソッド see() を使ってセットした計算式が見えるようにスクロールしています。Tkinter では see('end') で最後尾にスクロールするのですが、CTkListbox では動作しません。メソッド size() で項目数を求め、最後尾の項目の位置を計算して see() に渡しています。
リストボックスでダブルクリックすると関数 get_expr() が実行されます。ダブルクリックされた位置からメソッド get() でデータを取り出して buffer にセットします。選択した項目は active 状態になっています。これを元に戻すのがメソッド deactivate() です。
最後に、ウィジェットを pack で配置します。プログラムは次のようになります。
リスト : pack による配置 e.pack(padx = 2, pady = 2) lb.pack(padx = 2, pady = 2, fill = 'both') # フォーカスの設定 e.focus_set() root.mainloop()
これも簡単ですね。それでは実行してみましょう。
式の履歴が残るように改造した電卓
リスト : リストボックスのサンプル
import customtkinter as ctk
from CTkListbox import *
from math import *
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# 式を格納するオブジェクト
buffer = ctk.StringVar()
buffer.set("")
# Entry の生成
e = ctk.CTkEntry(root, width = 200, font = my_font, textvariable = buffer)
# Listbox の生成
lb = CTkListbox(root, width = 200, height = 300, font = my_font)
# 計算
def calc(event):
expr = buffer.get()
lb.insert('end', expr)
lb.see(lb.size() - 1) # 'end' は機能しない
value = eval(expr)
buffer.set(str(value))
e.icursor(0)
# 式の取り出し
def get_expr(event):
x = lb.curselection()
buffer.set(lb.get(x))
lb.deactivate(x)
e.focus_set()
# バインディング
e.bind('', calc)
lb.bind('', get_expr)
# pack による配置
e.pack(padx = 2, pady = 2)
lb.pack(padx = 2, pady = 2, fill = 'both')
# フォーカスの設定
e.focus_set()
root.mainloop()