Search code examples
pythonmatplotlibpyqt

Matplotlib + PyQt5, add custom Tools to the Toolbar


I'm developing a PYQt5 application with an embedded matplotlib canvas. The idea is, that an image is presented and the user then can draw rectangles into the image. I would like to realize this by adding a new tool to the matplotlib toolbar. The tool should work similarily to the zoom tool, the user selects the tool, draws a rectangle and I get the bounding box in Python, such that I can save it and also draw it for the user. However, I am currently unable to add ANY new tool to the toolbar.

My code currently looks like this:

import sys

import matplotlib
import numpy as np

matplotlib.use("Qt5Agg")
import matplotlib.pyplot as plt
from matplotlib.backend_tools import ToolBase
from matplotlib.backends.backend_qtagg import FigureCanvas
from matplotlib.backends.backend_qtagg import \
    NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (QAction, QApplication, QMainWindow, QVBoxLayout,
                             QWidget)

plt.rcParams['toolbar'] = 'toolmanager'


class ListTools(ToolBase):
    """List all the tools controlled by the `ToolManager`."""
    # keyboard shortcut
    default_keymap = 'm'
    description = 'List Tools'

    def trigger(self, *args, **kwargs):
        print('_' * 80)
        print("{0:12} {1:45} {2}".format('Name (id)', 'Tool description',
                                         'Keymap'))
        print('-' * 80)
        tools = self.toolmanager.tools
        for name in sorted(tools):
            if not tools[name].description:
                continue
            keys = ', '.join(sorted(self.toolmanager.get_tool_keymap(name)))
            print("{0:12} {1:45} {2}".format(name, tools[name].description,
                                             keys))
        print('_' * 80)
        print("Active Toggle tools")
        print("{0:12} {1:45}".format("Group", "Active"))
        print('-' * 80)
        for group, active in self.toolmanager.active_toggle.items():
            print("{0:12} {1:45}".format(str(group), str(active)))


class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        self.sc = FigureCanvas(Figure(figsize=(5, 3), tight_layout=True))
        self.sc.manager.toolmanager.add_tool('List', ListTools)
        self.toolbar = NavigationToolbar(self.sc, self)
        self.addToolBar(self.toolbar)
        print(dir(self.toolbar))

        uniform_data = np.random.rand(10, 12)

        axes = self.sc.figure.subplots()

        axes.imshow(uniform_data)

        widget = QWidget()
        self.setCentralWidget(widget)
        layout = QVBoxLayout(widget)
        layout.addWidget(self.sc)


def main():
    app = QApplication(sys.argv)
    w = MainWindow()
    w.show()
    app.exec_()


if __name__ == "__main__":
    main()

However, it fails at line 51:

self.sc.manager.toolmanager.add_tool('List', ListTools)

AttributeError: 'NoneType' object has no attribute 'toolmanager'

I couldn't find any additional information in the Matplotlib tutorial. Nor could I find any solutions about this issue on Stackoverflow, the only question I found was this, however, there are no answers. Futhermore, I only found one GitHub issue about this, which also does not go into detail how to fix this problem. Could somebody explain to me, what I am doing wrong?


Solution

  • I managed to force a solution by manually creating a "toolmanager". Seems the FigureCanvas as a function does not create the manager. Therefore the manager can be created with "matplotlib.backends.backend_qt.FigureManagerQT".

    import sys
    
    import matplotlib
    import numpy as np
    
    matplotlib.use("Qt5Agg")
    import matplotlib.pyplot as plt
    from matplotlib.backend_tools import ToolBase
    from matplotlib.backends.backend_qtagg import FigureCanvas
    from matplotlib.backends.backend_qtagg import \
        NavigationToolbar2QT as NavigationToolbar
    from matplotlib.figure import Figure
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import (QAction, QApplication, QMainWindow, QVBoxLayout,
                                 QWidget)
    
    plt.rcParams['toolbar'] = 'toolmanager'
    
    
    class ListTools(ToolBase):
        """List all the tools controlled by the `ToolManager`."""
        # keyboard shortcut
        default_keymap = 'm'
        description = 'List Tools'
    
        def trigger(self, *args, **kwargs):
            print('_' * 80)
            print("{0:12} {1:45} {2}".format('Name (id)', 'Tool description',
                                             'Keymap'))
            print('-' * 80)
            tools = self.toolmanager.tools
            for name in sorted(tools):
                if not tools[name].description:
                    continue
                keys = ', '.join(sorted(self.toolmanager.get_tool_keymap(name)))
                print("{0:12} {1:45} {2}".format(name, tools[name].description,
                                                 keys))
            print('_' * 80)
            print("Active Toggle tools")
            print("{0:12} {1:45}".format("Group", "Active"))
            print('-' * 80)
            for group, active in self.toolmanager.active_toggle.items():
                print("{0:12} {1:45}".format(str(group), str(active)))
    
    
    class MainWindow(QMainWindow):
        def __init__(self, *args, **kwargs):
            super(MainWindow, self).__init__(*args, **kwargs)
    
            self.sc = FigureCanvas(Figure(figsize=(5, 3), tight_layout=True))
            self.sc.manager = matplotlib.backends.backend_qt.FigureManagerQT(self.sc, 1)
            self.sc.manager.toolmanager.add_tool('List', ListTools)
            self.sc.manager.toolbar.add_tool("List", "navigation", 1)
            self.addToolBar(self.sc.manager.toolbar)
            #print(dir(self.toolbar))
    
            uniform_data = np.random.rand(10, 12)
    
            axes = self.sc.figure.subplots()
    
            axes.imshow(uniform_data)
    
            widget = QWidget()
            self.setCentralWidget(widget)
            layout = QVBoxLayout(widget)
            layout.addWidget(self.sc)
    
    
    def main():
        app = QApplication(sys.argv)
        w = MainWindow()
        w.show()
        app.exec_()
    
    
    if __name__ == "__main__":
        main()