Tkinter マウス操作でスクロール移動をする

## 事の発端
私はtkinterでcanvasにスクロールバーを追加して動作させることに成功しました。しかし、新たにマウスで右ボタンを押しながら画像を上下左右に動かしたいという要望が発生しました。また、マウスのスクロールによってズームインとズームアウトをしたいという要望も発生しました。

### 動作環境
windows 10
PyCharm Community 2019.2
python3.7.5

### 参考元サイト
https://codeday.me/jp/qa/20190625/1100104.html
このサイトのコードを自分にカスタマイズしました。

### 関連ページ
[tkinterでスクロールを表示するサンプル](https://qiita.com/hatorijobs/items/afa037278ef442cddbc5)
↑サンプルコード置いています。

## コード
```python3
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()
```
### 実行結果
<img width="650" alt="canvas_scroll_move_1.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262477/a3550313-9047-0ab5-d8eb-6cae2761d313.png">
・マウスをクリックしたまま上下左右に動かすと、画像も移動します。
・ウィンドウの枠を広げると画像のフレームも一緒に拡大します。
・不満な点として、Exampleクラスは、canvasの機能をもっているので、フォーム全体の機能と分離したいと思いました。その方が、プログラムが大きくなったときに見やすくなると思うためです。

## さらに改修したコードの例
```python3
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()
```
### 実行結果
<img width="617" alt="canvas_scroll_move_2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262477/6e1d9c2e-a5a0-de52-9110-2c29a426bccd.png">
・私はフォームのクラスとCanvasのクラスに分けました。
・canvasの配置の方法をgridではなくて、packに変更しました。
・マウス動作処理の関数はFormクラスに所属させました。
・これはとても素晴らしいサンプルだと思います。みなさんの参考になりますか？


以下、2020年2月追記

## マウスホイールでcanvasがｙ方向にスクロールさせる
ズームの代わりに、マウスホイールで縦スクロールするように修正しました。

``` python
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と一緒にボタンもスクロールさせるのは、簡単に見えてなかなか思うようにいきませんでした。
マウスホイールのスクロールでこんなに時間がかかるとは思いませんでした。自分が悩んだので、コードを公開します。

```python 
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()

```

### 実行結果
以下のような画面が現れて、スクロールでy軸にスライドさせることが出来ます。
![scroll_2020_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/262477/d2f76eba-67ce-303e-1504-74db18cc93eb.png)