第 6 回 GUI

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

6-1. PythonのGUI

Python 自身はグラフィックの機能はなく、 ライブラリを利用してGUIを実現します。 様々なライブラリがありますが、標準で付属してくるライブラリにTkinterが あります。 これは、純粋にPython用に作成されたものではなく、別言語である Tcl のグラフィックツールキット Tk を動作させるインターフェイスになり ます。

Tcl言語は、さまざまなアプリケーションに埋め込むための標準スクリ プト言語として開発された、軽量な言語です。 そして、Tcl用に作られたGUIライブラリが Tk です。 但し、Tkは人気になり、Tcl以外の言語からでも使えるようになり、そ の言語の一つとして Pythonがあります。 Tkinter は Python に標準に付属してくるライブラリで、Tkを使用する ためのインターフェイスです。

Tkの基本構造

Tkで主に使用するオブジェクトクラスの関係を示します。

ウィンドウ

Tkinter では Tk オブジェクトのコンストラクタでルートウィンドウを作ります。 title メソッドでウィンドウのタイトルを決め、geometry メソッドでウィンドウサイズを指定します。

例6-1


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 が 加えられました。

ウィジェットを生成する場合、 配置される親オブジェクトの変数を p 、配置するウィジェットの変数を w とすると、 基本的には次の2つのアクションで表示されます。


w = ウィジェットのコンストラクタ(p)
w.ジオメトリマネージャ()

w = tkinter.ttk.Label(p, text="example")
w.pack()

ここで、ウィジェットのコンストラクタはそのまま使用したいウィジェッ トのクラス名になります。 また、引数には通常親オブジェクトを指定します(後で指定することも できます)。 ただし、この時点では表示は自動でされません。 その後で、ジオメトリマネージャという配置を決めるメソッドを呼び出 すことで、配置されます。 このジオメトリマネージャは、pack, grid, place の3つがあります。 どれかを指定して配置を決定します。 ジオメトリマネージャの説明は章を改めて説明します。

本稿では下記のウィジェットのみを説明します。

tkinter.ttk.Button
tkinter.ttk.Frame
tkinter.ttk.Label
tkinter.ttk.Style

Frame

tkiner.ttk.Frame は何も見えないウィジェットです。 複数配置するこ とにより、表示領域を分割することができます。 また、これはウィジェットなので、さまざまなウィジェットや、さらに Frame の中に Frame を含ませることができます。 そのため、 GUI の外観は特定の Frame の包含関係で表すことができます。 包含関係で表せるということは、 Tk オブジェクトを根とする木構造で表現できること を意味しています。

レイアウト

tkでのレイアウトは以下の3通りがあります。

  1. pack
  2. grid
  3. place

packレイアウト

packレイアウトはウィジェットを順に詰め込んでいくレイアウト方式です。 詰め込む向きは で指定できます。

例6-2


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レイアウト

grid レイアウトはウィジェットを碁盤の目のように四角い領域に区切り、 xとyの座標で指定した位置に詰め込んでいくレイアウト方式です。

例6-3


import tkinter
import tkinter.ttk
root = tkinter.Tk()
root.title("Pack test")
root.geometry("400x300")
frm = tkinter.ttk.Frame(root)
label = [tkinter.ttk.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レイアウト

place レイアウトはウィジェットを指定した座標に置くレイアウト方式です。 relx, rely で相対座標を指定できます。

例6-4


import math
import tkinter
import tkinter.ttk
root = tkinter.Tk()
root.title("Pack test")
root.geometry("400x300")
frm = tkinter.ttk.Frame(root)
label = [tkinter.ttk.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()

表示スタイル

イベント処理1(ボタン)

以上で静的なグラフィック画面が表示できました。 しかし、これだけでは何の操作もできません。 そこで、まず、ボタンを押すことや、ラベルを変化させるなどを考えましょう。

はじめにボタンのイベント処理を考えましょう。 ボタンには command という引数に関数名を指定することで、ボタンを押した ときにその関数を呼び出すことができます。 このような手法をオブザーバデザインパターンと言います。

例6-5

ここでは abc, def, ghi の名前のついた 3 つのボタンを配置します。 そして、各ボタンが押されたら、そのボタンに登録された関数を呼び出し、 標準出力に表示するプログラムを示します。


import tkinter
import tkinter.ttk

def abcbuttonListener():
    print("abc")
def defbuttonListener():
    print("def")
def ghibuttonListener():
    print("ghi")

root = tkinter.Tk()
root.title("Event test")
root.geometry("400x300")
panel = tkinter.ttk.Frame(root)
abcbutton=tkinter.ttk.Button(panel, text = "abc",command = abcbuttonListener)
abcbutton.pack(side=tkinter.LEFT)
defbutton=tkinter.ttk.Button(panel, text = "def",command = defbuttonListener)
defbutton.pack(side=tkinter.LEFT)
ghibutton=tkinter.ttk.Button(panel, text = "ghi",command = ghibuttonListener)
ghibutton.pack(side=tkinter.LEFT)
panel.pack()
root.mainloop()

tkinterでは基本的にはコマンドを引数無しで実行させるため、工夫をしな いとこのような冗長で、具体的なデータに依存したプログラムになってしま います。 但し、lambda式を使うと次のようにきれいに書けます。


import tkinter
import tkinter.ttk

def buttonListener(s):
    print(s)

root = tkinter.Tk()
root.title("Event test")
root.geometry("400x300")
panel = tkinter.ttk.Frame(root)
button = [ tkinter.ttk.Button(panel, text = title,
    command = lambda title=title:buttonListener(title))
    .pack(side=tkinter.LEFT)
    for title in ["abc", "def", "ghi" ]]
panel.pack()
root.mainloop()

さて、逆に GUI において、値を表示することを考えます。 これは何らかの表示する値が生じたときにイベントを発生し、 GUI 側で表示 させるようにします。

例6-6

標準入力を一行ずつラベルに表示するようなプログラムを考えましょう。 そのため、主プログラムでは input を while の無限ループで回す必要があり ます。 この場合、 root.mainloop() 呼び出しは使えなくなりますが、 ループの中で root.update()呼び出しをするとGUIの機能は維持 されます。


import tkinter
import tkinter.ttk

root = tkinter.Tk()
root.title("Command Line Input to Label")
root.geometry("400x300")
label = tkinter.ttk.Label(root)
label.pack()
while True:
    try:
        s = input()
        label.config(text=s)
        root.update() 
    except EOFError:
        break

例6-7

前の例を合わせ、ボタンが押されたらボタンのラベルを表示するプログ ラムを示します。


import tkinter
import tkinter.ttk

def setter(label,s):
    label.config(text=s)

root = tkinter.Tk()
root.title("Button to Label")
root.geometry("400x300")
label = tkinter.ttk.Label(root)
label.pack()
panel = tkinter.ttk.Frame(root)
button = [ tkinter.ttk.Button(panel, text = title,
         command = lambda title=title, label=label:setter(label,title))
           .pack(side=tkinter.LEFT)
    for title in ["abc", "def", "ghi" ]]
panel.pack()
root.mainloop()

イベント処理2(bind)

6-2. キーボードの作成

概要

さて、GUI でボタンを連続で押すとそのボタンの文字の列が得られる ようなオブジェクトを考えます。 つまり、ボタンを押すと文字が蓄積され、Enter のような入力終了を意味す るキーを押すと文字列を返す、具体的には登録されている関数にその文字列を 入れて呼び出すプログラムを作成します。 作成する GUI は tkinter.tk.Frameクラスを継承して、内部にボタンを埋め込 むことにします。 これを実現するには、 やはりオブザーバデザインパターンを使用します。 押したボタンで作られる文字列が逐次表示できるlabelと、 Enterキーに相当するボタンを押したときに、蓄えた文字列を引数に呼び出す 関数を指定する commandを指定するようにします。

必要なメソッドはクリックしたボタンで指定した文字をためる click_button(self, char)メソッドと、 Enter を押した際に蓄えた文字列を指定された関数に渡して呼び出し、蓄え た文字列を消去する enter(self)メソッドです。

コンストラクタでは、親クラスのコンストラクタ呼び出しの後、label, command の退避、文字列領域の初期化をしたのち、 ボタンを生成し、それぞれ、 clickButton を呼ぶか、 enter を呼ぶかを指 定します。

以上を実装したのが下記のTenkeyBoardクラスです。 テストプログラムをつけて示します。 "="キーを押すと、標準入力に文字列が表示されます。


import tkinter
import tkinter.ttk

class TenkeyBoard(tkinter.ttk.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)):
            tkinter.ttk.Button(self, text=i,
                               command=lambda s=i: self.click_button(s)
                               ).pack()
        tkinter.ttk.Button(self, text="+",
                           command=lambda s="+": self.click_button(s)
                           ).pack()
        tkinter.ttk.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 = tkinter.Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = tkinter.ttk.Label(root)
label.pack()
keyboard = TenkeyBoard(root, label=label, command=on_enter)
keyboard.pack()

root.mainloop()

6-3. 簡易電卓の作成

さてここでは簡易的な電卓を考えましょう。 「自然数("+"自然数)*"="」という構文のみを解釈して足し算の結果だけを表 示するものです。

表示部

電卓の計算の結果を示す部分を 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))

主プログラム

さて、以上をまとめ、動作するプログラムを作ります。

  1. パーサを生成します(再掲)
  2. トランスフォーマーを生成します(再掲)
  3. ルートウィンドウを生成し、タイトルと大きさを指定します
  4. ラベルを生成し、表示します(再掲)
  5. キーボードをラベルと計算用の関数を指定して生成し、表示します
  6. 主ループに入ります

parser = lark.Lark(grammar, start='start')
transformer = Calc()
root = tkinter.Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = tkinter.ttk.Label(root)
label.pack()
keyboard = TenkeyBoard(root, label=label, command=on_enter)
keyboard.pack()

root.mainloop()

6-4. 付録

動作可能な簡易電卓


import lark
import tkinter
import tkinter.ttk

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(tkinter.ttk.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)):
            tkinter.ttk.Button(self, text=i,
                               command=lambda s=i: self.click_button(s)
                               ).pack()
        tkinter.ttk.Button(self, text="+",
                           command=lambda s="+": self.click_button(s)
                           ).pack()
        tkinter.ttk.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 = tkinter.Tk()
root.title("Keyboard test")
root.geometry("400x400")
label = tkinter.ttk.Label(root)
label.pack()
keyboard = TenkeyBoard(root, label=label, command=on_enter)
keyboard.pack()

root.mainloop()

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科