matplotlib pyqt5画布上两个可拖动点之间的线

Posted

技术标签:

【中文标题】matplotlib pyqt5画布上两个可拖动点之间的线【英文标题】:Line between two draggable points on a matplotlib pyqt5 canvas 【发布时间】:2018-06-23 20:45:09 【问题描述】:

我正在尝试在 matplotlib 画布上嵌入 pyqt5 应用程序的两个可拖动点之间画一条线。我使用Draggable line with draggable points 作为参考。但是,我正在尝试创建多条线,并在单击按钮时创建一对 DraggablePoint 对象。我遇到了几个问题:

    只有在拖动点时才会出现线条(已解决,请参阅下面的更新) 这条线偏移了一个边距,我认为这是画布和实际 matplotlib 图之间的差异。但是缩放比例也不对。 第二个点对的线根本没有显示

以下是我改编的代码:

可拖动点

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
import matplotlib


class DraggablePoint:



    lock = None 

    def __init__(self, parent, dominant, x=10, y=10, size=1):
        """Creates a draggable Point on a matplotlib canvas"""
        matplotlib.matplotlib_fname()
        # The FigureCanvas
        self.parent = parent
        # The Point
        self.point = patches.Ellipse((x, y), size, size, fc='r', alpha=0.5, edgecolor='r')
        #Coordinates of the point
        self.x = x
        self.y = y
        self.dy = 645
        self.dx = 263
        # Adds the point to the Plot
        parent.fig.axes[0].add_patch(self.point)
        # Used in the on_press() function
        self.press = None
        self.background = None
        # initiate the mpl_connects
        self.connect()
        # The Other DraggablePoint, with whom the line shall connect with.
        self.partner = None
        # The Line2D
        self.line = None
        # TODO
        self.dominant = dominant

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1]:
                    line_x = [pair[0].x+self.dx, pair[1].x+self.dx]
                    line_y = [pair[0].y+self.dy, pair[1].y+self.dy]
                    self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
                    parent.fig.axes[0].add_line(self.line)


    def connect(self):

        'connect to all the events we need'
        # print("LOG.INFO: DraggablePoint.connect")
        self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
        self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
        self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)


    def on_press(self, event):
        '''Initiates when a Point is clicked on'''
        # print(self.partner)
        # print(event.xdata, event.ydata)
        if event.inaxes != self.point.axes: return
        if DraggablePoint.lock is not None: return
        contains, attrd = self.point.contains(event)
        if not contains: return
        self.press = (self.point.center), event.xdata, event.ydata
        DraggablePoint.lock = self


        # draw everything but the selected rectangle and store the pixel buffer
        canvas = self.point.figure.canvas
        axes = self.point.axes
        self.point.set_animated(True)
        for pair in self.parent.point_pairs:
            if self == pair[1]:
                self.line.set_animated(True)
            elif self == pair[0]:
                self.partner.line.set_animated(True)


        #TODO
        canvas.draw()
        self.background = canvas.copy_from_bbox(self.point.axes.bbox)

        # now redraw just the rectangle
        axes.draw_artist(self.point)

        # and blit just the redrawn area
        canvas.blit(axes.bbox)


    def on_motion(self, event):

        # print("LOG.INFO: DraggablePoint.on_motion")
        if DraggablePoint.lock is not self:
            return
        if event.inaxes != self.point.axes: return
        # print("LOG.INFO: DraggablePoint.on_motion.after_lock")
        # self.parent.updateFigure()
        self.point.center, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)


        canvas = self.point.figure.canvas
        axes = self.point.axes
        # restore the background region
        canvas.restore_region(self.background)

        # redraw just the current rectangle
        axes.draw_artist(self.point)

        for pair in self.parent.point_pairs:
            if self in pair:
                if self == pair[1]:
                    axes.draw_artist(self.line)
                else:
                    pair[1].line.set_animated(True)
                    axes.draw_artist(pair[1].line)



        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            if self == pair[1]:
                line_x = [pair[0].x+self.dx, self.x+self.dx]
                line_y = [pair[0].y+self.dy, self.y+self.dy]
                self.line.set_data(line_x, line_y)
            elif self == pair[0]:
                line_x = [pair[1].x+self.dx, self.x+self.dx]
                line_y = [pair[1].y+self.dy, self.y+self.dy]
                pair[1].line.set_data(line_x, line_y)

        # blit just the redrawn area
        canvas.blit(axes.bbox)
        # print(self.line)


    def on_release(self, event):

        # print("LOG.INFO: DraggablePoint.on_release")
        'on release we reset the press data'
        if DraggablePoint.lock is not self:
            return

        # print("LOG.INFO: DraggablePoint.on_release.after_lock")
        self.press = None
        DraggablePoint.lock = None

        # turn off the rect animation property and reset the background
        self.point.set_animated(False)
        axes = self.point.axes

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1] == self:
                    self.line.set_animated(False)
                else:
                    pair[1].line.set_animated(False)



        self.background = None

        # redraw the full figure
        self.point.figure.canvas.draw()

        self.x = self.point.center[0]
        self.y = self.point.center[1]
        print(self.line.__str__() + "RELEASE")

    def disconnect(self):

        'disconnect all the stored connection ids'

        self.point.figure.canvas.mpl_disconnect(self.cidpress)
        self.point.figure.canvas.mpl_disconnect(self.cidrelease)
        self.point.figure.canvas.mpl_disconnect(self.cidmotion)

    def setLine(self, line):
        self.line = line

嵌入图形用户界面

class PlotCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        self.fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = self.fig.add_subplot(111)

        FigureCanvas.__init__(self, self.fig)
        self.setParent(parent)

        FigureCanvas.setSizePolicy(self,
                                   QSizePolicy.Expanding,
                                   QSizePolicy.Expanding)
        FigureCanvas.updateGeometry(self)
        self.xcoords = []
        self.ycoords = []
        self.lines = []
        self.list_points = []
        self.point_pairs = []
        self.number_of_lines = 0
        # self.mpl_connect('button_press_event', self.plot_draggable_point)
        self.plot_line = False
        self.plot()
        self.create_draggable_points()


    def plot(self):
        # data = [random.random() for i in range(25)]

        # self.a = plt.scatter(M[:, 0], M[:, 1])
        data = dataset2.get_matrix()
        ax = self.figure.add_subplot(111)
        ax.scatter(data[:, 0], data[:, 1], picker=10)
        ax.set_title('PyQt Matplotlib Example')
        self.draw()

    def create_draggable_points(self):
        self.list_points.append(DraggablePoint(self, True, 618.5047115210559, 338.5460575139148, 20))
        self.list_points.append(DraggablePoint(self, False, 338.5460575139148, 118.5047115210559, 20))
        # TODO Koordinaten an den Plot anpassen (+500)
        i = self.list_points[0]
        j = self.list_points[1]
        i.partner = j
        j.partner = i
        i.setLine(Line2D([i.x, j.x], [i.y, j.y], color='r', alpha=0.5))
        j.setLine(Line2D([i.x, j.x], [i.y, j.y], color='r', alpha=0.5))
        self.lines.append(i.line)
        self.lines.append(j.line)
        print(self.lines)

        self.point_pairs.append((i, j))

        self.updateFigure()


    def plot_draggable_point(self, event, size=60):
        if self.plot_line:
            self.xcoords.append(event.xdata)
            self.ycoords.append(event.ydata)
            print(event.xdata)
            print(event.ydata)
            self.list_points.append(DraggablePoint(self, event.xdata, event.ydata, size))
            if len(self.xcoords) == 2:
                self.list_points.append(DraggablePoint(self, event.xdata, event.ydata, size))
                self.xcoords[:] = []
                self.ycoords[:] = []
                self.list_points[:] = []
                self.plot_line = False
            self.updateFigure()

    def updateFigure(self):
        print(self.point_pairs)
        self.draw()

类是这样调用的:

    layout = QGridLayout()
    self.m = PlotCanvas(self, width=10, height=8)
    layout.addWidget(self.m, 0, 0, 5, 1)

更新:

第一个问题已经解决了,我忘记在on_release()方法中重画线了。现在看起来像这样:

def on_release(self, event):

    'on release we reset the press data'
    if DraggablePoint.lock is not self:
        return

    self.press = None
    DraggablePoint.lock = None

    # turn off the rect animation property and reset the background
    self.point.set_animated(False)
    axes = self.point.axes

    for pair in self.parent.point_pairs:
        if self in pair:
            if pair[1] == self:
                self.line.set_animated(False)
            else:
                pair[1].line.set_animated(False)



    self.background = None

    # redraw the full figure
    self.point.figure.canvas.draw()

    self.x = self.point.center[0]
    self.y = self.point.center[1]


    for pair in self.parent.point_pairs:
        if self in pair:
            if pair[1] == self:
                axes.draw_artist(self.line)
            else:
                axes.draw_artist(pair[1].line)

【问题讨论】:

【参考方案1】:

我通过引入缩放和偏移解决了问题2的问题。其他两个问题是由于画布在创建的线条上重绘造成的。这些问题现在已经解决了。 DraggablePoint 的类现在看起来像这样:

import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.lines import Line2D
import matplotlib


class DraggablePoint:



    lock = None

    def __init__(self, parent, dominant, x=10, y=10, size=1):
        """Creates a draggable Point on a matplotlib canvas"""
        matplotlib.matplotlib_fname()
        # The FigureCanvas
        self.parent = parent
        # The Point
        self.point = patches.Ellipse((x, y), size, size, fc='r', alpha=0.5, edgecolor='r')
        #Coordinates of the point
        self.x = x
        self.y = y
        self.x_offset = 260
        self.y_offset = 640
        self.dy = 640
        self.dx = 260
        self.x_offset_factor = 0.06
        self.y_offset_factor = -0.089
        self.x_scaling = self.x * self.x_offset_factor
        self.y_scaling = self.y * self.y_offset_factor
        self.dy = self.y_offset + self.y_scaling
        self.dx = self.x_offset + self.x_scaling
        # Adds the point to the Plot
        parent.fig.axes[0].add_patch(self.point)
        # Used in the on_press() function
        self.press = None
        self.background = None
        # initiate the mpl_connects
        self.connect()
        # The Other DraggablePoint, with whom the line shall connect with.
        self.partner = None
        # The Line2D
        self.line = None
        self.dominant = dominant

        for pair in self.parent.point_pairs:
            if self in pair:
                if self == pair[1]:
                    line_x = [pair[0].x + pair[0].dx, self.x+self.dx]
                    line_y = [pair[0].y + pair[0].dy, self.y+self.dy]
                    self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
                    parent.fig.axes[0].add_line(self.line)
                else:
                    line_x = [pair[1].x + pair[1].dx, self.x + self.dx]
                    line_y = [pair[1].y + pair[1].dy, self.y + self.dy]
                    self.line = Line2D(line_x, line_y, color='r', alpha=0.5)
                    parent.fig.axes[0].add_line(self.line)
        for pair in self.parent.point_pairs:
            self.point.axes.draw_artist(pair[1].line)


    def connect(self):

        'connect to all the events we need'
        # print("LOG.INFO: DraggablePoint.connect")
        self.cidpress = self.point.figure.canvas.mpl_connect('button_press_event', self.on_press)
        self.cidrelease = self.point.figure.canvas.mpl_connect('button_release_event', self.on_release)
        self.cidmotion = self.point.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)


    def on_press(self, event):
        '''Initiates when a Point is clicked on'''
        if event.inaxes != self.point.axes: return
        if DraggablePoint.lock is not None: return
        contains, attrd = self.point.contains(event)
        if not contains: return
        self.press = (self.point.center), event.xdata, event.ydata
        DraggablePoint.lock = self


        # draw everything but the selected rectangle and store the pixel buffer
        canvas = self.point.figure.canvas
        axes = self.point.axes
        self.point.set_animated(True)
        for pair in self.parent.point_pairs:
            if self == pair[1]:
                self.line.set_animated(True)
            elif self == pair[0]:
                self.partner.line.set_animated(True)


        canvas.draw()
        self.background = canvas.copy_from_bbox(self.point.axes.bbox)

        # now redraw just the rectangle
        axes.draw_artist(self.point)

        # and blit just the redrawn area
        canvas.blit(axes.bbox)


    def on_motion(self, event):

        # print("LOG.INFO: DraggablePoint.on_motion")
        if DraggablePoint.lock is not self:
            return
        if event.inaxes != self.point.axes: return
        # print("LOG.INFO: DraggablePoint.on_motion.after_lock")
        # self.parent.updateFigure()
        self.point.center, xpress, ypress = self.press
        dx = event.xdata - xpress
        dy = event.ydata - ypress
        self.point.center = (self.point.center[0]+dx, self.point.center[1]+dy)

        #Update the scaling of the offset
        self.x_scaling = self.x * self.x_offset_factor
        self.y_scaling = self.y * self.y_offset_factor
        self.dy = self.y_offset + self.y_scaling
        self.dx = self.x_offset + self.x_scaling

        canvas = self.point.figure.canvas
        axes = self.point.axes
        # restore the background region
        canvas.restore_region(self.background)

        # redraw just the current rectangle
        axes.draw_artist(self.point)

        for pair in self.parent.point_pairs:
            if self in pair:
                axes.draw_artist(pair[1].line)
            if self == pair[1]:
                self.x_scaling = self.x * self.x_offset_factor
                self.y_scaling = self.y * self.y_offset_factor
                self.dy = self.y_offset + self.y_scaling
                self.dx = self.x_offset + self.x_scaling



        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            if self == pair[1]:
                line_x = [pair[0].x + pair[0].dx, self.x+self.dx]
                line_y = [pair[0].y + pair[0].dy, self.y+self.dy]
                self.line.set_data(line_x, line_y)
            elif self == pair[0]:
                line_x = [pair[1].x + pair[1].dx, self.x+self.dx]
                line_y = [pair[1].y + pair[1].dy, self.y+self.dy]
                pair[1].line.set_data(line_x, line_y)

        # blit just the redrawn area
        canvas.blit(axes.bbox)
        # print(self.line)


    def on_release(self, event):

        # print("LOG.INFO: DraggablePoint.on_release")
        'on release we reset the press data'
        if DraggablePoint.lock is not self:
            return

        # print("LOG.INFO: DraggablePoint.on_release.after_lock")
        self.press = None
        DraggablePoint.lock = None

        # turn off the rect animation property and reset the background
        self.point.set_animated(False)
        axes = self.point.axes

        for pair in self.parent.point_pairs:
            if self in pair:
                if pair[1] == self:
                    self.line.set_animated(False)
                else:
                    pair[1].line.set_animated(False)

        print(self.x_scaling, self.y_scaling)


        self.background = None

        # redraw the full figure
        self.point.figure.canvas.draw()

        self.x = self.point.center[0]
        self.y = self.point.center[1]

        for pair in self.parent.point_pairs:
            axes.draw_artist(pair[1].line)
        print(self.line.__str__() + "RELEASE")

    def disconnect(self):

        'disconnect all the stored connection ids'

        self.point.figure.canvas.mpl_disconnect(self.cidpress)
        self.point.figure.canvas.mpl_disconnect(self.cidrelease)
        self.point.figure.canvas.mpl_disconnect(self.cidmotion)

    def setLine(self, line):
        self.line = line

【讨论】:

以上是关于matplotlib pyqt5画布上两个可拖动点之间的线的主要内容,如果未能解决你的问题,请参考以下文章

在 pyqt5 界面中动态更新 matplotlib 画布

无法在 pyqt5 中嵌入的 matplotlib 上绘制线条

如何使fabricJS画布在移动设备上可水平拖动?

只需要一些关于画布上动态可拖动输入元素的指导

可拖动图像上的画布绘图

如何在画布上制作 HTML5 可拖动对象?