次は GUI には欠かせないメニューの作り方を説明します。Tk ではメニューのためのウィジェットがいくつか用意されていて、いろいろなメニューを構成することができます。今回は、Windows でも標準になっている「メニューバー」という方法を説明します。
メニューバーを作る場合、最初に Menu() でメニューのオブジェクトを生成します。そして、メインウィンドウのオプション menu に生成したオブジェクトを configure() で設定します。プログラムは次のようになります。
リスト : メニューバーの設定 import tkinter as tk import sys root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) # メニューバー menubar = tk.Menu(root) root.configure(menu = menubar)
これでメインウィンドウにメニューバーが設定されました。あとはこのメニューバーに具体的なメニューを追加します。メニューを設定する主なメソッドを示します。
各メソッドはオプションを設定することができます。add_cascade() を指定すると、そのメニューを選択したときに複数のメニューを表示します。add_checkbutton() は yes/no のような二者択一の情報を設定するために使います。
add_command() はメニューが選択されたときに、オプション command で指定したコールバック関数を実行します。add_radiobutton() は複数の値からひとつを選ぶ場合に使います。add_separator() は区切りを表示するだけです。
add_checkbutton() と add_radiobutton() はメニューバーに直接定義するのではなく、add_cascade() と組み合わせて使うことが一般的です。add_checkbutton() と add_radiobutton() を使う場合、選択する値をオプション value で指定し、その値を格納するオブジェクトをオプション variable で指定します。また、command オプションを設定することもできます。この場合、オブジェクトに値がセットされるとともに、指定した関数が実行されます。
たとえば、リバーシのような対戦ゲームのメニューを考えてみましょう。最低限必要となるメニューは、ゲームの開始、先手と後手の選択、コンピュータの強さの設定、などでしょうか。最初の 2 つはメニュー Games で設定し、強さはメニュー Level で選択することにします。この場合、まず Games と Level をメニューバーに追加します。
リスト : メニュー Games と Level の設定 games = tk.Menu(menubar, tearoff = False) levels = tk.Menu(menubar, tearoff = False) menubar.add_cascade(label="Games", underline = 0, menu=games) menubar.add_cascade(label="Level", underline = 0, menu=levels)
まず Menu() で Games と Level 用のメニューオブジェクトを生成します。これらのメニューは menubar に配置されるので、Menu() の第 1 引数には menubar を指定します。オプション tearoff は、そのメニューをウィンドウから引きちぎることができるかを設定します。デフォルトでは True になっています。その場合、メニューを選択すると一番上に破線が表示され、そこをクリックするとそのメニューが独立したウィンドウになります。
add_cascade() のオプション label はメニューに表示するテキストを設定します。オプション underline (under でも可) は、ラベルの文字に下線を付け加えます。Windows の場合、Alt キーでメニューを選択できますが、この状態で下線のついた文字をキーボードから入力すると、そのメニューを選ぶことができます。そして、オプション menu で表示するメニューオブジェクトを指定します。
それではメニュー Games を設定しましょう。
リスト : Games の設定 games.add_command(label = "Start", underline = 0, command = start) games.add_separator games.add_radiobutton(label = "first", variable = action, value = 0) games.add_radiobutton(label = "second", variable = action, value = 1) games.add_separator games.add_command(label = "exit", underline = 0, command = sys.exit)
Start を選ぶとゲームを開始します。ゲームを開始する関数、これはゲームによって異なりますが、この例では start() を実行します。
先手・後手の選択はラジオボタンを使っています。これで、先手、後手のどちらかを選ぶことができます。たとえば、後手をクリックすると、action の値は 1 にセットされ、ラベルの左側にレ点がつきます。使用するオブジェクトはあらかじめ IntVar() で生成して set() で初期化しておきましょう。
これで Games をクリックすると、Start、先手・後手、Exit という 3 つのメニューが現れます。
Games をクリックしたときの動作
次はメニュー Level の設定です。
リスト : Level の設定 levels.add_radiobutton(label = 'Level 1', variable = level, value = 1) levels.add_radiobutton(label = 'Level 2', variable = level, value = 2) levels.add_radiobutton(label = 'Level 3', variable = level, value = 3)
ラジオボタンを使えば 3 つの中からひとつを選ぶことができます。ゲームの中身は空ですが、このように簡単にメニューを設定することができます。
Level をクリックしたときの動作
リスト : メニューのサンプルプログラム import tkinter as tk import sys root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) # variable 用のオブジェクト action = tk.IntVar() action.set(0) level = tk.IntVar() level.set(1) # ダミー def start(): pass # メニューバー menubar = tk.Menu(root) root.configure(menu = menubar) games = tk.Menu(menubar, tearoff = False) levels = tk.Menu(menubar, tearoff = False) menubar.add_cascade(label="Games", underline = 0, menu=games) menubar.add_cascade(label="Level", underline = 0, menu=levels) # Games games.add_command(label = "Start", underline = 0, command = start) games.add_separator games.add_radiobutton(label = "first", variable = action, value = 0) games.add_radiobutton(label = "second", variable = action, value = 1) games.add_separator games.add_command(label = "exit", underline = 0, command = sys.exit) # Labels levels.add_radiobutton(label = 'Level 1', variable = level, value = 1) levels.add_radiobutton(label = 'Level 2', variable = level, value = 2) levels.add_radiobutton(label = 'Level 3', variable = level, value = 3) # ラベル tk.Label(root, text = "***** Menu Test *****").pack() root.mainloop()
今までの例題は、マウスで操作するものばかりでした。今度はキーボードからの入力を受け付けるウィジェットを説明します。
エントリー (entry) は 1 行の文字列を入力、または編集することができます。例題として、数式を入力して計算する calc.py を作ります。これはとても簡単に作ることができます。まずエントリーから説明しましょう。
エントリーオブジェクトは Entry() で生成します。よく使うオプションは textvariable です。エントリーで入力されたデータは指定したオブジェクトに格納されます。また、オブジェクトの値が変更されると、エントリーの内容も変更されます。面白いオプションが show です。これはパスワードのように画面に見えてはいけない文字列を打ち込むときに使います。たとえば、show = '*' とすれば、入力された文字は * として表示されます。
メソッドは cget(), configure() のほかに、文字列の取得、挿入、削除、カーソルの移動、カット & ペースト、スクロールなど、たくさん用意されていますが、文字列の入力だけならば、それらを使う機会はあまりないでしょう。
それではプログラムを作りましょう。データの入力が完了したらボタンを押してもらってもいいのですが、データはキーボードから入力するのですから、マウスよりもキーボードで操作した方がいいでしょう。リターンキーの入力で式を計算するようにします。キー入力もイベントのひとつですからバインディングを設定することができます。
プログラムは次のようになります。リスト : 式入力電卓 import tkinter as tk from math import * root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) # 式を格納するオブジェクト buffer = tk.StringVar() buffer.set("") # 計算 def calc(event): if buffer.get(): value = eval(buffer.get()) buffer.set(str(value)) # エントリー e = tk.Entry(root, textvariable = buffer) e.pack() e.focus_set() # バインディング e.bind('<Return>', calc) root.mainloop()
計算は関数 eval() を使えば簡単です。eval() は与えられた文字列を Python の式として評価します。とても簡単なプログラムですが、モジュール math をインポートしておけば、sin(), cos(), tan() などの関数を呼び出すことができるので、簡単な関数電卓としても使うことができます。
一般の GUI アプリケーションの場合、キー入力はアクティブになっているウィンドウに渡されます。Tk では、これを「フォーカスウィンドウ (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 tkinter as tk root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) buffer = tk.StringVar() buffer.set('') # キーの表示 def print_key(event): key = event.keysym buffer.set('push key is {}'.format(key)) # ラベルの設定 tk.Label(root, text = '*** push any key ***').pack() a = tk.Label(root, textvariable = buffer) a.pack() a.bind('<Any-KeyPress>', print_key) a.focus_set() root.mainloop()
関数 print_key() にはイベントオブジェクト event が渡されるので、event.keysym で押されたキーの名前を求めることができます。実際に試してみると、F1 や F2 キーには F1, F2 という名前が割り当てられていることがわかります。
F1 キーを押したときの動作
次は、リストボックスとスクロールバーというウィジェットを説明します。リストボックスは複数の文字列を表示し、ユーザーはその中からひとつ以上の文字列を選ぶことができます。スクロールバーは、ほかのウィジェットの表示範囲を制御します。例題として、calc.pl で入力した計算式をリストボックスに格納しておいて、必要なときに取り出せるように改造してみましょう。
最初にリストボックスから説明します。リストボックスのオブジェクトは Listbox() で生成します。Listbox() で指定する主なオプションは表示範囲のコントロールと選択方法です。
xscrollcommand | x 方向のスクロールメソッドを指定 |
yscrollcommand | y 方向のスクロールメソッドを指定 |
selectmode | セレクションモード |
xscrollcommand/yscrollcommand には、スクロールバーウィジェットのメソッド set() を指定します。リストボックスの表示範囲が変更されたときに指定したメソッド set() が呼び出されます。これは Scrollbar() と一緒に説明します。セレクションには次のモードが用意されています。
selectmode のデフォルト値は browse です。データの挿入、削除、取得は次のメソッドで行います。
このほかにもいろいろなメソッドがありますが、とくにスクロールバーに関係する xview/yview メソッドが重要です。これは Scrollbar() のところで説明します。
位置の指定には次の方法があります。
セレクションモードが extended のときにドラッグで選択した場合、最初の行が anchor で最後の行が active となります。したがって、delete に anchor と active を指定すると、選択した行をリストボックスから削除することになります。
次はスクロールバーを説明します。スクロールバーは、その両端に矢印がつき、中央付近には四角いスライダが表示されます。矢印を左クリックするか、スライダをドラッグすることで表示位置を変更します。また、矢印とスライダの隙間をクリックすると 1 画面分スクロールします。スクロールバーは Scrollbar() でオブジェクトを生成します。Scrollbar() で主に使用されるオプションには次のものがあります。
orient | スクロールバーの方向 |
troughcolor | 矢印とスクロールの隙間の色 |
command | スクロールバーが動いたときに実行するメソッド |
orient はスケールと同じくスクロールバーの方向を指定するもので、horizontal または h を指定すると水平になり、vertical または v で垂直になります。command はスクロールバーを動かしたときに実行する関数を指定します。リストボックスとスクロールバーを連動させる場合、ここにはリストボックスの表示位置を制御するメソッド xview() や yview() を指定します。
スクロールバーで重要なメソッドは set() です。
widget.set(*args)
set() には 2 つの引数 first と last を渡します。first と last は 0 から 1 の間の実数で、表示されている範囲を表しています。たとえばリストボックスと連動している場合、全体の行数が 100 行で 20 行目から 30 行分表示されているとすると、set(0.2, 0.5) となります。つまり、データ全体の 20 % の位置から 50 % の位置まで表示されていることを表します。スクロールバーではこのデータからスライダの位置と大きさを調整します。
リストボックスのオプション xscrollcommand や yscrollcommand に set() を指定する場合、次のように行います。
xscrollcommand = widget.set
wodget はスクロールバーのオブジェクトです。リストボックスで表示範囲が変更されると set() が実行されます。このとき、リストボックスの表示範囲 (start, last) が引数として渡されます。これでリストボックスの内容が変更されたとき、それをスクロールバーに反映することができます。
逆に、スクロールバーを変更したとき、それをリストボックスに反映させるためのオプションが command です。ここにリストボックスのメソッド xview や yview を指定します。指定方法は簡単で、リストボックスのオブジェクトを widget とすると次のようになります。
command = widget.yview
スクロールバーの操作によって、メソッドには次に示す文字列が引数として渡されます。
まあ、付け加えられるデータを無理に覚える必要はありません。スクロールバーを使うときは、連動するウィジェットのスクロールオプションに set() を指定して、スクロールバーの command に表示を制御するウィジェットコマンドを指定する、と理解しておけば十分でしょう。
それでは、calc.py を改造しましょう。まず必要なウィジェットを生成します。
リスト : ウィジェットの生成と配置 import tkinter as tk from math import * root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) # 式を格納するオブジェクト buffer = tk.StringVar() buffer.set("") # Entry の生成 e = tk.Entry(root, width = 30, textvariable = buffer) # Listbox の生成 lb = tk.Listbox(root, width = 30, selectmode = 'single') # Scrollbar の生成 sb1 = tk.Scrollbar(root, orient = 'v', command = lb.yview) sb2 = tk.Scrollbar(root, orient = 'h', command = lb.xview) # Listbox の設定 lb.configure(yscrollcommand = sb1.set) lb.configure(xscrollcommand = sb2.set) # grid による配置 e.grid(row = 0, columnspan = 2, sticky = 'ew') lb.grid(row = 1, column = 0, sticky = 'nsew') sb1.grid(row = 1, column = 1, sticky = 'ns') sb2.grid(row = 2, column = 0, sticky = 'ew')
xscrollcommand / yscrollcommand の設定にはスクロールバーのオブジェクトが必要なので、スクロールバーを生成してから configure() で設定します。
リストボックスとスクロールバーは grid() で配置します。grid() は格子状にウィジェットを配置するジオメトリマネージャです。ウィンドウを M 行 N 列のセルに分割し、そこにウィジェットを配置するのです。x 方向の位置はオプション column で指定し、y 方向の位置は row で指定します。
grid() には pack() とは違うオプション columnspan() と rowspan() があります。これは、複数のセルにまたがってウィジェットを配置するために使います。columnspan は x 方向にまたがるセルの数、rowspan は y 方向にまたがるセルの数を指定します。
それから、pack() ではオプション fill でウィジェットを引き伸ばすことができましたが、grid() ではオプション sticky を使います。
n | 上寄せ |
s | 下寄せ |
e | 右寄せ |
w | 左寄せ |
ns | 上下方向に引き伸ばす |
ew | 左右方向に引き伸ばす |
sticky は pack のオプション anchor と同じ機能もあわせ持っています。エントリーはいちばん上に配置しますが、columnspan で x 方向にセルをつなげて、sticky = 'ew' で左右に広げています。
次はバインディングを設定します。リストボックスからデータを選ぶ処理ですが、ダブルクリックしてもらうことにします。プログラムは次のようになります。
リスト : バインディングの設定 # 計算 def calc(event): expr = buffer.get() lb.insert('end', expr) lb.see('end') value = eval(expr) buffer.set(str(value)) e.icursor(0) # 式の取り出し def get_expr(event): xs = lb.curselection() if len(xs) != 0: buffer.set(lb.get(xs[0])) e.focus_set() # バインディング e.bind('<Return>', calc) lb.bind('<Double-1>', get_expr)
エントリーではリターンキーが入力されると関数 calc() が実行されます。calc() では式をリストボックスに代入し、計算結果を buffer にセットします。これが逆になると、答えをリストボックスに代入することになります。それから、メソッド see() を使ってセットした計算式が見えるようにスクロールしています。
リストボックスでダブルクリックすると関数 get_expr() が実行されます。ダブルクリックされた位置からメソッド get() でデータを取り出して buffer にセットします。ダブルクリックですから、位置の指定は active と anchor どちらでもかまいません。
式の履歴が残るように改造した電卓
リスト : リストボックスとスクロールバーのサンプル import tkinter as tk from math import * root = tk.Tk() root.option_add('*font', ('Noto Sans CJK JP', 14)) # 式を格納するオブジェクト buffer = tk.StringVar() buffer.set("") # Entry の生成 e = tk.Entry(root, width = 30, textvariable = buffer) # Listbox の生成 lb = tk.Listbox(root, width = 30, selectmode = 'single') # Scrollbar の生成 sb1 = tk.Scrollbar(root, orient = 'v', command = lb.yview) sb2 = tk.Scrollbar(root, orient = 'h', command = lb.xview) # Listbox の設定 lb.configure(yscrollcommand = sb1.set) lb.configure(xscrollcommand = sb2.set) # 計算 def calc(event): expr = buffer.get() lb.insert('end', expr) lb.see('end') value = eval(expr) buffer.set(str(value)) e.icursor(0) # 式の取り出し def get_expr(event): xs = lb.curselection() if len(xs) != 0: buffer.set(lb.get(xs[0])) e.focus_set() # バインディング e.bind('<Return>', calc) lb.bind('<Double-1>', get_expr) # grid による配置 e.grid(row = 0, columnspan = 2, sticky = 'ew') lb.grid(row = 1, column = 0, sticky = 'nsew') sb1.grid(row = 1, column = 1, sticky = 'ns') sb2.grid(row = 2, column = 0, sticky = 'ew') # フォーカスの設定 e.focus_set() root.mainloop()