tkinter でスクロールできるフレーム:マウスホイールにも対応![ScrollableFrame]
はじめに
PythonでCanvasをリサイズできるようにしてみた を元に自分好みに使いやすくしてみました.
略儀ながら、ここに感謝の意を表します.
こんな感じのフレームのクラスです.
概要
Python の Tkinter のスクロールバー付きのフレームのクラスです。
ttkwidgets の AutoHideScrollbar を使用することで、自動的に隠れるスクロールバーを使用できます。
また、クラス内の bind_child 関数を使用することで、入れ子となっている全てのウィジェットに bind を設定できます。
ソースコード
こちらをクリックしてください.
class ScrollableFrame(ttk.Frame):
def __init__(self, parent, size=(1, 1), fit_w=False, fit_h=False):
"""
parent: tk.Frame
親となるフレーム
minimal_size: tuple
最小キャンバスサイズ
fit_w: bool
キャンバス内のフレームの幅をキャンバスに合わせるか否か
fit_h: bool
キャンバス内のフレームの高さをキャンバスに合わせるか否か
"""
ttk.Frame.__init__(self, parent)
# 変数の初期化
self.minimal_canvas_size = size
self.fit_width = fit_w
self.fit_height = fit_h
# 縦スクロールバー
#vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
vscrollbar = AutoHideScrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
# 横スクロールバー
#hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
hscrollbar = AutoHideScrollbar(self, orient=tk.HORIZONTAL)
hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=False)
# Canvas
self.canvas = tk.Canvas(
self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set
)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# スクロールバーを Canvas に関連付け
vscrollbar.config(command=self.canvas.yview)
hscrollbar.config(command=self.canvas.xview)
# Canvas の位置の初期化
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# スクロール範囲の設定
self.canvas.config(
scrollregion=(0, 0, self.minimal_canvas_size[0], self.minimal_canvas_size[1])
)
# Canvas 内にフレーム作成
self.interior = ttk.Frame(self.canvas)
self.interior_id = self.canvas.create_window(
0, 0, window=self.interior, anchor=tk.NW,
)
# 内部フレームの大きさが変わったらCanvasの大きさを変える関数を呼び出す
self.interior.bind('<Configure>', self._configure_interior)
self.canvas.bind('<Configure>', self._configure_canvas)
#self.canvas.bind_all(sequence="<MouseWheel>", func=self._on_mousewheel, add="+")
def bind_child(self, frame=None, sequence=None, func=None, add=None):
"""
入れ子に bind を設定
Parameters
----------
frame: tk.Frame
bind を設定するフレーム
sequuence: str
イベント内容
func: function
イベント内容が実行された場合に呼ばれる関数
add: str
一つ前に宣言されるbind関数を実行するのか設定
"": default, "+"
"""
children = frame.winfo_children()
for child in children:
c_type = type(child)
if (c_type == tk.Canvas) or (c_type == tk.Frame) or (c_type == ttk.Frame):
self.bind_child(frame=child, sequence=sequence, func=func, add=add)
child.bind(sequence=sequence, func=func, add=add)
def update_minimal_canvas_size(self, size):
"""
Parameters
----------
size: tuple
最小キャンバスサイズ
"""
self.minimal_canvas_size = size
def destroy_child(self, frame=None):
"""
入れ子のウィジェットを削除
Parameters
----------
frame: tk.Frame
親となるフレーム
"""
for child in frame.winfo_children():
child.destroy()
def _configure_interior(self, event):
"""
Canvas の大きさを変える関数
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
size = (
max(self.interior.winfo_reqwidth(), self.minimal_canvas_size[0]),
max(self.interior.winfo_reqheight(), self.minimal_canvas_size[1])
)
self.canvas.config(scrollregion=(0, 0, size[0], size[1]))
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
self.canvas.config(width = self.interior.winfo_reqwidth())
if self.interior.winfo_reqheight() != self.canvas.winfo_height():
self.canvas.config(height = self.interior.winfo_reqheight())
def _configure_canvas(self, event):
"""
Canvas 内のアイテムの大きさを変える関数
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
if (self.interior.winfo_reqwidth() != self.canvas.winfo_width()) and self.fit_width:
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width())
if (self.interior.winfo_reqheight() != self.canvas.winfo_height()) and self.fit_height:
# update the inner frame's height to fill the canvas
self.canvas.itemconfigure(self.interior_id, height=self.canvas.winfo_height())
def _on_mousewheel(self, event=None):
"""
キャンバスの Y スクロールとマウスホイールスクロールを関連付け
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
#if event: self.canvas.yview_scroll(int(-1*(event.delta//120)), 'units')
if event: self.canvas.yview_scroll(int(-1 * (event.delta / abs(event.delta))), "units")
ソースコードの説明
ScrollableFrame.init()
-
縦スクロールバーをフレームの右側に、横スクロールバーをフレームの底に配置して、空いたスペースに Canvas を配置.
-
スクロールバーと Canvas の連携を設定して、Canvas のスクロール範囲を設定.
-
interior という名前のフレームを canvas の子に作って、create_window というメソッドでinterior フレームを canvas に配置.
-
_configure_interior という関数を interior に bind ._configure_interior メソッドについては以下を参照.
canvas の子の要素として Frame を配置することで、フレームウィジェットのサイズに canvas を合わせることができます.
bind_child
- 引数の frame の子の要素に bind を設定.
destroy_child
- 引数の frame の子の要素を削除.
_configure_interior
- interior のサイズを取得して、canvas のスクロール範囲を interior のサイズに合わせる.さらに canvas のサイズを interior のサイズに合わせる.
つまり、interior に _configure_interior をバインドして、interior のサイズが変更されると、このイベントが呼び出されます.イベントが発行されると、バインドされた関数が実行されます。
_configure_canvas
- canvas 内の子の要素のサイズを canvas のサイズに合わせる.
- ScrollableFrame をインスタンス化する時に fit_w, fit_h パラメータで幅、高さをサイズに合わせるかを決定できる.
_on_mousewheel
- キャンバスの Y スクロールとマウスホイールスクロールを関連付け.
テストコード
test_figure.png
こちらをクリックしてください.
# -*- coding: utf-8 -*-
# /* Import library */
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
try:
from ttkwidgets import AutoHideScrollbar
except ImportError:
from ttkwidgets import AutoHideScrollbar
class ScrollableFrame(ttk.Frame):
def __init__(self, parent, size=(1, 1), fit_w=False, fit_h=False):
"""
parent: tk.Frame
親となるフレーム
minimal_size: tuple
最小キャンバスサイズ
fit_w: bool
キャンバス内のフレームの幅をキャンバスに合わせるか否か
fit_h: bool
キャンバス内のフレームの高さをキャンバスに合わせるか否か
"""
ttk.Frame.__init__(self, parent)
# 変数の初期化
self.minimal_canvas_size = size
self.fit_width = fit_w
self.fit_height = fit_h
# 縦スクロールバー
#vscrollbar = AutoHideScrollbar(self, orient=tk.VERTICAL)
vscrollbar = ttk.Scrollbar(self, orient=tk.VERTICAL)
vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
# 横スクロールバー
#hscrollbar = AutoHideScrollbar(self, orient=tk.HORIZONTAL)
hscrollbar = ttk.Scrollbar(self, orient=tk.HORIZONTAL)
hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=False)
# Canvas
self.canvas = tk.Canvas(
self, bd=0, highlightthickness=0,
yscrollcommand=vscrollbar.set, xscrollcommand=hscrollbar.set
)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# スクロールバーを Canvas に関連付け
vscrollbar.config(command=self.canvas.yview)
hscrollbar.config(command=self.canvas.xview)
# Canvas の位置の初期化
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
# スクロール範囲の設定
self.canvas.config(
scrollregion=(0, 0, self.minimal_canvas_size[0], self.minimal_canvas_size[1])
)
# Canvas 内にフレーム作成
self.interior = ttk.Frame(self.canvas)
self.interior_id = self.canvas.create_window(
0, 0, window=self.interior, anchor=tk.NW,
)
# 内部フレームの大きさが変わったらCanvasの大きさを変える関数を呼び出す
self.canvas.bind('<Configure>', self._configure_canvas)
#self.canvas.bind_all(sequence="<MouseWheel>", func=self._on_mousewheel, add="+")
self.interior.bind('<Configure>', self._configure_interior)
def bind_child(self, frame=None, sequence=None, func=None, add=None):
"""
入れ子に bind を設定
Parameters
----------
frame: tk.Frame
bind を設定するフレーム
sequuence: str
イベント内容
func: function
イベント内容が実行された場合に呼ばれる関数
add: str
一つ前に宣言されるbind関数を実行するのか設定
"": default, "+"
"""
children = frame.winfo_children()
for child in children:
c_type = type(child)
if (c_type == tk.Canvas) or (c_type == tk.Frame) or (c_type == ttk.Frame):
self.bind_child(frame=child, sequence=sequence, func=func, add=add)
child.bind(sequence=sequence, func=func, add=add)
def update_minimal_canvas_size(self, size):
"""
Parameters
----------
size: tuple
最小キャンバスサイズ
"""
self.minimal_canvas_size = size
def destroy_child(self, frame=None):
"""
入れ子のウィジェットを削除
Parameters
----------
frame: tk.Frame
親となるフレーム
"""
for child in frame.winfo_children():
child.destroy()
def _configure_interior(self, event):
"""
Canvas の大きさを変える関数
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
size = (
max(self.interior.winfo_reqwidth(), self.minimal_canvas_size[0]),
max(self.interior.winfo_reqheight(), self.minimal_canvas_size[1])
)
self.canvas.config(scrollregion=(0, 0, size[0], size[1]))
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
self.canvas.config(width = self.interior.winfo_reqwidth())
if self.interior.winfo_reqheight() != self.canvas.winfo_height():
self.canvas.config(height = self.interior.winfo_reqheight())
def _configure_canvas(self, event):
"""
Canvas 内のアイテムの大きさを変える関数
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
if (self.interior.winfo_reqwidth() != self.canvas.winfo_width()) and self.fit_width:
# update the inner frame's width to fill the canvas
self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width())
if (self.interior.winfo_reqheight() != self.canvas.winfo_height()) and self.fit_height:
# update the inner frame's height to fill the canvas
self.canvas.itemconfigure(self.interior_id, height=self.canvas.winfo_height())
def _on_mousewheel(self, event=None):
"""
キャンバスの Y スクロールとマウスホイールスクロールを関連付け
Parameters
----------
event
実行される関数の引数へ付与されるイベント情報
"""
#if event: self.canvas.yview_scroll(int(-1*(event.delta//120)), 'units')
if event: self.canvas.yview_scroll(int(-1 * (event.delta / abs(event.delta))), "units")
class App(ttk.Frame):
def __init__(self, read_image, size, master=None):
super().__init__(master)
self.master = master
self.read_img = read_image
self.size = size
self.master.title('scrollbar trial')
self.pack(fill=tk.BOTH, expand=True)
self.create_widgets()
def create_widgets(self):
self.canvas_frame = ScrollableFrame(self, size=self.size)
self.canvas_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.control_frame = ttk.Frame(self)
self.control_frame.pack(side=tk.TOP, fill=tk.Y, expand=False)
self.label_title1 = ttk.Label(self.control_frame, text='Window coordinate')
self.label_title1.pack()
self.point_x = tk.StringVar()
self.point_y = tk.StringVar()
self.label_x = ttk.Label(self.control_frame, textvariable=self.point_x)
self.label_x.pack()
self.label_y = ttk.Label(self.control_frame, textvariable=self.point_y)
self.label_y.pack()
self.label_title2 = ttk.Label(self.control_frame, text='Canvas coordinate')
self.label_title2.pack()
self.point_xc = tk.StringVar()
self.point_yc = tk.StringVar()
self.label_xc = ttk.Label(self.control_frame, textvariable=self.point_xc)
self.label_xc.pack()
self.label_yc = ttk.Label(self.control_frame, textvariable=self.point_yc)
self.label_yc.pack()
# canvasに画像をセットする
self.img_canvas = tk.Canvas(self.canvas_frame.interior)
self.img_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.img_canvas.bind('<ButtonPress-1>', self.pickup_point)
self.im = ImageTk.PhotoImage(self.read_img, master=self.master)
self.img_canvas.config(width=self.read_img.width, height=self.read_img.height)
self.img_canvas.photo = self.im
self.img_canvas.create_image((0, 0), anchor=tk.NW, image=self.im)
# ポインタの座標を取得する
def pickup_point(self, event):
self.point_x.set('x : ' + str(event.x))
self.point_y.set('y : ' + str(event.y))
self.point_xc.set('x : ' + str(self.img_canvas.canvasx(event.x)))
self.point_yc.set('y : ' + str(self.img_canvas.canvasy(event.y)))
print(event.x, event.y, self.img_canvas.canvasx(event.x), self.img_canvas.canvasy(event.y))
if __name__ == "__main__":
read_image = Image.open("test_figure.png")
canvas_width, canvas_height = read_image.size
minimal_canvas_size = read_image.size
# アプリケーション起動
root = tk.Tk()
app = App(master=root, read_image=read_image, size=minimal_canvas_size)
app.mainloop()
起動すると、このように表示されます.
ウィンドウを大きくすると、このようになります.スクロールバーがウィンドウの大きさに追従することが確認できます.
ウィンドウを小さくすると、このようになります.スクロールバーがアクティブになります.
参考
Discussion