このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
Python 自身はグラフィックの機能はなく、 ライブラリを利用してGUIを実現します。 様々なライブラリがありますが、標準で付属してくるライブラリにTkinterが あります。 これは、純粋にPython用に作成されたものではなく、別言語である Tcl のグラフィックツールキット Tk を動作させるインターフェイスになり ます。
Tcl言語は、さまざまなアプリケーションに埋め込むための標準スクリ プト言語として開発された、軽量な言語です。 そして、Tcl用に作られたGUIライブラリが Tk です。 但し、Tkは人気になり、Tcl以外の言語からでも使えるようになり、そ の言語の一つとして Pythonがあります。 Tkinter は Python に標準に付属してくるライブラリで、Tkを使用する ためのインターフェイスです。
Tkで主に使用するオブジェクトクラスの関係を示します。
Tkinter では Tk オブジェクトのコンストラクタでルートウィンドウを作ります。 title メソッドでウィンドウのタイトルを決め、geometry メソッドでウィンドウサイズを指定します。
from tkinter import *
root = Tk()
root.title("Tk Test")
root.geometry("400x300")
root.mainloop()
ウィジェット は GUI の部品を指します。 Widget の子クラスです tkinter には様々なウィジェットが含まれます。 但し、最近のバージョンで統一したテーマを共有できる tkinter.ttk パッケージが既存のウィジェットを上書きするように作られたため、 本稿では tkinter.ttk パッケージ内のウィジェットを使用します。
tkinter.ttk のウィジェットのうち、 Tkinter にも同名で含まれるのは次のウィジェトです。 Button, Checkbutton, Entry, Frame, Label, LabelFrame, Menubutton, PanedWindow, Radiobutton, Scale, Scrollbar, Spinbox 。 tkinter.ttkにはこれに Combobox, Notebook, Progressbar, Separator, Sizegrip, Treeview が 加えられました。
tkinter.ttk のウィジェットは tkinter のウィジェットを上書きして使う ようにせっけいされているので、 通常は 次のように import して使うように作られています。
from tkinter import *
from tkinter.ttk import *
ウィジェットを生成する場合、 配置される親オブジェクトの変数を p 、配置するウィジェットの変数を w とすると、 基本的には次の2つのアクションで表示されます。
w = ウィジェットのコンストラクタ(p)
w.ジオメトリマネージャ()
w = Label(p, text="example")
w.pack()
ここで、ウィジェットのコンストラクタはそのまま使用したいウィジェッ トのクラス名になります。 また、引数には通常親オブジェクトを指定します(後で指定することも できます)。 ただし、この時点では表示は自動でされません。 その後で、ジオメトリマネージャという配置を決めるメソッドを呼び出 すことで、配置されます。 このジオメトリマネージャは、pack, grid, place の3つがあります。 どれかを指定して配置を決定します。 ジオメトリマネージャの説明は章を改めて説明します。
本稿では下記のウィジェットのみを説明します。
tkでのレイアウトは以下の3通りがあります。
packレイアウトはウィジェットを順に詰め込んでいくレイアウト方式です。 詰め込む向きは で指定できます。
import tkinter
import tkinter.ttk
root = tkinter.Tk()
root.title("Pack test")
root.geometry("400x300")
frm1 = tkinter.Frame(root)
frm2 = tkinter.ttk.Frame(root)
label = [tkinter.Label(frm1,text="tkinter{0}".format(i)).pack() for i in range(6)]
labelttk = [tkinter.ttk.Label(frm2,text="ttk{0}".format(i)).pack() for i in range(6)]
frm1.pack(side=tkinter.LEFT)
frm2.pack(side=tkinter.LEFT)
root.mainloop()
grid レイアウトはウィジェットを碁盤の目のように四角い領域に区切り、 xとyの座標で指定した位置に詰め込んでいくレイアウト方式です。
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Pack test")
root.geometry("400x300")
frm = Frame(root)
label = [Label(frm,text="{0}".format(i*3+j+1))
.grid(row = i, column = j)
for j in range(3) for i in range(3)]
frm.pack()
root.mainloop()
place レイアウトはウィジェットを指定した座標に置くレイアウト方式です。 relx, rely で相対座標を指定できます。
import math
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Pack test")
root.geometry("400x300")
frm = Frame(root)
label = [Label(frm,text="{0}".format(i))
.place(anchor= tkinter.CENTER,
relx = 0.5+0.4*math.sin(i*2*math.pi/12),
rely = 0.5-0.4*math.cos(i*2*math.pi/12))
for i in range(1,13)]
frm.place(relwidth=1.0, relheight=1.0)
root.mainloop()
以上で静的なグラフィック画面が表示できました。 しかし、これだけでは何の操作もできません。 そこで、まず、ボタンを押すことや、ラベルを変化させるなどを考えましょう。
はじめにボタンのイベント処理を考えましょう。 ボタンには command という引数に関数名を指定することで、ボタンを押した ときにその関数を呼び出すことができます。 このような手法をオブザーバデザインパターンと言います。
ここでは abc, def, ghi の名前のついた 3 つのボタンを配置します。 そして、各ボタンが押されたら、そのボタンに登録された関数を呼び出し、 標準出力に表示するプログラムを示します。
from tkinter import *
from tkinter.ttk import *
def abcbuttonListener():
print("abc")
def defbuttonListener():
print("def")
def ghibuttonListener():
print("ghi")
root = Tk()
root.title("Event test")
root.geometry("400x300")
panel = Frame(root)
abcbutton = Button(panel, text = "abc",command = abcbuttonListener)
abcbutton.pack(side = LEFT)
defbutton = Button(panel, text = "def",command = defbuttonListener)
defbutton.pack(side = LEFT)
ghibutton = Button(panel, text = "ghi",command = ghibuttonListener)
ghibutton.pack(side = LEFT)
panel.pack()
root.mainloop()
tkinterでは基本的にはコマンドを引数無しで実行させるため、工夫をしな いとこのような冗長で、具体的なデータに依存したプログラムになってしま います。 但し、lambda式を使うと次のようにきれいに書けます。
from tkinter import *
from tkinter.ttk import *
def buttonListener(s):
print(s)
root = Tk()
root.title("Event test")
root.geometry("400x300")
panel = Frame(root)
button = [ Button(panel, text = title,
command = lambda x = title:buttonListener(x))
.pack(side = LEFT)
for title in ["abc", "def", "ghi" ]]
panel.pack()
root.mainloop()
さて、逆に GUI において、値を表示することを考えます。 これは何らかの表示する値が生じたときにイベントを発生し、 GUI 側で表示 させるようにします。
標準入力を一行ずつラベルに表示するようなプログラムを考えましょう。
そのため、主プログラムでは input を while の無限ループで回す必要があり
ます。
この場合、 root.mainloop() 呼び出しは使えなくなりますが、
ループの中で root.update()呼び出しをするとGUIの機能は維持
されます。
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Command Line Input to Label")
root.geometry("400x300")
label = Label(root)
label.pack()
while True:
try:
s = input()
label.config(text = s)
root.update()
except EOFError:
break
前の例を合わせ、ボタンが押されたらボタンのラベルを表示するプログ ラムを示します。
from tkinter import *
from tkinter.ttk import *
def setter(label,s):
label.config(text = s)
root = Tk()
root.title("Button to Label")
root.geometry("400x300")
label = Label(root)
label.pack()
panel = Frame(root)
button = [ Button(panel, text = title,
command = lambda x = title, y = label:setter(x,y))
.pack(side = LEFT)
for title in ["abc", "def", "ghi" ]]
panel.pack()
root.mainloop()
Tkinter では画面表示を変える方法がいくつかあります。
tk直属の tkinter.Button などにはfg, bg などのオプションがあり、それを 次の3つのやり方で指定することができます。
button = Button(frame, fg="red", bg="blue")
button.config(fg="red", bg="blue")
button["fg"] = "red"
button["bg"] = "blue"
オプションで使える共通の属性は下記のとおりです。 この他、Widget 毎に個別のオプションが用意されている場合もあります。
| 属性 | 説明 | 値 |
|---|---|---|
| anchor | ウィジェットを配置する位置 | tk.CENTER(中央) tk.W(左寄せ) tk.E(右寄せ) tk.N(上寄せ) tk.S(下寄せ) tk.NW(左上) tk.SW(左下) tk.NE(右上) tk.SE(右下) |
| width | ウィジェットの幅 | 数値 |
| height | ウィジェットの高さ | 数値 |
| bg or background | 背景色 | red,blue,grayなどの色 |
| bd or borderwidth | ウィジェットの枠の幅 | 数値 |
| relief | ボーダー部分の浮き彫り | flat,raised,sunken,groove,ridge |
| padx | 縁とウィジェットの間の横の余白 | 数値 |
| pady | 縁とウィジェットの間の縦の余白 | 数値 |
| fg or foreground | 文字や線を描く色 | red,blue,grayなど色 |
| text | ウィジェットに表示する文字列 | 任意の文字列 |
| textvariable | テキストを格納するオブジェクト | オブジェクト名 |
| font | フォントの指定 | タプルまたはtkinter.font.Fontオブジェクトで指定 |
| image | ウィジェット内に表示する画像ファイル | 画像ファイルへのパス |
| bitmap | ウィジェット内に表示する bitmap | 組み込み名前付きビットマップ: 'error' 'gray25' 'gray50' 'hourglass' 'info' 'questhead' 'question' 'warning' |
font指定のタプルの各項目の意味は下記のとおりです。
6項目ありますが、すべて指定する必要はなく、
('Helvetica',16)で Helveticaの16ptを意味し、
('Times', -24, 'bold', 'italic') で
Times の24pixelで太字の斜字体を意味します。
| 項目 | データ型 | 説明 |
|---|---|---|
| family | 文字列 | フォントの名前(フォントファミリー) 例:"Times", "Courier" |
| size | 文字列 or 整数 | フォントの大きさ ( 正の整数でポイント単位 負の整数でピクセル単位) |
| weight | 文字列 | フォントの太さ 太字: "bold", tkinter.font.BOLD, 通常: "normal", tkinter.font.NORMAL |
| slant | 文字列 | フォントの傾斜 斜体: "italic", tkinter.font.ITALIC, 通常: "roman", tkinter.font.ROMAN |
| underline | ブール or 整数 | 下線の有無 |
| overstrike | ブール or 整数 | 取り消し線の有無 |
tkinter.ttk のWidgetには、 特定のオプション指定の組み合わせを一つのスタイルとして名前をつけて、 style オプションで指定することができます。 これにはtkinter.ttk.Style オブジェクトを生成して、 config メソッドで作 成します。
各ウィジェットはデフォルトのスタイルを持っています。 これは 「T+ウィジェットクラス名」です。 tkinter.ttk.Frame だったら TFrame, tkinter.ttk.Label だったら TLabel, tkinter.ttk.Button だったら TButton になります。 特定のボタンのスタイルを変更したい場合などは、カスタムスタイルを定義 する必要があります。それには、「任意の文字列.デフォルトスタイル」と いうスタイル名を使います。
ttk に属するウィジェットには style オプションがあり、それにスタイル 名を与えてスタイルを指定します。
デフォルトスタイルの変更や、カスタムスタイルを定義するには Style オ ブジェクトの configure メソッドを呼びます。
ボタンのように、動的に状態が変わるものについて、指定を行うのがmapで
す。
background を状態ごとに変えるには、
スタイル名と、(状態, 値)の配列を引数に与えて指定します。
状態は下記の通りで、さらに !状態でその状態以外の状態を
指定することもできます。
つまり
style.map("TButton",foreground=[("!active","red"),("active","blue")])
などと指定します。
| 状態 | 説明 |
|---|---|
| active | マウスカーソルがウィジェットの上にある |
| disabled | ウィジェットが無効化されている |
| focus | ウィジェットにキーボードフォーカスがある |
| pressed | ウィジェットは押されている |
| selected | 選択されている |
| background | マルチウィンドウで、そのウィンドウが最前面に無い |
| readonly | ウィジェットはユーザから変更不能 |
| alternate | ウィジェット特有の切り替え表示 |
| invalid | ウィジェットの値が不正 |
デフォルトスタイルの表示
from tkinter import *
from tkinter.ttk import *
root = Tk()
style = Style()
print(style.configure("TFrame"))
print(style.configure("TLabel"))
print(style.configure("TButton"))
root.destroy()
None
None
{'relief': 'raised', 'padding': '3 3', 'anchor': 'center', 'width': '-9', 'shiftrelief': 1}
カスタムスタイル例
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Style test")
root.geometry("400x300")
style=Style()
style.theme_use(themename="default")
style.configure("key.TFrame", background="skyblue")
style.configure("key.TLabel",
padding="5 10 5 0",
font=("Courier",14),
background="lightyellow",
width=20,
relief="groove")
style.configure("key.TButton",
font=("Courier",14),
relief="raised")
style.map("key.TButton",background=
[('!active', 'cyan'),
('pressed', 'pink'),
('active', 'lightgreen')])
frm = Frame(root,style="key.TFrame")
label = Label(frm,text="0",style="key.TLabel",anchor="e").pack(fill="x")
frmb = Frame(frm,style="key.TFrame")
button = [Button(frmb,text="{0}".format(i*3+j+1),style="key.TButton")
.grid(row = i, column = j)
for j in range(3) for i in range(3)]
frm.pack()
frmb.pack()
root.mainloop()
スタイルの集まりをテーマとして管理することができます。 また、システムには予めいくつかのテーマが用意されていて、それをそのまま 使ったり、修正して使うこともできます。
style.theme_use(themename=x)
メソッドは引数なしで現在使用しているテーマ名を返し、
themename でテーマ名を指定するとそのテーマを使い、再描画します。
style.theme_settings(テーマ名, セッティング)
は指定したテーマの設定を変更します。
セッティングはデフォルトスタイル名の辞書になっていて、
辞書の値は configure, map, layout, element_create のキーによる辞書で、
それらの値はスタイルの設定となる。
例えば、つぎのような書式になる。
style=tkinter.ttk.Style()
style.theme_settings("default",
{"TFrame":
{"configure":
{"background":"skyblue"}
}
}
)
使用可能なテーマごとの見栄えの切り替え
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Theme test")
root.geometry("400x400")
style = Style()
frm0 = Frame(root)
for t in style.theme_names():
Button(frm0,text=t,
command=lambda x=t:style.theme_use(themename=x)
).pack()
frm0.pack()
frm = Frame(root)
lbl = Label(frm,text="test label").pack(side=LEFT)
button = Button(frm,text ="test button").pack(side=LEFT)
frm.pack()
root.mainloop()
各テーマにおけるデフォルトスタイルの内容
from tkinter import *
from tkinter.ttk import *
root = Tk()
style = Style()
for t in style.theme_names():
style.theme_use(themename=t)
print(f"theme: {t}")
print(style.configure("TFrame"))
print(style.configure("TLabel"))
print(style.configure("TButton"))
root.destroy()
theme: winnative
None
None
{'relief': 'raised', 'anchor': 'center', 'width': '-11', 'shiftrelief': 1}
theme: clam
None
None
{'relief': 'raised', 'padding': '5', 'anchor': 'center', 'width': '-11'}
theme: alt
None
None
{'highlightcolor': '#d9d9d9', 'relief': 'raised', 'padding': '1 1', 'anchor': 'center', 'highlightthickness': 1, 'width': '-11', 'shiftrelief': 1}
theme: default
None
None
{'relief': 'raised', 'padding': '3 3', 'anchor': 'center', 'width': '-9', 'shiftrelief': 1}
theme: classic
None
None
{'relief': 'raised', 'padding': '3m 1m', 'anchor': 'center', 'shiftrelief': 1}
theme: vista
None
None
{'padding': '1 1', 'anchor': 'center', 'width': '-11'}
theme: xpnative
None
None
{'padding': '1 1', 'anchor': 'center', 'width': '-11'}
カスタムテーマ
from tkinter import *
from tkinter.ttk import *
root = Tk()
root.title("Theme test")
root.geometry("400x300")
style=Style()
style.theme_settings("default", {
"TFrame":{
"configure":{"background":"skyblue"}
},
"TLabel":{
"configure":{"padding":"5 10 5 0",
"font":("Courier",14),
"background":"lightyellow",
"width":20,
"relief":"groove"}
},
"TButton":{
"configure":{"font":("Courier",14),
"relief":"raised"},
"map":{"background":
[('!active', 'cyan'),
('pressed', 'pink'),
('active', 'lightgreen')]}
}
})
style.theme_use(themename="default")
frm = Frame(root)
label = Label(frm,text="0",anchor="e").pack(fill="x")
frmb = Frame(frm)
button = [Button(frmb,text="{0}".format(i*3+j+1))
.grid(row = i, column = j)
for j in range(3) for i in range(3)]
frm.pack()
frmb.pack()
root.mainloop()
フォントは tkinter.font パッケージで管理できます。
これは
import tkinter.*
では読み込まれないので、
import tkinter.font as font
で読み込む必要があります。
フォントをタプルでなく、オブジェクトで使うにはFontオブジェクトを生成 し、コンストラクタでオプションを指定するか、configメソッドで変更しま す。
使えるフォントファミリーを出力する
from tkinter import *
import tkinter.font as font
root = Tk()
available_fonts = list(font.families())
available_fonts.sort()
for f in available_fonts:
print(f)
root.destroy()
イベントと任意のウィジェットを結びつけることができます。
さて、GUI でボタンを連続で押すとそのボタンの文字の列が得られる ようなオブジェクトを考えます。 つまり、ボタンを押すと文字が蓄積され、Enter のような入力終了を意味す るキーを押すと文字列を返す、具体的には登録されている関数にその文字列を 入れて呼び出すプログラムを作成します。 作成する GUI は tkinter.tk.Frameクラスを継承して、内部にボタンを埋め込 むことにします。 これを実現するには、 やはりオブザーバデザインパターンを使用します。 押したボタンで作られる文字列が逐次表示できるlabelと、 Enterキーに相当するボタンを押したときに、蓄えた文字列を引数に呼び出す 関数を指定する commandを指定するようにします。
必要なメソッドはクリックしたボタンで指定した文字をためる click_button(self, char)メソッドと、 Enter を押した際に蓄えた文字列を指定された関数に渡して呼び出し、蓄え た文字列を消去する enter(self)メソッドです。
コンストラクタでは、親クラスのコンストラクタ呼び出しの後、label, command の退避、文字列領域の初期化をしたのち、 ボタンを生成し、それぞれ、 clickButton を呼ぶか、 enter を呼ぶかを指 定します。
以上を実装したのが下記のTenkeyBoardクラスです。 テストプログラムをつけて示します。 "="キーを押すと、標準入力に文字列が表示されます。
from tkinter import *
from tkinter.ttk import *
class TenkeyBoard(Frame):
def __init__(self, master=None, label=None, command=None, **kwargs):
super().__init__(master, **kwargs)
self.label = label
self.command = command
self.input_string = ""
for i in map(str, range(10)):
Button(self, text=i,
command = lambda s=i: self.click_button(s)
).pack()
Button(self, text="+",
command = lambda s = "+": self.click_button(s)
).pack()
Button(self, text="=",
command = self.enter
).pack()
def enter(self):
self.command(self.input_string)
self.input_string = ""
def click_button(self, char):
self.input_string += char
self.label.config(text=self.input_string)
def on_enter(s):
print(s)
root = Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = Label(root)
label.pack()
keyboard = TenkeyBoard(root, label = label, command=on_enter)
keyboard.pack()
root.mainloop()
さてここでは簡易的な電卓を考えましょう。 「自然数("+"自然数)*"="」という構文のみを解釈して足し算の結果だけを表 示するものです。
電卓の計算の結果を示す部分を Label で作成します。
label = tkinter.ttk.Label(root)
label.pack()
受け取った文字列を数式として解釈し、計算した値をlabelに書き込む関数を 作ります。 これを、キーボードの "=" ボタンのcommandとして与えます。 文法を grammer に書き Lark により、パーサーを生成します。
import lark
grammar = """
start: sum
sum: num ( PLUSOP num)*
num: NUM
NUM: /\\d+/
PLUSOP: "+"
%ignore /\\s/
"""
parser = lark.Lark(grammar, start='start')
パーサの出力の構文解析木を計算する Transformer を作成します。
構文ごとに値の列が得られるので、sum に関して
[ num OP num OP ... OP num ]という構造が関数の引数に与
えられます。
そのため、最初の値を得た後、演算子が "+" であることを確認しながら
得られた値を加算しています。
class Calc(lark.Transformer):
def sum(self, item):
result = item[0]
for i in range(1,len(item),2):
if item[i] == "+":
result += item[i+1]
return result
def num(self, item):
return int(item[0])
def start(self, item):
return item[0]
transformer = Calc()
このパーサをテストするプログラムを次に示します。
parser = lark.Lark(grammar, start='start')
transformer = Calc()
tree = parser.parse("1+2+3")
s = transformer.transform(tree)
print(s)
最後に受け取った文字列に対して、パーサで構文解析木を得て、さらにトラ ンスフォーマにより計算をし、その結果をラベルに反映する関数を作成しま す。
def on_enter(s):
tree = parser.parse(s)
result = transformer.transform(tree)
label.config(text=str(result))
さて、以上をまとめ、動作するプログラムを作ります。
parser = lark.Lark(grammar, start='start')
transformer = Calc()
root = Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = Label(root)
label.pack()
keyboard = TenkeyBoard(root, label=label, command=on_enter)
keyboard.pack()
root.mainloop()
import lark
from tkinter import *
from tkinter.ttk import *
grammar = """
start: sum
sum: num ( PLUSOP num)*
num: NUM
NUM: /\\d+/
PLUSOP: "+"
%ignore /\\s/
"""
class Calc(lark.Transformer):
def sum(self, item):
result = item[0]
for i in range(1,len(item),2):
if item[i] == "+":
result += item[i+1]
return result
def num(self, item):
return int(item[0])
def start(self, item):
return item[0]
class TenkeyBoard(Frame):
def __init__(self, master=None, label=None, command=None, **kwargs):
super().__init__(master, **kwargs)
self.label = label
self.command = command
self.input_string = ""
for i in map(str, range(10)):
Button(self, text=i,
command=lambda s=i: self.click_button(s)
).pack()
Button(self, text="+",
command=lambda s="+": self.click_button(s)
).pack()
Button(self, text="=",
command=self.enter
).pack()
def enter(self):
self.command(self.input_string)
self.input_string = ""
def click_button(self, char):
self.input_string += char
self.label.config(text=self.input_string)
def on_enter(s):
tree = parser.parse(s)
result = transformer.transform(tree)
label.config(text=str(result))
parser = lark.Lark(grammar, start='start')
transformer = Calc()
root = Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = Label(root)
label.pack()
keyboard = TenkeyBoard(root, label=label, command=on_enter)
keyboard.pack()
root.mainloop()