前回はtkinterのpackレイアウトシミュレーターを作成した。
thom.hateblo.jp
今回は折角なのでこのシミュレーターを使ってPackレイアウトの仕組みについて説明しようと思う。
目次
- まずはtkのコードから説明
- packのレイアウトの考え方
- 占有領域の考え方
- packする順番の重要性
- packの実用レイアウト
- 実際のアプリへの適用
- packできるようになった後は
- 画面遷移
- 終わりに
まずはtkのコードから説明
tkのコードを見たことが無い人は意味が分からないと思うので、まずは簡単なtkコードを紹介してみようと思う。初見の方に向けてくどいほどコメントを入れたので面倒臭そうな印象を受けるかもしれないが、たった6行のコードだ。
コード
#tkinterモジュールをtkという名前を付けてインポート import tkinter as tk #変数rootにtkウインドウをセットし、サイズを400x300に指定 root = tk.Tk() root.geometry("400x300") #変数item1にラベルをセットする。 #この時、親オブジェクトをroot(つまりtkウインドウ)に指定し、 #テキストをItem 1と指定し、背景色をピンクに指定する。 item1 = tk.Label(master=root, text="Item 1", bg="pink") #変数item1(つまりラベル)を親ウインドウにパック(詰めこむ)。 #詰め込む方向は上(side="top"、隙間は埋めない(fill="none")、 #占有領域の拡張も要求しない(expand=False) item1.pack(side="top", fill="none", expand=False) #実は上記3設定はpackのデフォルト値なので、単にitem1.pack()と引数無しで実行しても同じことである。 #今回は説明の都合上、フルで記載した。 #準備できたのでウインドを表示させる。 root.mainloop()
実行結果
ラベルを増やしてみる
ラベルを増やすには、単純にitem変数をもう一つ準備して、そちらもパックすれば良い。
import tkinter as tk root = tk.Tk() root.geometry("400x300") item1 = tk.Label(master=root, text="Item 1", bg="pink") item2 = tk.Label(master=root, text="Item 2", bg="lightblue") item1.pack(side="top", fill="none", expand=False) item2.pack(side="top", fill="none", expand=False) root.mainloop()
実行結果はこのとおり。
このとき、item1の生成→item2の生成→item1のパック→item2のパックという順にコードを並べている。
一見すると生成した流れでそのままパックする方が、「変数は使用する直前に宣言する原則」に従っているような気がするけれど、これにはちゃんと理由がある。
tkの他のレイアウト方式であるgridレイアウトやplaceレイアウトでは配置の際の引数で場所を特定できるのに対し、packレイアウトではpackする順序がとても重要になる。よって、生成は生成、レイアウトはレイアウトでコードを固めた方がコントロールしやすい。
pack順が影響を及ぼす例を出すと、次のようにItem 1を下に、Item 2を上に配置させたとする。
分かりやすいようにfillとexpandも有効にしてみた。
item1.pack(side="bottom", fill="both", expand=True) item2.pack(side="top", fill="both", expand=True)
結果はこうなる。
ウインドウの端をドラッグして小さくしていくと、以下のようにItem2が先に隠れてしまうことがわかる。
これは先にpackしたitem1が優先されるためだ。
次にpackの順番を変えてみる。
item2.pack(side="top", fill="both", expand=True) item1.pack(side="bottom", fill="both", expand=True)
すると先ほどと全く同じようなウインドウが表示されるが、縮めてみるとこのとおり。
今度は先にpackしたitem2が優先される。
こういうサンプルで示されてもそれがどうした?となりがちなので、私が実際に困った実例も紹介しておこう。
まさにtkinterのpackレイアウトシミュレーターを作っていたとき、設定パネルが乗ったフレームを最後にpackしていたので、画面を縮めると先に設定パネルから隠れてしまい、これを何とかしたかった。
packする順番を変えたのが、現在のコード。
さて、とりあえずここまででpackレイアウトのオプション指定方法やpack順の話は一区切りとして、次に実際のレイアウトに適用する方法を紹介していく。
packのレイアウトの考え方
packは「詰める・梱包・荷造り」という意味があるので、まずはそのイメージを持ってもらえればと思う。
例えば旅行に出かけるのにスーツケースの端から色々詰め込むイメージ。
詰める方向は上下左右のいずれかを指定するが、ピンポイントで「このあたりに」という操作はできない。
占有領域の考え方
packで詰め込まれたアイテムは、詰め込まれた方向と垂直方向に見えない占有領域を持っている。
たとえば、Item1にside="top"を指定して上にpackした場合、その左右いっぱいがItem1の占有領域となる。
ここに、Item2にside="left"を指定して左にpackした場合、下方向へはウインドウの端まで、上方向へはItem1の占有領域とぶつかるところまでがItem2の占有領域となる。
このように、ウインドウ全体で見たとき、左にpackしたItem2が上下の中心からわずかに下に見えるのはそのせいである。占有領域内ではちゃんと中心に位置している。
ここでfillを使うと、占有領域内いっぱいにアイテムを広げることができる。
以下では、Item1をx方向へ、Item2をy方向へfillしている。
ただしfillでは占有領域が広がるわけではない。
試しにItem 1にy方向のfillを、Item 2にx方向のfillを指定してみるが何も変わらない。
fillは占有領域を拡張してくれる訳ではないからだ。
そこで登場するのがexpandである。
expandを使用すると、sideで指定された詰め込み方向と水平方向に占有領域が拡張される。
また、占有領域は詰め込み方向と垂直方向からの力に弱いため、他の占有領域が拡張されると押しつぶされることがある。
たとえば上方向に詰め込まれたItem 1をexpandすると上下方向に占有領域が拡張される。
そして左方向に詰め込まれたItem 2は上下方向からの力に弱いため、Item 1の占有領域拡張に伴って押しつぶされる。
実際にやってみると、今度はy方向のfillが効いて占有領域いっぱいにY方向にItemが引き延ばされるのが分かる。
ちなみにfillをnoneに設定すると占有領域の中央にItemが配置されているのが分かる。
今回は見えない占有領域を図示しているので簡単に思えるかもしれないが、この見えない占有領域の考え方が理解できるまではfillとexpandの挙動がさっぱり分からなくて苦労した。今なら理解できる。
例1) expand(拡張)してるのにアイテムが伸びずに宙ぶらりんになる。→fillできてないから。
例2) fill(埋め)してるのにアイテムが伸びない。→占有領域は水平方向には自動拡張しないから。
packする順番の重要性
tkinterではpackする順序によって基本的な占有領域の配置が決まってしまう。
私が作成したpack layout simulatorでは常にItem番号が若いほうからpackされるようにしてあるので、試しに様々な方向から詰め込んでみよう。
この図から、まずItem 1を横から押せるアイテムは無いということ。オセロで角を取ったようなものだ。
Item 2を上から押せるアイテムはItem1だけだ。同様にItem3を横から押せるのはItem 2のみ。
つまり先にpackした方が占有領域において有利に働くことになる。
ちなみに同じ方向へのExpandは力が釣り合うところまで押し返すことができる。
たとえばItem1だけexpandするとこうなるが、
Item6もexpandしてやると、Item1と力が釣り合って同じYサイズのところで止まる。
packの実用レイアウト
例えばpackで次のようなレイアウトを作りたいとする。
このときに間違えがちなのが、素直にコンテンツ1から配置しようとすることだ。
これはBottomをフッターでまるごとX方向に占有する必要があるので、まずフッターを置かないと上手く行かない。
正解の設定はこんな感じで、ボトムをexpand無しでまず配置して、残りをLeftかRightに詰めてexpandすること。
packでレイアウトするときは、占有領域のことを考慮しなければならない。
ヘッダーを付けたいと思ったら、こんな感じでヘッダー・フッター・中身の順にpackすることになる。
フッター・ヘッダー・中身の順でも同じ見た目になるが、フォームを縮めたときにコンテンツが隠れる順が変わるのでケースバイケースで使い分けると良いかと思う。
ちなみにヘッダーやフッターをexpandしてしまうと中身が潰れるので注意。
実際のアプリへの適用
今回はtkのラベルを用いて説明したが、実際にはtk.Frameを用いて、その中にボタンやラベルやテキストボックス等のウィジェットを配置していく。
冒頭で紹介したコードをFrameに変更してみたのがこちら。
import tkinter as tk root = tk.Tk() root.geometry("400x300") header = tk.Frame(master=root, bg="pink") footer = tk.Frame(master=root, bg="lightblue") container1 = tk.Frame(master=root, bg="lightgreen") container2 = tk.Frame(master=root, bg="khaki") container3 = tk.Frame(master=root, bg="mediumpurple1") header.pack(side="top", fill="both", expand=False) footer.pack(side="bottom", fill="both", expand=False) container1.pack(side="left", fill="both", expand=True) container2.pack(side="left", fill="both", expand=True) container3.pack(side="left", fill="both", expand=True) root.mainloop()
実行してみると、あれ?ヘッダーとフッターが無い。。
これは心配無用。ラベルと違ってテキスト等の中身が無いので、中に何か配置されるまではcontainer1~3のexpandに押しつぶされているだけ。
次のようにフレームをmasterにしてLabelをフレームに配置してやれば、フレームの中身の高さは最低限確保される。
import tkinter as tk root = tk.Tk() root.geometry("400x300") # フレームを準備 header = tk.Frame(master=root, bg="pink") footer = tk.Frame(master=root, bg="lightblue") container1 = tk.Frame(master=root, bg="lightgreen") container2 = tk.Frame(master=root, bg="khaki") container3 = tk.Frame(master=root, bg="mediumpurple1") # フレームレイアウト header.pack(side="top", fill="both", expand=False) footer.pack(side="bottom", fill="both", expand=False) container1.pack(side="left", fill="both", expand=True) container2.pack(side="left", fill="both", expand=True) container3.pack(side="left", fill="both", expand=True) # ヘッダー内レイアウト title_label = tk.Label(master=header, text = "Sample App", bg=header["bg"]) title_label.pack() # フッター内レイアウト creator_label = tk.Label(master=footer, text = "Created by thom.", bg=footer["bg"]) creator_label.pack() root.mainloop()
実行結果は次のとおり。
以上がtkinterのpackについての解説である。
読んだだけでピンとこない人のために紹介したpackのレイアウトシミュレーターは以下で公開している。
github.com
packできるようになった後は
他のレイアウトについて
packの使い方が分かった後は、gridやplace等のレイアウトも使ってみると良いかと思う。
pack以外は比較的簡単なので、適当なサイトで調べればすぐ使いこなせると思う。
レイアウトメソッドはmasterごとに1種類のみ有効だ。
今回でいうと、rootをマスターにしたフレームの配置にはpackを使っているので、rootに配置するメソッドはpackに固定されてしまうが、その中に配置するフレーム内部ではまた別のレイアウトを選択できる。
よって、最近はpackでフレーム自体をレイアウトしてしまい、中身は場合に合わせてpackかgridを使い分けるといった感じ。
クラス化について
クラス化についてはpythonの公式ドキュメントでソースコードを確認できる。
docs.python.org
こちらについてはpythonの入門書(クラスの説明までカバーされているもの)を1冊読了してから挑戦してみると良いと思う。
tkinterによるUIプログラミングは規模が大きくなるにつれて散らかってくるので、クラスを使わないと手に負えなくなる。
先ほどのコードをクラス化したのがこちら。
import tkinter as tk class Application(tk.Frame): def __init__(self, master=None): super().__init__(master) master.geometry("400x300") self.master = master self.pack() self.layout_frames() self.layout_header() self.layout_footer() def layout_frames(self): # Generate self.header = tk.Frame(master=self.master, bg="pink") self.footer = tk.Frame(master=self.master, bg="lightblue") self.container1 = tk.Frame(master=self.master, bg="lightgreen") self.container2 = tk.Frame(master=self.master, bg="khaki") self.container3 = tk.Frame(master=self.master, bg="mediumpurple1") # Layout self.header.pack(side="top", fill="both", expand=False) self.footer.pack(side="bottom", fill="both", expand=False) self.container1.pack(side="left", fill="both", expand=True) self.container2.pack(side="left", fill="both", expand=True) self.container3.pack(side="left", fill="both", expand=True) def layout_header(self): self.title_label = tk.Label(master=self.header, text = "Sample App", bg=self.header["bg"]) self.title_label.pack() def layout_footer(self): self.creator_label = tk.Label(master=self.footer, text = "Created by thom.", bg=self.footer["bg"]) self.creator_label.pack() root = tk.Tk() app = Application(master=root) app.mainloop()
やたらとselfが付くのがまだ慣れないが、まだ私も勉強中なので非効率なことをしていたら申し訳ない。
画面遷移
画面遷移については過去の記事で紹介したのでリンクしておく。
thom.hateblo.jp
フレームを入れ子にして使うと中のフレームごと入れ替えることができるので、例えばヘッダーとフッターは入れ替えずに中身だけ画面遷移させるということもできる。
終わりに
Packレイアウトは最初難しく感じるかもしれないが、勝手が分かってしまえばシンプルで手軽な選択肢となる。
Gridレイアウトのようにマス目を意識しなくても良いし、Placeレイアウトのようにドット単位の座標を意識しなくても良い。
ただ詰め込むだけ。ポイントは詰め込む順番と占有領域を理解すること。
(注意) 今更だけど、今回の説明で使用した「占有領域」という言葉は私が勝手にそう呼んでいるだけなので、たぶん他のtkinter使いには通用しない。ただ、個人的にはそのように名付けると動作がイメージしやすかったので、もし気に入って他の人に説明する機会があれば使ってもらうと良いかと思う。
以上