今日も見に来てくださって、ありがとうございます。石川さんです。はい、しつこくCanvasについて調べております。
前回、Canvasにスクロールバーを付けて、描画したものを移動する、というのをやりました。今回は、その続きです。ちょっと改善して、機能追加しました。見た目は変わらなかったので、ソースコードだけ付けておきます。
ソースコード
import tkinter as tk
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Canvas move test")
self.geometry("400x200")
sx = tk.Scrollbar(self, orient=tk.HORIZONTAL)
sy = tk.Scrollbar(self, orient=tk.VERTICAL)
c = tk.Canvas(self, background="white",xscrollcommand=sx.set,yscrollcommand=sy.set)
sx.config(command=c.xview)
sy.config(command=c.yview)
self.info = tk.Label(self)
self.info.grid(row=2,columnspan=2)
c.grid(row=0, column=0, sticky=tk.NSEW)
sx.grid(row=1, column=0, sticky=tk.EW)
sy.grid(row=0, column=1, sticky=tk.NS)
self.canvas = c
self.start = None
c.bind("<Motion>",self.move)
c.bind("<ButtonPress>",self.button_press)
c.bind("<ButtonRelease>",self.button_release)
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
self.bind("<Configure>", self.resize)
c.create_rectangle(40,40,60,60)
def move(self, event):
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
self.info.configure(text=f"mouse position = ({event.x}, {event.y}) ({x},{y})")
if self.start and self.item:
original_x, original_y = self.start
self.canvas.move(self.item,x - original_x,y - original_y)
self.start = x, y
self.resize(event)
elif event.state == 256: # Button1
self.canvas.scan_dragto(event.x, event.y, gain=1)
def button_press(self, event):
x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
self.start = x, y
self.item = self.canvas.find_overlapping(x-10,y-10,x+10,y+10)
if self.item:
self.item = self.item[0]
else:
self.canvas.scan_mark(event.x, event.y)
if event.num == 3:
self.item = self.canvas.create_rectangle(x-10,y-10,x+10,y+10)
def button_release(self, event):
self.start = None
def resize(self, event):
region = self.canvas.bbox(tk.ALL)
self.canvas.configure(scrollregion=region)
if __name__ == "__main__":
app = App()
app.mainloop()
変更点の説明
前回のバージョンだと、作られた四角形をドラッグして表示外へ移動したときに、スクロールバーがうまく更新されていませんでした。しばらくして、何かのタイミングでちゃんと更新されるのですが、うれしくないなぁ、ということでここを修正することにしました。これは、今回のソースコードで言うと、39行目のself.resize(event)の呼び出しです。このメソッドはキャンバス上のすべての項目が入る領域を取得して、その領域をscrollregionへセットする、ということをしています。これで、スクロールバーがうまく更新されるようになりました。
次に、キャンバス上の矩形を移動させるのではなくて、キャンバスを移動させたい、と思いましたので、それを調べてみました。いろいろと探しまわって、ここにやっとだとりつきました。50行目のself.canvas.scan_mark(event.x, event.y)で、開始をセットして、41行目のself.canvas.scan_dragto(event.x, event.y, gain=1)を使ってキャンバスを移動します。もともとは、クリックしたときに四角形を書いていたので、51行目のように、event.numをチェックすることで右クリックのときに四角形を書くように変更しました。event.numは、左クリックが1、右クリックが3、左右の両方を同時クリックすると2が割り当てられていました。
あと、event.state == 256のときにscan_dragtoを実行するようにしました。このstateは、'Shift', 'Lock', 'Control','Mod1', 'Mod2', 'Mod3', 'Mod4', 'Mod5','Button1', 'Button2', 'Button3', 'Button4', 'Button5'の順で、2**0、2**1、2**2、、、となっていて、Button1は2**8=256でした、というのはtkinterモジュールの__init__.pyのソースコードを読んだのでわかりましたが、ちょっと追加されたときに対応できていないので、なんとかしたいところです。どなたか、もっとよいやり方を知っている方、教えてくださ~い♪
ちなみに、scan_dragtoに指定するgainは実際の移動に対して何倍移動するか、というのが指定可能です。シフトを押しながらマウスを動かしたときはたくさん動かす、といったときに使えるようですね。
まとめ
スクロールバーに移動したものを正しく反映させて、キャンバスを移動するだけで、ずいぶんと時間がかかってしまいました。もっと簡単にできるかな、と、思っていたのですけど意外と難しかったです。次回は、拡大、縮小かなぁ。

