Python tkinter GUIプログラミング Canvas の項目リサイズ

 こんにちは。すっかり秋になりましたね。石川さんです。久しぶりの投稿になりました。

 今回は、tkinterのCanvasに四角形を描いて、それの大きさをマウスで変更する、というのにチャレンジしました。

出来上がりイメージ

出来上がりイメージ

 今回使用するtkinterのオブジェクトはCanvasだけです。四角形はcreate_rectangleで描いて、マウスが上に来たら、それっぽいポインターに変わります。その上でドラッグすると、サイズが変更される、というシンプルなプログラムです。

ソースコード

import tkinter as tk


class App(tk.Tk):
    MARGIN = 5

    def __init__(self):
        super().__init__()
        self.geometry("800x700")
        self.title("Canvas item resize example")

        self.canvas = c = tk.Canvas(self, background="white")
        c.pack(fill=tk.BOTH, expand=True)

        x1, y1, x2, y2 = 100, 100, 400, 500
        self.rect = c.create_rectangle(x1, y1, x2, y2, width=App.MARGIN)
        c.bind("<ButtonPress>", self.press)
        c.bind("<ButtonRelease>", self.release)
        c.bind("<Motion>", self.move)
        self.coords = None
        self.position = None

    def get_cursor_position(self, x, y):
        left, right = x - App.MARGIN * 2, x + App.MARGIN * 2
        top, bottom = y - App.MARGIN * 2, y + App.MARGIN * 2
        x1, y1, x2, y2 = self.canvas.coords(self.rect)
        cursors = [
            ["ul_angle",          "sb_v_double_arrow", "ur_angle"],
            ["sb_h_double_arrow", "",                  "sb_h_double_arrow"],
            ["ll_angle",          "sb_v_double_arrow", "lr_angle"]
        ]
        if bottom < y1 or y2 < top:
            return "", None
        elif top <= y1 <= bottom:
            row = 0
        elif top <= y2 <= bottom:
            row = 2
        else:
            row = 1
        if right < x1 or x2 < left:
            return "", None
        elif left <= x1 <= right:
            column = 0
        elif left <= x2 <= right:
            column = 2
        else:
            column = 1
        return cursors[row][column], (row, column)

    def press(self, event):
        self.coords = self.canvas.coords(self.rect)

    def move(self, event):
        x, y = self.canvas.canvasx(event.x), self.canvas.canvasy(event.y)
        if self.coords and self.position:
            if self.position[1] == 0:  # Left
                self.coords[0] = x
            if self.position[0] == 0:  # Top
                self.coords[1] = y
            if self.position[1] == 2:  # Right
                self.coords[2] = x
            if self.position[0] == 2:  # Bottom
                self.coords[3] = y
            self.canvas.coords(self.rect, *self.coords)

        cursor, self.position = self.get_cursor_position(x, y)
        self.configure(cursor=cursor)

    def release(self, event):
        if self.coords:
            self.coords = None


if __name__ == "__main__":
    app = App()
    app.mainloop()

解説

 9行目でウィンドウのサイズ、10行目でタイトルをセットしています。12、13行目、Canvasをつくって配置しています。15、16行目のcreate_rectangle()で四角形を描いています。17~19行目ではbindを使って、<ButtonPress>マウスボタンが押されたとき、<ButtonRelease>マウスボタンが離されたとき、<Motion>マウスが動いたとき、に、それぞれ処理をあてがっています。21、22行は後の処理で利用するときにエラーとならないよう変数を初期化しています。

 23~48行目のメソッド、get_cursor_positionでは、マウスの現在位置からカーソルの種類と位置を戻しています。ポイントは27行目のcursorsリストを配列のように使っているところです。3×3の行列をイメージして、6種類のカーソルを場所によって特定するようにしています。ちなみに26行目のcoordsメソッドは、self.rectの左上の座標と右上の座標を順番に返します。

 50、51行目のメソッド、pressはマウスボタンが押されたときに呼び出される処理です。ここでインスタンス変数のcoordsへ値をセットしています。この値がセットされていることで、ボタンが押されている、ということが他の処理からでもわかるようになっています。ちなみに69~71行目のreleaseはマウスボタンが離されたときに呼び出される処理で、この値(coords)をNoneにセットすることでマウスボタンは押されていない、ということがわかります。

 53~67行目のmoveメソッドが主な処理部分ですね。前半部分はサイズを変更する処理で、後半部分はカーソルの形状を変更する処理です。54行目のcanvasxとcanvasyは、座標系の変更をしています。eventから取得したx、yは、Canvas上のx、yと異なり、画面系の座標なのでcanvasx、canvasyで変換して使う必要があります。55行目の判定は、self.coordsはマウスボタンが押されているときに限るための条件、self.positionはカーソルが近くにある時に限るための条件になっています。<Motion>イベントに対する処理なので、実行される条件をきちんと絞り込む必要がありますね。
 64行目、再びcoordsが登場するのですが、26行目とは違い、第二引数があります。前回は座標を取得するだけでしたが、この第二引数が指定された場合には、項目の座標を第二引数のとおりに変更する、という点が異なります。この一文を指定することで、マウスポインタの移動にあわせて四角形を変形させています。
 66、67行目はカーソルが角っこに移動したときにカーソルの形状を変更するためにここへ入れました。また、ここでself.positionをセットすることで、四角形から離れた場所からのドラッグでリサイズできるようになっています。

まとめ

 いろいろと試行錯誤していましたので条件判定が割合スッキリとできたのではないでしょうか。今回は簡単にするために、四角形はクラス化せずにひとつだけ用意しておくようにしました。実際の現場だったりするとひとつだけ用意すればよい、ということはほとんどないと思いますので、クラス化もやってみたいですね。