Skip to content

Python 的主要优势之一在于数据科学和可视化,它使用 Pandas、numpy 和 sklearn 等工具进行数据分析。使用 PyQt6 构建图形用户界面应用程序,您可以直接从应用程序中访问所有这些 Python 工具,从而构建复杂的数据驱动型应用程序和交互式仪表板。我们已经介绍了模型视图,它允许我们以列表和表格的形式显示数据。在本章中,我们将探讨这个难题的最后一块拼图——可视化数据。

在使用 PyQt6 开发应用程序时,您有两个主要选择——matplotlib(它也提供对 Pandas 图表的访问权限)和 PyQtGraph,后者使用 Qtnative 图形创建图表。在本章中,我们将探讨如何利用这些库在您的应用程序中可视化数据。

29. 使用 PyQtGraph 进行数据可视化

虽然您可以在 PyQt6 中嵌入 matplotlib 图表,但使用体验并不完全原生。对于简单且高度交互的绘图,您可能需要考虑使用 PyQtGraph 代替。PyQtGraph 基于 PyQt6 本机 QGraphicsScene 构建,可提供更好的绘图性能,特别是对于实时数据,同时提供交互性,并能够使用 Qt 图形控件轻松自定义绘图。

在本章中,我们将介绍使用 PyQtGraph 创建绘图控件的第一步,然后演示如何使用线条颜色、线条类型、轴标签、背景颜色和绘制多条线条来定制绘图。

开始使用

要使用 PyQtGraph 与 PyQt6,您首先需要将该包安装到您的 Python 环境中。您可以使用 pip 进行安装。

撰写本文时,PyQt6 还非常新,因此您需要使用 PyQtGraph 的开发者安装版本。

bash
pip install git+https://github.com/pyqtgraph/pyqtgraph@master

安装完成后,您应该能够像往常一样导入该模块。

创建 PyQtGraph 控件

在 PyQtGraph 中,所有图都是使用 PlotWidget 控件创建的。该控件提供了一个包含的画布,可以在上面添加和配置任何类型的图。在后台,该图控件使用 Qt 本地的 QGraphicsScene,这意味着它快速、高效且易于与应用程序的其他部分集成。您可以像创建其他控件一样创建 PlotWidget

以下是基本模板应用程序的示例,该应用程序在一个 QMainWindow 中包含一个 PlotWidget

information

在以下示例中,我们将创建 PyQtGraph 控件。但是,您也可以从 Qt Designer 中嵌入 PyQtGraph 控件。

Listing 220. plotting/pyqtgraph_1.py

python
import sys

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        hour = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
        
        # 绘制数据:x、y 值
        self.graphWidget.plot(hour, temperature)
        
        
app = QtWidgets.QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

tips

在以下所有示例中,我们使用 import pyqtgraph as pg 导入 PyQtGraph。这是 PyQtGraph示例中常见的约定,旨在保持代码整洁并减少重复输入。如果您更喜欢,也可以使用 import pyqtgraph 进行导入。

Figure216

图216:显示虚拟数据的自定义 PyQtGraph 控件。

PyQtGraph 的默认绘图样式非常简单——黑色背景,一条细细的(几乎看不见的)白色线条。在下一节中,我们将看看 PyQtGraph 中有哪些可用的选项,以改善绘图的外观和可用性。

样式绘制

PyQtGraph 使用 Qt 的 QGraphicsScene 来绘制图形。这使我们能够访问所有标准的 Qt 线条和形状样式选项,以用于绘制图。然而,PyQtGraph 提供了一个 API,用于绘制图和管理图画布。

下面我们将介绍创建和自定义自己的图所需的最常见的样式功能。

背景颜色

从上面的应用程序骨架开始,我们可以更改背景颜色,方法是调用 PlotWidget 实例(在 self.graphWidget 中)的 .setBackground 方法。下面的代码将背景设置为白色,方法是传入字符串 ‘w’。

python
self.graphWidget.setBackground('w')

您可以随时设置(并更新)图表的背景颜色。

Listing 221. plotting/pyqtgraph_2.py

python
import sys

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        hour = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
        
        self.graphWidget.setBackground("w")
        self.graphWidget.plot(hour, temperature)
        
        
app = QtWidgets.QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Figure217

图217:将PyQtGraph的绘图背景改为白色

使用单个字母可以生成多种简单颜色,这些颜色基于 Matplotlib 中使用的标准颜色。它们大多较为常见,唯一例外的是'k'用于表示黑色。

Table 7. Common color codes

颜色字母代号
blueb
greeng
redr
cyan (明亮的蓝绿色)c
magenta (亮粉色)m
yellowy
blackk
whitew

除了这些单字母代码外,您还可以使用十六进制表示法设置颜色例如,使用字符串 #672922

python
self.graphWidget.setBackground('#bbccaa') # 十六进制

RGB 和 RGBA 值可以分别作为 3 元组或 4 元组传递,使用0-255 的值。

python
self.graphWidget.setBackground((100,50,255)) # RGB each 0-255
self.graphWidget.setBackground((100,50,255,25)) # RGBA (A = alpha opacity)

最后,您还可以直接使用Qt的 QColor类型来指定颜色。

python
self.graphWidget.setBackground(QtGui.QColor(100,50,254,25))

如果您在应用程序的其他地方使用特定的 QColor 对象,或者将图形的背景设置为图形用户界面的默认背景颜色,此功能非常有用。

python
color = self.palette().color(QtGui.QPalette.Window) # 获取默认窗口背景,
self.graphWidget.setBackground(color)

线条颜色、宽度和样式

PyQtGraph 中的线条使用标准的 Qt QPen 类型绘制。这使您能够像在任何其他 QGraphicsScene 绘图中一样,对线条绘制拥有完全的控制权。要使用笔来绘制线条,您只需创建一个新的 QPen 实例,并将其传递给 plot 方法。

下面我们创建一个 QPen 对象,传入一个包含三个整数值的元组,指定一个RGB 值(全红)。我们也可以通过传入 ‘r’ 或一个 QColor 对象来定义它。然后我们将此对象传入 plot 函数的 pen 参数中。

python
pen = pg.mkPen(color=(255, 0, 0))
self.graphWidget.plot(hour, temperature, pen=pen)

完整的代码如下所示:

python
import sys

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        hour = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
        
        self.graphWidget.setBackground("w")
        
        pen = pg.mkPen(color=(255, 0, 0))
        self.graphWidget.plot(hour, temperature, pen=pen)
        
        
app = QtWidgets.QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Figure218

图218:更改线条颜色

通过更改 QPen 对象,我们可以更改线条的外观,包括使用标准 Qt 线条样式更改线条的像素宽度和样式(虚线、点线等)。例如,以下示例创建了一条 15 像素宽的红色虚线。

python
pen = pg.mkPen(color=(255, 0, 0), width=15, style=QtCore.Qt.PenStyle.DashLine)

结果如下所示,显示一条15像素的红色虚线。

Figure219

图219:更改线宽和样式。

您可以使用所有标准的 Qt 线条样式,包括 Qt.PenStyle.SolidLineQt.PenStyle.DashLineQt.PenStyle.DotLineQt.PenStyle.DashDotLineQt.PenStyle.DashDotDotLine。下图显示了这些线条的示例,您可以在 Qt 文档 中阅读更多相关内容。

线条标记器

对于许多图表,在图表上添加标记(而非或替代线条)可能会有帮助。要在图表上绘制标记,可在调用 .plot 时传入要用作标记的符号,如下所示:

python
self.graphWidget.plot(hour, temperature, symbol='+')

除了 spmbol 外,您还可以传递符号大小(symbolSize)、符号画笔(symbolBrush)和符号钢笔(symbolPen)参数。传递给 symbolBrush 的值可以是任何颜色,或 QBrush 类型,而符号钢笔可以传递任何颜色或 QPen 实例。钢笔用于绘制形状的轮廓,而画笔用于填充。

例如,下面的代码将生成一个大小为30的蓝色十字标记,位于一条粗厚的红色线条上。

python
pen = pg.mkPen(color=(255, 0, 0), width=15, style=QtCore.Qt.PenStyle.DashLine)
self.graphWidget.plot(hour, temperature, pen=pen, symbol='+',symbolSize=30, symbolBrush=('b'))

Figure220

图220:每个数据点上都显示了符号。

除了 + 图标标记外,PyQtGraph还支持以下标准标记,如表中所示。这些标记均可按相同方式使用。

变量标记的类型
o圆形
s方形
t三角形
d菱形
+交叉点

tips

如果您有更复杂的需求,还可以传入任何 QPainterPath 对象,从而能够绘制完全自定义的标记形状。

图表标题

图表标题对于提供图表所展示内容的上下文非常重要。在PyQtGraph中,您可以通过调用 PlotWidget 上的 setTitle() 方法,并传入标题字符串,来添加主图表标题。

python
self.graphWidget.setTitle("Your Title Here")

您可以通过传递附加参数来为标题(以及 PyQtGraph 中的任何其他标签)应用文本样式,包括颜色、字体大小和粗细。可用的样式参数如下所示:

样式类型
color(str) e.g. CCFF00
size(str) e.g. 8pt
bold(bool) True or False
italic

下面的代码将颜色设置为蓝色,字体大小为30pt。

python
self.graphWidget.setTitle("Your Title Here", color="b", size="30pt")

如果您愿意,还可以使用 HTML 标签语法来设置标题样式,尽管这样会降低可读性。

python
self.graphWidget.setTitle("<span style=\"color:blue;font-size:30pt\">Your Title Here</span>")

Figure221

图221:带有样式标题的图例

坐标轴标题

与标题类似,我们可以使用 setLabel() 方法来创建轴标题。此方法需要两个参数:位置和文本。位置可以是 leftrighttopbottom 中的任意一个,用于描述轴上文本的位置。第二个参数文本是您希望用于标签的文本。

您可以将额外的样式参数传递给该方法。这些参数与标题的参数略有不同,因为它们需要是有效的 CSS 名称-值对。例如,大小现在是 font-size。由于名称 font-size 中包含连字符,因此您不能将其直接作为参数传递,而必须使用 **dictionary 方法

python
styles = {'color':'r', 'font-size':'30pt'}
self.graphWidget.setLabel('left', 'Temperature (°C)', **styles)
self.graphWidget.setLabel('bottom', 'Hour (H)', **styles)

这些也支持 HTML 语法,您可以自由选用。

python
self.graphWidget.setLabel('left', "<span style=\"color:red;fontsize:30px\">Temperature (°C)</span>")
self.graphWidget.setLabel('bottom', "<span style=\"color:red;fontsize:30px\">Hour (H)</span>")

Figure222

图222:自定义样式的坐标轴标签

图例

除了坐标轴和图例标题外,您通常还希望显示一个图例,用于标识特定线条所代表的内容。这在您开始向图表中添加多条线条时尤为重要。向图表添加图例可以通过调用 PlotWidget.addLegend 方法实现,但在此之前,您需要在调用 .plot() 方法时为每条线条提供一个名称。

下面的示例将我们使用 .plot() 绘制的线条命名为“Sensor 1”。该名称将在图例中用于标识该线条。

python
self.graphWidget.plot(hour, temperature, name = "Sensor 1", pen = NewPen, symbol='+', symbolSize=30, symbolBrush=('b'))
self.graphWidget.addLegend()

Figure223

图223:带有图例的图表,显示单个项目。

information

图例默认显示在左上角。如果您希望移动图例,可以轻松地将图例拖动到其他位置。您还可以通过在创建图例时将一个2元组传递给 offset 参数来指定默认偏移量。

背景网格

添加背景网格可以使您的图表更易于阅读,尤其是在试图比较相对的 x 和 y 值时。您可以通过调用 PlotWidget 上的 .showGrid 方法来启用图表的背景网格。您可以独立地切换 x 和 y 网格。

以下内容将为X轴和Y轴创建网格。

python
self.graphWidget.showGrid(x=True, y=True)

Figure224

图224:背景网格

设置坐标轴的范围

有时,限制在图表上显示的数据范围,或将坐标轴锁定在固定范围(例如已知的最小值和最大值范围)可能是有用的。在 PyQtGraph 中,可以通过调用 .setXRange().setYRange() 方法实现这一点。这些方法会强制图表仅显示每个坐标轴上指定范围内的数据。

下面我们设置两个范围,每个轴一个。第一个参数是最小值,第二个参数是最大值。

python
self.graphWidget.setXRange(5, 20, padding=0)
self.graphWidget.setYRange(30, 40, padding=0)

可选的填充参数会使范围设置得比指定的范围更大,具体取决于指定的百分比(默认值在0.02到0.1之间,具体取决于视图框的大小)。如果您想完全移除这个填充,请传入0。

python
self.graphWidget.setXRange(5, 20, padding=0)
self.graphWidget.setYRange(30, 40, padding=0)

到目前为止的完整代码如下所示:

python
import sys

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        hour = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
        
        # 将背景颜色设置为白色
        self.graphWidget.setBackground("w")
        
        # 添加标题
        self.graphWidget.setTitle(
            "Your Title Here", color="b", size="30pt"
        )
        # 添加坐标轴标题
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **
                                  styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        # 添加图例
        self.graphWidget.addLegend()
        # 添加背景网格
        self.graphWidget.showGrid(x=True, y=True)
        # 设置范围
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)
        
        pen = pg.mkPen(color=(255, 0, 0))
        self.graphWidget.plot(
            hour,
            temperature,
            name="Sensor 1",
            pen=pen,
            symbol="+",
            symbolSize=30,
            symbolBrush=("b"),
        )    
        
        
app = QtWidgets.QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Figure225

图225:限制轴的范围。

绘制多条线

绘图通常涉及多条线。在 PyQtGraph 中,这就像在同一个 PlotWidget 上多次调用 .plot() 一样简单。在以下示例中,我们将绘制两条类似的数据线,每条线使用相同的线样式、厚度等,但更改线颜色。

为了简化这一过程,我们可以在 MainWindow 上创建自己的自定义绘图方法。该方法接受用于绘图的 xy 参数、用于图例的线条名称以及颜色参数。我们使用该颜色同时设置线条和标记的颜色。

python
    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(x, y, name=plotname, pen=pen, symbol=
                              '+', symbolSize=30, symbolBrush=(color))

要绘制单独的线条,我们将创建一个名为 temperature_2 的新数组,并用与 temperature(现在为 temperature_1 )类似的随机数填充它。将这些线条并排绘制,我们可以将它们进行比较。现在,您可以调用 plot 函数两次,这将在图表上生成两条线。

python
self.plot(hour, temperature_1, "Sensor1", 'r')
self.plot(hour, temperature_2, "Sensor2", 'b')

Listing 224. plotting/pyqtgraph_5.py

python
import sys

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        hour = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
        temperature = [30, 32, 34, 32, 33, 31, 29, 32, 35, 45]
        
        # 将背景颜色设置为白色
        self.graphWidget.setBackground("w")
        
        # 添加标题
        self.graphWidget.setTitle(
            "Your Title Here", color="b", size="30pt"
        )
        # 添加坐标轴标题
        styles = {"color": "#f00", "font-size": "20px"}
        self.graphWidget.setLabel("left", "Temperature (°C)", **
                                  styles)
        self.graphWidget.setLabel("bottom", "Hour (H)", **styles)
        # 添加图例
        self.graphWidget.addLegend()
        # 添加背景网格
        self.graphWidget.showGrid(x=True, y=True)
        # 设置范围
        self.graphWidget.setXRange(0, 10, padding=0)
        self.graphWidget.setYRange(20, 55, padding=0)
        
        self.plot(hour, temperature_1, "Sensor1", "r")
        self.plot(hour, temperature_2, "Sensor2", "b")
        
    def plot(self, x, y, plotname, color):
        pen = pg.mkPen(color=color)
        self.graphWidget.plot(
            x,
            y,
            name=plotname,
            pen=pen,
            symbol="+",
            symbolSize=30,
            symbolBrush=(color),
        )
        
        
app = QtWidgets.QApplication(sys.argv)
main = MainWindow()
main.show()
app.exec()

Figure226

图226:包含两条线的图表

tips

您可以尝试使用此功能,自定义您的标记、线条宽度、颜色及其他参数。

清空图表

最后,有时您可能需要定期清除并刷新绘图。您可以通过调用 .clear() 方法轻松实现这一点。

python
self.graphWidget.clear()

这将从图中删除线条,但保持所有其他属性不变。

更新图表

虽然您可以简单地清除绘图区域并重新绘制所有元素,但这意味着Qt必须销毁并重新创建所有QGraphicsScene 对象。对于小型或简单的图形,这可能不会被注意到,但如果您想创建高性能的流式图形,最好直接更新数据。PyQtGraph 会获取新数据并更新绘制的线条以匹配,而不会影响图形中的其他元素。

要更新一条线,我们需要获取该线对象的引用。该引用在首次使用 .plot 方法创建线时返回,我们可以将其存储在变量中。请注意,这是对线对象的引用,而非对绘图的引用。

python
my_line_ref = graphWidget.plot(x, y)

一旦我们获得了引用,更新图表只需调用 .setData方法,将新数据应用到该引用即可。

Listing 225. plotting/pyqtgraph_6.py

python
import sys
from random import randint

from PyQt6 import QtWidgets
import pyqtgraph as pg # 在导入Qt之后导入PyQtGraph


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        
        self.graphWidget = pg.PlotWidget()
        self.setCentralWidget(self.graphWidget)
        
        self.x = list(range(100)) # 100个时间点
        self.y = [
            randint(0, 100) for _ in range(100)
        ] # 100个数据点
        
        self.graphWidget.setBackground("w")
        
        pen = pg.mkPen(color=(255, 0, 0))
        self.data_line = self.graphWidget.plot(
            self.x, self.y, pen=pen
        ) #1
        
        self.timer = QtCore.QTimer()
        self.timer.setInterval(50)
        self.timer.timeout.connect(self.update_plot_data)
        self.timer.start()
        
    def update_plot_data(self):
        self.x = self.x[1:] # 移除第一个 y 元素.
        self.x.append(
            self.x[-1] + 1
        ) # 添加一个比上一个值大1的新值.
        self.y = self.y[1:] # 删除第一个
        self.y.append(randint(0, 100)) # 添加一个新的随机值.
        self.data_line.setData(self.x, self.y) # 更新数据.
  1. 这里我们引用了之前绘制的线,并将它存储为 self.data_line

我们使用 QTimer 每 50 毫秒更新一次数据,将触发器设置为调用自定义的槽方法 update_plot_data,在那里我们将更改数据。我们在 __init__ 块中定义此计时器,因此它会自动启动。

如果运行该应用程序,您应该会看到一个图表,其中随机数据快速向左滚动,X 值也会同步更新并滚动,仿佛在流式传输数据。您可以用自己的真实数据替换随机数据,例如从实时传感器读数或 API 中获取。PyQtGraph 性能足够强大,可以支持使用此方法创建多个图表。

总结

在本章中,我们学习了如何使用 PyQtGraph 绘制简单的图表,并自定义线条、标记和标签。要全面了解 PyQtGraph 的方法和功能,请参阅 PyQtGraph文档及API参考PyQtGraph在GitHub上的仓库 中还包含了一系列更复杂的示例图表,这些示例位于Plotting.py文件中(如下所示)。

Figure227

图227:示例图表摘自PyQtGraph文档。