0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Tkinterで「1画面で完結する」デスクトップアプリのレイアウトを組む

0
Posted at

はじめに

Tkinter の入門記事は pack() でボタンを並べるところで終わっているものが多く、
「検索バー・一覧・詳細ペインが 1 画面に収まっていて、ウィンドウを広げると
一覧だけが伸びる」ような実用的な画面をどう組むかは案外まとまっていません。

この記事では、次のような 1 画面構成をゼロから組み立てます。

+-------------------------------------------------------------+
| 検索バー(固定の高さ)                                      |
+---------------------------+---------------------------------+
| 一覧 (Treeview)           |  詳細ペイン                     |
|                           |                                 |
|   ← ウィンドウを広げるとここだけが伸びる →                  |
+---------------------------+---------------------------------+
| ステータスバー(固定の高さ)                                |
+-------------------------------------------------------------+

環境は Python 3.10 以降(tkinter は標準添付なので追加インストール不要)です。

1. packgrid は「混ぜない」が、「使い分ける」

最初に踏む地雷がこれです。

同じ親ウィジェットの中で pack()grid() を混ぜてはいけない。

混ぜるとフリーズします(例外すら出ずに固まることがあります)。
ただしこれは「同じ親の中では」という条件付きなので、
親が違えばフレームごとに好きなほうを使えます

実際、今回の構成では次のように使い分けるのが素直です。

  • 全体の骨格grid(どの行が伸びるかを指定したいから)
  • 検索バーの中で部品を横に並べるpack(side="left")(順番に置くだけだから)
import tkinter as tk
from tkinter import ttk

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("サンプル")
        self.geometry("1000x640")
        self.minsize(800, 480)

        # 骨格は grid。row=1(真ん中)だけが伸びるようにする
        self.rowconfigure(1, weight=1)
        self.columnconfigure(0, weight=1)

        self._build_search_bar()   # row=0
        self._build_main_pane()    # row=1
        self._build_status_bar()   # row=2

weight を付けた行・列だけが伸びる

ここが grid の肝です。

self.rowconfigure(1, weight=1)   # row=1 は余白を全部もらう
self.columnconfigure(0, weight=1)

weight の既定値は 0、つまり伸びない
だから検索バー(row=0)とステータスバー(row=2)は自然と固定の高さになり、
真ん中だけが伸びます。「ステータスバーを下に貼り付ける」ための特別な指定は要りません。

weight は比率としても使えます。rowconfigure(1, weight=2)
rowconfigure(2, weight=1) なら、余った高さが 2:1 で配られます。

sticky を忘れると伸びない

weight を付けてもウィジェット自身に sticky が無いと、セルは広がるのに
中身が真ん中に小さく居座ります。

frame.grid(row=1, column=0, sticky="nsew")   # 上下左右いっぱいに広げる

"nsew" は north/south/east/west。weightsticky はセットで書くと覚えてしまうのが早いです。

2. 検索バーは pack(side=...) で素直に

横一列に並べるだけなら grid より pack のほうが読みやすいです。

def _build_search_bar(self):
    bar = ttk.Frame(self, padding=(10, 8))
    bar.grid(row=0, column=0, sticky="ew")   # bar 自体は grid で配置

    self.keyword_var = tk.StringVar()

    # bar の「中」は pack。親が違うので混ざらない
    ttk.Label(bar, text="キーワード").pack(side="left")
    entry = ttk.Entry(bar, textvariable=self.keyword_var, width=20)
    entry.pack(side="left", padx=(4, 10))

    ttk.Button(bar, text="検索", command=self._search).pack(side="left")

    # 右端に寄せたいものは side="right"。右から順に詰まっていく
    ttk.Button(bar, text="更新", command=self._reload).pack(side="right")

side="right" で置いたものは右から順に詰まります。
2 つ並べたいときは、右端に置きたいほうを先に pack します。

padx にタプルを渡すと左右を別々に指定できる

entry.pack(side="left", padx=(4, 10))   # 左に 4px、右に 10px

地味ですが、ラベルと入力欄はくっつけて、次の項目とは離す、という調整が
これだけで書けます。

3. tkttk は「見た目が違う」

tkinter には古い tk.Button と、新しい ttk.Button があります。

  • tk.* … OS に関係なく同じ(古臭い)見た目
  • ttk.* … OS のテーマに合わせた見た目

特別な理由が無ければ ttk を使います。 見た目が今風になるだけでなく、
ttk.Treeviewttk.PanedWindow のような便利なウィジェットは ttk にしかありません。

ただし ttk に無いものもあります。tk.StringVar のような変数クラス、
tk.Tk(ウィンドウ本体)、tk.Canvas などは tk 側です。混在は正常です。

4. PanedWindow で「境界をドラッグできる」分割

左右の幅をユーザーが変えられるようにすると、それだけでアプリらしくなります。

def _build_main_pane(self):
    pane = ttk.PanedWindow(self, orient="horizontal")
    pane.grid(row=1, column=0, sticky="nsew", padx=10, pady=(0, 6))

    left = ttk.Frame(pane)
    right = ttk.Frame(pane)

    # weight は初期の配分比率
    pane.add(left, weight=2)
    pane.add(right, weight=3)

pane.add() に渡すのは直接の子フレームです。
PanedWindow の中に grid で置こうとすると効きません(add が配置そのものです)。

5. Treeview を「表」として使う

ttk.Treeview は名前のとおりツリー表示のウィジェットですが、
show="headings" を指定するとツリーの列が消えて、ただの表になります。

columns = ("occurred_at", "place", "magnitude", "scale")
tree = ttk.Treeview(left, columns=columns, show="headings", selectmode="browse")

headings = {
    "occurred_at": ("発生日時", 140, "w"),
    "place":       ("場所",     170, "w"),
    "magnitude":   ("規模",      60, "center"),
    "scale":       ("最大震度",  70, "center"),
}
for key, (text, width, anchor) in headings.items():
    tree.heading(key, text=text)                       # 見出しの文字
    tree.column(key, width=width, anchor=anchor,       # 列の幅と揃え
                stretch=(key == "place"))              # 伸ばす列を 1 つだけ選ぶ

ポイントは 3 つです。

show="headings"
これを書かないと、左端に空っぽのツリー列(#0)が居座ります。
表として使うなら必ず指定します。

stretch
既定では全部の列が均等に伸びて、幅の指定が台無しになります。
伸びてほしい列だけ stretch=True にすると、他の列は指定幅を保ちます。

selectmode="browse"
1 行だけ選べるモード。既定の "extended" は複数選択できてしまうので、
「選んだ 1 件の詳細を出す」ような画面では browse のほうが素直です。

行の追加と、行 ID の使い方

tree.insert("", "end", iid=item.id, values=(a, b, c, d))

第 1 引数は親(表として使うなら空文字)、第 2 引数は挿入位置。

覚えておくと効くのが iid です。行に自分で ID を付けられます。
ここに元データの ID を入れておくと、選択されたときに辞書 1 つで元データを引けます。

self._items = {item.id: item for item in items}   # 表示中のデータ

def _on_select(self, _event=None):
    selection = tree.selection()      # 選択中の iid のタプル
    if not selection:
        return
    item = self._items[selection[0]]  # 行 → 元データ が一発で引ける
    self._show_detail(item)

tree.bind("<<TreeviewSelect>>", self._on_select)

インデックス番号で管理して、並べ替えのたびにズレて悩む……というのを避けられます。

スクロールバーは自分で繋ぐ

Treeview は自動でスクロールバーを出しません。相互に繋ぎます。

scrollbar = ttk.Scrollbar(left, orient="vertical", command=tree.yview)
tree.configure(yscrollcommand=scrollbar.set)

left.rowconfigure(0, weight=1)
left.columnconfigure(0, weight=1)
tree.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")

command=tree.yview(バーを動かしたら表を動かす)と
yscrollcommand=scrollbar.set(表が動いたらバーを動かす)の両方向が必要です。
片方だけだと「ドラッグはできるがホイールでバーが動かない」といった半端な状態になります。

6. 表示は StringVar 経由にする

ステータスバーのように後から書き換えるラベルは、StringVar を挟むと楽です。

self.status_var = tk.StringVar(value="準備中…")
ttk.Label(frame, textvariable=self.status_var, anchor="w").grid(row=2, column=0, sticky="ew")

# あとはどこからでも
self.status_var.set("12 件見つかりました")

ラベルのオブジェクトを持ち回らなくてよくなるので、
「あのラベルどこで作ったっけ」と探す時間が消えます。

7. Enter キーで検索できるようにする

毎回ボタンを狙わせるのは不親切なので、キーバインドを足します。

self.bind("<Return>", lambda _event: self._search())

bind に渡す関数にはイベントオブジェクトが 1 つ渡ってくるので、
引数を受け取らない関数をそのまま渡すと TypeError になります。
lambda _event: ... で捨てるのが定番です。

まとめ

やりたいこと 書き方
特定の行・列だけ伸ばす rowconfigure(i, weight=1) + sticky="nsew"
横一列に並べる 親フレームを分けて pack(side="left")
見た目を今風にする tk.* ではなく ttk.*
境界をドラッグできる分割 ttk.PanedWindow + add(child, weight=n)
表を出す ttk.Treeview(show="headings") + stretch
行と元データを結ぶ insert(..., iid=元のID)
ラベルの書き換え StringVar + textvariable

gridweightsticky さえ腹落ちすれば、Tkinter のレイアウトで
困ることはかなり減ります。

次は、この画面で通信を始めたときに必ずぶつかる
「ボタンを押すと画面が固まる」問題を扱います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?