Interactive Figures become non-interactive when embedded in a PyQt5 GUI interface

I am trying to build a PyQt5 GUI, and an interactive figure is needed. Like if I have a three-dimension numpy array and the third dimension means time. I want to use the key ‘left’ and ‘right’ to switch pictures at different time points. Only making the figure interactive is easy to implement, using mpl_connect, while, if I want to embed the figure into a PyQt5 intereface, I cannot switch the pictures any more, and it seems that the function that is connected failed. Here is my sample code:

import matplotlib
matplotlib.use('Qt5Agg')
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
from PyQt5 import QtWidgets
import matplotlib.pyplot as plt
import sys
import numpy as np



class My_Main_window(QtWidgets.QMainWindow):
    def __init__(self,parent=None):
        super(My_Main_window,self).__init__(parent)

        self.canvas = FigureCanvas(Figure())
        self.canvas.draw()
        self.ax = self.canvas.figure.add_subplot()

        self.con_1 = np.arange(12000).reshape((30, 40, 10))
        self.num = 0
        con_plot = self.con_1[:, :, self.num]
        self.ax.matshow(con_plot)
        self.ax.set_title(self.num)
        self.canvas.mpl_connect('key_press_event', self.on_move)
        self.setCentralWidget(self.canvas)
        plt.ion()

    def on_move(self, event):
        plt.cla()
        print('activate this')
        if event.key is 'left':
            if self.num == 0:
                self.num = 0
                con_plot = self.con_1[:, :, self.num]
                self.ax.matshow(con_plot)
                self.ax.set_title(self.num)
            else:
                self.num -= 1
                con_plot = self.con_1[:, :, self.num]
                self.ax.matshow(con_plot)
                self.ax.set_title(self.num)
        elif event.key is 'right':
            if self.num == self.con_1.shape[2] - 1:
                self.num = 0
                con_plot = self.con_1[:, :, self.num]
                self.ax.matshow(con_plot)
                self.ax.set_title(self.num)
            else:
                self.num += 1
                con_plot = self.con_1[:, :, self.num]
                self.ax.matshow(con_plot)
                self.ax.set_title(self.num)
        else:
            con_plot = self.con_1[:, :, self.num]
            self.ax.matshow(con_plot)
            self.ax.set_title(self.num)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setApplicationName('Plot')
    main_win = My_Main_window()
    main_win.show()
    app.exec()

The issue is that Qt is not forwarding the keyboard events to the Canvas widget (this took me a bit to sort out!). The solution is to lift some additional configuration that we do in the setting up the “standard” window for pyplot. A working version of this code (with a few debugging prints and some style changes) is:

import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.qt_compat import QtCore
from matplotlib.figure import Figure
from PyQt5 import QtWidgets
import matplotlib.pyplot as plt
import sys
import numpy as np


class My_Main_window(QtWidgets.QMainWindow):
    def __init__(self, parent=None):
        super(My_Main_window, self).__init__(parent)

        self.canvas = FigureCanvas(Figure(constrained_layout=True))
        self.canvas.draw()
        self.ax = self.canvas.figure.add_subplot()

        # self.con_1 = np.arange(12000).reshape((30, 40, 10))
        self.con_1 = np.random.random((30, 40, 10))   
        self.num = 0
        con_plot = self.con_1[:, :, self.num]
        self.im = self.ax.matshow(con_plot)
        self.ax.set_title(self.num)
        self.canvas.figure.colorbar(self.im)
        self.canvas.mpl_connect('key_press_event', self.on_move)
        self.canvas.mpl_connect(
            "button_press_event", lambda *args, **kwargs: print(args, kwargs)
        )
        self.canvas.mpl_connect(
            "key_press_event", lambda *args, **kwargs: print(args, kwargs)
        )

        self.setCentralWidget(self.canvas)

        # Give the keyboard focus to the figure instead of the manager:
        # StrongFocus accepts both tab and click to focus and will enable the
        # canvas to process event without clicking.
        # https://doc.qt.io/qt-5/qt.html#FocusPolicy-enum
        self.canvas.setFocusPolicy(QtCore.Qt.StrongFocus)
        self.canvas.setFocus()

    def on_move(self, event):
        print(f"activate this {event.key}, {self.num}")
        if event.key == "left":
            if self.num == 0:
                self.num = 0
                con_plot = self.con_1[:, :, self.num]
            else:
                self.num -= 1
                con_plot = self.con_1[:, :, self.num]
        elif event.key == "right":
            if self.num == self.con_1.shape[2] - 1:
                self.num = 0
                con_plot = self.con_1[:, :, self.num]
            else:
                self.num += 1
                con_plot = self.con_1[:, :, self.num]
        else:
            con_plot = self.con_1[:, :, self.num]
        # update the image in place
        self.im.set_data(con_plot)
        self.ax.set_title(self.num)
        # tell Qt that the widget needs to be re-repainted
        self.canvas.draw_idle()

if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setApplicationName("Plot")
    main_win = My_Main_window()
    main_win.show()
    app.exec()

The key change is the last two lines of __init__!

If this works would you be willing to update the embedding example in docs with this information @BarryLiu-97?

I would love to. Let me have a try first.

Well, how do I update? I have never start a PR. Should I follow thishttps://matplotlib.org/devel/index.html? And then I add a new tutorial(maybe called interactive figures embeddind in QT).

My first thought was to add it to matplotlib/embedding_in_qt_sgskip.py at master · matplotlib/matplotlib · GitHub (which renders at Embedding in Qt — Matplotlib 3.3.3 documentation when the docs are built).

The upside of adding it to the existing example is that by having only one “embed in Qt example” it gives people one place to look. The upside of making a new example is that it keeps each example simpler so people have to wade through less code unrelated to what they want to find the detail they need. On net, I think one slightly more complex example is better here, but could be convinced otherwise.

I suggest looking at The Matplotlib Developers' Guide — Matplotlib 3.3.2.post2038+ga8f52d977 documentation (the dev version of where you linked so it has some updates to make it clearer (we hope)). Very specifically look at Contributing — Matplotlib 3.3.2.post2038+ga8f52d977 documentation which boils down to

  1. fork in github
  2. clone and set up both matplotlib/matplotlib and your fork as remotes
  3. create a branch, make changes
  4. push to your fork, open PR
  5. iterate with reviewers until merged

If you have any questions joining the gitter (matplotlib/matplotlib - Gitter) is useful (as somethings are better handleded in chat!).

Got it! Thank u so much!