Pack ジオメトリマネージャ
メインページ>コンピュータの部屋#Python>Tkinter Tips
Tkinterにはウィジェットを別のウィジェット(親ウィジェット)上に配置するための「ジオメトリマーネジャ」というものを持っています。 ジオメトリマネージャとは、ウィジェットの位置や大きさを決定する仕掛けです。
TkInterには Pack, Grid, Place の3種類のジオメトリマネージャがありますが、この記事では Pack ジオメトリマネージャを紹介します。
目次
Pack ジオメトリマネージャとは?
Pack ジオメトリマネージャとは、ウィジェットをPack(「積めて」)配置してゆくタイプのジオメトリマネージャです。所謂エラスティックなGUIデザインを実現してくれます。 言葉で説明するのは難しいので、早速図とコードをたくさん使った説明に入りましょう。
packメソッドを使ってみる
Packジオメトリマネージャの主役となるのは、全てのウィジェットに用意され散る pack というメソッドです。
最初は packメソッド最も簡単なコードを紹介しましょう。ウィンドウ全面に貼られた FrameウィジェットにButtonを貼り付けてみます。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200) frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack() root.mainloop()
これを実行すると、こんな感じになります。
このコードでは packメソッドの動きが分かりやすくなるように、ちょっとしたおまじないのコードを使っています。
frame.pack_propagate(False)
は
tk.Button(frame, text="A").pack()
の影響が Frameウィジェットに及ぶのを防ぐためのコードです。このコードがないと、 Frameのサイズは Buttonウィジェットを表示するのに必要な最小限のサイズに自動的に縮んでしまいます。
frame.pack_propagate(False)
としておくと、Frameウィジェットの大きさは コンストラクタに渡した width, height パラメータの値のままに保たれます。
ここで何が起きているかを簡単に説明しておきましょう。
tk.Button(frame, text="A").pack()
を実行すると、Buttonウィジェットは frameウィジェットの上端に張り付きます。
図の黄色いところは「Packメソッドによって ButtonウィジェットがFrameウィジェット上に確保したエリア」です。 これは、Buttonウィジェットのサイズとは異なることに注意してください。
「Packメソッドによって ButtonウィジェットがFrameウィジェット上に確保したエリア」はFrameオブジェクトの最上端に、Buttonウィジェットのテキストを表示するのに必要なだけの高さが確保され、横方向はFrameの横方向全体が確保されます。Buttonは確保されたエリアの「中央」に表示されます。
packメソッドを2つ使ってみる
これまで
tk.Button(frame, text="A").pack()
というコードでButtonウィジェットを配置してきましたが、実はこれは
tk.Button(frame, text="A").pack(side=tk.TOP)
と同じです。side=tk.TOP は親ウィジェットの上端にウィジェットを配置することを意味します。 ではこれを2回連続して使ったらどうなるでしょうか?
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200) frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.TOP) root.mainloop()
を実行すると、下図のように縦積みになって表示されます。
これはつまり、Buttonウィジェットが下図のように自身を表示するエリアを確保するからです。
つまり2番目のButtonウェイジェットは1番目のボタンウィジェットで確保されたエリアを除いた部分のFrameウィジェットのエリアの最上端を確保するのです。
side=tk.LEFT を使ってみる
さて、さらに size=tk.Left を使ってみましょう。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200) frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.LEFT) root.mainloop()
このコードを実行すると、下図のように、今度は BボタンがFrameウィジェットの左側に貼り付いて表示されます。
BボタンはFrameウィジェットの左側に張り付いてますが、少し縦中央より下に表示されていることにお気づきでしょうか? このずれは以下の図を見ればわかります。
BボタンはFrameウィジェットのAボタンが占めていない残りの部分の左側に張り付きます。 side=tk.LEFT で左へ張り付く場合、ボタンの占めるエリアは、幅はボタンのキャプションを表示するのに十分なだけの幅になりますが、 縦はFrameウィジェットのAボタンが占めていない残りの部分の高さと同じになります。つまり高さはとれるだけ目いっぱいになるのです。
Bボタンはこうして取られたエリアの中央に置かれまず。だからFrameウィジェットの中央より若干下になるのです。
tk.Bottom, tk.Right も使ってみる
そろそろ慣れてきたでしょうから、tkBottomと tk.Right も使ってみましょう。 もうすでに察しはついていると思いますが、tk.Bottom はウィジェットを下に、tk.Right はウィジェットを右に張り付かせる指定です。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200) frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.LEFT) tk.Button(frame, text="C").pack(side=tk.BOTTOM) tk.Button(frame, text="D").pack(side=tk.RIGHT) root.mainloop()
このコードを実行すると、既に予想されていると思いますが、下の図のようになります。
もうわかっているとは思いますが、念のため、ボタンの確保したエリアとボタンの位置を図で示しておきましょう。
Frameウィジェットを必要最小限の面積にしてみる(pack()の既定の動作)
さてここまではボタンウィジェットの動きをわかりやすくするため、Frameウィジェットのサイズを強制的に 200 x 200 にしてきましたが、 pack()メソッドの本来の動作では Frameウィジェットは、ボタンを表示する最小限の大きさに自動縮小します。上で示した4個のボタンの場合で試してみましょう。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green", ) #frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.LEFT) tk.Button(frame, text="C").pack(side=tk.BOTTOM) tk.Button(frame, text="D").pack(side=tk.RIGHT) root.mainloop()
このコードでは Frameの自動縮小を抑えるためのおまじない frame.pack_propagate(False) を削除しています。
またわかりやすいように Frameウィジェットの背景色を green(緑) にしてみました。結果は以下のようになります。
この動作を一口で説明するのは大変難しいです。
Frameの大きさが固定の場合は、ボタンは tk.XXX の指定に従って Frame の四隅に張り付くだけでした。 Frameの大きさは十分広く、ボタンを表示するだけの十分なエリアがありました。
実はボタンにはボタンを表示するための必要最小限のサイズがあります。ボタンの場合、既定ではボタンのテキスト(キャプション)を表示するのに必要な大きさのことです。
Packジオメトリマネージャは Frame の大きさを色々変えてみてボタンを貼り付け、全てのボタンのテキストが全て正常に表示できるような最小の大きさを計算し、その大きさに Frameウィジェットのサイズを変更するのです。正確なアルゴリズムはよくわかりませんが(^^; 直感的には大変わかりやすい動きです。
fillを使ってみる
さてここまでは packメソッドでボタンの位置決めを行いましたが、ボタンの形はボタン自身に任せてきました。しかし、packメソッドのfill引数を使うと、張り付き先の形に合うようにウィジェットを変形できます。
fill引数とはウィジェットが packメソッドで確保したエリアに合わせて、ウィジェットの大きさを伸長させるパラメータです。
早速使用例をお見せしましょう。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200) frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.X) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.Y) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.Y) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH) root.mainloop()
実行結果がこれ
図を見れば明らかですが, fill=tk.X は、ウィジェット用に「確保されたエリア」の横方向(X方向)いっぱいにウィジェットを引き延ばします。 同様に fill=tk.Y は、方向が縦(Y方向)になるだけで動きは同じです。 Cボタンで fill=tk.Y が効かないのは、もちろん、packがcボタンのエリアをテキストの高さの分しか確保しないからです。
fill=tk.BOTH は常に、ウィジェットのために確保されたエリアいっぱいにウィジェットを引き延ばします。
Frameを自動縮小するようにするとこんな風になります。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") #frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.X) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.Y) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.Y) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH) root.mainloop()
Frameの大きさを変えてみる
ここまでの例では、プログラムを書いて実行して pack の様子を見ていただけでしたが、今度はFrameウィジェットの大きさを実行時に変えてみましょう。
下のプログラムは今まで使ってきたFrameウィジェットに4個のボタンを表示するだけのプログラムですが(分かりやすいようにFrameウィジェットの背景を緑に変えてありますが)ウィンドウのサイズをマウスを使って変えてやると、下の図ようになります。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack() tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.X) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.Y) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.Y) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH) root.mainloop()
まだ説明してはいませんが、Frameウィジェットは side=tk.TOP で トップレベルウィンドウに貼り付けてあるので、 トップレベルウィンドウがリサイズするとこのような動きになります。
しかし、ここではトップレベルウィンドウのリサイズに従って、Frameウィジェットを同じ大きさに追従させたいので、コードを若干手直しします。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.X) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.Y) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.Y) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH) root.mainloop()
frame.pack(fill=tk.BOTH, expand=True)
の tk.BOTH の意味は既にお分かりだとは思いますが、expand=True の意味はまだ説明していません。とりあえずここでは、Frameウィジェットがトップレベルウィンドウのリサイズに追従して動くために必要なものとお考え下さい。これでトップレベルウィンドウとFrameウィジェットが一緒にリサイズするようになりました。
さて、Frameウィジェットのサイズが実行時に変わると何が起きているのでしょうか?
図から明らかなように、各ボタンの「占めるエリア」は、Frameウィジェットがリサイズされると改めて packした時の指示内容と順番で取り直されます。そしてそれに従ってボタンウィジェットが配置しなおされます。 Frameウィジェットのリサイズに伴ってこの処理は瞬時に実行されるので、利用者がウィンドウを連続してリサイズすると、ボタンが連続して移動し、伸び縮みしているかのように見えます。
これは、ウィジェットをPackメソッドで置いたときの指示と順番が、コンテナのウィジェットのリサイズ時もリアルタイムに保たれている、というように考えることもできるでしょう。つまり、各ウィジェットは、ウィンドウがリサイズされると、packで指示されたルールに従ってダイナミックに再配置されることになります。
expandを使ってみる
それでは最後に、packメソッドの引数として最も分かりにくく、かつ必要不可欠な expand引数について説明しましょう。
expand引数は既に Frameウィジェットをトップレベルウィンドウいっぱいに広げるのに使いましたが、 ウィジェットが pack で占拠するエリアを確保する際、残っている全てのエリアを占拠するように指示する引数です。
まずはこのコードと実行結果を見てください。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.LEFT) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) root.mainloop()
図から何が起きているかお分かりとは思いますが、Cボタンを expand=1 で packメソッドで配置すると、Cボタンのために確保された領域の大きさは AボタンとBボタンで確保された領域以外の残された部分全部であることが分かります。side=tk.BOTTOM ですからCボタンは下に張り付くはずですが、残りの領域全域を確保してしまっています。
では、side=tk.BOTTOM に何か意味はあるのでしょうか? さらにボタンを追加したらどこに追加されるのでしょうか?
やってみましょう。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP) tk.Button(frame, text="B").pack(side=tk.LEFT) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) tk.Button(frame, text="D").pack(side=tk.RIGHT) root.mainloop()
なんと、DボタンはAボタンとCボタンの間にねじ込まれました。つまり、side=tk.BOTTOM, expand=1 で配置されたウィジェットの上部には、もう領域が残っていないにも関わらず別のウジェットをねじ込めるのです。ウィジェットが張り付く方向とは逆側が「やわらかい」、あるいは目に見えない隙間が空いていると考えてもよいかもしれません。
但しねじ込みは大変らしく、Dボタンの高さは side=tk.Right にも拘わらず、テキストを表示する最小限しか確保されていません。expand=1 で伸びた図形の柔らかさはその程度のようです。
次に、全てのボタンを fill=tk.BOTH で張り付けて、各ボタンのために確保された領域を明確にしておきましょう。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.BOTH) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.BOTH) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH) root.mainloop()
Dボタンが AボタンとCボタンの間の隙間の右端のみを占拠していることが分かります。size=tk.RIGHT は右端の上下方向を占拠しようとする圧力というか志向を持っていないからです。そうするには Dボタンも expand=1 で ぱ packする必要があります。
最後に、Dボタンにも expand=1 を付けたケース、さらに、fill=tk.BOTHを付けたケースも載せておきます。
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.BOTH) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.BOTH) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) tk.Button(frame, text="D").pack(side=tk.RIGHT, expand=1) root.mainloop()
import tkinter as tk root = tk.Tk() frame = tk.Frame(root, width=200, height=200, bg="green") frame.pack_propagate(False) frame.pack(fill=tk.BOTH, expand=True) tk.Button(frame, text="A").pack(side=tk.TOP, fill=tk.BOTH) tk.Button(frame, text="B").pack(side=tk.LEFT, fill=tk.BOTH) tk.Button(frame, text="C").pack(side=tk.BOTTOM, fill=tk.BOTH, expand=1) tk.Button(frame, text="D").pack(side=tk.RIGHT, fill=tk.BOTH, expand=1) root.mainloop()