M.Hiroi's Home Page

Lightweigth Language

新・お気楽 Python/Tkinter 入門

[ PrevPage | Python | NextPage ]

Toplevel と Message

●複数のウィンドウを作る

Tkinter は Tk() でメインウィンドウを生成しますが、このほかにも複数のウィンドウを生成することができます。新しいウィンドウは Tk() でも生成することができますが、この方法では最初のメインウィンドウとは独立したウィンドウ、つまり、新しいメインウィンドウとして扱われます。このため、最初のウィンドウを閉じても、新しいウィンドウはそのまま存在します。メインウィンドウと連動したウィンドウを生成するには Toplevel() を使います。

sub_win = Toplevel()

これで新しいウィンドウが生成されます。Toplevel() は親ウィンドウの指定を省略してもかまいません。メインウィンドウが閉じられると sub_win も閉じられます。また、メインウィンドウで設定したオプションは sub_win でも有効です。あとはいままでのように、ウィンドウ sub_win にウィジェットを配置します。

それでは簡単な例題として、アプリケーションの情報などを表示するためのウィンドウを作ってみましょう。メインウィンドウのメニュー About が選択されたら、新しいウィンドウを開いてメッセージを表示します。まず、メインウィンドウとメニューを設定します。

リスト : メインウィンドウとメニュー

import tkinter as tk

root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 14))

# メニューの設定
m = tk.Menu(root)
root.configure(menu = m)
m.add_command(label = 'About', under = 0, command = message_window)

# ラベルの設定
tk.Label(root, text = 'メニュー About を選んでね').pack()

root.mainloop()

ここまでは簡単ですね。ウィンドウの生成は関数 message_window() で行います。次のリストを見てください。

リスト : ウィンドウの生成

# メッセージの表示
def message_window():
    sub_win = tk.Toplevel()
    tk.Message(sub_win, text = 'message のサンプルプログラムです').pack()

最初に Toplevel() で新しいウィンドウ sub_win を生成します。次に、Message() でメッセージウィジェットを作りテキストを表示します。メッセージウィジェットはラベルと違い、複数行の文字列を表示することができます。デフォルトでは、縦と横の比率が 150 % になるように、文字列を表示する領域を調整します。この例では、text で指定した文字列は 3 行に渡って表示されます。この比率を指定するオプションが aspect です。

aspect は width よりも優先順位が低いので、width の値が優先されます。メッセージウィジェットの場合、width の値は文字数ではなくドット数になるので注意してください。

これでプログラムは完成です。たったこれだけで、メニュー About をクリックするとウィンドウが表示されます。

メインウィンドウ メインウィンドウ

サブウィンドウ About をクリックしてサブウィンドウを表示

ところが、このままでは都合の悪いことがあるのです。このウィンドウを表示したまま、もう一度 About をクリックしてみてください。もうひとつ同じウィンドウが表示されてしまいます。それから、ウィンドウに表示されるタイトルが tk になっています。きちんとしたタイトルをつけた方が良いでしょう。

●ウィジェットの状態を調べる

ウィンドウの状態を調べる場合、Tcl/Tk ではコマンド winfo を使いますが、Tkinter では winfo のサブコマンドに相当するメソッドが多数用意されています。ウィンドウの状態を調べるメソッドの一部を表に示します。

表 : ウィジェットの状態を調べるメソッド
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 のサブコマンドに相当するメソッドが多数用意されています。ウィンドウの状態を設定するメソッドの一部を表に示します。

表 : ウィンドウの設定を行うメソッド (一部)
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 tkinter as tk

root = tk.Tk()
root.title('Main')
root.option_add('*font', ('Noto Sans CJK JP', 14))
sub_win = None

# メッセージの表示
def message_window():
    global sub_win
    if sub_win is None or not sub_win.winfo_exists():
        sub_win = tk.Toplevel()
        sub_win.title('About')
        tk.Message(sub_win, aspect = 200,
                   text = 'message のサンプルプログラムです').pack()

# メニューの設定
m = tk.Menu(root)
root.configure(menu = m)
m.add_command(label = 'About', under = 0, command = message_window)

# ラベルの設定
tk.Label(root, text = u'メニュー About を選んでね').pack()

root.mainloop()

sub_win はグローバル変数として定義し、None で初期化しておきます。sub_win が None でなければ、メソッド winfo_exists() でウィンドウ sub_win が開いているかチェックします。まだ開いていないのであれば、Toplevel() でウィンドウを生成します。次に、title() でタイトルを設定します。あとはいままでと同じです。実際にプログラムを実行すると、ウィンドウが開いた状態でメニュー about をクリックしても、新しいウィンドウは開きません。

サブウィンドウ サブウィンドウ(改良版)


ウィジェットの状態

●state オプション

Tk にはいろいろなウィジェットが用意されていますが、場合によっては、ウィジェットの機能を無効にしたいことがあります。たとえば、ボタンやメニューに割り当てた機能が動作しない場合、ボタンやメニューの選択を無効にしなければいけませんが、そのことをユーザーに知らせた方が使いやすいアプリケーションになります。この場合、ウィジェットの状態を制御する state オプションを使うと便利です。state の値を表に示します。

表 : state の値
normal通常の状態
activeアクティブな状態
disabled無効な状態

ボタンなどのウィジェットでは、その上にマウスカーソルがくるとアクティブな状態になります。Tk では、ウィジェットがアクティブな状態になったときに、そのウィジェットを強調表示することができます。そのことで、マウスボタンを押したときに何か処理が行われることを表すことができます。

state に disabled を設定すると、そのウィジェットは無効な状態になります。ボタンウィジェットであれば、ラベルの色が変わりマウスでボタンをクリックしても押すことができなくなります。テキストの色はオプションで指定することができます。

表 : テキストの色を指定するオプション
activeforegroundアクティブ時の色を指定
activebackgroundアクティブ時の背景色を指定
disabledforeground無効時の色を指定

無効時の背景色は通常の背景色と同じになります。

●ボタンの状態を変更する

それでは簡単な例を示しましょう。ラジオボタンを使ってボタンの状態を設定します。

リスト : ボタンの状態を変更する

import tkinter as tk

root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 14))

var = tk.StringVar()
var.set('normal')

# ボタン
b = tk.Button(root, text = 'button',
              activeforeground = 'green', disabledforeground = 'red')
b.pack(fill = tk.X)

# 状態の変更
def change_state(): b.configure(state = var.get())

# ラジオボタンの設定
for x in ('normal', 'active', 'disabled'):
    tk.Radiobutton(root, text = x, value = x,
                   variable = var, command = change_state).pack(anchor = tk.W)

root.mainloop()

ラジオボタンで選択した値はグローバル変数 var に格納し、関数 change_state() でボタンの状態を変更します。変数 var は、あらかじめ normal に初期化しておきます。change_state() では、configure() を使って state に変数 var の値をセットするだけです。これでボタンの状態を変更することができます。

normal button active button disable button ボタンの状態を変更

●メニューの状態を変更する

次はメニューの状態を変更してみましょう。一般に、メニューには複数の項目を登録しますが、それらの項目に対していろいろなオプションを設定することができます。項目を操作するために複数のメソッドが用意されていますが、オプションを設定するメソッドが entryconfigure() です。

entryconfigure(項目, option, value)

メニュー項目に対する configure コマンドと考えてください。項目の指定には次の方法があります。

表 : entryconfigure の項目
N数値で指定 (先頭の項目が 0 番目となる)
@N画面上端から N ピクセルだけ下にある項目
end, last最後の項目
activeアクティブな状態にある項目
noneどれでもない項目
(全ての項目を非アクティブにするために使用する)
パターンパターンと一致するラベル名を持つ項目

簡単な例として、次のようなメニューを考えてみましょう。

リスト : メニューの状態を変更する

import tkinter as tk

root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 14))

var = tk.StringVar()
var.set('normal')

def dummy(): pass

# メニューの設定
m0 = tk.Menu(root)
root.configure(menu = m0)

m1 = tk.Menu(m0, tearoff = False)
m0.add_cascade(label = 'Menu', under = 0, menu = m1)
m1.add_command(label = 'Menu1', command = dummy)
m1.add_command(label = 'Menu2', command = dummy)
m1.add_command(label = 'Menu3', command = dummy)

# 状態の変更
def change_state():
    m1.entryconfigure('Menu1', state = var.get())

# ラジオボタンの設定
for x in ('normal', 'active', 'disabled'):
    tk.Radiobutton(root, text = x, value = x,
                   variable = var, command = change_state).pack(anchor = tk.W)

root.mainloop()

Menu1 の状態をラジオボタンで設定します。ラジオボタンが選択されたら、change_state() でメニューの状態を変更します。項目の指定にはパターンを使いました。数値を使うよりもこの方がわかりやすいでしょう。メニューも色を指定することができますが、無効時の色を指定する disabledforeground は用意されていません。メニューの状態を disabled に設定すると、Menu1 が灰色に表示され選択することができなくなります。

normal menu 通常のメニュー

disable menu disabled に設定 (Menu1 が灰色)


ウィンドウのリサイズ

今回はウィンドウの大きさを変更してみましょう。もちろん、Tk はデフォルトでウィンドウのリサイズに対応しています。いままでのサンプルプログラムでも、マウスでウィンドウの大きさを変更することができます。ただし、ウィジェットの大きさは変化しません。ウィンドウを小さくしたらウィジェットが表示されなくなった、ということも起こります。まあ、これはウィンドウの大きさを制限することで回避することができます。ですが、アプリケーションによっては、ウィンドウのリサイズに合わせてウィジェットの大きさを変更した方がよい場合もあるでしょう。

●Packer のリサイズ

ところで、ウィジェットのリサイズは面倒だな、と思われた方はいませんか。まじめに考えると、ウィンドウのサイズからウィジェットのサイズを計算して、大きさを変更する処理が必要になるのですが、Tk ではそんな難しいことをする必要はありません。ジオメトリマネージャーに用意されているオプションを設定するだけで、ウィンドウのサイズに合わせてウィジェットの大きさを変更することができます。Packer を使う場合は、次のオプションを設定します。

余白をウィジェットに割り当てただけでは、ウィジェットは大きくなりません。ウィジェットを引き伸ばすための fill オプションを設定してください。それでは簡単な例を示しましょう。次のプログラムを見てください。

リスト : Packer のリサイズ

import tkinter as tk

root = tk.Tk()
root.option_add('*font', ('Noto Sans CJK JP', 10))

tk.Button(root, text = 'button 0').pack(expand = True, fill = 'both')
tk.Button(root, text = 'button 1').pack(expand = True, fill = 'both')

root.mainloop()

2 つのボタン Packer によるボタンの配置

2 つのボタン(大) ウィンドウを拡大する

ウィンドウ全体に 2 つのボタンが表示されます。マウスでウィンドウの大きさを変えてみてください。ウィンドウに合わせてボタンの大きさも変化します。このように、Tk ではオプションを設定するだけで、ウィンドウのリサイズにも簡単に対応することができるのです。

ウィジェットを配置する順番も大切です。Packer はウィンドウが小さくなるとウィジェットを圧縮しますが、本当にスペースが無くなるとウィジェットは表示されなくなります。このとき、配置された逆順でウィジェットが削除されます。つまり、最初に配置されたウィジェットが最後まで残るのです。大切なウィジェットは最初に配置した方がいいでしょう。

●Gridder のリサイズ

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()

4 つのボタン Gridder によるボタンの配置

grid_columnconfigure() は縦方向に配置されたマスのオプションを設定します。次のように、0 列に weight = 1 を設定します。

root.grid_columnconfigure(0, weight = 1)

ボタンはメインウィンドウに配置されているので、grid_columnconfigure() はメインウィンドウのオブジェクト root のメソッドとして呼び出します。これで、ウィンドウが横に大きくなると、0 列に配置されたボタン button 0 と button 1 も横に大きくなります。

4 つのボタン button 0, 1 は横方向に伸びる

1 列目は weight オプションを設定していないので、余白は割り当てられません。それでは、次のプログラムを追加してみましょう。

root.grid_columnconfigure(1, weight = 2)

4 つのボタン 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 つのボタン 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()

キャンバス キャンバスウィジェットを配置

キャンバス ウィンドウを縮小

キャンバス ウィンドウを拡大

キャンバスウィジェットの背景は darkgreen で、その上には赤い四角形が描かれています。ご覧のように、マウスでウィンドウの大きさを変えると、キャンバスウィジェットの大きさは変わりますが、図形の大きさは変わりません。

図形は pack() で配置されているわけではないので、Packer はキャンバスウィジェットを引き伸ばすことはできても、その中の図形を操作することはできないのです。図形はユーザーが定義したものですから、Packer が関知しないのは当然のことですね。したがって、ウィンドウのリサイズに対応するには、ユーザー側で図形を再描画する処理をプログラムする必要があるのです。

●図形の再描画

図形を再描画するには、ウィンドウがリサイズされたときに発生するイベント Configure を使います。このイベントをバインドして、ウィンドウの大きさが変わったら図形を再描画します。バインドはメインウィンドウに対して設定します。

root.bind('<Configure>', change_size)

キャンバスウィジェットは fill と expand を設定して pack されているので、ウィンドウの大きさが変わると、キャンバスの大きさも変わります。このときに図形の大きさを変える関数 change_size() を実行すればいいわけです。

キャンバスウィジェットの大きさですが、これはメソッド cget() では求めることができません。実際、ウィンドウがリサイズされキャンバスウィジェットが引き伸ばされても、最初に設定されたオプションの値そのままになっています。キャンバスウィジェットの大きさを求めるには、ウィジェットの情報を取得するメソッド winfo_width() と winfo_height() を使います。

また、ウィンドウが小さくなると図形が見えなくなるので、ウィンドウの大きさを制限します。これはメソッド minsize() と maxsize() で設定 [*1] することができます。幅と高さはピクセル単位で指定します。

プログラムは次のようになります。

リスト : キャンバスウィジェットのリサイズ (2)

import tkinter as tk

# メインウィンドウの設定
root = tk.Tk()
root.minsize(100, 100)
root.maxsize(400, 400)

# キャンバスの設定
c0 = tk.Canvas(root, bg = 'darkgreen', width = 200, height = 200)
id = c0.create_rectangle(20, 20, 180, 180, fill = 'red')
c0.pack(fill = tk.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() で図形の位置を変更するだけです。とても簡単ですね。たったこれだけの処理で、ウィンドウの大きさに合わせて図形の大きさを変更することができます。

キャンバス ウィンドウを縮小

キャンバス ウィンドウを拡大

-- note --------
M.Hiroi の実行環境 (Python 3.8.0, WSL2 + WSLg, Windows 10) では、minsize(), maxsize() の制限は機能しませんでした。また、title() の引数に日本語を指定すると、タイトルが文字化けします。他の環境 (Windows, CYGWIN + CYGWIN-X) では正常に動作します。WSLg は Windows 10 に対応してからまもないので、安定動作するにはもう少し時間がかかるかもしれません。

初版 2006 年 3 月 4 日
改訂 2023 年 1 月 3 日

Copyright (C) 2006-2023 Makoto Hiroi
All rights reserved.

[
PrevPage | Python | NextPage ]