CustomTkinter は CTk() でメインウィンドウを生成しますが、このほかにも複数のウィンドウを生成することができます。新しいウィンドウは CTk() でも生成することができますが、この方法では最初のメインウィンドウとは独立したウィンドウ、つまり、新しいメインウィンドウとして扱われます。このため、最初のウィンドウを閉じても、新しいウィンドウはそのまま存在します。メインウィンドウと連動したウィンドウを生成するには CTkToplevel() を使います。
sub_win = ctk.CTkToplevel()
これで新しいウィンドウが生成されます。CTkToplevel() は親ウィンドウの指定を省略してもかまいません。メインウィンドウが閉じられると sub_win も閉じられます。また、メインウィンドウで設定したオプションは sub_win でも有効です。あとはいままでのように、ウィンドウ sub_win にウィジェットを配置します。
それでは簡単な例題として、アプリケーションの情報などを表示するためのウィンドウを作ってみましょう。メインウィンドウのボタン About が押されたら、新しいウィンドウを開いてメッセージを表示します。まず、メインウィンドウとボタンを設定します。
リスト : メインウィンドウとボタン
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 16)
# ボタンの設定
ctk.CTkButton(root, text = 'About', font = my_font, command = message_window).pack()
ctk.CTkLabel(root, text = 'About を押してね', font = my_font).pack()
root.mainloop()
ここまでは簡単ですね。ウィンドウの生成は関数 message_window() で行います。次のリストを見てください。
リスト : ウィンドウの生成
# メッセージの表示
def message_window():
sub_win = ctk.CTkToplevel()
ctk.CTkLabel(sub_win, text = 'TopLevel のサンプル\nプログラムです',
justify="center", font = my_font).pack()
最初に CTkToplevel() で新しいウィンドウ sub_win を生成します。複数行のテキストを表示する場合、Tkinter には「メッセージウィジェット」という便利なウィジェットがあるのですが、CustomTkinter にはありません。CTkLabal() のオプション text で改行文字を含む文字列を指定すると、複数行のテキストを表示することができます。この時、オプション justify で文字列の揃えを指定することができます。
これでプログラムは完成です。たったこれだけで、ボタン About を押すとウィンドウが表示されます。
メインウィンドウ
About をクリックしてサブウィンドウを表示
ところが、このままでは都合の悪いことがあるのです。このウィンドウを表示したまま、もう一度 About をクリックしてみてください。もうひとつ同じウィンドウが表示されてしまいます。それから、ウィンドウに表示されるタイトルが CTk になっています。きちんとしたタイトルをつけた方が良いでしょう。
ウィンドウの状態を調べる場合、Tcl/Tk ではコマンド winfo を使いますが、Tkinter では winfo のサブコマンドに相当するメソッドが多数用意されています。これらのメソッドは CustomTkinter でも使用することができます。ウィンドウの状態を調べるメソッドの一部を表に示します。
| widget.winfo_geometry() | ウィジェットの位置を文字列 (幅x高さ+x+y) で返す |
| widget.winfo_width() | ウィジェットの幅を返す |
| widget.winfo_height() | ウィジェットの高さを返す |
| widget.winfo_x() | 親ウィンドウ内での x 座標を返す |
| widget.winfo_y() | 親ウィンドウ内での y 座標を返す |
| widget.winfo_rootx() | ディスプレイ上での x 座標を返す |
| widget.winfo_rooty() | ディスプレイ上での y 座標を返す |
| widget.winfo_exists() | ウィジェットが存在するか |
winfo_geometry() でウィジェットを指定した場合、返される座標はディスプレイを基準にした座標ではなく、そのウィジェットが配置されたウィンドウを基準にした座標となります。また、winfo_x(), winfo_y() でメインウィンドウを指定すると、ディスプレイ上での座標を返します。このほかにも多数のメソッドがあるので詳細は Module docs の Tkinter をお読みください。
このプログラムで必要になる、ウィンドウの存在を調べるメソッドは winfo_exists() です。たとえば、ウィンドウ sub_win を調べるには、sub_win.winfo_exists() とすればいいわけです。sub_win が開いていれば真を、そうでなければ偽を返します。
Tcl/Tk の場合、ウィンドウの設定はコマンド wm (Window Manager) で行いますが、Tkinter では wm のサブコマンドに相当するメソッドが多数用意されています。これらのメソッドも CustomTkinter で使用することができます。ウィンドウの状態を設定するメソッドの一部を表に示します。
| window.withdraw() | ウィンドウを画面から取り除く |
| window.deiconify() | ウィンドウを見える状態に戻す |
| window.iconify() | ウィンドウをアイコン化する |
| window.geometry(string) | ウィンドウを表示する位置を文字列で (幅x高さ+x+y) で指定する |
| window.maxsize(幅, 高さ) | ウィンドウの最大値を指定 |
| window.minsize(幅, 高さ) | ウィンドウの最小値を指定 |
| window.title(タイトル名) | ウィンドウのタイトルを指定 |
このほかにも多数のメソッドがあるので詳細は Module docs の Tkinter をお読みください。
タイトルを設定するにはメソッド title() を使います。ウィンドウ sub_win にタイトルをつけるには、sub_win.title('タイトル') とすればいいわけです。
それではプログラムを改良してみましょう。
リスト : ウィンドウの生成 (改良版)
import customtkinter as ctk
root = ctk.CTk()
root.title('Main')
my_font = ('Noto Sans CJK JP', 16)
sub_win = None
# メッセージの表示
def message_window():
global sub_win
if sub_win is None or not sub_win.winfo_exists():
sub_win = ctk.CTkToplevel()
sub_win.title('About')
ctk.CTkLabel(sub_win, text = 'TopLevel のサンプル\nプログラムです',
justify = "center", font = my_font).pack()
# ボタンの設定
ctk.CTkButton(root, text = 'About', font = my_font, command = message_window).pack(padx = 4, pady = 4)
ctk.CTkLabel(root, text = 'About を押してね', font = my_font).pack(padx = 4, pady = 4)
root.mainloop()
sub_win はグローバル変数として定義し、None で初期化しておきます。sub_win が None でなければ、メソッド winfo_exists() でウィンドウ sub_win が開いているかチェックします。まだ開いていないのであれば、Toplevel() でウィンドウを生成します。次に、title() でタイトルを設定します。あとはいままでと同じです。実際にプログラムを実行すると、ウィンドウが開いた状態でボタン About を押しても、新しいウィンドウは開きません。
サブウィンドウ (改良版)
Tk にはいろいろなウィジェットが用意されていますが、場合によっては、ウィジェットの機能を無効にしたいことがあります。たとえば、ボタンやメニューに割り当てた機能が動作しない場合、ボタンやメニューの選択を無効にしなければいけませんが、そのことをユーザーに知らせた方が使いやすいアプリケーションになります。この場合、ウィジェットの状態を制御する state オプションを使うと便利です。
CustomTkinter の場合、Tkinter とは違って state の値は normal (通常の状態) と disabled (無効な状態) の 2 つだけです。state に disabled を設定すると、そのウィジェットは無効な状態になります。ボタンウィジェットであれば、テキストの色が変わりマウスでボタンをクリックしても押すことができなくなります。無効時のテキストの色はオプション text_color_disabled で指定することができます。無効時の背景色は通常の背景色と同じになります。
それでは簡単な例を示しましょう。ラジオボタンを使ってボタンの状態を設定します。
リスト : ボタンの状態を変更する
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
var = ctk.StringVar()
var.set('normal')
# ボタン
b = ctk.CTkButton(root, text = 'button', font = my_font)
b.pack(padx = 4, pady = 4, fill = 'x')
# 状態の変更
def change_state(): b.configure(state = var.get())
# ラジオボタンの設定
for x in ('normal', 'disabled'):
ctk.CTkRadioButton(root, text = x, value = x, font = my_font,
variable = var, command = change_state).pack(padx = 4, pady = 4, anchor = 'w')
root.mainloop()
ラジオボタンで選択した値はグローバル変数 var に格納し、関数 change_state() でボタンの状態を変更します。変数 var は、あらかじめ normal に初期化しておきます。change_state() では、configure() を使って state に変数 var の値をセットするだけです。これでボタンの状態を変更することができます。
ボタンの状態を変更
ところで、Tkinter のメニューバーはメソッド entryconfigure() を使って state の値を変更することができますが、外部ライブラリ CTkMenuBar にそのような機能はないようです。メニューの状態を変更したい場合は、Tkinter のメニューバーを使ったほうがよさそうです。ご注意くださいませ。
メソッド grid() は格子状にウィジェットを配置するジオメトリマネージャです。ウィンドウを M 行 N 列のセルに分割し、そこにウィジェットを配置するのです。x 方: 向の位置はオプション column で指定し、y 方向の位置は row で指定します。
grid() には pack() とは違うオプション columnspan() と rowspan() があります。これは、複数のセルにまたがってウィジェットを配置するために使います。columnspan は x 方向にまたがるセルの数、rowspan は y 方向にまたがるセルの数を指定します。
それから、pack() ではオプション fill でウィジェットを引き伸ばすことができましたが、grid() ではオプション sticky を使います。
sticky は pack のオプション anchor と同じ機能もあわせ持っています。
簡単な使用例と実行結果を示します。
リスト : ボタンを格子状に配置
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 16)
for x in range(5):
for y in range(5):
b = ctk.CTkButton(root, text = '{}'.format(y * 5 + x + 1), font = my_font)
b.grid(column = x, row = y ) # , sticky = 'ew')
root.mainloop()
ボタンを格子状に配置
今回はウィンドウの大きさを変更してみましょう。もちろん、Tk はデフォルトでウィンドウのリサイズに対応しています。いままでのサンプルプログラムでも、マウスでウィンドウの大きさを変更することができます。ただし、ウィジェットの大きさは変化しません。ウィンドウを小さくしたらウィジェットが表示されなくなった、ということも起こります。
まあ、これはウィンドウの大きさを制限することで回避することができます。ですが、アプリケーションによっては、ウィンドウのリサイズに合わせてウィジェットの大きさを変更した方がよい場合もあるでしょう。
ところで、ウィジェットのリサイズは面倒だな、と思われた方はいませんか。まじめに考えると、ウィンドウのサイズからウィジェットのサイズを計算して、大きさを変更する処理が必要になるのですが、Tk ではそんな難しいことをする必要はありません。ジオメトリマネージャーに用意されているオプションを設定するだけで、ウィンドウのサイズに合わせてウィジェットの大きさを変更することができます。Packer を使う場合は、次のオプションを設定します。
余白をウィジェットに割り当てただけでは、ウィジェットは大きくなりません。ウィジェットを引き伸ばすための fill オプションを設定してください。それでは簡単な例を示しましょう。次のプログラムを見てください。
リスト : Packer のリサイズ
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
ctk.CTkButton(root, text = 'button 0', font = my_font).pack(expand = True, fill = 'both')
ctk.CTkButton(root, text = 'button 1', font = my_font).pack(expand = True, fill = 'both')
root.mainloop()
Packer によるボタンの配置
ウィンドウを拡大する
ウィンドウ全体に 2 つのボタンが表示されます。マウスでウィンドウの大きさを変えてみてください。ウィンドウに合わせてボタンの大きさも変化します。このように、Tk ではオプションを設定するだけで、ウィンドウのリサイズにも簡単に対応することができるのです。
ウィジェットを配置する順番も大切です。Packer はウィンドウが小さくなるとウィジェットを圧縮しますが、本当にスペースが無くなるとウィジェットは表示されなくなります。このとき、配置された逆順でウィジェットが削除されます。つまり、最初に配置されたウィジェットが最後まで残るのです。大切なウィジェットは最初に配置した方がいいでしょう。
Gridder のリサイズは、マスの状態を設定するメソッド grid_columnconfigure() と grid_rowconfigure() で行います。
window.grid_columnconfigure(column_index, options ) window.grid_rowconfigure(row_index, options )
| minsize | 最小の幅/高さを数値で指定する |
| weight | 余白を配分するときの割合を数値で指定する |
| pad | 詰め物を数値で指定する |
リサイズに対応するには、オプション weight に 1 以上の整数値を指定します。簡単な使用例を示しましょう。ボタンを 4 つ Gridder で配置します。
リスト : Gridder のリサイズ
import tkinter as tk
root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 10))
column_data = (0, 0, 1, 1)
row_data = (0, 1, 0, 1)
for x in range(4):
b = tk.Button(root, text = f'button {x}')
b.grid(column = column_data[x], row = row_data[x], sticky = 'nsew')
root.mainloop()
Gridder によるボタンの配置
grid_columnconfigure() は縦方向に配置されたマスのオプションを設定します。次のように、0 列に weight = 1 を設定します。
root.grid_columnconfigure(0, weight = 1)
ボタンはメインウィンドウに配置されているので、grid_columnconfigure() はメインウィンドウのオブジェクト root のメソッドとして呼び出します。これで、ウィンドウが横に大きくなると、0 列に配置されたボタン button 0 と button 1 も横に大きくなります。
button 0, 1 は横方向に伸びる
1 列目は weight オプションを設定していないので、余白は割り当てられません。それでは、次のプログラムを追加してみましょう。
root.grid_columnconfigure(1, weight = 2)
4 つのボタンが横方向に伸びる
今度は、1 列目にも余白が割り当てられますが、-weight オプションの設定が 2 なので 0 列の 2 倍の余白が割り当てられます。つまり、ボタン button 2 と button 3 の方が大きく伸びるわけです。
このままではウィジェットの縦方向が大きくなりません。これに対応するには grid_rowconfigure() を使います。次のプログラムを追加してください。
root.grid_rowconfigure(0, weight = 1) root.grid_rowconfigure(1, weight = 2)
4 つのボタンが縦横方向に伸びる
縦に増えた余白は、0 行と 1 行に 1 対 2 の割合で配分されます。したがって、ウィンドウを大きくするとボタン button 3 がいちばん大きくなります。縦と横の関係で混乱しそうですが、実際にプログラムを動かしてみてください。納得してもらえると思います。
次に、キャンバスウィジェットのリサイズを行ってみましょう。キャンバスもウィジェットなので、pack() や grid() のオプションを指定することで、ウィンドウのリサイズに対応することができます。次のプログラムを実行してください。
リスト : キャンバスウィジェットのリサイズ (1) import tkinter as tk root = tk.Tk() c0 = tk.Canvas(root, bg = 'green', width = 200, height = 200) c0.create_rectangle(20, 20, 180, 180, fill = 'red') c0.pack(fill = tk.BOTH, expand = True) root.mainloop()
キャンバスウィジェットを配置
ウィンドウを縮小
ウィンドウを拡大
キャンバスウィジェットの背景は green で、その上には赤い四角形が描かれています。ご覧のように、マウスでウィンドウの大きさを変えると、キャンバスウィジェットの大きさは変わりますが、図形の大きさは変わりません。
図形は pack() で配置されているわけではないので、Packer はキャンバスウィジェットを引き伸ばすことはできても、その中の図形を操作することはできないのです。図形はユーザーが定義したものですから、Packer が関知しないのは当然のことですね。したがって、ウィンドウのリサイズに対応するには、ユーザー側で図形を再描画する処理をプログラムする必要があるのです。
図形を再描画するには、ウィンドウがリサイズされたときに発生するイベント Configure を使います。このイベントをバインドして、ウィンドウの大きさが変わったら図形を再描画します。バインドはメインウィンドウに対して設定します。
root.bind('<Configure>', change_size)
キャンバスウィジェットは fill と expand を設定して pack されているので、ウィンドウの大きさが変わると、キャンバスの大きさも変わります。このときに図形の大きさを変える関数 change_size() を実行すればいいわけです。
キャンバスウィジェットの大きさですが、これはメソッド cget() では求めることができません。実際、ウィンドウがリサイズされキャンバスウィジェットが引き伸ばされても、最初に設定されたオプションの値そのままになっています。キャンバスウィジェットの大きさを求めるには、ウィジェットの情報を取得するメソッド winfo_width() と winfo_height() を使います。
また、ウィンドウが小さくなると図形が見えなくなるので、ウィンドウの大きさを制限します。これはメソッド minsize() と maxsize() で設定 [*1] することができます。幅と高さはピクセル単位で指定します。
プログラムは次のようになります。
リスト : キャンバスウィジェットのリサイズ (2)
import customtkinter as ctk
import tkinter as tk
# メインウィンドウの設定
root = ctk.CTk()
root.minsize(100, 100)
root.maxsize(400, 400)
# キャンバスの設定
c0 = tk.Canvas(root, bg = 'green', width = 200, height = 200)
id = c0.create_rectangle(20, 20, 180, 180, fill = 'red')
c0.pack(fill = 'both', expand = True)
# 図形の大きさを変更
def change_size(event):
w = c0.winfo_width()
h = c0.winfo_height()
c0.coords(id, 20, 20, w - 20, h - 20)
# バインディングの設定
root.bind('<Configure>', change_size)
root.mainloop()
関数 change_size() の処理は簡単です。メソッド winfo_width() と winfo_height() でキャンバスの大きさを求めたら、メソッド coords() で図形の位置を変更するだけです。とても簡単ですね。たったこれだけの処理で、ウィンドウの大きさに合わせて図形の大きさを変更することができます。
ウィンドウを縮小
ウィンドウを拡大
「スイッチウイジェット」は、従来の Tkinter にある Checkbutton や CustomTkinter の CTkCheckBox の役割を、トグルスイッチ (ON / OFF ボタン) のような UI で実現したものです。設定の 有効 / 無効 の切り替えなど、2 つの状態を選択させる際に使用します。
スイッチウィジェットは CTkSwitch() で生成します。主なオプションを以下に示します。
よく用いられるメソッドを以下に示します。
簡単な使用例と実行結果を示します。
リスト : スイッチウィジェットの使用例
import customtkinter as ctk
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
sw_var = ctk.IntVar()
sw_var.set(0)
def switch_event():
print(f"スイッチの状態: {sw_var.get()}")
sw = ctk.CTkSwitch(root, text = "設定を有効にする", command=switch_event,
variable = sw_var, font = my_font)
sw.pack(padx=20, pady=10)
root.mainloop()
スイッチ OFF の状態
スイッチ ON の状態
CTkSegmentedButton は、複数の選択肢から 1 つを選ぶためのモダンなウィジェットです。従来のラジオボタンを洗練させたような外観で、直感的なタブ切り替えやモード選択に適しています。基本的な使い方は簡単で、値をリスト形式で渡し、command でクリック時の動作を指定します。
ウィジェットは CTkSegmentedButton() で生成します。主なオプションとメソッドを以下に示します。
簡単な使用例と実行結果を示します。
リスト : CTkSegmentedButton の使用例
import customtkinter as ctk
def callback(value):
print("選択された値:", value)
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
seg = ctk.CTkSegmentedButton(root, font = my_font,
values = ["Option 1", "Option 2", "Option 3"],
command = callback)
seg.pack(pady=20)
seg.set("Option 1") # 初期値を設定
root.mainloop()
初期状態
Option 3 を選択
コンボボックスウィジェットはエントリー (CTkEntry) とドロップダウンリスト (CTkOptionMenu) を合わせたようなウィジェットです。ドロップダウンリストから項目を選択することができるとともに、エントリーからテキストを直接入力することもできます。
コンボボックスウィジェットは CTkComboBox() で生成します。主なオプションとメソッドを以下に示します。
簡単な使用例と実行結果を示します。
リスト : コンボボックスの使用例
import customtkinter as ctk
def combobox_callback(choice):
print("選択された値:", choice)
root = ctk.CTk()
my_font = ('Noto Sans CJK JP', 14)
# コンボボックスの作成
cb = ctk.CTkComboBox(
root,
values = ["Option 1", "Option 2", "Option 3"],
command = combobox_callback,
font = my_font
)
cb.pack(padx=20, pady=20)
# 初期値を設定する場合
cb.set("Option 2")
# バインディング
cb.bind('<Return>', lambda event: combobox_callback(cb.get()))
root.mainloop()
初期状態
Option 3 を選択
Option 999 を入力