This post is related to a recent topic where I asked help to use blit when operating with more than one interactor.
At this point I wanted to integrate the solution with PyQt5, since I am writing an interactive software.
In this example I draw two triangles and I can modify their shape by moving one of their vertices.
The code has two implementations.
By running the code with argument ‘0’, the polygon interactors are managed directly by a function.
This version works.
When running the code with argument ‘1’, the interactors are managed by a class.
Obviously, I am missing something important, since this version does not work.
Any help is welcome.
This is the code:
#!/usr/bin/env python
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtCore import QObject
import numpy as np
import sys
from matplotlib.figure import Figure
from matplotlib.lines import Line2D
from matplotlib.artist import Artist
from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenu, QVBoxLayout,
QSizePolicy, QWidget, QPushButton)
from PyQt5.QtCore import pyqtSlot
from matplotlib.backends.backend_qt5agg import FigureCanvas
from PyQt5 import QtWidgets
from matplotlib.patches import Polygon
class InteractorManager(QObject):
def __init__(self, fig, ax, interactors):
super().__init__()
self.interactors = interactors
self.ax = ax
self.fig = fig
self.canvas = self.fig.canvas
self.canvas.mpl_connect('draw_event', self.draw_callback)
self.canvas.mpl_connect('motion_notify_event', self.motion_notify_callback)
def draw_callback(self, event):
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
for interactor in self.interactors:
interactor.draw_callback(event)
def motion_notify_callback(self, event):
if event.inaxes is None:
return
elif event.button != 1:
return
else:
self.canvas.restore_region(self.background)
for interactor in self.interactors:
interactor.motion_notify_callback(event)
interactor.draw_callback(event)
self.canvas.blit(self.ax.bbox)
class PolygonInteractor(QObject):
"""
A polygon editor.
"""
showverts = True
epsilon = 5 # max pixel distance to count as a vertex hit
def __init__(self, ax, poly):
super().__init__()
self.ax = ax
canvas = ax.figure.canvas
self.poly = poly
self.ax.add_patch(self.poly)
x, y = zip(*self.poly.xy)
color = poly.get_ec()
self.line = Line2D(x, y,
marker='o', markerfacecolor=color,
color=color,
animated=True)
self.ax.add_line(self.line)
self.cid = self.poly.add_callback(self.poly_changed)
self._ind = None # the active vert
canvas.mpl_connect('button_press_event', self.button_press_callback)
canvas.mpl_connect('button_release_event', self.button_release_callback)
self.canvas = canvas
self.artists = [self.poly, self.line]
def poly_changed(self, poly):
'this method is called whenever the polygon object is called'
# only copy the artist props to the line (except visibility)
vis = self.line.get_visible()
Artist.update_from(self.line, poly)
self.line.set_visible(vis) # don't use the poly visibility state
def get_ind_under_point(self, event):
'get the index of the vertex under point if within epsilon tolerance'
# display coords
xy = np.asarray(self.poly.xy)
xyt = self.poly.get_transform().transform(xy)
xt, yt = xyt[:, 0], xyt[:, 1]
d = np.hypot(xt - event.x, yt - event.y)
indseq, = np.nonzero(d == d.min())
ind = indseq[0]
if d[ind] >= self.epsilon:
ind = None
return ind
def draw_callback(self, event):
self.ax.draw_artist(self.poly)
self.ax.draw_artist(self.line)
def button_press_callback(self, event):
'whenever a mouse button is pressed'
if not self.showverts:
return
if event.inaxes is None:
return
if event.button != 1:
return
self._ind = self.get_ind_under_point(event)
def button_release_callback(self, event):
'whenever a mouse button is released'
if not self.showverts:
return
if event.button != 1:
return
self._ind = None
def motion_notify_callback(self, event):
'on mouse movement'
if not self.showverts:
return
elif self._ind is None:
return
elif event.inaxes is None:
return
elif event.button != 1:
return
else:
x, y = event.xdata, event.ydata
self.poly.xy[self._ind] = x, y
if self._ind == 0:
self.poly.xy[-1] = x, y
elif self._ind == len(self.poly.xy) - 1:
self.poly.xy[0] = x, y
self.line.set_data(zip(*self.poly.xy))
class GUI(QMainWindow):
def __init__(self):
super().__init__()
if len(sys.argv) > 1:
self.arg = sys.argv[1]
else:
self.arg = '0'
self.left = 10
self.top = 10
self.title = 'PyQt5 matplotlib example with two polygons'
self.width = 640
self.height = 500
self.initUI()
def initUI(self):
self.setWindowTitle(self.title)
self.setGeometry(self.left, self.top, self.width, self.height)
wid = QWidget()
self.setCentralWidget(wid)
mainLayout = QVBoxLayout(wid)
button = QPushButton('Quit', self)
button.setToolTip('Quit button')
mainLayout.addWidget(button)
button.resize(100,100)
button.clicked.connect(self.fileQuit)
mainplot = PlotCanvas(self, width=5, height=4, arg=self.arg)
mainLayout.addWidget(mainplot)
self.show()
@pyqtSlot()
def fileQuit(self):
""" Quitting the program """
self.close()
class PlotCanvas(FigureCanvas):
def __init__(self, parent=None, width=5, height=4, dpi=100, arg=0):
self.fig = Figure(figsize=(width,height), dpi=dpi)
self.arg = arg
self.ax = self.fig.add_subplot(111)
FigureCanvas.__init__(self, self.fig)
self.setParent(parent)
FigureCanvas.setSizePolicy(self,
QtWidgets.QSizePolicy.Expanding,
QtWidgets.QSizePolicy.Expanding)
FigureCanvas.updateGeometry(self)
self.plot()
def plot(self):
theta = np.arange(0, 2*np.pi, 2*np.pi/3.)
r = 1.5
xs = r * np.cos(theta)
ys = r * np.sin(theta)
poly1 = Polygon(np.column_stack([xs, ys]), animated=True,
color='blue', fill=False)
theta += np.pi/3
xs = r * np.cos(theta)
ys = r * np.sin(theta)
poly2 = Polygon(np.column_stack([xs, ys]), animated=True,
color='red', fill=False)
p1 = PolygonInteractor(self.ax, poly1)
p2 = PolygonInteractor(self.ax, poly2)
interactors = [p1, p2]
if self.arg == '0':
# Version with function
print('Version with function')
self.poly_manager(interactors)
else:
# Version with class
print('Version with class')
intManager = InteractorManager(self.fig, self.ax, interactors)
self.ax.set_title('Click and drag a point to move it')
self.ax.set_xlim((-2, 2))
self.ax.set_ylim((-2, 2))
self.ax.grid(True)
self.draw()
def poly_manager(self, interactors):
self.interactors = interactors
self.fig.canvas.mpl_connect('draw_event', self.poly_draw_callback)
self.fig.canvas.mpl_connect('motion_notify_event', self.poly_motion_notify_callback)
def poly_draw_callback(self, event):
self.fig.background = self.fig.canvas.copy_from_bbox(self.ax.bbox)
for interactor in self.interactors:
interactor.draw_callback(event)
def poly_motion_notify_callback(self, event):
if event.inaxes is None:
return
elif event.button != 1:
return
else:
self.fig.canvas.restore_region(self.fig.background)
for interactor in self.interactors:
interactor.motion_notify_callback(event)
interactor.draw_callback(event)
self.fig.canvas.blit(self.ax.bbox)
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = GUI()
sys.exit(app.exec_())