PythonのGUI(ジオメトリーマネージャ)

プログラミング

Pythonは今、最も旬なプログラミング言語だと思います。雑誌見てもpython特集ーばっかり。

といっても1991年からグイド・ヴァン・ロッサムが独占的に開発している言語。
(個人的にはPythonがあるのにRubyがある理由がわからんのだよ。松本さんには悪いけどね。)

で、Raspberry Piなんかでプログラムを書こうと考えるとPython一択。

悪魔のV2やら他の落とし穴

が、Pythonには悪魔の影がある。

Version 2.xシリーズ。

Macを買うとMac OSが搭載されている。

これはFreeBSD系のUnixをAppleさんが大幅にカスタマイズしたものです。
影で動くさまざまなメンテナンスシステムにはPython V2シリーズが使われているようです。

ところが2020年の1月からPython V.2シリーズはサポート切れです。

普通はこういう時、v2はあっさり消すのですが、Pythonの場合はダラダラ残してしまってる。

だからpython v2とv3の共存にとても多くの人が困っている。

環境を仮想化して切り替えるなんてワザもあります。

しかし、他の言語環境ではそんな変なモノは、私が知る限り、ありません。本末転倒なので採用しません。

例えば, 俺がこの記事を書いている最大の理由がPython V3でGUIアプリを作りたいなんだが、面倒なことばかり起きます。

通常、macではpython3のようなオープンソースシステムアプリはHomebrewというパッケージ管理ソフトを利用して導入します。
このようなパッケージ管理ソフトは前提ソフトや主な関係ソフトを自動的にインストールしてくれるので、便利です。

しかし2020年9月時点でHomebrewでインストールするPython3にはGUIパッケージのTkinter(後述)は8.5です。
このパッケージにはグラフィック処理で致命的なバグがあり、使えません。
8.6を使いたいとなるとHomebrewは無力です。

結局、python.orgからMac 64bit用のパッケージをダウンロードしてインストールすることにしました。

Homebrewは他にPHPのパッケージを入れているので捨てさるわけにはいかず、

Python3は

 brew uninstall --ignore-dependencies python3

で、強制的にアンインストールしました。
メンドクサ。

PythonでGUIアプリを作るためには

さて、GUIツールのKivyが今風だというのでインストールしようとするが、MacでもRaspberryでも失敗する。
こういうインストレーションで(専門家じゃないから)書いてあるシナリオどおりに作業するしか方法がない。
途中でトラブったら、もうわからんよ。
で、KivyでGUIを書くことは諦める。

それゆえPython標準のTkinterでええやんと考える。
PythonV3では組み込まれているGUIパッケージはTkinterと呼ばれているのだ。

しかし、Python V3では

tkinter (すべて小文字)でTkinterを呼びます。

なんだよ、巷でいまだに例文でTkinterってプログラム内に記載されまくってる。

繰り返すけどV2はもうサポートされないパッケージなんだよ.

ネット上の情報ってこういう、「かつて動いたことがあります」的情報が最も役にたたない。

Googleで検索するときは、一年前から、とか半年前から、とか指定して調べないと。

猫も杓子もpythonと言ってるが、Pythonも決して使いやすい言語ではないのだ。

Tkinterのジオメトリーマネージャ

GUIのクラスの呼び出し方とか、各種パーツ(ウィジェット)の呼び出し方なんてのは、あちこちで書かれているのでそちらを研究すればよく、ジオメトリーマネージャについてはあまり書かれていないのでここで研究してみよう。

JavaのGUIパッケージSwingで初めて学んだっけ?画面のパーツの配置を管理する存在にまかせられるように配置する。

Tkinterではジオメトリーマネージャーという。

  • .pack()
  • .gird()
  • .place()

があります。

それぞれのframeでひとつのジオメトリーマネージャだけが使えます。
でもframeが違えばジオメトリーマネージャは違うものでOK。

.pack()

パックは”Packingアルゴリズム”でウィジェットの置き場所が決まります。次の2つのステップのことを言っています。

  1. parcel(パーセル)という長方形のエリアを計算し、ウィジェットが十分に入る高さと幅を計算します。
  2. とくに場所を指定されない限り、パーセルの中央にウィジェットを置きます。

個人的には小さいフレームで縦並びでいい時だけですね。

それぞれのウィジェットはできるだけ上に置かれます。(言い換えると隙間なく縦に並べられる)
ウィジェットそれぞれにパーセルがあり、じゅんじゅんに積み下げられた状態です。

.pack()はウィジェットの置き場所のいくつかの指定が可能です。たとえばfillキーワードでフレームのどの方向に置くかを指定できます。

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, height=100, bg=”red”)
frame1.pack(fill=tk.X)

frame2 = tk.Frame(master=window, height=50, bg=”yellow”)
frame2.pack(fill=tk.X)

frame3 = tk.Frame(master=window, height=25, bg=”blue”)
frame3.pack(fill=tk.X)

window.geometry(“100×200+100+300”)

window.mainloop()

これですべてのウィジェットは一定の幅になります。

ジオメトリマネージャとしてのPackはウィンドウのりサイズに対応します。上のコードだと、X軸に伸ばすと色(フレーム)も伸びます。Y軸に伸ばしても動きません。

.pack()のsideキーワードはウィジェットをウィンドウのどの再度に置くかを指定します。次のオプションがあります。

  • tk.TOP
  • tk.BOTTOM
  • tk.LEFT
  • tk.RIGHT

.pack()はこれらを指定しないと、すでに見てきたようにできる限りTOPを取ります。

では次のコードではどうなるでしょうか?

import tkinter as tk

window = tk.Tk()

frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.Y, side=tk.LEFT)

frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.Y, side=tk.LEFT)

frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.Y, side=tk.LEFT)

window.mainloop()

すべてがLeft(左)から順に並びます。
リサイズするとY軸方向には追従しますが、X軸方向には動きません。
しかし、さらにexpandパラメータを入れるとどちらの方向にも追従します。



window = tk.Tk()

frame1 = tk.Frame(master=window, width=200, height=100, bg="red")
frame1.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

frame2 = tk.Frame(master=window, width=100, bg="yellow")
frame2.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

frame3 = tk.Frame(master=window, width=50, bg="blue")
frame3.pack(fill=tk.BOTH, side=tk.LEFT, expand=True)

window.mainloop()

.place()

X,Y(単位はピクセル)でウィジェットの場所を指定したい時はこのジオメトリマネージャを使います。原点はフレームのもっとも上で、もっとも左です。


import tkinter as tk

window = tk.Tk()

frame = tk.Frame(master=window, width=150, height=150)
frame.pack()

label1 = tk.Label(master=frame, text="I'm at (0, 0)", bg="red")
label1.place(x=0, y=0)

label2 = tk.Label(master=frame, text="I'm at (75, 75)", bg="yellow")
label2.place(x=75, y=75)

window.mainloop()

frame.pack()はウィンドウにフレームをpackで貼り付けています。
label1はX=0,Y=0の位置に明示的に貼り付けられます。
label2はx=75,Y=75の位置に貼り付けられます。

しかしウィンドウのリサイズで位置が保たれることはありません。placeはリサイズがない場合にオススメします。

.grid()

もっとも好んで使われるのはグリッドです。
余談ですが、ウェブのUIもさまざまな形が試みられましたが、最近は四角を並べた形がもっとも多いのではないでしょうか。

グリッドは行(row)と列(column)にフレーム内をわけて指定します。
どちらもインデックス値としては0からはじまります。
以下のコードは3×3をラベルでgridを利用して埋める例です。


import tkinter as tk

window = tk.Tk()

for i in range(3):
    for j in range(3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1,
        )
        frame.grid(row=i, column=j)
        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack()

window.mainloop()

frame.grid(row=i, column=j)に注目してください。これで作ったframeをセットしていっています。
しかし、labelをみてください。labelはpackで貼り付けています。

ここでウィンドウのリサイズが起きたとしましょう。.grid()はそれぞれ呼び出され、ウィンドウオブジェクトを予期したとおりに管理します。同時にそれぞれのフレーム内のウィジェットは.pack()ジオメトリマネージャーによりコントロールされます。

この例ではラベルはきっちりフレームに入っています。もし、グリッドとラベルの間に隙間を開けたい時はPaddingを指定します。
paddingにはexternalとinternalがあります。

externalはグリッドのセルの周りにスペースを設けます。
padxは水平方向のパディング、padyは垂直方向のパディングです。
単位はピクセルです。
なお、.pack()も同様にパディングを指定できます。

さてここでリサイズをしてみても、期待した動きになりません。

ウィンドウオブジェクトの.columnconfigure()と .rowconfigure()を使用して、ウィンドウのサイズが変更されたときにグリッドの行と列がどのように拡大するかを指定しなくてはなりません。

各フレームウィジェットで.grid()を呼び出している場合でも、グリッドはウィンドウにアタッチされていることに注意してください。 .columnconfigure()と.rowconfigure()は3つの重要な引数を取ります。

1.グリッドのcolumnとrowのインデックス (または、複数の行または列を同時に指定するためのインデックスのリスト)
2. weigtと呼ばれるキーワード ウィンドウのサイズ変更に応じて、相対的にどのように応答するかを決定する
3. minsizeと呼ばれるキーワード 行の高さまたは列の幅の最小サイズをピクセル単位で設定する

weightはデフォルトで0に設定されています。これは、ウィンドウのサイズが変更されても列または行が拡大しないことを意味します。すべての列と行に1の重みが与えられている場合、それらはすべて同じ速度で成長します。 1つの列の重みが1で、別の列の重みが2の場合、2番目の列は最初の列の2倍の速度で拡張します。

コードを見たほうが早いです。



import tkinter as tk

window = tk.Tk()

for i in range(3):
    window.columnconfigure(i, weight=1, minsize=75)
    window.rowconfigure(i, weight=1, minsize=50)

    for j in range(0, 3):
        frame = tk.Frame(
            master=window,
            relief=tk.RAISED,
            borderwidth=1
        )
        frame.grid(row=i, column=j, padx=5, pady=5)

        label = tk.Label(master=frame, text=f"Row {i}\nColumn {j}")
        label.pack(padx=5, pady=5)

window.mainloop()

.columnconfigure()と.rowconfigure()でcolumnとrowは1の重みを持つように構成されます。
これにより、ウィンドウのサイズが変更されると各行と列が同じレートで拡張されます。
minsize引数は、列は75、行は50に設定されています。 これにより、ウィンドウサイズが非常に小さい場合でも、Labelウィジェットが文字を切り取らずに常にテキストを表示するようになります。

結果は、ウィンドウのサイズが変更されるとスムーズに拡大および縮小するグリッドレイアウトです。

また、stickyパラメーターによりラベルがグリッドのどこに「貼り付く」かを指定することができます。

  • “n”か”N”はセルのトップ中心を指定したことになります。
  • “e””E”はセルの右の中心を指定したことになります。
  • “s”か”S”はセルのボトムの中心を指定したことになります。
  • “w”か”W”はセルの左の中心を指定したことになります。

このパラメーターは隣り合ったものは同時に指定できます。
どういうことかというと、


import tkinter as tk

window = tk.Tk()
window.columnconfigure(0, minsize=250)
window.rowconfigure([0, 1], minsize=100)

label1 = tk.Label(text="A")
label1.grid(row=0, column=0, sticky="ne")

label2 = tk.Label(text="B")
label2.grid(row=1, column=0, sticky="sw")

window.mainloop()

結果はこうなります。

ウィジェットがスティッキーで配置されている場合、ウィジェット自体のサイズは、その中にテキストやその他のコンテンツを含めるのに十分な大きさです。 グリッドセル全体を埋めることはありません。
グリッドを塗りつぶすには、「ns」を指定してウィジェットにセルを垂直方向に塗りつぶすか、「ew」を指定してセルを水平方向に塗りつぶすことができます。 セル全体を埋めるには、stickyを「nsew」に設定します。 次の例は、これらの各オプションを示しています。


import tkinter as tk

window = tk.Tk()

window.rowconfigure(0, minsize=50)
window.columnconfigure([0, 1, 2, 3], minsize=50)

label1 = tk.Label(text="1", bg="black", fg="white")
label2 = tk.Label(text="2", bg="black", fg="white")
label3 = tk.Label(text="3", bg="black", fg="white")
label4 = tk.Label(text="4", bg="black", fg="white")

label1.grid(row=0, column=0)
label2.grid(row=0, column=1, sticky="ew")
label3.grid(row=0, column=2, sticky="ns")
label4.grid(row=0, column=3, sticky="nsew")

window.mainloop()

結果はこれ。

上記の例が示しているのは、.grid()ジオメトリマネージャのstickyパラメータを使用すると.pack()ジオメトリマネージャのfillパラメータと同じ効果を実現できることです。 次の表に、stickyパラメータとfillパラメータの対応をまとめます。

.grid()は強力なジオメトリマネージャです。 多くの場合、.pack()よりも理解しやすく、.place()よりもはるかに柔軟です。

イベントとイベントハンドラー

ジオメトリーマネージャーを理解すれば、ウィジェットを置いてGUIを作ることはできます。

ここからはウィジェットのイベントを扱うことでユーザーとやり取りのできるアプリケーションの作り方を学びます。

Tkinterのアプリケーションを作ると、最後にかならず window.mainloop()とイベントループを呼び出さねばなりません。これはイベントループを開始します。

GUIではさまざまなイベントが起きます。たとえば、キーを押した、離した、マウスを動かした、クリックした、ダブルクリックした、などなど。。。

イベントループによりTkinterのユーザーは自分の欲しいイベントについてのみイベントハンドラーを書けばいいのです。

.bind()

イベントとイベントハンドラーを結びつけるひとつの方法がbind()を使うことです。
以下のコードをみてください。


import tkinter as tk

window = tk.Tk()

def handle_keypress(event):
    print(event.char)
 
 
 window.bind("", handle_keypress)

window.mainloop()

bind()で、hadle_keypress()イベントハンドラーが”“イベントと結び付けられています。
このように.bind()はふたつの引数が必要です。

  • event イベント名。イベント名はtkinterで定義されているものです。
  • イベントハンドラー イベントが起きたら呼び出す関数の名前

 

command

こちらの方法のほうが、実際のプログラミング例では見かけます。

どのButtonウィジェットもcommand属性をもち、ブタンを押した時、動作させたい関数を定義できます。
以下のコードはボタンを押すと数値が変わる例です。


import tkinter as tk

window = tk.Tk()

window.rowconfigure(0, minsize=50, weight=1)
window.columnconfigure([0, 1, 2], minsize=50, weight=1)

def decrease():
	value=int(lbl_value["text"])
	lbl_value["text"] = f"{value-1}"

def increase():
	value=int(lbl_value["text"])
	lbl_value["text"] = f"{value+1}"

btn_decrease = tk.Button(master=window, text="-", command=decrease)
btn_decrease.grid(row=0, column=0, sticky="nsew")

lbl_value = tk.Label(master=window, text="0")
lbl_value.grid(row=0, column=1)

btn_increase = tk.Button(master=window, text="+", command=increase)
btn_increase.grid(row=0, column=2, sticky="nsew")

window.mainloop()

値をlbl_value[“text”]から持ってきていることに注目してください。
また値のアップデートはそのtext値を書き換えることで達成しています。

そして、btn_decreaseとbtn_increaseにcommand=パラメーターで関数との関連をつけています。

これでTkinterのジオメトリーとイベントの基礎を終わります。

(https://realpython.com/python-gui-tkinter/ より抜粋. Thanks David.)

ウィジェット変数

Tkinterにはウィジェット変数というものがあります。
四種類

  • IntVAR 整数型の保持
  • DoubleVar 浮動週数点型の保持
  • BooleanVar 真偽値を保持
  • StringVar 文字列の保持

例えば

text = tk.StringVar()  #定義
text.set("first value")
label=tk.Label(root, textvariable=text)  #関連付け
text.set("second value")

とtext変数を変えるとlabelのtextも変わります。いちいちGUIのプロパティを読み出したり、書き込んだりするよりもエレガントなコードになりますね。

コメント