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画布上两个可拖动点之间的线的主要内容,如果未能解决你的问题,请参考以下文章