Python Tkinter で スクロールバー付の Frame クラスを作りました。
Frame ウィジェットは、直接 Scrollbar ウィジェットと関連付けられません。
そのため Canvas ウィジェットを介してスクロールバーを付けます。
フォントの一覧をスクロールバー付 Frame で表示するアプリを例に、スクロールバーの付け方をサンプルコードも交えて説明します。
まず、Frame にスクロールバーを付ける方法を説明します。
後からそれに基づいて作成した ScrolledFrame クラスを説明します。
最後に、スクロールバー付 Frame を使ったフォント一覧を紹介します。
目次
- ◆Frameウィジェットにスクロールバーを付ける方法
- ◆ScrolledFrame(スクロールバー付Fame)クラス
- ◆フォント一覧の作り方
- ◆必要なパッケージ
- ◆ソースの取得
- ◆免責事項
- ◆さいごに
- ◆参考
◆Frameウィジェットにスクロールバーを付ける方法
◇考え方
Frame ウィジェットには次のような考慮すべき点があります。それぞれを対策します。
Frame にスクロールバーは付けられない
➡ CanvasにFrameを配置して Canvas をスクロールする
canvas にはcreate_window()
メソッドで Frame を配置Canvas にスクロール範囲を知らせる必要がある
そうしないと、スクロールバーが機能しない
➡ Canvas のscrollregion
オプションを設定する
【処理】
- Canvas オブジェクトの作成(親はrootなど)
- Frame オブジェクトの作成(親は Canvas)
- Scrollbar オブジェクトの作成(親はrootなど、縦、横の2つ)
- Canvas とスクロールバーを関連付け(縦、横の2つ)
- ウィジェットの配置
- 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 クラスに準ずる
属性
プロパティ使い方
- 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(シーケンス, コールバック関数, 追加フラグ)
引数
▼コード▼マウスホイールでスクロール
# 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で提供されるフォントをラジオボタンで一覧表示(ScrolledFrame を使用)
- fontsフォルダのフォントを一覧表示
◇familiesで提供されるフォントをラベルで一覧表示
families()
メソッドで提供されるフォントをラベルで一覧表示します。
フォントごとにラベルを作成しフレームに配置します。
FontLib クラスのコンストラクタでフレームにスクロールバーを付けています。
ラベルは、font
オプションでフォントを指定します。
フォントは、Font コンストラクタでフォントオブジェクトを作成します。
▶Fontクラスのコンストラクタ
- 【構文】
Font(option, ...)
- よく使う使い方(サンプル)
【構文】Font(root, family="メイリオ", size=16, weight="bold")
【構文】Font(root, ("メイリオ", 16, "bold"))
位置引数にする時はタプルで指定 - オプション
family
:フォントファミリー名size
:サイズ(ポイント)weight
:bold
:太字、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の場合)から取得します。
フォント名とフォントファイル名を対応付けた一覧を出力します。
▶◎処理
Windows\fonts
フォルダからファイル名を取得
files = glob.glob(os.environ.get("WINDIR") + r"\fonts\*")
- 取得したファイル名ごとに以下を実行する
-
ImageFont.truetype()
メソッドでImageFontオブジェクトを作成する
拡張子には、ttf,ttc,fonなどがあり、fonなどはロードすると例外となるので除く
戻り値はImageFontオブジェクト ImageFontオブジェクト.getname()
メソッドでファミリー名とフォントスタイル名を取得
※ここで返るファミリー名は日本語フォントの場合に文字化けしている
⇒そこで tkinter の Font オブジェクトを作成し
Fontオブジェクト.actual("family")
メソッドで日本語のファミリー名を取得する- フォント名、フォントファイル名、サンプル文字、フォント名でのサンプルをラベルで出力
-
▼コード▼
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
◆必要なパッケージ
◇必要な Python パッケージ
- pillow
- 【インストール】pip install Pillow
- 【インポート】 from PIL import ImageFont
◆ソースの取得
全体のソースはこちらから取得できます。
- フォント一覧
- ソース:fonts_list.py
- 取得先:GitHub juu7g/Python-fontlist
- Tkinter用ライブラリ
- ソース:tkinter_libs.py
- 取得先:GitHub juu7g/Python-tkinter-libs
- フォント一覧のソースと同じフォルダに置いて使います
◆免責事項
ご利用に際しては、『免責事項』をご確認ください。
お気づきの点がございましたら『お問い合わせ』からお問い合わせください。
ただし、回答をお約束するものではありません。
◆さいごに
記事『画像サイズを変更し文字透かしを入れるアプリ【フリー】』で提供させていただいた「フォント一覧ツール」を作成した時の内容を記事にしました。
「フォント一覧ツール」は、初め、スクロールバーのない状態で作ったのですが、フォントの種類が多くて表示しきれませんでした。
自分で使う分にはそれでもよかったのですが、使ってもらおうと思うと何とかしなければと思いました。
それでスクロールバーを付けることにしました。
誰かに使ってもらおうと思うと細かいところにまでこだわるようになっていきます。
基本的に自分で使えそうなプログラムを作成していますが、使いたいと思う方がきっといるとも思っています。
自分のためだけじゃないというところに作る喜びも生まれているような気がします。
- SQLクライアントアプリの作り方(Tkinterで表)【Python】
- Excel viewer アプリの作り方(Tkinterでタブと表)【Python】
- CSV viewer アプリの作り方(ドラッグアンドドロップ)【Python】
- 画像ビューアの作り方(Treeviewに画像と疑似チェックボックス)【Python】
- シンプル画像ビューアの作り方(マウスホイール対応)【Python】
- ScrolledFrameとwrapped_gridで作る画像一覧の作り方【Python】
◇ご注意
本記事は次のバージョンの下で動作した内容を元に記述しています。
- Python 3.8.5
- Pillow 8.3.0