Python tkinter GUIプログラミング Canvasで箱をつなぐ線を描くその2

 今日も見に来てくださって、ありがとうございます。石川さんです。

 前回の宣言通り、今回は陰線処理です。箱の中心から中心へ線を引くところまでは同じなのですが、箱にかぶる部分は線を引かないようにしましょう。このページを参考にさせていただきました。

できあがりイメージ

 できあがりイメージは前回とほぼ変化なしです。中心から箱までの線が描かれなくなっただけです。

箱と箱を線で結ぶ。ただし、箱の中の線を取り除く。

ソースコード

 ソースコードは以下の通りです。変更点はConnectionクラスだけです。

import tkinter as tk


class Entity():
    ''' Entity class '''
    def __init__(self, canvas, x, y, width=60, height=40):
        self.canvas = canvas
        self.x, self.y, self.width, self.height = x, y, width, height
        self.start_x = self.start_y = None
        self.connections = []

        self.id = self.canvas.create_rectangle(x, y, x + width, y + height,
                                               fill="lightblue", width=3)
        self.canvas.tag_bind(self.id, "<ButtonPress>", self.button_press)
        self.canvas.tag_bind(self.id, "<Motion>", self.move)
        self.canvas.tag_bind(self.id, "<ButtonRelease>", self.button_release)

    def button_press(self, event):
        ''' マウスのボタンが押されたときの処理 '''
        self.start_x = self.canvas.canvasx(event.x)
        self.start_y = self.canvas.canvasy(event.y)

    def move(self, event):
        ''' マウスが移動したときの処理 '''
        if event.state & 256:  # マウスボタン1が押されているときだけ(ドラッグ中のみ)
            can_x = self.canvas.canvasx(event.x)
            can_y = self.canvas.canvasy(event.y)
            coords = self.canvas.coords(self.id)
            coords[0] -= self.start_x - can_x
            coords[1] -= self.start_y - can_y
            coords[2] -= self.start_x - can_x
            coords[3] -= self.start_y - can_y
            self.canvas.coords(self.id, coords)
            self.start_x = can_x
            self.start_y = can_y
            self.x, self.y = coords[0:2]
            for connection in self.connections:
                connection.move(self)

    def button_release(self, event):  # pylint: disable=unused-argument
        ''' マウスのボタンが離されたとき '''
        self.start_x = self.start_y = None

    def get_center(self):
        ''' 中心座標を戻します '''
        return self.x + self.width//2, self.y + self.height//2

    def add_listener(self, connection):
        ''' コネクションのリスナーを登録します '''
        self.connections.append(connection)


class Connection():
    ''' Connection class '''
    def __init__(self, canvas, start_entity, end_entity):
        self.canvas = canvas
        self.start_e = start_entity
        self.end_e = end_entity
        start_entity.add_listener(self)
        end_entity.add_listener(self)

        self.id = self.canvas.create_line(self.get_intersection(start_entity),
                                          self.get_intersection(end_entity))

    def move(self, entity):
        ''' エンティティが移動したときの処理(エンティティから呼び出される)'''
        coords = self.canvas.coords(self.id)
        if entity == self.start_e:
            coords[0:2] = self.get_intersection(entity)
            coords[2:4] = self.get_intersection(self.end_e)
        elif entity == self.end_e:
            coords[0:2] = self.get_intersection(self.start_e)
            coords[2:4] = self.get_intersection(entity)
        self.canvas.coords(self.id, coords)

    def get_intersection(self, entity):
        ''' 矩形との接点を求める '''
        x, y = entity.get_center()
        height, width = entity.height // 2, entity.width // 2
        dx, dy = self.end_e.x - self.start_e.x, self.end_e.y - self.start_e.y
        if entity == self.end_e:
            dx, dy = -dx, -dy
        if abs(dy / dx) < (height / width):  # 垂直側
            x_pos = x + width if dx > 0 else x - width
            y_pos = y + dy * width / abs(dx)
        else:  # 水平側
            x_pos = x + dx * height / abs(dy)
            y_pos = y + height if dy > 0 else y - height
        return x_pos, y_pos


class Application(tk.Tk):
    ''' Application class '''
    def __init__(self):
        super().__init__()
        self.title("Connecter test 2")
        self.geometry("640x320")

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

        entity1 = Entity(self.canvas, 40, 80)
        entity2 = Entity(self.canvas, 240, 160)
        Connection(self.canvas, entity1, entity2)
        entity3 = Entity(self.canvas, 420, 60)
        Connection(self.canvas, entity2, entity3)


def main():
    ''' main function '''
    application = Application()
    application.mainloop()


if __name__ == "__main__":
    main()

解説

 今回は、上述したように、Connectionクラスを変更しただけです。主な変更点は、get_intersection()メソッドの追加と、これまで箱側のget_center()メソッドを使って開始点、終点を算出していたところ、この追加したget_intersection()メソッドに変更したところです。このメソッドをどこに持つのがいいのかなぁ、というのはちょっと悩みましたが、箱と箱の関係によって線との交点が変わってくるので、Connectionクラスの方へ実装することにしました。

 get_intersection()メソッドでの計算のポイントは、箱と箱の中心間を結ぶ直線の傾き(dx / dy)と、箱そのものの縦横比(height / width)を比較することで、縦の線と交わるのか、横の線と交わるのか、というのを決めているところですね。縦の線と交わるときはx座標はそのまま戻す、横の線の場合はy座標をそのまま戻す感じになります。実際には上下、左右と二種類あるので、dx、dyの正負を条件にして切り替えるようにしています。座標の位置がそのままじゃない方については、縦線または横線の長さに直線の傾きの比率を掛けることで、求めるようにしています。

 Connectionクラスのmove()メソッドの変更点としては、これまで移動した方の箱の中心点だけ変更していればよかったところを、開始点と終了点の両方を計算する必要がでてきたので、68~73行目で対応しました。

 あと、実は箱が重なった時に変なところに線が引かれてしまうのと、dxが0になったときに「ZeroDivisionError: float division by zero」のエラーが発生しますので、余裕のある方は修正してみてください。

まとめ

 出来上がってみるとなんと言うことはないのですけど、作るのは結構難しいですね。参考になればうれしいです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です


reCaptcha の認証期間が終了しました。ページを再読み込みしてください。