step04 tkinterライブラリの使い方

ここまでのstepでは、GUIツールキットであるtkinterを使って、簡単なGUIアプリケーションを作ってみた。GUIツールキットとは、ユーザーとインタラクション(情報のやり取り)を行うパーツ、すなわちウィジェット(widget)の道具箱であるが、これまでは主としてキャンバスウィジェットしか使っていなかった。
このstepでは、残りのウィジェットを総ざらいで取り上げ、それらの使い方を1つずつ調べていく。サンプルプログラムはやや長いが、ウィジェットの種類毎に整理されているので、読解に苦労はないだろう。
tkinterの全機能をこの講義資料で解説するのは無理である。詳細についてはTkinter 8.5 reference manualを参照すること。

ウィジェットのテストプログラム

まずは、さまざまなウィジェットをテストするアプリケーションwidgets.pywを起動してみよう。拡張子は「.pyw」だから、ダブルクリックだけでも動作するが、ここでの目的のためには、コマンドプロンプトから起動した方がよい。
図4-1に、widgets.pywの起動画面を示す。実行中のコマンドプロンプト画面(図4-2を参照)を見れば分かるように、いろいろなテスト用(デバッグ用)情報をコマンドプロンプトに出力している。このように、GUIアプリケーション中でも、print関数で出力した情報は、コマンドプロンプトに表示される。アプリケーション画面に表示する場合には、必ずそのためのウィジェットが必要なのだ。
このアプリでは、10種類のウィジェットを画面に表示し、テストできる。さまざまな情報を入力して、コマンドプロンプトに表示される情報を見てみよう。

図4-1: 各種ウィジェットのテスト widgets.pyw
図4-2: 実行時のコマンドプロンプト

ウィジェットの配置

個別のウィジェットについて解説する前に、これらのウィジェットがどのように配置されるのか、そのルールを説明する。
配置する方式には、つぎの3通りがあり、アプリの複雑さや性格によって使い分ける。それぞれのルール通りに配置を実行する機能をジオメトリマネージャという。

pack方式
一番簡単な方式で、単に指定した順にウィジェットを詰め込んで(pack)ゆく。これまでのサンプルプログラムでは、すべてこれを利用していた。ウィジェットの配置にこだわるまでもない、簡単なプログラムに適している。
grid方式
フォーム(ウィンドウ上の長方形の領域を、グリッド(格子)状に区切り、column番号row番号で指定したマス目に、ウィジェットを配置してゆく方式。図4-1に示したウィジェットのテストプログラムでは、この方式を利用している。この画面におけるグリッド分割の様子を、図4-3に示す。縦横に隣接する複数のセルを、columnspan、rowspanというオプション引数で連結しても使えるので、配置はかなり自由である。次のstepで取り組んでもらう電卓アプリケーションは、まさにgrid方式に最適なサンプルである。
place方式
フォーム上のx, y座標でドット単位に位置を指定する方式で、もっともきめ細かく配置を制御できるが、当然プログラミングの手間もかかる。【番外編】で紹介する書誌情報取得ツールは、この方式を用いている。
図4-3と、後に示すリスト4-1のソースコードを比較すれば、grid方式の概要が理解できるだろう。各マス目の中でのウィジェットの配置には、anchorstickyなどのオプション引数が用いられる。

図4-3: グリッド分割の様子

各ウィジェットの詳細

リスト4-1に示すwidgets.pywプログラムを読みながら、10種類のウィジェットについて、情報の受け渡し方法を学んでゆく。すでに学んだキャンバスウィジェットについては、省略する。
まず、ウィンドウ全体に、トップレベルのフレームを配置する。これまでそうしてきた通り、ウィンドウに直接、ウィジェットを置くこともできるが、余白の設定ができないとか、タイマーなどのイベントの受け皿として、このフレームを配置する習慣をつけるべきだ。他の10種類のウィジェットは、すべこのフレーム(self.tf)を親(parent)として作成される。ウィジェットが配置されるgridのcolumn番号とrow番号(前述)も、それぞれの親ウィジェット(ここではフレーム)中でのローカルな位置であることに注意してほしい。


リスト4-1: 各種ウィジェットのテスト widgets.pyw

ラベル

フォーム上に文字列を配置するだけのウィジェットであり、ユーザからデータを受け取ることはない。


# ラベル
self.label = tk.Label(self.tf, text = 'ラベル')
self.label.grid(column = 0, row = 0, sticky = 'w')

ボタン

押すと何らかの動作を行うウィジェット。マウスの左ボタンでプッシュ(<Button-1>)するのが一般的である。押されたときの動作は、イベントハンドラ(self.bfunc)で記述する。
イベントハンドラの呼び出しでは、引数を渡せないが※1、呼び出されるハンドラには、必ずイベントオブジェクト(e)が渡される。このプログラムでは、イベントハンドラをWidgetsWindowクラスのメソッドとして定義しているので、ウィンドウオブジェクト自身を示すselfと合わせて、2つの引数が渡されていることになる。

1 この制限を回避するには、lambda関数(無名関数)部分関数を使って、ややトリッキーな書き方をしなければならないが、複雑なのでここでは省略する。


# ボタン
self.b1 = tk.Button(self.tf, text = 'ボタン1')
self.b2 = tk.Button(self.tf, text = 'ボタン2')
self.b1.grid(column = 0, row = 1, sticky = 'w')
self.b2.grid(column = 1, row = 1, sticky = 'w')
self.b1.bind("<Button-1>", self.bfunc)
self.b2.bind("<Button-1>", self.bfunc)

def bfunc(self, e): # ボタンが押された print('{0}が押されました。'.format(e.widget['text']))

ラベルフレーム

他のウィジェットをグループ化できる、ラベルのついた囲い。ラベルと同様、これ自体がユーザからデータを受け取ることはないが、右下の例にあるように、チェックボタンなどのウィジェットをラベル代わりにできる。
ここでは「ボタン3」「ボタン4」を、子ウィジェットとして配置している。これらのボタンもgrid方式で配置されているが、グリッド自体は、このラベルフレームの中だけのローカルなグリッドである。


# ラベルフレーム
self.lf = tk.LabelFrame(self.tf, text = 'ラベルフレーム', padx = 10, pady = 10)
self.b3 = tk.Button(self.lf, text = 'ボタン3')
self.b4 = tk.Button(self.lf, text = 'ボタン4')
self.b3.grid(in_ = self.lf, column = 0, row = 0)
self.b4.grid(in_ = self.lf, column = 1, row = 0)
self.b3.bind("<Button-1>", self.bfunc)
self.b4.bind("<Button-1>", self.bfunc)
self.lf.grid(column = 0, columnspan = 2, row = 2, sticky = 'w')

エントリー

ユーザから文字列を入力してもらうためのウィジェット。表示される内容は、以下のようにメソッドを使って変更することもできる。


self.ent.delete(0, tk.END)
self.ent.insert(tk.END, "別に用はありません。")
しかし、それではあまりに煩雑だ。そこで、エントリーを含むいくつかのウィジェットでは、コントロール変数にウィジェットの保持する内容を結びつけ、その変数経由で値を受け取ったり、セットしたりできる。極めて便利な仕組みなので、ここで是非マスターしよう。
コントロール変数に指定できるものは、以下の3種類あり、ウィジェット毎にそれぞれ決められている。エントリーでは、tk.StringVar型を使う。
tk.IntVar
整数やBoolean型(True/False)のデータを保持する変数型。pythonの整数型とは異なる。
tk.DoubleVar
浮動小数点型のデータを保持する変数型。pythonの浮動小数点型とは異なる。
tk.StringVar
文字列型のデータを保持する変数型。pythonの文字列型とは異なる。
各コントロール変数には、それぞれ以下の2つのメソッドがある。
set()
コントロール変数に値を代入する。コントロール変数に代入演算子「=」で直接値を代入することはできない。
get()
コントロール変数の値を取得する。コントロール変数を代入式の右辺において、直接値を参照することはできない。
ソースを見てみよう。まず、self.entVarとして、tk.StringVar型のコントロール変数を作り、初期値を代入している。つぎにエントリーウィジェットを生成するが、オプション引数textvariableで、コントロール変数をウィジェットに登録している。これ以降はいつでもコントロール変数を通じてウィジェットに双方向のアクセスができるので、極めて簡潔にプログラムが書ける。
エントリーウィジェット中で「Entry」キーが押されたというイベントに対して、イベントハンドラself.entfuncを登録している。


# エントリー
self.entVar = tk.StringVar()	# コントロール変数
self.entVar.set('なにかご用ですか?')
self.ent = tk.Entry(self.tf, textvariable = self.entVar)
self.ent.bind('<Return>', self.entfunc)
self.ent.grid(column = 0, columnspan = 2, row = 3)

def entfunc(self, e): # エントリーの内容が変化した print('エントリーから「{0}」が入力されました。'.format(self.entVar.get()))

スケール

一定範囲中の特定の値を受け取れるウィジェット。操作してみれば使用法は明らかだろう。3種類のコントロール変数のどれでも結びつけられるが、ここではtk.DoubleVar型のコントール変数を使い、-1.0~1.0の範囲の浮動小数点数を、0.05刻みで取得するように設定した。グリッドを2列にまたがって、横長に配置している。
このスケールなど、何種類かのウィジェットでは、オプション引数commandで指定される、コールバック関数を利用して、イベントハンドラと同様の処理を実行できる。コールバック関数に渡される引数は、ウィジェット毎に異なるので、リファレンスマニュアルで確認すること。スケールの場合は、現在の値(v)が渡される。


# スケール
self.scVar = tk.DoubleVar()		# コントロール変数
self.scVar.set(0.25)
self.sc = tk.Scale(self.tf, label = '難易度', orient = 'h', from_ = -1.0, to = 1.0, resolution = 0.05, variable = self.scVar, command = self.change_scale)
self.sc.grid(column = 0, columnspan = 2, row = 4, sticky = ('e' + 'w'))

def change_scale(self, v): # スケールを変化させた場合 print('難易度の現在値: {0}'.format(v))

リストボックス

いくつかの選択肢の内から選択してもらうためのウィジェット。単一または複数の値を選択できるが、ここでは、selectmode = tk.SINGLEを指定して、単一値を選択するように設定した。いずれかの値をダブルクリック(<Double-Button-1>)することにより、その値が選択される。イベントハンドラ中で選択された値にアクセスする方法に注意。複数の値が選択されるモードで使用したときは、curselection()メソッドを用いて項目のリストにアクセスする。
項目がたくさんある場合には、スクロールバーを使って、スクロール可能なリストボックスを作ることもできる。しかし、スクロールバーはリストボックスの付属品ではなく、独立したウィジェットだから、ウィジェット作成と、相互にデータをやり取りするための、追加のコードが必要になる。テキストウィジェットとスクロールバーを結びつける例を後述するので、参考にしてほしい。


# リストボックス
self.lbVar = tk.StringVar()		# コントロール変数
self.lbVar.set('1年生 2年生 3年生 4年生')
self.lb = tk.Listbox(self.tf, listvariable = self.lbVar, selectmode = tk.SINGLE, activestyle = tk.NONE, height = 5)
self.lb.bind('<Double-Button-1>', self.lbfunc)
self.lb.grid(column = 2, row = 0, rowspan = 2)

def lbfunc(self, e): # リストボックスの項目がダブルクリックされた print('リストボックスで「{0}」が選択されました。'.format(self.lb.get(tk.ANCHOR)))

スピンボックス

スケール同様、一定範囲中の特定の値を受け取るウィジェットであり、ここでは0~9までの整数値から1つを選択できる。この例で見るように、利用するのは極めて簡単だが、コントロール変数がなぜかtk.StringVar型であることに注意が必要である。
イベントハンドラではなく、コールバック関数を用いて制御する。


# スピンボックス
self.sbVar = tk.StringVar()		# コントロール変数(文字列であることに注意)
self.sbVar.set('5')
self.sb = tk.Spinbox(self.tf, textvariable = self.sbVar, from_ = 0, to = 9, command = self.change_spinbox, takefocus = False, width = 5)
self.sb.grid(column = 2, row = 2)

def change_spinbox(self): # スピンボックスを変化させた場合 print('スピンボックスの現在値: {0}'.format(self.sbVar.get()))

チェックボタン

ユーザからTrue/Falseの2値を受け取るウィジェット。コントロール変数はtk.IntVar型(またはtk.StringVar型)であり(tk.BooleanVar型というものはない)、値は0または1になる。
動作はコールバック関数を利用して行う。特に複雑なことはなにもないが、このプログラムでは、ラジオボタンを3つグループ化したラベルフレームのラベル代わりに使われているのが特徴的だ。チェックボタンの状態に応じて、3つのラベルボタン(リストself.rbsに格納されている)を活性化/不活性化するという、よくあるUIが実現されている。


# ラベルフレーム(チェックボタンをラベルにできる)
self.cbVar = tk.IntVar()		# コントロール変数
self.cbVar.set(0)
self.cb = tk.Checkbutton(self.tf, text = '性別を指定する', variable = self.cbVar, command = self.push_checkbutton)
self.clf = tk.LabelFrame(self.tf, labelwidget = self.cb, padx = 10, pady = 10)
self.選択肢 = ['男性', '女性', 'それ以外']
self.rbs = list()
self.rbVar = tk.IntVar()
self.rbVar.set(0)				# コントロール変数(3つのラジオボタンで共有)
for n in (0, 1, 2):
	rb = tk.Radiobutton(self.clf, text = self.選択肢[n], value = n, variable = self.rbVar, state = tk.DISABLED, command = self.push_radiobutton)
	rb.pack(anchor = 'w')
	self.rbs.append(rb)
self.clf.grid(column = 2, row = 3, rowspan = 2)

def push_checkbutton(self): # チェックボタンを押した場合 print('性別選択ボタングループの状態を「{0}」に変更しました。'.format(['選択不可', '選択可'][self.cbVar.get()])) for rb in self.rbs: rb.configure(state = [tk.DISABLED, tk.NORMAL][self.cbVar.get()]) def push_radiobutton(self): # ラジオボタンを押した場合 print('性別選択の値: {0}'.format(self.選択肢[self.rbVar.get()]))

ラジオボタン

実をいえば、プログラミングの観点からは、ラジオボタンとチェックボタンは、ロケットとミサイルくらい同じモノなのだが、ユーザインタフェースと使われ方は、はるか古代から厳しく区別されている。すなわち、

チェックボタン
単独の項目について、Yes/Noを指定する。
ラジオボタン
複数の項目をグループ化し、その内1つを選ぶ。
コントロール変数はtk.IntVar型(またはtk.StringVar型)であり、ここでは値を0, 1, 2のいずれかに制限し、各ラジオボタンのvalueとして設定している(ここだけがチェックボタンと異なる)。設定した値は、ラジオボタンを押せば自動的にコントロール変数にset()される。
グループ化したラジオボタンの内1つしか押せないのは、同じコントール変数を共有しているからである。
チェックボタン同様、動作はコールバック関数を利用して行う。プログラムは前項のチェックボタンでまとめて挙げている。

テキスト

最後のウィジェットとして、垂直方向と水平方向のスクロールバーを備えたテキストウィジェットを解説する。エントリーが一般に1行だけの短い文字列を入力するのに用いるのに対し、テキストは一般に改行を含む長い文字列を入力したり、自由に編集できる、かなり複雑な動作が可能な巨大ウィジェットである。さまざまなイベントハンドラを定義すれば、テキストエディタを作成することもできる。
このstepではさほど複雑なことはしていないが、長いテキスト(宮沢賢治の「雨ニモマケズ」)を、比較的小さな領域に表示し、垂直および水平のスクロールバーで自由にナビゲートできるようにしている。
リストボックスの項目で説明したように、スクロールバーはリストボックスの付属品ではなく、独立したウィジェットであるから、相互にデータをやり取りするためのコードが必要になる。すなわち、

の2つが定義されなければならない。これは、以下の部分で行われている。

self.hsb = tk.Scrollbar(self.tf, orient = 'h', command = self.tx.xview)
self.tx.configure(xscrollcommand = self.hsb.set)	# 水平スクロールバーをアタッチ
すなわち、2つのウィジェットのコールバック関数として、互いのメソッドを呼び合っているのである。リストボックスとスクロールバーについても、同じ方式が適用できる。


# テキスト(スクロールバー付き)
self.tx = tk.Text(self.tf, width = 16, height = 6, padx = 10, pady = 10, wrap = tk.NONE)
self.hsb = tk.Scrollbar(self.tf, orient = 'h', command = self.tx.xview)
self.tx.configure(xscrollcommand = self.hsb.set)	# 水平スクロールバーをアタッチ
self.vsb = tk.Scrollbar(self.tf, orient = 'v', command = self.tx.yview)
self.tx.configure(yscrollcommand = self.vsb.set)	# 垂直スクロールバーをアタッチ
self.tx.grid(column = 0, row = 5, sticky = ('e', 'w'))	# 密着するよう配置
self.hsb.grid(column = 0, row = 6, sticky = ('e', 'w'))
self.vsb.grid(column = 1, row = 5, sticky = ('n', 's', 'w'))
self.tx.insert('1.0', '''
雨ニモマケズ 宮澤賢治

雨ニモマケズ
	:
ワタシハナリタイ
'''[1:-1])


これ以外にもまだ数種類のウィジェットがあるし、テーマ付きウィジェット(ttk)という拡張版もあるが、基本的なプログラムは、ここに挙げたものだけで十分に書けるはずである。ここが理解できれば、tkinterについては初級クラス卒業と思ってよい。