Python Tkinter で Frame にウィジェットを追加し、grid で配置する時に、Frame の幅に収まるだけウィジェットを配置するメソッドを作成しました。
いわゆるラップ(折り返し)ですね。
追加するウィジェットの幅を固定する場合と固定しない場合の2種類に対応しています。
二つの例をフォントの一覧を表示するアプリを例に、サンプルコードも交えて説明します。
まず、どのようにラップするかを説明します。
次に、それに基づいて作成した wrapped_grid() メソッドを説明します。
最後に、フォント一覧を紹介します。
目次
◆Frameの幅に合わせてgrid数を決める方法
◇考え方
Frame ウィジェットに子ウィジェットを配置する場合に Frame の端まで配置したらラップして配置させたい場合があります。
子ウィジェットの幅を均一にする場合としない場合について考えます。
可変幅:子ウィジェットの幅をそのままで、行によって個数を変える場合
親の幅を細かく分割し、子の幅が分割した幅の何個分か求めてその個数でspanする(またがらせる)
固定幅:子ウィジェットの幅を均一にして、行によって個数を変えない場合
子の幅の最大値を求めて親の幅から割って一行の子の個数を求める
※子ウィジェットの表示が切れないように最大幅を求めています
【処理】
可変幅:子ウィジェットの幅をそのままで行によって個数を変える場合
子の幅は分割した幅1単位が何個分かを求める
親の幅(pw)をd分割した時の単位幅(dw)は、dw = pw / d
子の幅(cw)に入る単位幅(dw)の個数は、cw / dw
この個数でスパンする。スパンする個数は、cw / dw = cw / (pw / d) = (cw * d) / pwcspan = max(1, int(widget_width * float(divisions) / parent_width))
子ウィジェットの幅を足して親ウィジェットの幅を超えたら行を変えカラムを0にしてgridする
grid は columnspan を指定
widget.grid(row=r, column=c, columnspan=cspan, **kwargs)
次のカラムは、現在のカラム+スパンの数
固定幅:子ウィジェットの幅をすべて同じにして行によって個数を変えない場合
◆wrapped_grid()メソッド
前節の手順で対応したものをクラスメソッドとして作成しました。
◇wrapped_grid()メソッドの使い方
▶インポート
from tkinter_libs import TkinterLib
※もちろんインポートでなく、コピーしても使えます
▶メソッド
- 【構文】
wrapped_grid(cls, parent, *widgets, event=None, flex=False, force=False, divisions=None, **kwargs)
引数
使い方
▶bind時の注意
- bindさせる場合、widgetsのgridでサイズ変更が起きないウィジェットで行う。 そうしないとgrid直後に再度呼ばれて無限ループになる
- ScrolledFrameクラスのオブジェクトにbindする場合、parent_canvasプロパティにbindする
ScrolledFrameオブジェクトにbindするとイベントが発生しないため
【コード】
▼wrapped_grid()メソッド
class TkinterLib: @classmethod def wrapped_grid(cls, parent, *widgets, event=None, flex=False, force=False, divisions=None, **kwargs): """ parentの幅に合わせてwidgetsをgridで再配置する gridの列数は(parentの幅//widgetsの要素で最大の幅)で求める flexがTrueの時は、parentの幅に1行ごとに入れられるだけのwidgetを入れる bindさせる場合、widgetsのgridでサイズ変更が起きないウィジェットで行う そうしないとgrid直後に再度呼ばれて無限ループになる ScrolledFrameクラスのオブジェクトにbindする場合、parent_canvasプロパティにbindする Args: Any: 親ウィジェット *Any: 子ウィジェット bool: 子ウィジェットの幅を固定するかどうか(True:固定しない、False:固定) bool: 親ウィジェットの幅がサイズ変更してなくても動作させる int: 親ウィジェットの幅の分割数 """ if not widgets: return # コマンドライン引数指定の場合、mainloopが起動していないのでupdateする parent.update() print(f"Enter wrapped_grid parent:{parent.winfo_width()}, child:{widgets[0].winfo_width()}") if not divisions: divisions = 500 force_f = force # サイズ変更がなくてもgridしたい時 try: parent.previous_width except: parent.previous_width = -1 parent_width = parent.winfo_width() # parentの幅が変わっていなかったら抜ける if parent_width == parent.previous_width and not force_f: return # parent.previous_width = parent_width if flex: r = c = width = 0 print(f"Do grid by flex") # for DEBUG for widget in widgets: widget_width = widget.winfo_width() cspan = max(1, int(widget_width * float(divisions) / parent_width)) width += widget_width if width > parent_width: r += 1 width = widget_width c = 0 widget.grid(row=r, column=c, columnspan=cspan, **kwargs) c += cspan else: # parentに前のカラム数を覚えさせる try: parent.previous_column except: parent.previous_column = -1 max_width = max([x.winfo_width() for x in widgets]) column_num = max(1, parent_width // max_width) # 0を除く if parent.previous_column == column_num and not force_f: return parent.previous_column = column_num print(f"Do grid by fixed colum num : {column_num} ({parent_width}//{max_width})") # for DEBUG for i, widget in enumerate(widgets): widget.grid(row=i // column_num, column=i % column_num, **kwargs) parent.update_idletasks() # update() return
◆フォント一覧の作り方
2種類のフォント一覧の作り方を紹介します。
2種類と言っても wrapped_grid()
メソッドの呼び方を変えているだけなのでまとめて説明します。
- familiesで提供されるフォントを可変幅ラベルで一覧表示
- familiesで提供されるフォントを固定幅ラベルで一覧表示
families()
メソッドで提供されるフォントをラベルで一覧表示します。
◇特徴
- ScrolledFrame クラスを使用してスクロールバーの表示とマウスホイールでのスクロールができます
- ウィンドウの幅を変えてもウィンドウの幅でラベルを折り返します
◇作り方
ScrolledFrame クラスを使用してスクロールを可能にしています。
※ScrlledFrame クラスについては、記事『スクロールバー付Frameで作るフォント一覧の作り方【Python】』を参照してください。
サンプルコードでは、ScrolledFrame クラスのコンストラクタで背景色のオプションを指定しています。
Frame ウィジェットと同じように使えます。
ScrolledForm オブジェクトの parent_canvas プロパティに Tkinterlib.wrapped_grid()
メソッドをバインドします。
※ScrolledForm オブジェクトにバインドするとイベントが発生しませんでした。
これで、フレームのサイズ変更が発生した際に wrapped_grid()
を実行します。
ScrolledForm オブジェクトに表示用のラベルを追加します。
ラベルは、font
オプションでフォントを指定します。
ここでは、fontオプションに直接フォントの内容を指定しています。
この場合、タプルで指定します。
font=(font_name, self.FONT_SIZE),
追加したラベルは一度仮に grid() します。
後は、mainloop() が実行されて、ウィジェットのサイズが確定するとバインドしたTkinterlib.wrapped_grid()
メソッドが動作して、ラベルを grid() し直します。
ラベルを可変幅で表示するか固定幅で表示するかは、Tkinterlib.wrapped_grid()
メソッドの引数 flex
を True
にするか False
にするかで使い分けます。
▼可変幅で作成したフォント一覧
▼固定幅で作成したフォント一覧
◇使い方
FontLib クラスのコンストラクタに渡す引数を指定して実行
text
:表示テキストfont_size
:フォントサイズflex
:可変幅にするかどうか
【コード】
▼FontLib クラス
class FontLib(): """ フォントリスト表示用クラス """ def __init__(self, text:str="sample サンプル ", font_size:int=14, flex=False) -> None: """ コンストラクタ Args: str: 出力文字 int: 文字のフォントサイズ bool: 幅に入るだけgridするかどうか(True:入るだけ、False:列数固定) """ self.root = tk.Tk() self.root.geometry("1124x800") # サイズ self.fonts = tk.font.families() self.text = text self.FONT_SIZE = font_size # scroll付きFrameの作成 self.frame = ScrolledFrame(self.root, background="lavender", has_h_bar=True) # bind wrapped_gridへのbind 親の幅に合わせてgridする # frame だと発生しない、canvasだと発生、rootだと発生しまくり self.labels = [] self.frame.parent_canvas.bind("<Configure>", lambda event: TkinterLib.wrapped_grid( self.frame.parent_canvas, *self.labels, event=event, flex=flex), add=True) def font_list_by_label_wrapped_grid(self) -> None: """ familiesを対象、ラベルで実装 """ self.labels = [] for i, font_name in enumerate(self.fonts): label = tk.Label(self.frame, text=f"{i+1}:{self.text} {font_name}", font=(font_name, self.FONT_SIZE), relief=tk.RIDGE, wraplength=200) self.labels.append(label) # 仮にgridする label.grid(row=i, column=0, sticky=tk.W) if __name__ == '__main__': """ familiesを対象、ラベルで実装。wrapped_grid使用 """ kwargs = {} kwargs["text"] = "" # 表示テキスト(""でもフォント名は出る) kwargs["font_size"] = 11 # フォントサイズ kwargs["flex"] = False # 可変幅にするかどうか a = FontLib(**kwargs) a.font_list_by_label_wrapped_grid() a.root.mainloop()
◆必要なパッケージ
◇必要な Python パッケージ
◆ソースの取得
全体のソースはこちらから取得できます。
- フォント一覧
- ソース:fonts_list_wrapped_grid.py
- 取得先:GitHub juu7g/Python-fontlist
- Tkinter用ライブラリ
- ソース:tkinter_libs.py
- 取得先:GitHub juu7g/Python-tkinter-libs
- フォント一覧のソースと同じフォルダに置いて使います
◆免責事項
ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
ただし、回答をお約束するものではありません。
◆さいごに
Tkinter のウィジェットを配置するメソッド、grid() より pack() の方が好みです。
pack() でラップできないかなと調べていたのですが、無理そうでした。
それではと、grid() で調べていたら smart_grid() を見つけました。
自分では思いつかなかったかもしれません。
常日頃、自分で思いつくようなことは誰かが既に思いついているとは思っているのですが。
固定幅のやり方は自分でも思いつくことなので対応したのですが、可変幅でも実現できると思います。
子ウィジェットを作成する時に同じ幅で作成すれば、flex=Trueで動かしても、flex=Falseで動かしても結果は同じだからです。
でも、メソッドで固定幅とした方がピンと来るので用意してみました。
よく考えると、子ウィジェットの幅の最大幅がいくつになるかはそれを作っている時は分からないんだから、必要ってことなのかもしれません。
記事が出来上がってから思いついたのですが、span の数を固定して固定幅の対応をすることもできそうです。
どちらが使いやすいのか、コードがすっきりするのか、今後の検討課題にします。
「今後の検討課題」って、体のいいお断りですよね。
- SQLクライアントアプリの作り方(Tkinterで表)【Python】
- Excel viewer アプリの作り方(Tkinterでタブと表)【Python】
- CSV viewer アプリの作り方(ドラッグアンドドロップ)【Python】
- 画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】
- シンプル画像ビューアの作り方(マウスホイール対応)【Python】
- ScrolledFrameとwrapped_gridで作る画像一覧の作り方【Python】
◇ご注意
本記事は次のバージョンの下で動作した内容を元に記述しています。
- Python 3.8.5