今回からはTkinterというPythonのGUIライブラリを使って、GUIアプリケーションの使い方について学んでみたいと思います。

GUIは初心者向けとは言いづらいところがありますが、

  • 自分が作りたいものを想像しやすい
  • オブジェクト指向に慣れ親しむのに向いている

という点から、初級者から中級者になるためのステップとしてはベストなものだと私は考えています。

PythonでGUIを使うといってもさまざまなライブラリがあり、Tkinterがベストではない場合もあります。また、そもそもPythonではなくほかの言語を用いて作るべきである可能性すらあります。今回はあくまでも学習目的メインなので、そのあたりのことはご容赦ください。

また、本連載はGUIをメインとしているわけではないので、当然ながらTkinterのすべてはカバーしきれません。簡単な使い方紹介や実例をとおして、GUIの仕組みやオブジェクト指向について理解してもらうことを目標にしているので、興味があるかたはほかのWebページや本などを活用してください。

Tkinterの紹介と本連載で作るサンプル

WindowsやMac上のアプリケーションも当然ながらプログラムで作られています。それらを思い浮かべると、ある共通点があることがわかりませんか。まず一般的にアプリケーションはウィンドウという形で提供され、そのなかにボタンやテキストを書く領域、それにメニューなどが存在しています。

GUIはこれらの「見た目」を作ることに特化したライブラリです。ライブラリの特定の関数を呼び出すことでウィンドウやメニューを作成したり、場合によってはライブラリのクラスを継承することで細かい挙動や見た目を作ったりします。

TkinterもこういったGUIのライブラリであり、Pythonで利用されます。ただ、TkinterはPythonで一から作られたものというよりは、Tkという汎用のGUIのツールキットをPythonで利用できるようにしたものです。この仕組みを以下の図に記します。

Tkinterの仕組み

上記図にあるように、まずTkと呼ばれているGUIのツールキットがあり、Pythonがそれを「ラップ」しているようなイメージです。そのラップされたPythonのライブラリをTkinterと呼んでいます。Tkはほかの言語でも利用されていて、たとえば図にあるように、Rubyでも利用されています。

今回はこのTkinterとPythonを使って、以下のようなカウンターを実装することを目標にします。

今回作成するカウンター

見てもらうとわかりますが、clickと書かれているボタンを押すと表示されているカウンターの数が増えます。

GUIの基本的な考え方

GUIといってもブラウザ上で動くものやデスクトップアプリとして動くものなどいろいろあります。ただ、ある一定の規模のGUIのアプリを構築する場合、基本的にGUIのウィンドウは入れ子構造で構成されている場合が多いです。

たとえば以下の図のようになります。

アプリの画面の概念図とGUIのパーツの階層構造

左側にアプリの画面の概念図、右側に GUIのパーツの階層構造が書かれています。みてわかるようにFrame1はFrame2, 3を内部に持っていて、それらを縦に並べて表示させています。そしてFrame2も中にWidget1,2,3,4を持っており、それを横に表示。Frame3はWidget5,6を持ち、縦に表示といった具合です。

このとき大切なのは、Frame1,2,3 やWidgetは必要に応じて適切なものが選ばれ、場合によっては「継承」でカスタマイズされているということです。このあたりの話は後半で扱います。

Tkinterの作法

詳しいGUIの使い方を理解するためには、まず基礎となる作法を学ぶ必要があります。 そのため、簡単なものから順を追って説明していきたいと思います。

まず最初に以下の図のものを作ります。

これを実現するコードは以下となります。

import Tkinter as tk

font=("Helevetica", 32, "bold")
label = tk.Label(text="Hello Tkinter", font=font, bg="red")
label.pack()
label.mainloop()

コードの解説をすると、まず最初にTkinterを使うためにライブラリをインポートしています。import X as Yとすると、XというライブラリをYという名前で使うことができます。Tkinterという名前が長いのでtkとしています。

次にfontの定義ですが、これは単にタプルでフォント、文字サイズ、大文字と指定しているだけです。

tk.Label()関数でGUIのパーツである「ラベル」と呼ばれているWidgetを作成します。名前付き引数を使って作るパーツの詳細をオプションとして指定していますが、今回は表示する文字列を "Hello Tkinter" とし、フォントは定義したもの、背景色(BackGround)を赤としています。

そして、作成されたラベルのpack()と呼ばれているメソッドで label を配置し、mainloop()メソッドでGUIを表示するという流れになります。

今回はラベルひとつだけを表示するので比較的簡単でしたが、複数のウィジェットと呼ばれているGUIパーツをまとめたアプリケーションを作る場合はもうちょっと複雑になってきます。

複数のWidgetの利用

先程はラベルひとつだけの単純なパーツ構成だったので、今回はもう少し複雑なものを作ってみます。とはいっても単純で、以下のような2つのラベルを表示するだけのアプリケーションです。

これを実現するためにはFrameを使うのが簡単です。Frameはその中にWidgetを複数持つことができ、それらをどのように表示するかを管理してくれます。まぁ、まずは簡単なのでコードを見てみましょうか。

import Tkinter as tk

frame = tk.Frame()
font=("Helevetica", 32, "bold")
label1 = tk.Label(frame, text="Hello Tk", font=font, bg="red")
label2 = tk.Label(frame, text="Hello Python", font=font, bg="blue")
label1.pack()
label2.pack()
frame.pack()
frame.mainloop()

上記のコードで気をつけてもらいたいのは、まずtk.Frame()関数によりFrameのインスタンスを作っているところです。

そして次に、先ほどと同じようにtk.Label()関数でラベルを作っているのですが、 その第一引数に親となるFrameを与えています。このようにすることで新しく作られたラベルは先程のFrameの中に格納されます。要するにラベルがFrameの子要素となったということです。

そして、ラベル、Frameというように内側から順にpackメソッドを呼び出していき、最後に一番外側のFrameに対してmainloop を呼び出します。簡単ですね。

GUIのフレームワークにより、このパーツの親子関係の指定方法は変わってきます。今回は子に親を指定していますが、どちらかというと親に対して "親のパーツ.addChild(中のパーツ)" というような形で指定することのほうが一般的かもしれません。基本となる思想はそれほど変わらないので、フレームワークの流儀に合わせてください。

Frameの表示オプション

先ほどのフレームでの表示はちょっと雑だったので、オプションを付けてもう少し丁寧に表示してみます。オプションはpackメソッドの中で「どのように表示するか」を指定できます。たとえば以下のようなものがあります。

基本的にFrameの中に「縦に並べるか」「横に並べるか」を指定し、あとは細かい調整をするという考え方でいいと思います。今回は割愛していますが、中に格納するWidget間のスペースといったことも調整できます。

Frameの入れ子構造

複雑なWidgetの配置をしたい場合は、Frameの中にFrameを入れるという入れ子構造にします。たとえば以下のような配置にしたいとしましょう。

これは次のようにすれば大丈夫です。

import Tkinter as tk

frame1 = tk.Frame()

# Child1

frame2 = tk.Frame(frame1)
font=("Helevetica", 32, "bold")
label1 = tk.Label(frame2, text="Hello Tk", font=font, bg="red")
label2 = tk.Label(frame2, text="Hello Python", font=font, bg="blue")
label1.pack(side=tk.LEFT)
label2.pack(side=tk.LEFT)
frame2.pack()

# Child2

label3 = tk.Label(frame1, text="Hello Wodld", font=font, bg="green")
label3.pack(fill=tk.X)

frame1.pack()
frame1.mainloop()

注目して欲しいのはframe2の親にframe1が指定されていることです。frame2の中には2つのラベルが入っていますが、これはframe1から見るとframe2というひとつの要素に過ぎません。そしてlabel3は、frame1にとってframe2と同じく自分の子供となります。つまりframe1にとってはlabel1,2は孫で、label3は子供になります。

誰が誰の親で、中から外に順にpackしていくということに注意を払えばそれほど難しくないですね。

Widgetの継承

今まで利用してきたGUIのパーツはFrameとLabelだけでしたが、実際にはボタンやテキストボックスなど、さまざまなものが存在しています。ただ、よく考えてください。ボタンやラベルにはそれぞれ共通した役割である「フレームに格納可能」「画面に表示される」「クリックなどのイベント処理が可能」といった共通した特性があります。

これらの共通した特性を複数のクラスで実現する必要がある場合は、前回お話ししたように「継承」が便利です。GUIはコンポジションより継承をするべき場面です。

たとえばTkinterというよりも、一般的なGUIのライブラリでは、以下のようにラベルやボタンといったパーツは基本となるWidgetから継承される場合が多いです。

こうすることで基本機能はWidgetに搭載し、その差分のみ子クラスであるボタンやラベルで実装すれば機能を実現できます。この基本ルールは自分でGUIのパーツを新しく作る際もあてはまります。

確認のために簡単なGUIのパーツを作ってみます。冒頭でお話したカウンタを作ります。

これを実装する場合、別に継承を使わないでも実現可能です。たとえば以下のようなものもそうです。

import Tkinter as tk

class Counter:

def __init__(self, value):
    self.value = value
    frame = tk.Frame()
    font = ("Helevetica", 32, "bold")
    self.label = tk.Label(frame,
                          text=self.getText(),
                          font=font, bg="red")
    button = tk.Button(frame, text="Click",
                       command=self.clicked)
    self.label.pack()
    button.pack()
    frame.pack()
    frame.mainloop()

def clicked(self):
    self.value += 1
    self.label.configure(text=self.getText())

def getText(self):
    return "Count:{}".format(self.value)

c = Counter(0)

コードを読んでみるとわかりますが、クラスCounterは、特にTkinterのパーツを継承しておらず、その初期化の際にFrameを作り、その中にラベルとボタンを格納して表示するという仕組みになっています。なお、ボタンが押されたときのアクションは、登録されたメソッドが呼び出されるというものになっています。

一方、同じことを継承を使って実現することもできます。こちらのほうが一般的な GUIアプリの作り方だと思います。

 import Tkinter as tk

class Counter(tk.Frame):
    def init(self, master=None, value=0):
        self.value = value

   tk.Frame.__init__(self, master)
    font = ("Helevetica", 32, "bold")
    self.label = tk.Label(self,
                          text=self.getText(),
                          font=font, bg="red")
    self.button = tk.Button(self, text="Click",
                       command=self.clicked)
    self.label.pack()
    self.button.pack()

def clicked(self):
    self.value += 1
    self.label.configure(text=self.getText())

def getText(self):
    return "Count:{}".format(self.value)

f = tk.Frame()
c1 = Counter(value=0, master=f)
c2 = Counter(value=5, master=f)
c1.pack()
c2.pack()
f.pack()
f.mainloop()

ちょっとコードが複雑になっていますが、Counterはtk.Frameを継承して作り、自分自身の中にラベルとボタンを配置するという構成になっています。

また、継承を使わないコードと使うコードの最後の部分を見比べてもらうとわかりますが、継承をしない場合はあくまでも「俺ルール」で表示をさせているのに対して、継承を使う場合はほかのtkのウィジェットのGUIパーツとほとんど違いなく利用できています。ほかのGUIパーツのように使えるので、今回はFrameの中に入れて2つ利用するということをやっています。これは継承を使わないコードでは「俺ルール」で仕組みを作らないと実現できませんし、そんなコードは汎用性が低くて使いにくいです。

GUIのアプリケーションはパーツとパーツが複雑に絡み合うため、大規模なものを作るとなると難易度が一気に増えます。ただ、そのような問題をいかに乗り越えるかということに四苦八苦し、自分でいろいろ考えたアイディアを試したり先人の知恵を借りたりして解決していくということを繰り返すことでプログラマとしてのレベルが上がってくれるのではないかと思います。


今回でオブジェクト指向編は終わりです。また演習の解説と回答、およびコラムを挟んだうえで、今までの話でカバーできなかった例外処理などの話題を扱っていきたいと思います。

執筆者紹介

伊藤裕一(ITO Yuichi)

シスコシステムズでの業務と大学での研究活動でコンピュータネットワークに6年関わる。専門はL2/L3 Switching とデータセンター関連技術およびSDN。TACとしてシスコ顧客のテクニカルサポート業務に従事。社内向けのソフトウェア関連のトレーニングおよびデータセンタとSDN関係の外部講演なども行う。

もともと仮想ネットワーク関連技術の研究開発に従事していたこともあり、ネットワークだけでなくプログラミングやLinux関連技術にも精通。Cisco社内外向けのトラブルシューティングツールの開発や、趣味で音声合成処理のアプリケーションやサービスを開発。

Cisco CCIE R&S, Red Hat Certified Engineer, Oracle Java Gold,2009年度 IPA 未踏プロジェクト採択

詳細(英語)はこちら