今日も見に来てくださって、ありがとうございます。石川さんです。
少しあいて、一週間ぶりの更新ですね。お仕事で、ちょっと忙しくしていたのもあるのですけど、採用活動のページも作ったりしていたので、まあ、仕方ありませんね。ということで今日は、またしてもtkinterのCanvasについて書いていきたいと思います。Canvas上に描かれた要素の移動については、以前にも書いているのですが、今回は、移動するものをクラスとして扱ったらどうなるのか、ということでまとめてみました。
できあがりイメージ
このプログラムは実行すると、最初にふたつ箱が表示されます。何もないところをクリックすると赤い箱が追加されます。追加された箱はマウスでドラッグすることができます。これまでは、箱の移動についてはメインのクラスで実行していたのですが、今回別のクラスに動作を委譲している、というところが大きく異なる点です。
ソースコード
import tkinter as tk class App(tk.Tk): def __init__(self): super().__init__() self.title("Canvas move with class") self.geometry("1200x800") self.canvas = tk.Canvas(self, background="white") self.canvas.grid(row=0, column=0, sticky=tk.NSEW) self.rowconfigure(0, weight=1) self.columnconfigure(0, weight=1) self.canvas.bind("<Motion>", self.move) self.canvas.bind("<ButtonPress>", self.button_press) self.canvas.bind("<ButtonRelease>", self.button_release) self.movers = [Movable(self.canvas, 160, 160, "AliceBlue"), Movable(self.canvas, 200, 200, "LightYellow")] def move(self, event): for m in self.movers: m.move(event) def button_press(self, event): event.consume = False for m in self.movers: m.button_press(event) if event.consume: return m = Movable(self.canvas, event.x, event.y, "Red") self.movers.append(m) m.button_press(event) def button_release(self, event): for m in self.movers: m.button_release(event) class Movable(): def __init__(self, canvas, x, y, color): self.canvas = c = canvas self.textid = c.create_text(x, y, text=color) self.rectid = c.create_rectangle(c.bbox(self.textid), fill=color) c.tag_lower(self.rectid, self.textid) self.start = None def move(self, event): if self.start is None: return x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) dx, dy = x - self.start[0], y - self.start[1] self.canvas.move(self.textid, dx, dy) self.canvas.move(self.rectid, dx, dy) self.start = x, y def button_press(self, event): x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y) b = self.canvas.bbox(self.rectid) if b[0] <= x <= b[2] and b[1] <= y <= b[3]: self.start = x, y event.consume = True def button_release(self, event): self.start = None if __name__ == "__main__": app = App() app.mainloop()
説明
初期化処理(__init__)の、6~8行目は、Tkの初期化処理、タイトルとサイズの設定をしています。10行目で背景が白色のCanvasを作成して、12~14行目でgridを使用して配置しています。そして、16~18行目でCanvasに対するイベントをメソッドに割り当てています。20行目は、初期値として、Movableクラスを二つ追加しています。
イベントに割り当てたメソッドはそれぞれ追加したMovableクラスの同名のメソッドを実行するように記述してあります。button_pressだけちょっと変更していて、どのMovableクラスのインスタンスも対応する処理を実行しなかったとき、要するに、クリックしたのが箱じゃなかったとき、赤い箱を追加するようにしました。
Movableクラスの初期化処理では、渡されたキャンバスのx,yに色の名前のテキストをcreate_text()メソッドを使って描画します。その後、そのテキストを囲む箱をcreate_rectangle()メソッドで描画します。このままだと、テキストの上に箱が描画されて字が見えないので、tag_lower()メソッドを呼び出すことで上下を入れ替えてテキストが表示されるようにしています。あと、移動開始用のポジションを保持するための変数を初期化しています。
50~57行目もMovableクラスのmove()メソッドは、self.startの位置からeventが発生させた位置へ移動させます。self.startがセットされていない場合は特に動作しません。このmove()メソッドが動作するために、59~64行目のbutton_press()メソッドでは、自分自身が選択された場合に限り、self.startで開始位置をセットします。箱がクリックされたかどうかを判定するために、event.consumeを利用するようにしています。
66~67行目はbutton_release()では、マウスボタンが離されたときは箱が動かないように、self.startにNoneをセットしています。
もっといいやり方があるかも知れませんが、これで責任範囲を何とか分離することができました。他に、クリックされたら選択されたような枠の印をつけるとか、その枠を使って大きさを拡大縮小するとか、ダブルクリックしたときにメニューを出すとか、必要に応じてMovableクラスへ実装することができるようになりました。
まとめ
キャンバス上の項目をひとつのクラスの中でまとめて一度に扱うことができるようになりました。いずれは、MVCパターンを検討してみたいと思います。