プログラムでおかえしできるかな

定年を過ぎて何かの役に立てないかなと始めた元SEのブログです

ラップするgrid(wrapped_grid)で作るフォント一覧の作り方【Python】

このエントリーをはてなブックマークに追加

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) / pw

      cspan = max(1, int(widget_width * float(divisions) / parent_width))

    • ウィジェットの幅を足して親ウィジェットの幅を超えたら行を変えカラムを0にしてgridする

      grid は columnspan を指定

      widget.grid(row=r, column=c, columnspan=cspan, **kwargs)

      次のカラムは、現在のカラム+スパンの数

  • 固定幅:子ウィジェットの幅をすべて同じにして行によって個数を変えない場合

    • すべての子ウィジェットの最大幅を求める

      max_width = max([x.winfo_width() for x in widgets])

    • ウィジェットの幅を子ウィジェットの最大幅で割って1行のカラム数を決める

    • カラム数を固定して grid する

      widget.grid(row=i // column_num, column=i % column_num, **kwargs)

◆wrapped_grid()メソッド

前節の手順で対応したものをクラスメソッドとして作成しました。

◇wrapped_grid()メソッドの使い方

インポート

  • from tkinter_libs import TkinterLib
    ※もちろんインポートでなく、コピーしても使えます

メソッド

  • 【構文】 wrapped_grid(cls, parent, *widgets, event=None, flex=False, force=False, divisions=None, **kwargs)
  • 引数

    • parent:親ウィジェット(親として幅を取得するウィジェット)
    • *widgets:子ウィジェット(既にgridされていること。そうしないと幅が確定しない)
    • flex:子ウィジェットの幅を可変にするか(True:可変、False:固定)
    • force:親ウィジェットにサイズ変更がなくても処理する
      画面作成時に使うと良い
    • divisionsflex=True にした場合の親ウィジェットの幅の分割数
      デフォルトは500。大きくすれば精密になるがあまり大きくしても差が出ない
    • **kwargs:grid に準ずる
  • 使い方

    • TkinterLib.wrapped_grid()
      ※クラスメソッドなのでインスタンスの作成は不要
    • メソッドが呼ばれる前に *widgets で渡すウィジェットを grid しておく
    • 画面作成時は、force オプションを True にして呼び出す
    • ウィジェットのサイズが変わった時に動作するようにバインドすると良い

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() メソッドの引数 flexTrue にするか 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 パッケージ

TkinterPythonの標準パッケージです。

◆ソースの取得

全体のソースはこちらから取得できます。

◆免責事項

ご利用に際しては、『免責事項』をご確認ください。

お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
ただし、回答をお約束するものではありません。

◆さいごに

Tkinterウィジェットを配置するメソッド、grid() より pack() の方が好みです。
pack() でラップできないかなと調べていたのですが、無理そうでした。

それではと、grid() で調べていたら smart_grid() を見つけました。
自分では思いつかなかったかもしれません。
常日頃、自分で思いつくようなことは誰かが既に思いついているとは思っているのですが。

固定幅のやり方は自分でも思いつくことなので対応したのですが、可変幅でも実現できると思います。
ウィジェットを作成する時に同じ幅で作成すれば、flex=Trueで動かしても、flex=Falseで動かしても結果は同じだからです。

でも、メソッドで固定幅とした方がピンと来るので用意してみました。

よく考えると、子ウィジェットの幅の最大幅がいくつになるかはそれを作っている時は分からないんだから、必要ってことなのかもしれません。

記事が出来上がってから思いついたのですが、span の数を固定して固定幅の対応をすることもできそうです。
どちらが使いやすいのか、コードがすっきりするのか、今後の検討課題にします。

「今後の検討課題」って、体のいいお断りですよね。

あわせて読みたい - ScrolledFrame

あわせて読みたい - Tkinter関連記事

◇ご注意

本記事は次のバージョンの下で動作した内容を元に記述しています。

◆参考

投稿: