PythonのTkinterでGUIアプリを作る

PythonでGUIアプリを作ることになって，Tkinter使ったら早かったので，次使うとき用の備忘録です．

# 雛形
```py
import Tkinter as Tk

class Application(Tk.Frame):
    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.pack()

root = Tk.Tk()
app = Application(master=root)
app.mainloop()
```
ここに色々なWidgetをくっつけていくことになります．

# Widget[^1]
|名称|役割|
|:--|:--|
|Button|ボタン|
|Canvas|画像を貼り付けたり描画したり|
|Checkbutton|チェックボタン|
|Entry|1行のテキストエリア．複数行はText|
|Frame|他のWidgetをまとめるフレーム|
|Label|ラベル．画像も表示できる|
|Listbox|リストボックス|
|Menu|メニューバーの作成|
|Message|複数行のラベル|
|OptionMenu|コンボボックス|
|Radiobutton|ラジオボタン|
|Scale|スライダー|
|Scrollbar|スクロールバー|
|Text|複数行のテキストエリア|
|Toplevel|ポップアップウィンドウなどを作る用|

[^1]: 全てをカバーしているわけではありません

# Widgetの配置
Widgetの配置にはpack，place，gridメソッドのいずれかを使います．
packはWidgetを一列，または1行に並べていきます．placeは各Widgetの場所を指定して配置します．gridは2次元座標上にWidgetを配置していきます（Excel方眼紙のようなイメージです）．
以降は基本的にgridを使います．

```py
    def __init__(self, master=None):
        # 省略
        self.create_widgets()

    def create_widgets(self):
        self.label = Tk.Label(self, text=u'入力ファイル')
        self.entry = Tk.Entry(self)
        self.button = Tk.Button(self, text=u'開く')
        self.check = Tk.Checkbutton(self, text=u'拡張子をtxtに限定')
        self.text = Tk.Text(self)

        self.label.grid(column=0, row=0)
        self.entry.grid(column=1, row=0)
        self.button.grid(column=2, row=0)
        self.check.grid(column=0, row=1)
        self.text.grid(column=0, columnspan=3, row=2)
```

# 配置の調整と伸縮への対応
はじめにexpand，fill，anchorを設定し，親フレームを伸縮に対応させます．
anchorや後述のstickyで出てくるTk.XXは位置を東西南北（NSEW）で指定できます．

```py
    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.pack(expand=1, fill=Tk.BOTH, anchor=Tk.NW)
        self.create_widgets()
```

そして各Widgetをgridで配置するときに，stickyでどこを起点にどう伸縮させるかを指定します．
もう一つ大事なのは，gridでWidgetを配置したときには，columnconfigureとrowconfigureでどの行・列がどの比率で伸縮するのかを指定してやる必要があります．
デフォルトではweightパラメータが0（伸縮しない）になっているので，単純には伸縮させたい行・列のweightを1にしてやります．
以下の例ですと，選択したファイル名が入ることを想定しているEntryのある1列目をTextのある2行目のみが伸縮に対応します．

```py
        self.label.grid(column=0, row=0, sticky=Tk.W)
        self.entry.grid(column=1, row=0, sticky=Tk.EW)
        self.button.grid(column=2, row=0, sticky=Tk.E)
        self.check.grid(column=0, columnspan=2, row=1, sticky=Tk.W)
        self.text.grid(column=0, columnspan=3, row=2, sticky=Tk.NSEW)

        self.columnconfigure(1, weight=1)
        self.rowconfigure(2, weight=1)
```

# Entryの中身やCheckbuttonのチェック状態の取得
Entryの中身やCheckbuttonのチェック状態は(text)variableオプションに変数を設定しておくことで取得できます．

```py
        self.var_entry = Tk.StringVar()
        self.entry = Tk.Entry(self, textvariable=self.var_entry)
        self.var_check = Tk.BooleanVar()
        self.check = Tk.Checkbutton(self, text=u'拡張子をtxtに限定',
                                    variable=self.var_check)
```

Tk.StringVar()やTk.BooleanVar()の値の設定や取得にはset()，get()を使います．

# ボタン押下時のアクション
ボタン押下時に実行するメソッドはcommandオプションで指定します．

```py
    def create_widgets(self):
        self.var_entry = Tk.StringVar()
        self.entry = Tk.Entry(self, textvariable=self.var_entry)
        self.button = Tk.Button(self, text=u'開く', command=self.button_pushed)

    def button_pushed(self):
        self.var_entry.set(u'ボタンが押されました．')
```

# ファイルダイアログ
ファイルダイアログを開くには，tkFileDialogをimportして，askopenfilenameを使います．filetypesオプションを使うことでファイルタイプの指定ができます．
tkFileDialogには他に，askopenfile，askopenfiles，askopenfilenames，asksaveasfile，asksaveasfilename，askdirectoryなどがあります．

```py
import tkFileDialog as tkFD

    def button_pushed(self):
        ft = [('text files', '.txt')] if self.var_check.get() else []
        self.var_entry.set(tkFD.askopenfilename(filetypes=ft))
```

# 変数の変化に応じたアクション
変数を監視して変化した時にメソッドを呼ぶには，traceメソッドを使います．

```py
    def create_widgets(self):
        self.var_entry = Tk.StringVar()
        self.var_entry.trace('w', self.entry_changed)

    def entry_changed(self, *args):
        if os.path.exists(self.var_entry.get()):
            self.text.delete('1.0', Tk.END)
            self.text.insert('1.0', open(self.var_entry.get()).read())
```

var_entryの値が書き換えられた時にentry_changedが呼ばれます．
最初にTextの内容を全部消去してから，ファイルの内容を表示します．

# スクロールバー
Textにスクロールバーをつけることができます．[^2]
垂直方向のスクロールバーが最初から付いているScrolledTextというWidgetもあるようです．

```py
        self.text = Tk.Text(self, wrap=Tk.NONE)
        self.yscroll = Tk.Scrollbar(self, command=self.text.yview)
        self.xscroll = Tk.Scrollbar(self, command=self.text.xview,
                                    orient=Tk.HORIZONTAL)
        self.text['yscrollcommand'] = self.yscroll.set
        self.text['xscrollcommand'] = self.xscroll.set

        self.text.grid(column=0, columnspan=3, row=2, rowspan=2, sticky=Tk.NSEW)
        self.yscroll.grid(column=2, row=2, sticky=Tk.NS + Tk.E)
        self.xscroll.grid(column=0, columnspan=3, row=3, sticky=Tk.EW + Tk.S)
```

[^2]: スクロールバーの交点，もう少しやりようがある気がします

# 画像の貼り付け
Tkinterでは基本的にgifを想定しているようです．一方で例えばmatplotlib(pyplot)のsavefigではgifでの画像保存ができません（よね？）．そこで私は以下のように変換してから貼り付けているのですが，もう少しスマートな方法ないでしょうか．

```py
from PIL import Image
    def create_widgets(self):
        self.canvas = Tk.Canvas(self)
        self.canvas.grid(column=0, columnspan=3, row=4, sticky=Tk.NSEW)

    def set_image(self):
        img = Image.open('foo.png')
        img.save('_tmp.gif')
        self.image_data = Tk.PhotoImage(file='_tmp.gif')
        self.canvas.create_image(200, 100, image=self.image_data)
```

# 標準出力のリダイレクト
stackoverflowからのコピペですが．．．
標準出力の内容をTextなどに出力させることができます．

```py
import sys

    def __init__(self, master=None):
        Tk.Frame.__init__(self, master)
        self.pack(expand=1, fill=Tk.BOTH, anchor=Tk.NW)
        self.create_widgets()
        sys.stdout = self.StdoutRedirector(self.text)
        sys.stderr = self.StderrRedirector(self.text)

    class IORedirector(object):
        def __init__(self, text_area):
            self.text_area = text_area

    class StdoutRedirector(IORedirector):
        def write(self, st):
            self.text_area.insert(Tk.INSERT, st)

    class StderrRedirector(IORedirector):
        def write(self, st):
            self.text_area.insert(Tk.INSERT, st)

sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
```

# Python3対応
Tkinterはtkinter，tkFileDialogはfiledialogに名前が変わっているようです．