0

Prithee, wizened elders of StackOverflow,

I am trying to make a small GUI application using PyQt5, with matplotlib.FigureCanvas objects used as widgets to display data which I also want to make more interactive through the use of matplotlib's callback functions. However, these callbacks don't appear to work for a FigureCanvas embedded in the PyQt5 window.

I'm thinking it's likely that instantiating the Qt5 application interferes with matplotlib's event handler, but I'm not sure how to proceed. Is there a way to make the matplotlib events raise Qt5 signals? Could I have two separate threads in which both event handlers can run?

The example below is the simplest thing I could pare down which illustrates the issue.

Case A: Running in Matplotlib Window

First run as-is noting the desired behavior: the onclick function is called upon a click in the figure once it pops up. (also note that the second code block, Case B won't run after the first)

Case B: Running in PyQt5

Now, comment out the Case A code block, and run: clicking in the figure doesn't call the callback function.

import sys
import numpy as np

from PyQt5 import QtCore, QtWidgets, uic

import matplotlib
matplotlib.use('QT5Agg')
import matplotlib.pylab as plt
from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
matplotlib.rcParams["toolbar"] = "toolmanager"

def onclick(event):
    '''example callback function'''
    print('got click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
          ( event.button, event.x, event.y, event.xdata, event.ydata))

class MPLWidget(QtWidgets.QWidget):
    '''
    Widget which should act like a matplotlib FigureCanvas, but embedded in PyQt5
    '''
    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)
        
        # Making Matplotlib figure object with data
        fig, ax = plt.subplots()
        fig.suptitle("Plot in PyQt5: click and look in console")
        ax.plot(np.linspace(0,1,2))
        # Attaching matplotlib callback function which doesnt seem to work
        cid = fig.canvas.mpl_connect('button_press_event', onclick)

        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)      
        self.plotWidget = FigureCanvas(fig)
        self.toolbar = NavigationToolbar(self.plotWidget, self)

        self.layout.addWidget(self.toolbar)
        self.layout.addWidget(self.plotWidget)


class TestWindow(QtWidgets.QMainWindow):
    '''Window which is a stand in for the larger PyQt5 application which needs a plot embedded'''
    def __init__(self):
        super().__init__()
        self.mplwidget = MPLWidget(self)
        self.setCentralWidget(self.mplwidget)

if __name__ == '__main__':

    # Case A: Using matplotlib without PyQt5, 
    # Note that the callback is triggered and something is printed when you click in the figure
    fig, ax = plt.subplots()
    fig.suptitle("Standalone matplotlib: click and look in console")
    ax.plot(np.linspace(0,1,2))
    cid = fig.canvas.mpl_connect('button_press_event', onclick)
    plt.show()
    
    # Case B:  Running PyQt5 + Matplotlib widget
    # Note that nothing is printed when you click in the figure
    app = QtWidgets.QApplication(sys.argv)
    window = TestWindow()
    window.show()
    sys.exit(app.exec_())

1 Answers1

2

You don't have to use "plt" if you're going to use FigureCanvas from pyqt5 but build using Figure() as the official example shows:

import sys
import numpy as np

from PyQt5 import QtCore, QtWidgets

import matplotlib

matplotlib.use("QT5Agg")

from matplotlib.backends.backend_qt5agg import FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar

matplotlib.rcParams["toolbar"] = "toolmanager"

from matplotlib.figure import Figure


def onclick(event):
    """example callback function"""
    print(
        "got click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f"
        % (event.button, event.x, event.y, event.xdata, event.ydata)
    )


class MPLWidget(QtWidgets.QWidget):
    """
    Widget which should act like a matplotlib FigureCanvas, but embedded in PyQt5
    """

    def __init__(self, parent=None, **kwargs):
        super().__init__(parent, **kwargs)

        # Making Matplotlib figure object with data
        self.canvas = FigureCanvas(Figure())
        ax = self.canvas.figure.add_subplot(111)
        ax.set_title("Plot in PyQt5: click and look in console")
        ax.plot(np.linspace(0, 1, 2))

        cid = self.canvas.mpl_connect("button_press_event", onclick)

        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.toolbar = NavigationToolbar(self.canvas, self)

        self.layout.addWidget(self.toolbar)
        self.layout.addWidget(self.canvas)


class TestWindow(QtWidgets.QMainWindow):
    """Window which is a stand in for the larger PyQt5 application which needs a plot embedded"""

    def __init__(self):
        super().__init__()
        self.mplwidget = MPLWidget(self)
        self.setCentralWidget(self.mplwidget)


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    window = TestWindow()
    window.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Wow, I had never found that section of the matplotlib docs. Thank you so much for your response, this works perfectly! – Eric Valentino Oct 14 '20 at 13:38
  • @eyllanesc, hi, I have a similar question about how to connect the 'xlim_changed' signal of the axe in a canvas to a callback function? I am trying to do some downsampling. – bactone Dec 06 '21 at 00:56