事の発端
私はtkinterでcanvasにスクロールバーを追加して動作させることに成功しました。しかし、新たにマウスで右ボタンを押しながら画像を上下左右に動かしたいという要望が発生しました。また、マウスのスクロールによってズームインとズームアウトをしたいという要望も発生しました。
動作環境
windows 10
PyCharm Community 2019.2
python3.7.5
参考元サイト
https://codeday.me/jp/qa/20190625/1100104.html
このサイトのコードを自分にカスタマイズしました。
関連ページ
tkinterでスクロールを表示するサンプル
↑サンプルコード置いています。
コード
import tkinter as tk
import random
class Example(tk.Frame):
def __init__(self, root):
tk.Frame.__init__(self, root)
self.canvas = tk.Canvas(self, width=640, height=480, bg="white")
self.bar_x = tk.Scrollbar(self, orient="horizontal", command=self.canvas.xview)
self.bar_y = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.bar_y.set, xscrollcommand=self.bar_x.set)
self.canvas.configure(scrollregion=(0, 0, 1000, 1000))
self.bar_x.grid(row=1, column=0, sticky="ew")
self.bar_y.grid(row=0, column=1, sticky="ns")
self.canvas.grid(row=0, column=0, sticky="nsew")
# ウィンドウの枠を広げたときに、Frameが拡張するようにする
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
for n in range(50):
x0 = random.randint(0, 900)
y0 = random.randint(50, 900)
x1 = x0 + random.randint(50, 100)
y1 = y0 + random.randint(50, 100)
color = ("red", "orange", "yellow", "green", "blue")[random.randint(0, 4)]
self.canvas.create_rectangle(x0, y0, x1, y1, outline="black", fill=color, activefill="black", tags=n)
self.canvas.bind("<ButtonPress-1>", self.move_start)
self.canvas.bind("<B1-Motion>", self.move_move)
self.canvas.bind("<MouseWheel>", self.zoom_in_out)
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
def zoom_in_out(self, event):
if event.delta > 0:
self.canvas.scale("all", event.x, event.y, 1.05, 1.05)
elif event.delta < 0:
self.canvas.scale("all", event.x, event.y, 0.95, 0.95)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
実行結果
・マウスをクリックしたまま上下左右に動かすと、画像も移動します。 ・ウィンドウの枠を広げると画像のフレームも一緒に拡大します。 ・不満な点として、Exampleクラスは、canvasの機能をもっているので、フォーム全体の機能と分離したいと思いました。その方が、プログラムが大きくなったときに見やすくなると思うためです。さらに改修したコードの例
import tkinter as tk
import random
class ClassCanvas(tk.Canvas):
def __init__(self, master, img_width, img_height, bg):
super().__init__(master, bg=bg, height=img_height, width=img_width)
self.bar_x = tk.Scrollbar(self, orient="horizontal", command=self.xview)
self.bar_y = tk.Scrollbar(self, orient="vertical", command=self.yview)
self.configure(yscrollcommand=self.bar_y.set, xscrollcommand=self.bar_x.set)
self.configure(scrollregion=(0, 0, 1000, 1000))
self.bar_y.pack(side=tk.RIGHT, fill=tk.Y)
self.bar_x.pack(side=tk.BOTTOM, fill=tk.X)
self.place(x=0, y=0, relheight=1, relwidth=1)
for n in range(50):
x0 = random.randint(0, 900)
y0 = random.randint(50, 900)
x1 = x0 + random.randint(50, 100)
y1 = y0 + random.randint(50, 100)
color = ("red", "orange", "yellow", "green", "blue")[random.randint(0, 4)]
self.create_rectangle(x0, y0, x1, y1, outline="black", fill=color, activefill="black", tags=n)
class ClassForm:
def __init__(self, width, height):
self.root = tk.Tk()
# windowサイズの初期化
self.window_width = width
self.window_height = height
self.root.geometry(str(self.window_width) + "x" + str(self.window_height) + "+100+100")
self.bg_color = "snow"
self.canvas = ClassCanvas(self.root, 1000, 1000, bg="white")
self.canvas.bind("<ButtonPress-1>", self.move_start)
self.canvas.bind("<B1-Motion>", self.move_move)
self.canvas.bind("<MouseWheel>", self.zoom_in_out)
self.root.mainloop()
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
def zoom_in_out(self, event):
if event.delta > 0:
self.canvas.scale("all", event.x, event.y, 1.1, 1.1)
elif event.delta < 0:
self.canvas.scale("all", event.x, event.y, 0.9, 0.9)
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
def main():
ClassForm(640, 480)
if __name__ == "__main__":
main()
実行結果
・私はフォームのクラスとCanvasのクラスに分けました。 ・canvasの配置の方法をgridではなくて、packに変更しました。 ・マウス動作処理の関数はFormクラスに所属させました。 ・これはとても素晴らしいサンプルだと思います。みなさんの参考になりますか?以下、2020年2月追記
マウスホイールでcanvasがy方向にスクロールさせる
ズームの代わりに、マウスホイールで縦スクロールするように修正しました。
import tkinter as tk
import random
class ExampleApp:
def __init__(self):
self.root = tk.Tk()
self.canvas = tk.Canvas(self.root, width=640, height=480, bg="white")
self.bar_x = tk.Scrollbar(self.root, orient="horizontal", command=self.canvas.xview)
self.bar_y = tk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.bar_y.set, xscrollcommand=self.bar_x.set)
self.canvas.configure(scrollregion=(0, 0, 1000, 1000))
self.bar_y.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
self.bar_x.pack(fill=tk.X, side=tk.BOTTOM, expand=False)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# ウィンドウの枠を広げたときに、Frameが拡張するようにする
self.root.grid_rowconfigure(0, weight=1)
self.root.grid_columnconfigure(0, weight=1)
for n in range(50):
x0 = random.randint(0, 900)
y0 = random.randint(50, 900)
x1 = x0 + random.randint(50, 100)
y1 = y0 + random.randint(50, 100)
color = ("red", "orange", "yellow", "green", "blue")[random.randint(0, 4)]
self.canvas.create_rectangle(x0, y0, x1, y1, outline="black", fill=color, activefill="black", tags=n)
self.canvas.bind("<ButtonPress-1>", self.move_start)
self.canvas.bind("<B1-Motion>", self.move_move)
self.canvas.bind("<MouseWheel>", self.mouse_y_scroll)
self.root.mainloop()
def move_start(self, event):
self.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.canvas.scan_dragto(event.x, event.y, gain=1)
def mouse_y_scroll(self, event):
if event.delta > 0:
self.canvas.yview_scroll(-1, 'units')
elif event.delta < 0:
self.canvas.yview_scroll(1, 'units')
def main():
ExampleApp()
if __name__ == "__main__":
main()
実行結果
コピペで動作しますので、確認して下さい。
canvasの中にボタンを置いてマウスホイールでスクロールさせるサンプル
上のサンプルでは、canvasの上にボタンを置いても、canvasしかスライドしてくれません。
canvasと一緒にボタンもスクロールさせるのは、簡単に見えてなかなか思うようにいきませんでした。
マウスホイールのスクロールでこんなに時間がかかるとは思いませんでした。自分が悩んだので、コードを公開します。
import tkinter as tk
class ClassFrame(tk.Frame):
def __init__(self, master, bg=None, width=None, height=None):
super().__init__(master, bg=bg, width=width, height=height)
class ScrollFrame(ClassFrame):
def __init__(self, master, rect_num, bg=None, width=None, height=None):
super(ScrollFrame, self).__init__(master, bg=bg, width=width, height=height)
# スクロールバーの作成
self.scroll_bar = tk.Scrollbar(self, orient=tk.VERTICAL)
self.scroll_bar.pack(fill=tk.Y, side=tk.RIGHT, expand=False)
self.canvas = tk.Canvas(self, bg="blue", yscrollcommand=self.scroll_bar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.scroll_bar.config(command=self.canvas.yview)
# ビューをリセット
self.canvas.xview_moveto(0)
self.canvas.yview_moveto(0)
self.interior = tk.Frame(self.canvas, bg="gray90", borderwidth=10)
self.interior_id = self.canvas.create_window(0, 0, window=self.interior, anchor=tk.NW)
self.interior.bind('<Configure>', self.configure_interior)
self.canvas.bind('<Configure>', self.configure_canvas)
self.buttons = []
for i in range(rect_num):
self.buttons.append(tk.Button(self.interior, text="選択肢 " + str(i + 1)))
self.buttons[i].pack(anchor=tk.NW, fill=tk.X, padx=(30, 30), pady=(0, 10))
def configure_interior(self, event=None):
size = (self.interior.winfo_reqwidth(), self.interior.winfo_reqheight())
self.canvas.config(scrollregion="0 0 %s %s" % size)
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
self.canvas.config(width=self.interior.winfo_reqwidth())
def configure_canvas(self, event=None):
if self.interior.winfo_reqwidth() != self.canvas.winfo_width():
self.canvas.itemconfigure(self.interior_id, width=self.canvas.winfo_width())
class ScrollForm:
def __init__(self, width=300, height=400):
self.root = tk.Tk()
self.root.title("選択フォーム")
self.window_width = width
self.window_height = height
self.window_pos_x = (self.root.winfo_screenwidth() - self.window_width) // 2
self.window_pos_y = (self.root.winfo_screenheight() - self.window_height) // 2
self.root.geometry(str(self.window_width) + "x" + str(self.window_height)
+ "+" + str(self.window_pos_x) + "+" + str(self.window_pos_y))
pos_y = 0
height = 20
self.label_top = tk.Label(text="マウスホイールでスクロールして下さい。")
self.label_top.place(x=0, y=pos_y, relwidth=1.0, height=height)
pos_y += height
height = 20
self.label_top_2 = tk.Label(text="スクロールバーをドラッグして下さい。")
self.label_top_2.place(x=0, y=pos_y, relwidth=1.0, height=height)
pos_y += height
height = 300
self.frame = ScrollFrame(master=self.root, rect_num=50, width=self.window_width, height=self.window_height)
self.frame.place(x=0, y=pos_y, relwidth=1.0, height=height)
pos_y += height
height = 20
self.label = tk.Label(text="確認が終わっから、決定ボタンを押してください。")
self.label.place(x=0, y=pos_y, relwidth=1.0, height=height)
pos_y += height
height = 25
width = 70
self.decision_button = tk.Button(text="決定")
self.decision_button.place(x=(self.window_width - width) // 2, y=pos_y, width=width, height=height)
# self.frame.interior.bind("<ButtonPress-1>", self.move_start)
# self.frame.interior.bind("<B1-Motion>", self.move_move)
self.frame.interior.bind("<MouseWheel>", self.mouse_y_scroll)
for i in range(50):
self.frame.buttons[i].bind("<MouseWheel>", self.mouse_y_scroll)
if __name__ == "__main__":
self.root.mainloop()
def move_start(self, event):
self.frame.canvas.scan_mark(event.x, event.y)
def move_move(self, event):
self.frame.canvas.scan_dragto(event.x, event.y, gain=1)
def mouse_y_scroll(self, event):
if event.delta > 0:
self.frame.canvas.yview_scroll(-1, 'units')
elif event.delta < 0:
self.frame.canvas.yview_scroll(1, 'units')
def main():
ScrollForm()
if __name__ == "__main__":
main()