t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

Python GUI: tkinterのpackレイアウトシミュレーターを作成

Pythonの標準GUIツールとして有名なtkinterには、UIパーツをレイアウトするのに3種類の方法が用意されている。

それぞれ使ったことがあるが、個人的には次のような評価である。

レイアウト名 方式 柔軟性 分かりやすさ
Pack 詰め込み方式 ×
Grid 碁盤目方式
Place 座標方式 ×

Packを理解できれば結構手軽で柔軟性もあるので最近はフレームの大枠をPackで作って中身をグリッドにするという作り方が多い。

ただ1度理解してもやはり混乱が生じやすいのがPack。
特に、fillとexpandの違いが文章での説明ではとてもややこしい。
そこで今回はシミュレーターを作って簡単にPackの動作を検証できるようにしてみた。

完成したツール

python packsim.pyとして実行すると次の画面が表示される。(python3がデフォルトの前提)
f:id:t-hom:20220207125301p:plain

そしてチェックボックスとコンボボックスで設定を変更するたびに、各アイテムがそのとおりに配置される。
f:id:t-hom:20220207125447p:plain

入手先

GitHubの公開リポジトリに配置したのでそちらからpacksim.pyをダウンロードできる。
github.com

使い方

GitHubのReadme.mdに書いたので、ブログの方には説明のGoogle翻訳を張っておく。

設定名 タイプ 説明
pack Checkbox チェックするとシミュレーションウィンドウにアイテムが作成されます。
チェックを外すと、アイテムの他の構成はすべて無意味になります。
デフォルトでは、4つのアイテムが選択されています。
side Combobox アイテムは選択された側に梱包されます
fill Combobox アイテムは選択された方向に埋められようとします。
占有エリアによって制限されます。
expand Checkbox アイテムは占有エリアを拡大しようとします。

注) パックの順序は、設定変更操作に関係なく、常に最も若いアイテムから開始されます。何かを変更すると、実際にはすべてのアイテムが削除され、再度パックされます。

コード

そんなに長いコードでもないので、ブログにも貼っておく。
変数名が一部適当なのはツッコミ不要で。。自分で分かってる。
(ただしGitHub上のコードが将来メンテされてもこちらはUpdateしないつもり。)

import tkinter as tk
from tkinter import ttk

class Application(tk.Frame):
    def __init__(self, master=None):
        super().__init__(master)
        master.geometry("800x600")
        master.title("tkinter pack layout simulator")
        self.item_colors = ['IndianRed1', 'SteelBlue1', 'SpringGreen1', 'Goldenrod1', 'LightPink1','LightSteelBlue1', 'DarkSeaGreen1', 'Khaki1']
        self.item_count = len(self.item_colors)
        self.default_item_count = 4

        self.master = master
        self.pack(fill=tk.BOTH, expand=True)
        self.layout_panels()
        self.layout_control_panel()
        self.layout_items()

    def layout_panels(self):
        self.simulation_panel = tk.Frame(self, bg = "white")
        self.control_panel = tk.Frame(self)
        self.control_panel.pack(side=tk.BOTTOM)
        self.simulation_panel.pack(side=tk.TOP,expand=True, fill=tk.BOTH)
        self.contents_frame = tk.Frame(self.simulation_panel, bg="gray31")
        self.contents_frame.pack(side="top", fill="both",expand=True)

    def layout_control_panel(self):
        tk.Label(self.control_panel, text="pack").grid(column=0, row=1,sticky="news")
        tk.Label(self.control_panel, text="side").grid(column=0, row=2,sticky="news")
        tk.Label(self.control_panel, text="fill").grid(column=0, row=3,sticky="news")
        tk.Label(self.control_panel, text="expand").grid(column=0, row=4,sticky="news")
        for x in range(1,self.item_count+1):
            tk.Label(self.control_panel, text="Item " + str(x)).grid(column=x,row=0)

        self.sides = []
        self.sides.append(tk.StringVar())
        self.sides[0].set("top")

        self.fills = []
        self.fills.append(tk.StringVar())
        self.fills[0].set("none")

        self.creates = []
        self.creates.append(tk.BooleanVar())
        self.creates[0].set(False)

        self.expands = []
        self.expands.append(tk.BooleanVar())
        self.expands[0].set(False)

        for y in range(1,self.item_count+1):
            self.sides.append(tk.StringVar())
            self.sides[y].set("top")
            cb1 = ttk.Combobox(self.control_panel,textvariable=self.sides[y],state="readonly",values=['top', 'bottom', 'left', 'right'], width=7)
            cb1.bind("<<ComboboxSelected>>", self.layout_items)
            cb1.grid(column=y,row=2)

            self.fills.append(tk.StringVar())
            self.fills[y].set("none")
            cb2 = ttk.Combobox(self.control_panel,textvariable=self.fills[y],state="readonly",values=['none', 'x', 'y', 'both'], width=7)
            cb2.bind("<<ComboboxSelected>>", self.layout_items)
            cb2.grid(column=y,row=3)

            self.creates.append(tk.BooleanVar())
            self.creates[y].set(y <= self.default_item_count)


            chk1 = ttk.Checkbutton(self.control_panel,variable=self.creates[y],command=self.layout_items)
            chk1.grid(column=y,row=1)

            self.expands.append(tk.BooleanVar())
            self.expands[y].set(False)
            chk2 = ttk.Checkbutton(self.control_panel,variable=self.expands[y],command=self.layout_items)
            chk2.grid(column=y,row=4)


    def layout_items(self,*args):
        self.contents_frame.pack_forget()
        for child in self.contents_frame.winfo_children():
            child.destroy()
        self.contents_frame.pack(side="top", fill="both", expand=True)
        self.items = []
        self.items.append(tk.Label(self.contents_frame))
        for x in range(1,self.item_count+1):
            self.items.append(tk.Label(self.contents_frame))
            self.items[x] = tk.Label(self.contents_frame)
            self.items[x]["text"] = "Item " + str(x)
            self.items[x]["bg"] = self.item_colors[x-1]
            if self.creates[x].get():
                self.items[x].pack(side=self.sides[x].get(), fill=self.fills[x].get(), expand=self.expands[x].get())

    def say_hi(self):
        print("hi there, everyone!")

root = tk.Tk()
app = Application(master=root)
app.mainloop()

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。