PyQt6中的Matplotlib绘图—在你的GUI应用程序中嵌入图表

1,997 阅读12分钟

在之前的教程中,我们介绍了在PyQt6中使用PyQtGraph进行绘图。PyQtGraph使用基于Qt矢量的QGraphicsScene来绘制绘图,并为交互式和高性能的绘图提供了一个很好的界面。

然而,还有另一个Python的绘图库,它被更广泛地使用,并提供了更丰富的绘图种类 -Matplotlib。如果你正在将一个现有的数据分析工具迁移到Python GUI中,或者你只是想获得Matplotlib提供的一系列绘图能力,那么你会想知道如何在你的应用程序中包含Matplotlib的绘图。

在本教程中,我们将介绍如何在你的PyQt应用程序中嵌入Matplotlib绘图。

许多其他的Python库--比如seabornpandas--利用Matplotlib后端进行绘图。这些图可以以同样的方式嵌入到PyQt6中,并在绘图时传递对坐标轴的引用。在本教程的最后有一个pandas的例子。

安装Matplotlib

下面的例子假设你已经安装了Matplotlib。如果没有,你可以使用Pip正常地安装它,具体步骤如下

bash

pip install matplotlib

一个简单的例子

下面这个最小的例子设置了一个Matplotlib画布FigureCanvasQTAgg ,它创建了Figure ,并为其添加了一组单轴。这个canvas对象也是一个QWidget ,所以可以像其他Qt widget一样直接嵌入到一个应用程序中。

python

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt6 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

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

        # Create the maptlotlib FigureCanvas object,
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])
        self.setCentralWidget(sc)

        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

在这种情况下,我们将我们的MplCanvas widget添加为窗口上的中心widget,并将其与.setCentralWidget() 。这意味着它将占据整个窗口并和它一起调整大小。绘制的数据[0,1,2,3,4], [10,1,20,3,40] ,按照.plot 方法的要求,提供为两个数字列表(分别为x和y)。

Basic plot with embedded Matplotlib 嵌入Matplotlib的基本绘图

绘图控制

在PyQt6中显示的来自Matplotlib的图实际上是由Agg后端渲染成简单的(位图)图像。FigureCanvasQTAgg 类包装了这个后端,并在一个Qt widget上显示结果的图像。这种架构的效果是,Qt不知道线条和其他绘图元素的位置--只知道在widget上的任何点击和鼠标移动的x、y坐标。

然而,Matplotlib支持处理Qt鼠标事件,并将其转换为绘图上的交互。这可以通过一个自定义的工具栏来控制,该工具栏可以被添加到你的应用程序中,与绘图一起。在本节中,我们将看看如何添加这些控件,以便我们能够缩放、平移和从嵌入的Matplotlib图中获取数据。

完整的代码,导入工具条部件NavigationToolbar2QT ,并将其添加到界面中QVBoxLayout ,如下图所示--。

python

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt6 import QtCore, QtGui, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

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

        sc = MplCanvas(self, width=5, height=4, dpi=100)
        sc.axes.plot([0,1,2,3,4], [10,1,20,3,40])

        # Create toolbar, passing canvas as first parament, parent (self, the MainWindow) as second.
        toolbar = NavigationToolbar(sc, self)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(sc)

        # Create a placeholder widget to hold our toolbar and canvas.
        widget = QtWidgets.QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

我们将逐步完成这些变化。

首先,我们从matplotlib.backends.backend_qt5agg.NavigationToolbar2QT 中导入工具条部件,用更简单的名字NavigationToolbar 来重新命名。 我们通过调用NavigationToolbar ,用两个参数创建一个工具栏的实例,首先是画布对象sc ,然后是工具栏的父对象,在这里是我们的MainWindow 对象self 。传入画布将创建的工具条链接到画布上,使其能够被控制。由此产生的工具条对象被存储在变量toolbar

我们需要在窗口中添加两个小部件,一个在另一个上面,所以我们使用QVBoxLayout 。首先我们把我们的工具栏小部件toolbar ,然后把画布小部件sc 添加到这个布局中。最后,我们将这个布局设置到我们简单的widget 布局容器上,该容器被设置为窗口的中心部件。

运行上述代码将产生以下窗口布局,在底部显示绘图,在顶部显示作为工具条的控件。

Matplotlib plot with Toolbar 带有工具条的Matplotlib绘图

NavigationToolbar2QT 所提供的按钮可以实现以下操作

  • 主页、后退/前进、平移和缩放,用于在绘图中导航。后退/前进按钮可以通过导航步骤向前和向后移动,例如,放大后点击后退将返回到之前的缩放。原点返回到绘图的初始状态。
  • 绘图边缘/位置配置,可以在窗口内调整绘图。
  • 轴/曲线样式编辑器,可以修改绘图标题和轴的比例,以及设置绘图线的颜色和线条样式。颜色选择使用平台默认的颜色选择器,允许选择任何可用的颜色。
  • 保存,将生成的图形保存为图像(所有Matplotlib支持的格式)。

这些配置设置中的一些显示在下面。

Matplotlib figure options Matplotlib图表选项

Matplotlib curves figure options Matplotlib的曲线图选项

关于导航和配置Matplotlib图的更多信息,请看官方的Matplotlib工具条文档

更新图

在应用程序中,你经常想要更新图中显示的数据,无论是为了响应用户的输入还是来自API的更新数据。在Matplotlib中,有两种方法来更新图,一种是

  1. 清除并重绘画布(更简单,但速度更慢),或者。
  2. 通过保持对绘图线的引用并更新数据。

如果性能对你的应用程序很重要,建议你采用后者,但第一种方式更简单。

清除和重绘

蟒蛇

import sys
import random
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt6 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure


class MplCanvas(FigureCanvas):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

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

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QtCore.QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]
        self.canvas.axes.cla()  # Clear the canvas.
        self.canvas.axes.plot(self.xdata, self.ydata, 'r')
        # Trigger the canvas to update and redraw.
        self.canvas.draw()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

在这个例子中,我们把绘图移到了一个update_plot 方法中,以保持其自成一体。在这个方法中,我们利用我们的ydata 数组,用[1:] 删除第一个值,然后附加一个0到10之间的新的随机整数。这就产生了将数据向左滚动的效果。

要重新绘制,我们只需调用axes.cla() 来清除坐标轴(整个画布),然后调用axes.plot(…) 来重新绘制数据,包括更新的值。然后,通过调用canvas.draw() ,将得到的画布重新绘制到小部件上。

update_plot 方法每100毫秒调用一次,使用的是QTimer 。清空-刷新方法的速度足够快,可以在这个速度下保持绘图的更新,但正如我们很快会看到的,随着速度的增加,会出现动摇。

就地重绘

就地更新绘图线所需的变化相当小,只需要增加一个变量来存储和检索对绘图线的引用。更新后的MainWindow 代码如下所示。

class MainWindow(QtWidgets.QMainWindow):

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

        self.canvas = MplCanvas(self, width=5, height=4, dpi=100)
        self.setCentralWidget(self.canvas)

        n_data = 50
        self.xdata = list(range(n_data))
        self.ydata = [random.randint(0, 10) for i in range(n_data)]

        # We need to store a reference to the plotted line
        # somewhere, so we can apply the new data to it.
        self._plot_ref = None
        self.update_plot()

        self.show()

        # Setup a timer to trigger the redraw by calling update_plot.
        self.timer = QtCore.QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.update_plot)
        self.timer.start()

    def update_plot(self):
        # Drop off the first y element, append a new one.
        self.ydata = self.ydata[1:] + [random.randint(0, 10)]

        # Note: we no longer need to clear the axis.
        if self._plot_ref is None:
            # First time we have no plot reference, so do a normal plot.
            # .plot returns a list of line <reference>s, as we're
            # only getting one we can take the first element.
            plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
            self._plot_ref = plot_refs[0]
        else:
            # We have a reference, we can use it to update the data for that line.
            self._plot_ref.set_ydata(self.ydata)

        # Trigger the canvas to update and redraw.
        self.canvas.draw()

首先,我们需要一个变量来保存我们要更新的绘图线的引用,这里我们称之为_plot_ref 。 我们用None 来初始化self._plot_ref ,这样我们就可以在以后检查它的值,以确定是否已经画好了线--如果值还是None ,我们还没有画好线。

T> 如果你要画多条线,你可能想用一个listdict 数据结构来存储多个引用并跟踪哪个是哪个。

最后,我们像之前那样更新ydata ,将其向左旋转并附加一个新的随机值。然后,我们要么-

  1. 如果self._plotrefNone (也就是说,我们还没有画线),就画线并将引用存储在self._plot_ref ,或者
  2. 在原地更新该线,调用self._plot_ref.set_ydata(self.ydata)

我们在调用.plot 时获得一个对所绘制的参考。然而,.plot 返回一个列表(以支持一个.plot 调用可以绘制多条线的情况)。在我们的例子中,我们只绘制一条线,所以我们只需要列表中的第一个元素--一个单一的Line2D 对象。为了把这个单一的值输入我们的变量,我们可以把它赋给一个临时变量plot_refs ,然后把第一个元素赋给我们的self._plot_ref

plot_refs = self.canvas.axes.plot(self.xdata, self.ydata, 'r')
self._plot_ref = plot_refs[0]

你也可以使用元组解包,从列表中的第一个(也是唯一的)元素中挑选出-----。

self._plot_ref, = self.canvas.axes.plot(self.xdata, self.ydata, 'r')

如果你运行结果代码,在这个速度下,这个方法和之前的方法在性能上没有明显的区别。然而,如果你试图更快地更新绘图(例如,下降到每10毫秒),你会开始注意到,清除绘图和重新绘制需要更长的时间,而且更新没有跟上计时器的速度。我们可以比较下面的两个版本--

都使用100毫秒的计时器,左边是清空重绘,右边是原地更新。

两者都使用10毫秒的定时器,左边是清空和重绘,右边是原地更新。

这种性能差异在你的应用中是否足够重要,取决于你正在构建的东西,并应与保持和管理绘图线的引用的额外复杂性相权衡。

从Pandas中嵌入图画

Pandas是一个专注于处理表(数据框)和系列数据结构的Python包,对于数据分析工作流程特别有用。它内置了对Matplotlib绘图的支持,在这里我们将快速浏览一下如何将这些绘图嵌入到PyQt6。有了这个,你就可以开始构建围绕Pandas的PyQt6数据分析应用程序了。

Pandas的绘图函数可以直接从DataFrame 对象中访问。这个函数的签名相当复杂,给出了很多选项来控制如何绘制图表。

DataFrame.plot(
    x=None, y=None, kind='line', ax=None, subplots=False,
    sharex=None, sharey=False, layout=None, figsize=None,
    use_index=True, title=None, grid=None, legend=True, style=None,
    logx=False, logy=False, loglog=False, xticks=None, yticks=None,
    xlim=None, ylim=None, rot=None, fontsize=None, colormap=None,
    table=False, yerr=None, xerr=None, secondary_y=False,
    sort_columns=False, **kwargs
)

我们最感兴趣的参数是ax ,它允许我们传入自己的matplotlib.Axes 实例,潘达斯将在该实例上绘制DataFrame

蟒蛇

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt6 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure

import pandas as pd


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

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

        # Create the maptlotlib FigureCanvas object,
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)

        # Create our pandas DataFrame with some simple
        # data and headers.
        df = pd.DataFrame([
           [0, 10], [5, 15], [2, 20], [15, 25], [4, 10],
        ], columns=['A', 'B'])

        # plot the pandas DataFrame, passing in the
        # matplotlib Canvas axes.
        df.plot(ax=sc.axes)

        self.setCentralWidget(sc)
        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

这里的关键步骤是在调用DataFrame上的绘图方法时传入画布轴,在行df.plot(ax=sc.axes) 。你可以使用同样的模式来随时更新绘图,不过请记住,Pandas会清除和重绘整个画布,这意味着它并不是高性能绘图的理想选择。

通过Pandas生成的绘图如下所示

Pandas plot embedded in PyQt6 嵌入PyQt6的Pandas绘图

就像以前一样,你可以将Matplotlib工具栏和控制支持添加到使用Pandas生成的绘图中,允许你缩放/平移和实时修改它们。下面的代码结合了我们之前的工具栏例子和Pandas的例子。

蟒蛇

import sys
import matplotlib
matplotlib.use('Qt5Agg')

from PyQt6 import QtCore, QtWidgets

from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg, NavigationToolbar2QT as NavigationToolbar
from matplotlib.figure import Figure

import pandas as pd


class MplCanvas(FigureCanvasQTAgg):

    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)
        super(MplCanvas, self).__init__(fig)


class MainWindow(QtWidgets.QMainWindow):

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

        # Create the maptlotlib FigureCanvas object,
        # which defines a single set of axes as self.axes.
        sc = MplCanvas(self, width=5, height=4, dpi=100)

        # Create our pandas DataFrame with some simple
        # data and headers.
        df = pd.DataFrame([
           [0, 10], [5, 15], [2, 20], [15, 25], [4, 10],
        ], columns=['A', 'B'])

        # plot the pandas DataFrame, passing in the
        # matplotlib Canvas axes.
        df.plot(ax=sc.axes)

        # Create toolbar, passing canvas as first parament, parent (self, the MainWindow) as second.
        toolbar = NavigationToolbar(sc, self)

        layout = QtWidgets.QVBoxLayout()
        layout.addWidget(toolbar)
        layout.addWidget(sc)

        # Create a placeholder widget to hold our toolbar and canvas.
        widget = QtWidgets.QWidget()
        widget.setLayout(layout)
        self.setCentralWidget(widget)
        self.show()


app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
app.exec()

运行它,你应该看到以下窗口,显示一个嵌入PyQt6的Pandas图和Matplotlib工具条。

Pandas plot with Matplotlib toolbar 带有Matplotlib工具条的Pandas绘图

接下来的内容

在本教程中,我们研究了如何在PyQt6应用程序中嵌入Matplotlib绘图。能够在你的应用程序中使用Matplotlib绘图,使你能够从Python中创建自定义的数据分析和可视化工具。

Matplotlib是一个巨大的库,在这里无法详细介绍。如果你不熟悉Matplotlib绘图,并想尝试一下,请看一下文档示例图,看看有什么可能。如果你熟悉它,你现在应该可以在你的PyQt6应用程序中使用这些技能