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

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

スクロールバー付Frameで作るフォント一覧の作り方【Python】

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

Python Tkinter で スクロールバー付の Frame クラスを作りました。
Frame ウィジェットは、直接 Scrollbar ウィジェットと関連付けられません。
そのため Canvas ウィジェットを介してスクロールバーを付けます。

フォントの一覧をスクロールバー付 Frame で表示するアプリを例に、スクロールバーの付け方をサンプルコードも交えて説明します。

まず、Frame にスクロールバーを付ける方法を説明します。
後からそれに基づいて作成した ScrolledFrame クラスを説明します。
最後に、スクロールバー付 Frame を使ったフォント一覧を紹介します。

目次

◆Frameウィジェットにスクロールバーを付ける方法

◇考え方

Frame ウィジェットには次のような考慮すべき点があります。それぞれを対策します。

  • Frame にスクロールバーは付けられない
    CanvasにFrameを配置して Canvas をスクロールする
    canvas には create_window() メソッドで Frame を配置

  • Canvas にスクロール範囲を知らせる必要がある
    そうしないと、スクロールバーが機能しない
    Canvasscrollregion オプションを設定する

【処理】

  1. Canvas オブジェクトの作成(親はrootなど)
  2. Frame オブジェクトの作成(親は Canvas
  3. Scrollbar オブジェクトの作成(親はrootなど、縦、横の2つ)
  4. Canvas とスクロールバーを関連付け(縦、横の2つ)
  5. ウィジェットの配置
    1. 水平スクロールバーオブジェクトを pack()
    2. 垂直スクロールバーオブジェクトを pack()
    3. Canvas オブジェクトを pack()
    4. Frame オブジェクトを Canvas に配置 create_window()
      self.canvas.create_window((0,0), window=self.frame, anchor="nw")
      ※配置位置の座標はアンカー位置からの座標なので注意
       アンカーのデフォルトはセンター
  6. scrollregion オプションの設定
    Frame オブジェクトにウィジェットを追加したら Canvas オブジェクトの scrollregion オプションを設定
    設定前に Frame オブジェクトを update_idletasks() メソッドで更新しておく
    self.canvas.configure(scrollregion=self.canvas.bbox("all"))
    bbox("all")Canvas オブジェクト全体の範囲

【コード】

▼Frame にスクロールバーを付ける

        self.root = tk.Tk()
        # スクロールバー付Frameの作成(Canvasにスクロールバーを付けて)
        self.canvas = tk.Canvas(self.root)  # Canvasをrootに作成
        self.frame = tk.Frame(self.canvas)  # Frame をCanvasに作成
        self.vsb = tk.Scrollbar(self.root, orient=VERTICAL, command=self.canvas.yview)      # 縦スクロールバーをrootに作成
        self.hsb = tk.Scrollbar(self.root, orient=HORIZONTAL, command=self.canvas.xview)    # 横スクロールバーをrootに作成
        self.canvas.configure(yscrollcommand=self.vsb.set)  # 縦スクロールバーの動作をCanvasに設定
        self.canvas.configure(xscrollcommand=self.hsb.set)  # 横スクロールバーの動作をCanvasに設定
        # pack スクロールバーは先にpackする
        self.hsb.pack(side="bottom", fill="x")
        self.vsb.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        # canvasにウィジェットを配置
        self.canvas.create_window((0,0), window=self.frame, anchor="nw")

▼scrollregion オプションの設定

        # Frameの大きさを確定してCanvasにスクロール範囲を設定
        self.frame.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox("all"))

◇スクロールバーの設置

解説は別記事を参照してください ソースコードだけではよくわからない場合、こちらの記事を参照してください
CSV viewerアプリの作り方(ドラッグアンドドロップ)【Python】 - ◆スクロールバーの設置

◇スクロールバーを付けられるウィジェット

参考にスクロールバーが付けられるウィジェットです。

  • Listbox
  • Text
  • Canvas
  • Entry
  • Treeview

◆ScrolledFrame(スクロールバー付Fame)クラス

前節の手順で対応すれば、Frame にスクロールバーを付けられますが、毎回だと面倒です。
そこで、ScrolledFrame(スクロールバー付Fame)クラスを作成しました。
あわせて、マウスホイールでスクロールできるようにしました。

◇ScrolledFrame(スクロールバー付Fame)クラスの概要

このクラスは Frame ウィジェットにスクロールバーを付けるために Canvas ウィジェットと Scrollbar ウィジェットを追加しています。
更に note ウィジェットに add するには、トップが Canvas 型ではできないので Frame ウィジェットでラップして add ができるようにしています。

追加した Canvas ウィジェットや Frame ウィジェットは内部的なものですが、イベント処理やほかのウィジェットとの関係でアクセスする必要が出てくるので、属性にしてあります。
追加:2022-09-17

◇ScrolledFrame(スクロールバー付Fame)クラスの使い方

Frame クラスと同じように使っていただければ結構です。

インポート

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

コンストラク

  • 【構文】 ScrolledFrome(master, *args, has_h_bar=False, **kwargs)
  • よく使う使い方
    【構文】 ScrolledFrome(master, has_h_bar=True)
  • 引数

    • master:親ウィジェット
    • has_h_bar:水平スクロールバーの有無(True:あり、False:なし)
    • その他:Frame クラスに準ずる
  • 属性 プロパティ

    • parent_canvas:内部で持っている Canvas オブジェクト
      親が parent_frame master で子が self(ScrolledFrame オブジェクト)
    • parent_frameparent_canvas をラップしている Frame オブジェクト
      親が master で子が parent_canvas
      更新:2022-04-15
      master 内で配置(packなど)する場合はこれを使う
      note ウィジェットに add する場合はこれを使う
  • 使い方

    • Frame クラスを継承しているので Frame クラスと同じように使う

bind時の注意
selfのConfigure, self.parent_canvas の Configure, selfのMouseWheel
既にbindしているので、外部でバインドする場合 add=True オプションを指定します。
更新:2022-09-17

◇スクロールバー付Fameクラスの特徴

  • Frame クラスを継承
    Frame クラスを継承し、スクロールさせるためにクラス内で Canvas オブジェクトを作成
  • 垂直、水平スクロールバーを設置
    • 水平スクロールバーはコンストラクタの引数で無しにできる
  • マウスホイールで垂直スクロール
  • クラス内で作成した Canvas オブジェクトは、parent_canvas プロパティで参照可能

スクロールバーの実装は前節の解説に沿って行っています。

ただし、Canvas オブジェクトの scrollregion オプションの設定は、Frame オブジェクトのサイズが変わった時に動作するようにイベントにしています。

  • バインド:self.bind("<Configure>", self.on_frame_configure)

  • 関数:def on_frame_configure(self, event=None):

マウスホイールの対応は、マウスホイールで垂直スクロール対応で説明します。

【コード】

▼ScrolledFrame クラス

class ScrolledFrame(tk.Frame):
    """
    スクロールバー付Frameクラス
    ※bind時の注意
        selfのConfigure, self.parent_canvasのConfigure, selfのMouseWheelは
        既にbindしているので、外部でバインドする場合add=Trueオプションを指定する
    """
    def __init__(self, master, *args, has_h_bar=False, **kwargs) -> None:
        """
        Frameがスクロールバーを持つためにはCanvasでラップしたFrameを作成する。
        スクロールバーとCanvasはmasterに作成する
        """
        # 親としてcanvasを作成
        self.parent_canvas = tk.Canvas(master)
        super().__init__(self.parent_canvas, *args, **kwargs)
        # 水平スクロールバー作成(option)
        if has_h_bar:
            hsb1 = tk.Scrollbar(master, orient=tk.HORIZONTAL, command=self.parent_canvas.xview)
            self.parent_canvas.configure(xscrollcommand=hsb1.set)
            hsb1.pack(side="bottom", fill="x")
        # 垂直スクロールバー作成
        vsb1 = tk.Scrollbar(master, orient=tk.VERTICAL, command=self.parent_canvas.yview)
        self.parent_canvas.config(yscrollcommand=vsb1.set)
        vsb1.pack(side="right", fill="y")
        self.parent_canvas.pack(side="left", fill="both", expand=True)
        # キャンバスにフレームを割り当て 0,0はanchor位置なのでnwを指定
        self.frame_id = self.parent_canvas.create_window(0, 0, anchor="nw", window=self)
        # bind
        self.bind("<Configure>", self.on_frame_configure)
        self.bind_all("<MouseWheel>", self.on_frame_mouse_wheel)

    def on_frame_configure(self, event=None):
        """
        canvasを親に持つframeでサイズ変更があった場合にcanvasのscrollregionを更新する
        これでスクロールバーが動作する
        frameの<configure>シーケンスとbindして使う
        """
        if event:
            if type(event.widget.master) == tk.Canvas:
                canvas1 = event.widget.master
                canvas1.configure(scrollregion = canvas1.bbox("all"))

◇マウスホイールで垂直スクロール対応

マウスホイールの動作で垂直方向にキャンバスがスクロールするよう対応します。

◎垂直スクロール動作

垂直スクロール動作は、yview_scroll メソッドで行います。

yview_scroll メソッド

  • 【構文】 canvas.yview_scroll(number, what)
  • よく使う使い方
    【構文】 canvas.yview_scroll(n, "units")
  • 引数
    • number:方向のスクロール数(負:↑、正:↓)
    • what:単位("units":yScrollIncrement単位、"pages":ページ)

ここでスクロール数を、イベントオブジェクトの delta 属性を使って求めます。
上スクロールの時に上に動いてほしいので、変化量を考えず変化があったことを検知し、正負の向きだけ反転します。

self.parent_canvas.yview_scroll(int(-1 * (event.delta / abs(event.delta))), "units")

◎イベントクラス

バインドされたコールバック関数が呼び出されると event オブジェクトが引数で提供されます。
event オブジェクトの主な属性を示します。

  • 主な属性
    • char:イベントがKeyPressかKeyReleaseの場合に文字をセット
    • num:イベントがマウスボタン関連の場合、パタン番号をセット
    • type:イベントのタイプを意味する番号(KeyPress, Active, Buttonなどの番号)
    • widget:イベント要因のウィジェット(オブジェクト)を常にセット
    • delta:MouseWheelイベント用。整数。正:上スクロール、負:下にスクロール
      Windowsでは120の倍数。例:-240は2ステップ下にスクロールされた
    • xウィジェットの左上隅を基準にした、イベント時のマウスのx座標
    • yウィジェットの左上隅を基準にした、イベント時のマウスのy座標

◎マウスバインド

マウスのホイールに対して、垂直スクロールを割り当てます。
バインドには、bind() メソッドがありますが、ここでは bind_all() メソッドを使用します。
理由は、Frame オブジェクト上に様々なウィジェットを配置した場合、一番上のウィジェットのイベントが発生するため、Frame オブジェクトの bind() だとイベントが発生しないためです。

  • 【構文】ウィジェット.bind_all(シーケンス, コールバック関数, 追加フラグ)

  • 引数

    • シーケンス:イベント名(例:<Return>, <Button-1>, <MouseWheel>, <Control-MouseWheel>)
    • コールバック関数:関数には event 引数が必要
      ウィジェットの command オプションと共通の場合は、関数の引数定義を event=None とする
    • 追加フラグウィジェットに対してイベントを置き換えるか追加するか
      デフォルトは置き換え(引数を省略した場合)

▼コード▼マウスホイールでスクロール

        # bind
        self.bind_all("<MouseWheel>", self.on_frame_mouse_wheel)

    def on_frame_mouse_wheel(self, event=None):
        """
        Canvasをmouse wheelで垂直scrollさせる
        """
        if event:
            self.parent_canvas.yview_scroll(int(-1 * (event.delta / abs(event.delta))), "units")

◆フォント一覧の作り方

3種類のフォント一覧の作り方を紹介します。

◇familiesで提供されるフォントをラベルで一覧表示

families() メソッドで提供されるフォントをラベルで一覧表示します。

フォントごとにラベルを作成しフレームに配置します。
FontLib クラスのコンストラクタでフレームにスクロールバーを付けています。

ラベルは、font オプションでフォントを指定します。
フォントは、Font コンストラクタでフォントオブジェクトを作成します。

Fontクラスのコンストラク

  • 【構文】Font(option, ...)
  • よく使う使い方(サンプル)
    【構文】Font(root, family="メイリオ", size=16, weight="bold")
    【構文】Font(root, ("メイリオ", 16, "bold"))
    位置引数にする時はタプルで指定
  • オプション
    • family:フォントファミリー名
    • size:サイズ(ポイント)
    • weightbold:太字、normal:通常


▼コード▼

        self.fonts = tk.font.families()     # tkが管理しているフォントのリストを取得

    def font_list_by_label(self) -> None:
        """
        1.familiesを対象、ラベルで実装
        """
        for i, font_name in enumerate(self.fonts):
            # ラベルの作成 フォントは先にFontオブジェクトを作成してfontオプションで指定
            font_ = tkinter.font.Font(self.root, family=font_name, size=self.FONT_SIZE)
            label = tk.Label(self.frame, text=f"{self.text} {font_name}", font=font_)
            # grid
            # 最大行を指定して1列目から配置
            # label.grid(row=i % self.MAX_ROWS, column=i // self.MAX_ROWS, sticky="w")
            # 最大列を指定して1行目から配置
            label.grid(row=i // self.MAX_COLUMN, column=i % self.MAX_COLUMN, sticky="w")
        # Frameの大きさを確定してCanvasにスクロール範囲を設定
        self.frame.update_idletasks()
        self.canvas.config(scrollregion=self.canvas.bbox("all"))

◇familiesで提供されるフォントをラジオボタンで一覧表示

families() メソッドで提供されるフォントをラジオボタンで一覧表示します。

ScrolledFrame クラスを使用してスクロールを可能にしています。
サンプルコードでは、ScrolledFrame クラスのコンストラクタで背景色のオプションを指定しています。
Frame ウィジェットと同じように使えます。

フォントごとにラベルを作成し ScrolledFrame オブジェクトに配置します。

ラジオボタンは、font オプションでフォントを指定します。
この例では、fontオプションに直接フォントの内容を指定しています。
この場合、タプルで指定します。

  • font=(font_name, self.FONT_SIZE),


▼コード▼

        self.fonts = tk.font.families()     # tkが管理しているフォントのリストを取得

    def font_list_radiobutton(self) -> None:
        """
        2.familiesを対象、ラジオボタンで実装
            ScrolledFrameを使用
        """
        from tkinter_libs import ScrolledFrame
        # ScrolledFrameを使うため既にできているroot以下のウィジェットを削除
        for w in self.root.winfo_children():
            w.destroy()
        
        # ScrolledFrame
        self.frame = ScrolledFrame(self.root, has_h_bar=True, background="ivory")
        self.var_radio = tk.IntVar(value=0) # self.にしないとマウス移動で全選択になってしまう
        for i, font_name in enumerate(self.fonts):
            # ラジオボタンの作成 フォントはfontオプションで直に指定
            rb = tk.Radiobutton(self.frame, text=f"{self.text} {font_name}", 
                                font=(font_name, self.FONT_SIZE), variable=self.var_radio, value=i)
            # grid
            # 最大行を指定して1列目から配置
            rb.grid(row=i % self.MAX_ROWS, column=i // self.MAX_ROWS, sticky="w")
            # 最大列を指定して1行目から配置
            # rb.grid(row=i // self.MAX_COLUMN, column=i % self.MAX_COLUMN, sticky="w")

◇fontsフォルダのフォントを一覧表示

画像ライブラリの pillow はフォントの指定をフォントファイル名で行います。

フォントファイル名は「\Windows\fonts」フォルダ(Windowsの場合)から取得します。
フォント名とフォントファイル名を対応付けた一覧を出力します。

◎処理

  1. Windows\fonts フォルダからファイル名を取得
    files = glob.glob(os.environ.get("WINDIR") + r"\fonts\*")
  2. 取得したファイル名ごとに以下を実行する
    1. ImageFont.truetype() メソッドでImageFontオブジェクトを作成する
      拡張子には、ttf,ttc,fonなどがあり、fonなどはロードすると例外となるので除く
      戻り値はImageFontオブジェクト
    2. ImageFontオブジェクト.getname()メソッドでファミリー名とフォントスタイル名を取得
      ※ここで返るファミリー名は日本語フォントの場合に文字化けしている
       ⇒そこで tkinter の Font オブジェクトを作成し
        Fontオブジェクト.actual("family")メソッドで日本語のファミリー名を取得する
    3. フォント名、フォントファイル名、サンプル文字、フォント名でのサンプルをラベルで出力

▼コード▼

    def font_list_from_dir(self):
        """
        3.fontsフォルダを対象にpillowのtruetypeで実装
        """
        import glob, os
        from PIL import ImageFont 

        # windowsのフォントフォルダの取得
        windir = os.environ.get("WINDIR")
        files = glob.glob(windir + r"\fonts\*")
        for i, file in enumerate(files):
            basename = os.path.basename(file)
            try:
                img_font = ImageFont.truetype(file, 24)
            except:
                # pillowの対象外のファイルを除く
                # print(f"path:{basename}")
                pass
            else:
                font_name = img_font.getname()[0]
                font_ = tkinter.font.Font(self.root, family=font_name, weight="bold")
                # getnameのfamilyの日本語が文字化けするのでactualに置き換える
                font_name = font_.actual("family")
                # フォント名、フォントファイル名、サンプル文字、フォント名でのサンプルをラベルで出力
                tk.Label(self.frame, text=font_name).grid(row=i, column=1, sticky="w")
                tk.Label(self.frame, text=basename, bg="yellow").grid(row=i, column=2, sticky="w")
                tk.Label(self.frame, text=self.text,
                                font=(font_name, self.FONT_SIZE)).grid(row=i, column=3, sticky="w")
                tk.Label(self.frame, text=font_name,
                                font=(font_name, self.FONT_SIZE)).grid(row=i, column=4, sticky="w")

◇使い方

ソースコード内の「switch」変数を1-3に切り替えて実行

  • 画面の説明
    • switch=1:フォントをラベルで表示。縦横のスクロールバーが出る
    • switch=2:フォントをラジオボタンで表示。縦横のスクロールバーが出る。ScrolledFrameクラスを使用
    • switch=3:フォントとフォントファイル名を表示

▼コード▼

if __name__ == '__main__':
    """
    1:familiesを対象、ラベルで実装。
    2:familiesを対象、ラジオボタンで実装。ScrolledFrameを使用
    3:pillowのtruetypeで実装。fontsフォルダを対象。
    """
    switch = 1
追加:2022-03-24

◆必要なパッケージ

◇必要な Python パッケージ

  • pillow
    • 【インストール】pip install Pillow
    • 【インポート】 from PIL import ImageFont

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

◆ソースの取得

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

◆免責事項

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

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

◆さいごに

記事『画像サイズを変更し文字透かしを入れるアプリ【フリー】』で提供させていただいた「フォント一覧ツール」を作成した時の内容を記事にしました。

「フォント一覧ツール」は、初め、スクロールバーのない状態で作ったのですが、フォントの種類が多くて表示しきれませんでした。
自分で使う分にはそれでもよかったのですが、使ってもらおうと思うと何とかしなければと思いました。
それでスクロールバーを付けることにしました。

誰かに使ってもらおうと思うと細かいところにまでこだわるようになっていきます。

基本的に自分で使えそうなプログラムを作成していますが、使いたいと思う方がきっといるとも思っています。

自分のためだけじゃないというところに作る喜びも生まれているような気がします。

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

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

あわせて読みたい - wrapped_grid

◇ご注意

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

◆参考

投稿: 、更新: