Part 6: グラフィカル・ユーザー・インターフェイス

GUI プログラミングと Tk

GUI オブジェクト

Python で使われる Tk パッケージを含め、最近の GUI ツールキットは全て、オブジェクト指向モデルに基づくユーザーインターフェースを持っています。 典型的なオブジェクト型は、ウインドウ、入力欄、ボタン、テキスト欄、グラフィックス欄、メニュー、などです。 GUI を作るということは、必要なオブジェクトを全て創作し、それらを正しく集め、付随する動作を指定する、ということを意味します。

イベント駆動型の計算

旧来のプログラムでは、プログラムの流れはコードによって決定されます。 GUI では、利用者がメニューやボタンなどを操作することによって、何が起こるかが決定されます。 これには、従来とは異なったプログラム構造が必要です:メインループが利用者の操作を点検し、それに適したサブルーチンを呼びます。 メインループが停止するのは、利用者がプログラムを終了すると決めた時だけです。

利用者とのインターフェースの設計

このコースでは、GUI を作成する際の技術的な側面だけを扱います。 同様に重要なのは、利用者とのインターフェースの設計、すなわち何をどこにどのように設置するかという問題です。 この問題には多くの文献が存在します。しかし、最も大事なこととして推奨するのは、良く書かれたプログラムを調べ、その動作を模倣してみることです。 初心者が最も犯しがちな誤りは、革新的であろうとし過ぎることです。 そのアプリケーションに独特の技を用いて、殆どの機能を一つのウインドウに詰め込む様な GUI を創造するというのではなく、むしろ利用者が他のプログラムでの経験に基づいて直ちに理解できるような GUI を設計することを目指すべきです。

Tk と Python

Python は利用者とのインターフェースに独自のパッケージを持っていませんが、類似のパッケージへの Python からのインターフェースはいくつか存在します。 最も親しまれているのは Tk で、元々はとても単純なスクリプト言語である Tcl 用に開発されたものです。 Tk にはいくつかの利点があります:使用法が簡単で完全性があり、主だった全てのオペレーティングシステムで使えます。 しかし、Tk は Tcl とより密接に結び付いています。Python インターフェースからは出来るだけ隠すようにしてはいますが、Python とは相容れない様に見える側面も幾つか残っています。 初めから Python 用に設計されたインターフェースツールキットがあったら、おそらく Tk とは外見の異なるものだったでしょう。

Tk の世界の概観

Tk には二種類のオブジェクトがあります:ウインドウとウイジェット("window"と"gadget(小道具)"から来ています)です。 ウイジェットは、ウインドウ中に配置されるユーザーインターフェースオブジェクトです;全てのウイジェットはマスター(これは別のウイジェットであるか、または最外殻のウイジェットに対してはウインドウになります)を持ちます。ウイジェットのスクリーン上の位置や多くの性質はそのマスターに依存します。 ウインドウも同様の階層構造を成しますが、性質が異なります:ウインドウの位置と内容はそのマスターから完全に独立していますが、マスターが閉じられると自身も消えます。 ウインドウの階層構造の最高位には、マスターを持たないルートウインドウがあります。 ルートウインドウが閉じられると、Tk イベントの主ループは停止します。

典型的な Tk アプリケーションは、ルートウインドウに付加するウイジェットを作成し、その主ループを実行することから始まります。 ウイジェットはイベント(利用者の操作)に応じて呼ばれるべき関数を指定し、これらの関数はそのイベントが発生した時に Tk の主ループから呼び出されます。 これらの関数は何をしても構いません;制限はありません。 ウインドウを開閉したり、存在するウインドウ中のウイジェットを変更することさえできます。 また、Tk の主ループを停止するよう要求することもできます。

ウイジェットを作成した後には、それをマスターのどの位置に配置するかを決める必要があります。 Tk はウイジェットの配置に二つの方法を提供しています: placer は非常に柔軟であると同時にとても複雑な方法です。 一方 packer は、使い方が簡単で多くを自動で行いますが、柔軟さはある程度欠いています。 殆どの Tk プログラムは packer だけを用いており、このコースの例も全てそのようにします。

とても簡単な例

次のプログラムは、Tk のアプリケーションとしては最も単純なものです。 ボタン一つだけのウインドウを開きます。 ボタンが押されるとスクリーンにメッセージが印字されます。 ウインドウを閉じるとプログラムは停止します。

from Tkinter import *

def report():
    print "The button has been pressed"

object = Button(None, text = 'A trivial example', command = report)
object.pack()
object.mainloop()

最初の文は Tkinter モジュールから全ての名前を import します。 このモジュールには多くの定義があるので、個々に import するのは少し不便です。 次にイベント動作関数を定義します。これには任意の Python 関数が使えます。 次の行はボタンウイジェットを生成します。第一パラメータはそのウイジェットのマスターです;None はルートウインドウを示します。 残りのパラメータがボタンの詳細を指定します:表示するテキスト、ボタンが押された時に実行される命令、です。 次にウイジェットの pack() メソッドが呼ばれます。これはウイジェットを配置するのに packer が使われることを示します。 オブジェクトをどの様に配置するかについては何も指示されていません。 ウイジェットが一つだけなので選択肢が無いからです。 最後に主ループが開始されます。

ウイジェットの配置

一つの Tk ウインドウ中のウイジェットは一つだけです。 もちろん、ウイジェット一つだけのウインドウではあまり有用でないので、もっと複雑なものを作る方法があります:それにはフレームを使います。 フレームとは、サブウインドウの様に動作するウイジェットです。 フレームは通常目に見えません(ただし目に見える境界線を付けることも可能です)が、それに含まれるウイジェットは見えるものです。 フレームには任意の数、任意の種類のウイジェットを置けます。

フレームが複数のウイジェットを持つならば、それらをどのように配置するかを指定する必要があります。 この情報は、pack メソッドへのオプションのキーワード引数によって与えます。 最も簡単なものは、side=some_side で、TOPBOTTOMLEFTRIGHT から選びます。 すると packer はそのウイジェットを、他のウイジェットとかち合わない範疇で、フレームの指定された側のなるべく近くに配置しようとします。 例えば、フレーム中の全部のウイジェットに side=TOP とした場合は、それらは pack が呼ばれた最初のウイジェットを頂上にして縦に積み上げられるでしょう。 横方向には中央に置かれます。

フレーム中にフレームを入れることによって、より複雑なものを作ることができます。 任意のウイジェットに対する packer 指定は、その周りのフレーム内部での配置を決めます。そしてそのフレームには自身の packer オプションがあり、他のフレーム中での配置を決めます。

次に示すのは、2列に並んだ4つのボタンを含むウインドウを、フレームを使って作成した例です:

from Tkinter import *

def report():
    print "A button has been pressed"

outer_frame = Frame(None)
outer_frame.pack()

first_line = Frame(outer_frame)
first_line.pack(side=TOP)
button1 = Button(first_line, text='Button 1', command=report)
button1.pack(side=LEFT)
button2 = Button(first_line, text='Button 2', command=report)
button2.pack(side=LEFT)

second_line = Frame(outer_frame)
second_line.pack(side=TOP)
button3 = Button(second_line, text='Button 3', command=report)
button3.pack(side=LEFT)
button4 = Button(second_line, text='Button 4', command=report)
button4.pack(side=LEFT)

outer_frame.mainloop()

最初のフレームのマスターが、ルートウインドウを意味する None であることに注意して下さい。 内側のフレームは行になっており、上の外側フレームをマスターとしています。 ボタンはその内側フレームに付けられます。

自分自身のウイジェットを創る

上例の様に、オブジェクトを一つ一つ作り packer オプションを与えれば、原理的にはどんな GUI でも作れます。 しかし、その様なプログラムはすぐに大きくて読み難いものになり、変更するのも殆ど不可能になるでしょう。 何らかの方法で GUI オブジェクトを作る必要があります。 通常採られる方法は、アプリケーションに専用の高級ウイジェットを定義することです。 例えば、上の例では "ボタンの行" のウイジェットを作れば有効でしょう。

ウイジェットは、既に存るウイジェットをサブクラス化することで定義されます。 後者は標準的な Tk ウイジェットでも他のアプリケーションで定義されたウイジェットでも構いません。 殆ど全ての高級ウイジェットは、Frame のサブクラスです。 それらの初期化メソッドは、そのフレーム内の全ウイジェットを生成し、それらの配置を指定します。 すると、今度はそれらのウイジェットを標準ウイジェットの様に利用することが出来ます。

"ボタン行" ウイジェットを用いれば、上の4ボタンの例は次の様に書けます:

from Tkinter import *

def report():
    print "A button has been pressed"

class ButtonLine(Frame):

    def __init__(self, master, button_data):
	Frame.__init__(self, master)
	for text, command in button_data:
	    Button(self, text=text, command=command).pack(side=LEFT)

outer_frame = Frame(None)
outer_frame.pack()

first_line = ButtonLine(outer_frame, [('Button 1', report),
				      ('Button 2', report)])
first_line.pack(side=TOP)

second_line = ButtonLine(outer_frame, [('Button 3', report),
				       ('Button 4', report)])
second_line.pack(side=TOP)

outer_frame.mainloop()

Tk ウイジェット:概観

ここで全ての Tk ウイジェットを詳細まで記述するのは不可能ですが、それらを多く使われる応用も含め概観することは、Tk マニュアルをさらに調べるための良い出発点になるでしょう。

また、描画や写真などを表示する種々のイメージオブジェクトもあります。

これらのウイジェットについてのさらなる情報については、Tkinter の文書と Tk マニュアルページを参照してください。

それ程単純ではない例

次のプログラムは、利用者がファイル名を入力する小さなウインドウを表示し、その後にそのテキストをウインドウに表示するかまたは印刷します。 ファイル名をタイプする代りに、ファイルブラウザを使うこともできます。 これは、主ウインドウ、テキストウインドウ、ファイルブラウザが表示されているスクリーンスナップショットです:

[screen snapshot]

このプログラムは、標準的な Tk ウイジェットではなく、Python の標準ライブラリに含まれているウイジェットを三つ使っています:エラーメッセージを表示する "ダイアログボックス"、ファイルブラウザ、スクロールバー付きのテキストウイジェトです。 このプログラムは、自身のウイジェットも二つ定義しています:ラベル、ファイル名の記入欄、ファイルブラウザを起動するボタンから成る "ファイル名エントリー" と、左に整列したボタンの組と右に整列したボタンの組から成る "ボタンバー" です。 さらに、ウインドウ全体を表すウイジェットを二つ、クラスによって定義しています。一つはテキスト表示ウインドウ、もう一つは主ウインドウです。

このプログラムは、未だ述べていない packer オプションを一つ使っています。 通常、全てのウイジェットは、その中味に依って大きさが固定されます。 しかし、正確な大きさがあらかじめ明白でないウイジェットもあります。例えば入力欄です。 入力欄は、packer によって適当に大きさを割り当てられます。 fill=X というオプションを packer に指定すると、そのウイジェットは、マスター中で得られる限りの空間を埋める様に、x方向に引き伸ばされます。

ボタンに連動する動作は、そのウイジェットのメソッドとして定義されていることに注意しましょう。 これは、その動作がウイジェットの属性にアクセス出来るようにするために必要です。このことはしばしば必要になります。 実際の Tk プログラムでは、殆ど全ての動作がウイジェットのメソッドによって定義されています。

from Tkinter import *
from Dialog import Dialog
from FileDialog import LoadFileDialog
from ScrolledText import ScrolledText

class FilenameEntry(Frame):

    def __init__(self, master, text):
	Frame.__init__(self, master)
	Label(self, text=text).pack(side=LEFT)
	self.filename = StringVar()
	Entry(self, textvariable=self.filename).pack(side=LEFT, fill=X)
	Button(self, text="Browse...", command=self.browse).pack(side=RIGHT)

    def browse(self):
	file = LoadFileDialog(self).go(pattern='*')
	if file:
	    self.filename.set(file)

    def get(self):
	return self.filename.get()


class ButtonBar(Frame):

    def __init__(self, master, left_button_list, right_button_list):
	Frame.__init__(self, master, bd=2, relief=SUNKEN)
	for button, action in left_button_list:
	    Button(self, text=button, command=action).pack(side=LEFT)
	for button, action in right_button_list:
	    Button(self, text=button, command=action).pack(side=RIGHT)


class FileNotFoundMessage(Dialog):

    def __init__(self, master, filename):
	Dialog.__init__(self, master, title = 'File not found',
			text = 'File ' + filename + ' does not exist',
			bitmap = 'warning', default = 0,
			strings = ('Cancel',))


class TextWindow(Frame):

    def __init__(self, master, text):
	Frame.__init__(self, master)
	text_field = ScrolledText(self)
	text_field.insert(At(0,0), text)
	text_field.pack(side=TOP)
	text_field.config(state=DISABLED)
	ButtonBar(self, [],
		  [('Close', self.master.destroy)]).pack(side=BOTTOM, fill=X)


class MainWindow(Frame):

    def __init__(self, master):
	Frame.__init__(self, master)
	Label(self, text="Enter a filename and " +
                         "select an action").pack(side=TOP)
	self.filename_field = FilenameEntry(self, "Filename: ")
	self.filename_field.pack(side=TOP, fill=X)
	ButtonBar(self, [('Show', self.show), ('Print', self.lpr)],
		  [('Quit', self.quit)]).pack(side=BOTTOM, fill=X)

    def show(self):
	filename = self.filename_field.get()
	try:
	    text = open(filename).read()
	except IOError:
	    FileNotFoundMessage(self, filename)
	else:
	    new_window = Toplevel()
	    new_window.title(filename)
	    TextWindow(new_window, text).pack()

    def lpr(self):
	filename = self.filename_field.get()
	import os
	os.system('lpr ' + filename)


mw = MainWindow(None)
mw.pack()
mw.mainloop()

Tk についてさらに学ぶには

Tkinter に関する文書は長期に渡って不十分なままですし、入手可能であって完全な記述というのは未だにありません。 しかし、Pythonware が提供する文書はほぼ完全で、殆どの人達の必要を満たすに十分でしょう。 手始めに introduction を読んでから、プログラムを開発する際には、 reference を利用すると良いでしょう。


練習問題


目次