在之前的教程中,我们介绍了在PyQt6中使用PyQtGraph进行绘图。PyQtGraph使用基于Qt矢量的QGraphicsScene来绘制绘图,并为交互式和高性能的绘图提供了一个很好的界面。
然而,还有另一个Python的绘图库,它被更广泛地使用,并提供了更丰富的绘图种类 -Matplotlib。如果你正在将一个现有的数据分析工具迁移到Python GUI中,或者你只是想获得Matplotlib提供的一系列绘图能力,那么你会想知道如何在你的应用程序中包含Matplotlib的绘图。
在本教程中,我们将介绍如何在你的PyQt应用程序中嵌入Matplotlib绘图。
许多其他的Python库--比如seaborn和pandas--利用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)。
嵌入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绘图
NavigationToolbar2QT 所提供的按钮可以实现以下操作
- 主页、后退/前进、平移和缩放,用于在绘图中导航。后退/前进按钮可以通过导航步骤向前和向后移动,例如,放大后点击后退将返回到之前的缩放。原点返回到绘图的初始状态。
- 绘图边缘/位置配置,可以在窗口内调整绘图。
- 轴/曲线样式编辑器,可以修改绘图标题和轴的比例,以及设置绘图线的颜色和线条样式。颜色选择使用平台默认的颜色选择器,允许选择任何可用的颜色。
- 保存,将生成的图形保存为图像(所有Matplotlib支持的格式)。
这些配置设置中的一些显示在下面。
Matplotlib图表选项
Matplotlib的曲线图选项
关于导航和配置Matplotlib图的更多信息,请看官方的Matplotlib工具条文档。
更新图
在应用程序中,你经常想要更新图中显示的数据,无论是为了响应用户的输入还是来自API的更新数据。在Matplotlib中,有两种方法来更新图,一种是
- 清除并重绘画布(更简单,但速度更慢),或者。
- 通过保持对绘图线的引用并更新数据。
如果性能对你的应用程序很重要,建议你采用后者,但第一种方式更简单。
清除和重绘
蟒蛇
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> 如果你要画多条线,你可能想用一个list 或dict 数据结构来存储多个引用并跟踪哪个是哪个。
最后,我们像之前那样更新ydata ,将其向左旋转并附加一个新的随机值。然后,我们要么-
- 如果
self._plotref是None(也就是说,我们还没有画线),就画线并将引用存储在self._plot_ref,或者 - 在原地更新该线,调用
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生成的绘图如下所示
嵌入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工具条。
带有Matplotlib工具条的Pandas绘图
接下来的内容
在本教程中,我们研究了如何在PyQt6应用程序中嵌入Matplotlib绘图。能够在你的应用程序中使用Matplotlib绘图,使你能够从Python中创建自定义的数据分析和可视化工具。
Matplotlib是一个巨大的库,在这里无法详细介绍。如果你不熟悉Matplotlib绘图,并想尝试一下,请看一下文档和示例图,看看有什么可能。如果你熟悉它,你现在应该可以在你的PyQt6应用程序中使用这些技能了