如果想在桌面应用里画实时数据曲线,又不想被 matplotlib 那种几十毫秒才刷新一帧的龟速拖垮,PyQtGraph 基本是绕不开的选择。这个库在科研仪器控制、传感器数据采集、上位机开发圈子里用得非常频繁,我最早接触它是在做一个多通道生理信号采集的模拟项目,当时拿 matplotlib 刷新 4 条波形,CPU 直接飙到 60% 以上,换 PyQtGraph 之后降到 5% 左右,帧率还翻了好几倍,从那以后就再也没回头。
这篇东西不是官方文档的翻译,而是我在实际项目里把 PyQtGraph 从入门到进阶整个摸了一遍之后,把核心原理、操作细节、踩过的坑全部整理出来。无论你是刚接触 PyQt 的新手,还是已经在用 matplotlib 做可视化、想换更快的方案,都可以照着这篇文章一步步搭出属于自己的实时数据监控界面。
1. PyQtGraph 到底是什么,它解决了什么问题
PyQtGraph 是一个基于 PyQt/PySide 的纯 Python 图形库,核心目标就是高性能的实时数据可视化。它底层直接依赖 Qt 的 Graphics View 框架,同时把大量热点计算丢给 NumPy 数组运算,绘制路径在合适条件下还会走 OpenGL 管线,所以才能做到实时刷新不卡顿。
1.1 和 matplotlib 相比,它强在哪里
matplotlib 对科学出版级的静态图很友好,但它的架构决定了它不适合做高频交互界面。它每次重绘都要重新创建大量图形对象,坐标轴、刻度、标签全走一遍,这个开销在实时数据流面前就是灾难。PyQtGraph 的思路是:能复用的对象绝不重建,能批量更新的数据绝不逐个赋值。你把 PlotDataItem 创建一次,之后只是替换它的 setData 里的数组,底层会尽量只重绘发生变化的区域,这就是它性能优势的根本来源。
我个人的实测结果,同一台机器上绘制 1 万个点的滚动波形,matplotlib 刷新一次大概 80~120 毫秒,PyQtGraph 能跑到 10 毫秒以内。如果开启 OpenGL 加速,甚至能达到 3~5 毫秒。这不是说 matplotlib 烂,而是两者的设计目标本来就不同。
1.2 典型应用场景和适合人群
PyQtGraph 最常用的地方是这几类:
- 仪器仪表上位机:示波器、光谱仪、温控设备的数据实时显示
- 传感器采集系统:温度、压力、振动、电流电压等多通道波形
- 生物医学信号处理:心电、脑电、肌电信号的可视化回放
- 机器人调试:关节角度、力矩、轨迹数据的实时监控
- 量化交易:行情走势、策略回测结果的快速绘图
适合用它的人也很明确:你已经在用 PyQt/PySide 做桌面应用,又需要高性能绘图能力,不希望再引入一个重型依赖。它本身是 MIT 协议,商用也没有顾虑。
1.3 和 PyQtGraph 配合的生态选型
PyQtGraph 不是孤立的,它和 PyQt5/PyQt6/PySide2/PySide6 都兼容。这里要提一个选型上的关键点:PyQtGraph 官方维护支持 PySide6 的力度在不断加大,新版本对 pyqtgraph 的 Qt 绑定已经做到了自动识别。你在写代码时用import pyqtgraph as pg,它内部会自动帮你决定用当前环境里的哪个绑定库。
从我自己踩坑经验来看,如果新项目从零开始,优先推荐 PySide6。原因有二:LGPL 协议更友好(PyQt 的 GPL 对商用有一些额外要求),官方文档和示例也逐渐向 PySide6 倾斜。当然你如果已经有一套 PyQt5 的老项目,PyQtGraph 依然可以无缝接入,不需要改绑定库。
2. 环境准备与第一个 PyQtGraph 窗口
用 PyQtGraph 之前,先把环境理清楚。很多人一开始就卡在安装阶段,所以这里单独聊一聊依赖关系、安装方式、还有最基础的窗口生成。
2.1 依赖清单与推荐版本组合
PyQtGraph 的核心依赖只有三个:Python(3.8 以上都行)、NumPy、以及任意一套 Qt 绑定库。如果要开启 OpenGL 加速,还需要安装PyOpenGL。实际项目里我推荐把以下组合作为基准:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.9~3.12 | 太老的版本对新 NumPy 支持差 |
| NumPy | 1.24 以上 | PyQtGraph 内部大量依赖 NumPy 数组操作 |
| PySide6 | 6.5 以上 | 最推荐,协议友好且维护活跃 |
| PyQtGraph | 0.13.7 及以上 | 新版对 PySide6 支持非常完善 |
| PyOpenGL | 3.1.7 以上 | 仅 OpenGL 加速需要 |
安装方式非常直接。如果你用 PySide6,一条命令就能搞定:
pip install pyqtgraph PySide6 PyOpenGL如果只是学习,不想装 PySide 相关的完整环境,也可以直接用pip install pyqtgraph pyqt5,两个都行。我自己习惯建独立的虚拟环境来装,避免全局环境里 qt 依赖冲突。你如果管理多个 Python 项目,这一步千万别省。
2.2 5 分钟写出第一个实时窗口
先来一个最基础的静态图窗口,把流程跑通:
import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets app = QtWidgets.QApplication([]) win = pg.GraphicsLayoutWidget(show=True, title="Basic PyQtGraph") win.resize(800, 500) plot = win.addPlot(title="简单曲线") plot.plot([1, 2, 3, 4, 5], [3, 1, 4, 2, 6], pen='r') QtWidgets.QApplication.instance().exec()这段代码干了四件事:创建 QApplication、创建 GraphicsLayoutWidget 窗口、在里面加一个 Plot 对象、直接绘制一条红色折线。注意pg.plot和win.addPlot是两条完全不同的路径——前者是便捷函数,后者的控件可以在复杂的窗口布局里自由排列。
从 Qt 事件循环的角度讲,exec()是必须的。没有它窗口会一闪而过。如果你是在 Jupyter Notebook 里跑,需要加上%gui qt或者在创建 QApplication 前设置环境变量。
2.3 理解 GraphicsLayoutWidget 和 PlotItem 的关系
用 PyQtGraph 容易犯的第一个错误就是分不清 Widget 和 Item。Qt 原生概念里,Widget 是有实际窗口句柄的控件,Item 是绘制在场景里的图形对象。PyQtGraph 的GraphicsLayoutWidget是一个 Widget,它的作用相当于一个画板;而PlotItem是 Item,承载坐标轴、数据曲线、图例等所有绘图元素。你在GraphicsLayoutWidget上调用addPlot(),本质上是往画布里添加了一个绘图区域,之后可以继续用addLayout()添加其它区域,做多面板布局。
win = pg.GraphicsLayoutWidget() p1 = win.addPlot(row=0, col=0) # 左上 p2 = win.addPlot(row=1, col=0) # 左下 p3 = win.addPlot(row=0, col=1, rowspan=2) # 右侧跨两行这样就能很轻松地搭建一个 2x2 的自定义布局,这在多通道数据展示里特别常用。
3. 核心绘图功能拆解:从曲线到图像
PyQtGraph 支持的图形类型非常多,但实际项目里高频用到的就几类:折线图、散点图、条形图、图像热力图、等值线图。下面逐一说明它们的核心用法和不为人注意的细节。
3.1 折线图和 PlotDataItem 的正确用法
折线图是最基础也是最常用的功能。官方推荐的方式是把数据包装成PlotDataItem,因为这样后续更新数据可以只修改内部数据源,不需要从场景里删除重建:
curve = plot.plot(pen=pg.mkPen(color=(255, 100, 100), width=2)) curve.setData(y_data, x_data)一个小细节:setData的第一个参数是 y,第二个是 x。很多从 matplotlib 转过来的人会习惯写x, y,结果图像完全错乱。PyQtGraph 的函数签名是setData(y, x),plot()同样如此,这一点非常坑,但文档里其实写得很清楚。
另外如果数据量很大(超过几万个点),建议开启clipToView选项,让 Qt 只绘制视口范围内的点:
curve.setClipToView(True)这样缩放时性能提升非常明显。
3.2 散点图和分布显示
散点图在 PyQtGraph 里对应ScatterPlotItem。它和折线图最大的区别是每个点都可以独立设置位置、大小、颜色和形状。
scatter = pg.ScatterPlotItem( x=x_data, y=y_data, size=10, pen=pg.mkPen(None), brush=pg.mkBrush(0, 0, 255, 120) ) plot.addItem(scatter)这里有个重要技巧:把pen设为None可以取消散点描边,让整体视觉更干净。brush的第四个参数是透明度,适合大量点重叠时体现密度分布。如果你要做一个聚类结果的二维散点图,不同类别的点可以用不同颜色,PyQtGraph 性能足以轻松应对几万个散点。
3.3 条形图和柱状数据
条形图在 PyQtGraph 里的实现方式是BarGraphItem。注意它不接受x和y数组,而是需要x0/width/height或者x/y这种形式。赋值有点绕,但搞明白一次就够了:
bg = pg.BarGraphItem( x=[0, 1, 2, 3, 4], height=[1.2, 3.4, 2.1, 4.5, 0.9], width=0.6, brush='g' ) plot.addItem(bg)这里 x 表示柱的中心位置,width 决定柱子宽度。如果你需要绘制实时更新的柱状频谱,每帧调用setOpts去更新 height 即可,效率也很高。
3.4 图像与热力图显示
医疗影像和热力图的实时显示是 PyQtGraph 的强项,它通过ImageView控件来承载图像数据。
img_view = pg.ImageView() img_view.setImage(data_2d) # data_2d 是一个 numpy 2D 数组ImageView提供了内置的直方图控件,可以直接调整亮度和对比度。它还支持setImage的第 3 个参数autoLevels=False,这在处理连续帧时非常重要——如果不关闭自动色阶,每一帧的亮度都会重新统计,动态影像会闪烁。实际项目中我通常这样做:
img_view.setImage(new_frame, autoLevels=False)如果你不理解autoLevels的含义,可以类比手机相机的 HDR:它每张照片都会重新调整曝光。但视频场景下每帧都自动调整,画面就会狂闪。所以连续帧显示时一定要统一色阶范围。
3.5 等值线图和三维表面显示
对科研用户来说,等值线图也很常用。pg.isocurve()可以提供等值线数据,但更常见的是借助gaussianFilter平滑后再画。PyQtGraph 自带SurfacePlot之类的高级功能,但实际绘制三维曲面更多依赖GLSurfacePlotItem:
from pyqtgraph.opengl import GLSurfacePlotItem这块需要比较完整的 OpenGL 环境,新手可以暂时跳过,知道有这个能力就行。
4. 实时数据刷新机制:性能的核心
实时数据可视化是最多人关心的地方。这一部分我详细展开数据更新的原理、几种更新策略、以及如何减少 UI 卡顿。
4.1 为什么 setData 比 addItem 高效得多
PyQtGraph 里最影响性能的,就是你的数据更新方式。假如你用一个 Timer 每 30 毫秒向场景里addItem一个新的曲线,那 Qt 场景会不断膨胀,最终卡成幻灯片。正确的做法是创建一个PlotDataItem,然后在 timer 里反复调用它的setData方法。因为setData底层不会重建整套绘图对象,只是把内部数组的引用换掉,并且把重绘范围限制在有变化的区域。
这里有确定性结论:在绝大多数实时场景下,我们应该只做一次plot()创建,之后的每个更新周期只做setData。这是从 matplotlib 转型过来的人最容易犯的第一个错误,必须改掉。
4.2 定时器还是 QThread,选哪个
实时数据的来源不一样,更新方式也该不一样。我根据自己的经验整理成下表,你可以直接对着抄:
| 数据来源 | 推荐更新方式 | 优点 |
|---|---|---|
| 模拟数据、定时生成 | QTimer | 实现简单,开销小 |
| 串口/网络流 | QThread + Signal | 不阻塞 UI,数据到达即刷新 |
| 高频采集(>100Hz) | 数据缓冲+批量更新 | 避免每帧都触发重绘 |
用 QTimer 时注意一点:QTimer最小间隔受操作系统调度影响,实际精度远达不到标称值。如果你看到曲线抖动,不要先用「算法问题」去排查,先确认是不是定时器抖动导致的。
timer = QtCore.QTimer() timer.timeout.connect(update_plot) timer.start(33) # 约 30 fps4.3 应用缓冲批量更新提升帧率
如果你的数据显示频率非常高,比如采集卡输出每秒 5000 个点,完全没必要每个点都刷新一次 UI。常见做法是缓冲区攒一批数据,达到一定数量或一定时间间隔后再一次性更新。我一般使用 numpy 数组作为环形缓冲区,保持固定大小,每次只替换头部,这样标记为新数据然后整体 setData。
BUFFER_SIZE = 2000 data_buf = np.zeros(BUFFER_SIZE) idx = 0 def on_new_value(v): global idx data_buf[idx % BUFFER_SIZE] = v idx += 1 if idx % 50 == 0: # 累计 50 个点刷新一次 display_curve.setData(data_buf)这种批量更新的方式原理解释起来也很直观:UI 重绘本身有固定开销,你把这笔开销从「每个数据点付一次」变成「每批数据点付一次」,自然省下了大量 CPU。做数据可视化性能优化,核心就是尽量降低重绘次数,而不是让一次重绘更快。
4.4 开启 OpenGL 加速的注意事项
PyQtGraph 里开 OpenGL 很简单,在创建窗口后设置PlotItem的渲染模式即可:
plot.setUseOpenGL(True)不是所有绘图元素都支持 OpenGL。折线图支持得最好,散点图支持一般,绘图项和一些高档功能可能退化。开启 OpenGL 之后不要急着高兴,先对比实际帧率,确定是否真的更快——在某些老旧显卡或者驱动不完善的机器上,OpenGL 反而不如 CPU 绘制稳定。我记得在某个老旧嵌入式平台调试时,一开 OpenGL 花纹直接错乱,关掉反而一切正常。
5. 交互操作与样式定制:让界面像个正经产品
一个能用和好用的差距,很大程度体现在交互设计上。PyQtGraph 提供了一系列交互机制,合理利用它们可以让你的工具类软件直接拥有「商业软件」的质感。
5.1 缩放、平移和坐标十字线
PyQtGraph 的 PlotItem 默认支持中键拖拽平移、滚轮缩放、右键弹出菜单。如果你想禁用某一种交互,可以通过setMouseEnabled(x=False, y=False)单独控制。比如时间序列只允许横向缩放,把 y 轴缩放关掉,可以防止用户误操作把幅值轴拉爆。
坐标十字线(crosshair)是调试数据时非常好用的功能。官方推荐的做法是用InfiniteLine叠加。我常用的是SignalProxy加Scene.sigMouseMoved信号:
vline = pg.InfiniteLine(angle=90, movable=False) hline = pg.InfiniteLine(angle=0, movable=False) plot.addItem(vline, ignoreBounds=True) plot.addItem(hline, ignoreBounds=True) def mouse_moved(evt): pos = plot.vb.mapSceneToView(evt) vline.setPos(pos.x()) hline.setPos(pos.y())这个组合在数据排查时简直救命。你可以直接看到光标所在位置的 X/Y 数值,不需要脑补坐标。
5.2 坐标轴、图例和标题设置
标题要穿插一点实际经验:很多人设置标题直接调plot.setTitle("xxx"),这个标题默认颜色和背景对比度很低,在浅色背景下几乎看不见。所以我通常习惯用setLabel配合单独的颜色设置。
图例用法也比较简单:
plot.addLegend() curve1 = plot.plot(..., name="信号A", pen='r') curve2 = plot.plot(..., name="信号B", pen='b')如果你做了多面板布局,每个面板都可以有独立的坐标轴和标题,不互相干扰。左下角的 axis 方向需要特别留意,PyQtGraph 默认把第 0 轴放在下边,第 1 轴放在左边,和 matplotlib 习惯一致。
5.3 控件的组合:Checkbox、SpinBox 和绘图区联动
在真实桌面工具里,绘图区通常不是孤立的。你会有启动/停止按钮、阈值输入框、通道选择下拉框。PyQtGraph 本身是绘图库,但它搭档 Qt Widgets 毫无压力。把GraphicsLayoutWidget放进QMainWindow的 central widget 里,旁边加几个控件,组成一套完整的控制台。
举个例子:用 QSpinBox 控制波形幅值,用 QCheckBox 控制是否显示网格:
grid_checkbox = QtWidgets.QCheckBox("显示网格") grid_checkbox.toggled.connect(plot.showGrid) amp_spin = QtWidgets.QSpinBox() amp_spin.setRange(1, 100) amp_spin.valueChanged.connect(update_amp)这样把 Qt 原生控件的信号和 PyQtGraph 的接口串起来,一个简单的采集监控界面雏形就出来了。PyQtGraph 不会限制你用其它 Qt 能力,你可以自由发挥。
6. 性能优化策略与常见坑
这一部分绝大多数来自我在实际项目里的真实记录。PyQtGraph 看似简单,真正把它用对、用顺,还是有很多细节。
6.1 如何解决大点数曲线渲染卡顿
如果你要显示几十万甚至上百万个数据点,直接 setData 依然会卡。针对这类场景,PyQtGraph 提供了一个利器:setDownsampling(ds=auto)和setClipToView(True)。它的原理是:当数据点数远大于屏幕像素数时,先将数据做降采样,只保留每个像素区间的最大最小值,从而避免绘制过多不可见点。这个降采样是实时的、自动的,对用户完全透明。
curve.setDownsampling(ds=1, auto=True) curve.setClipToView(True)设置之后,哪怕你往里面丢 50 万个点,界面也能顺滑缩放平移。不过要注意,降采样用的是局部极值法,对信号峰值形态保留效果很好,但在统计意义上会损失一些随机细节。如果你在调试传感器原始数据,建议降采样只用于「浏览模式」,细看数据时临时关闭。
6.2 多曲线绘图时的性能陷阱
多曲线场景下,最容易踩的坑是每条曲线用独立的高消耗图元,比如给每条曲线加渐变填充、阴影、或者开启不必要的高分辨率抗锯齿。PyQtGraph 默认的抗锯齿已经足够好,你在QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)之后再额外调用pg.setConfigOptions(antialias=True),会让所有绘图元素默认抗锯齿。但抗锯齿是性能杀手,如果曲线数量很多且实时刷新,不建议全局开抗锯齿,只对少数重点曲线单独配制更好。
另一个细节是:把多条曲线放在同一个 PlotItem 中,比开多个 PlotItem 更高效。因为场景的重绘粒度是 Item 级别的,多条曲线在一个 PlotItem 里共享一次坐标变换计算。
6.3 OpenGL 的兼容性和稳定性问题
之前已经说过 OpenGL 的加速效果和硬件驱动强相关。这里再补充一个非常实际问题:在有些机器上开启 OpenGL 后,截屏或者用录屏软件抓取窗口内容会变成黑屏,这是 OpenGL 混成和桌面合成器之间的兼容问题。如果你做了工业级交付,这台机器用不上 GPU 加速,那就默认关闭 OpenGL,改用 CPU 绘制,稳定性和兼容性都更好。
6.4 抗锯齿、DPI 缩放和清晰度
在 Windows 高 DPI 环境下,PySide6 应用容易出现字体和绘图模糊。最有效的解决办法是在程序最开始设置 Qt::AA_EnableHighDpiScaling 属性,并调用pg.setConfigOptions(antialias=True)。这里的顺序很重要:先设置 QApplication 的属性,再创建 QApplication 实例,最后导入 PyQtGraph 并画图。如果顺序反了,DPI 缩放不生效,字全是糊的。
import sys from pyqtgraph.Qt import QtCore, QtWidgets QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) app = QtWidgets.QApplication(sys.argv) import pyqtgraph as pg pg.setConfigOptions(antialias=True)6.5 数据对象生命周期管理与内存泄漏
PyQtGraph 里最常见的隐性内存泄漏,是把PlotDataItem以局部变量的形式创建之后,没有保留引用,但仍存在于场景中。当你想清空旧数据、重新绘制新曲线时,旧曲线没有被正确移除,场景里的 Item 数量持续增长,内存越用越多。
正确做法是把所有动态添加的绘图项存到列表里,清空时逐个调用plot.removeItem(item),或者调用plot.clear()。如果是长期运行的采集程序,需要特别注意这一点,它不会立刻报警,但会让你的程序运行几小时后越来越卡。
7. 常见问题与排查技巧实录
这里整理一份我项目过程中遇到的经典问题速查表,很多问题在网上问来问去都找不到确切答案,实际上都是几个基础细节没搞对。
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
| 曲线显示顺序反了 | setData(y, x) 参数记反 | 统一使用 y, x 顺序 |
| 刷新实时图时 UI 卡死 | 在 UI 线程里做了大量耗时计算 | 耗时计算放入 QThread,只把结果发回主线程 |
| 波形显示闪屏 | 未关闭 autoLevels | setImage(..., autoLevels=False) |
| 大量点绘制卡顿 | 未开降采样 | setDownsampling + setClipToView |
| 文字模糊 | 没有启用高 DPI 缩放 | 启动最前设置 AA_EnableHighDpiScaling |
| 图例不显示 | 创建曲线时忘了给 name 参数 | plot(..., name="曲线名") 即可 |
| 窗口关闭后进程仍不退 | 有 QTimer 还在跑 | 关闭窗口时 stop timer |
先说一个我印象很深刻的问题:某次做一个实时频谱分析工具,曲线刷新频率 30 fps,逻辑上每一步都正确,但 UI 就是偶发卡顿。排查到最后发现罪魁祸首是在 UI 线程里做了一个 FFT 计算,这个计算耗时 50 多毫秒,直接把事件循环的刷新节奏打乱了。解决方案非常简单:把 FFT 丢到 QThread 里算,算完再通过信号把频谱数据传回主线程更新曲线。所以,任何耗时超过几毫秒的操作,都别放在 UI 线程里。
另一个高频坑是GraphicsLayoutWidget.show()之后忘了保留引用,导致控件被 Python 垃圾回收。现象就是你看到一个窗口闪了一下就消失,程序也不报错。如果你在类里直接把win = pg.GraphicsLayoutWidget()作为局部变量,后面没有持有引用,Qt 的 C++ 对象虽然还在,Python 包装对象被回收后行为就会变得诡异。务必把窗口对象挂到 QMainWindow 或某个长期存活的对象上。
还有一点:PyQtGraph 的SignalProxy是一个非常实用的工具。当你想监听鼠标移动事件刷新数据信息时,Scene.sigMouseMoved的信号频率可能非常高,直接触发槽函数会拖慢 UI。用SignalProxy做去抖,每次移动只触发有限的回调。我在做鼠标十字线追踪时,这个技巧帮我省掉了大量无效计算。
8. 从简单曲线到复杂仪表盘的完整示例
把前面所有东西串起来,我写一个相对完整的上位机数据监控示例。这个例子模拟一个多通道传感器数据源,包含实时曲线、散点统计和图像显示,你看完直接可以照着改成自己的采集程序。
8.1 主窗口与布局设计
import sys import numpy as np import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtWidgets QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) app = QtWidgets.QApplication(sys.argv) pg.setConfigOptions(antialias=True) win = QtWidgets.QMainWindow() win.setWindowTitle("多通道实时监控模拟") central = pg.GraphicsLayoutWidget() win.setCentralWidget(central) # 两个波形通道、一个图像区 p_wave1 = central.addPlot(row=0, col=0, title="通道A") p_wave2 = central.addPlot(row=1, col=0, title="通道B") p_spect = central.addPlot(row=0, col=1, title="散点分布") img_view = pg.ImageView()8.2 模拟数据更新逻辑
N = 1000 t = np.linspace(0, 10, N) wave1 = np.sin(t) wave2 = np.cos(t) curve1 = p_wave1.plot(pen='y') curve2 = p_wave2.plot(pen='c') scatter = pg.ScatterPlotItem() p_spect.addItem(scatter) frame = np.random.rand(64, 64) def update(): global wave1, wave2, frame wave1 = np.roll(wave1, 1) wave1[0] = np.sin(QtCore.QDateTime.currentMSecsSinceEpoch() / 1000.0) + np.random.normal() curve1.setData(wave1) wave2 = np.roll(wave2, 1) wave2[0] = np.cos(QtCore.QDateTime.currentMSecsSinceEpoch() / 1000.0) + np.random.normal() curve2.setData(wave2) scatter.setData( x=np.random.randn(200), y=np.random.randn(200), size=5, brush=pg.mkBrush(100, 200, 255, 80) ) frame = np.roll(frame, 1, axis=0) frame[0] = np.random.rand(64) img_view.setImage(frame, autoLevels=False) timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(50) win.show() app.exec()这个小例子里有几个细节值得注意:wave1 和 wave2 时刻保留各自的引用,update 里只变更局部数组内容再 setData,不产生新的 ProcessDataItem;散点图每次刷新是重新 setData,而不是 addItem;图像帧用 np.roll 做滚动更新,模拟数据流式到达。整个 50 毫秒刷新一次,CPU 占用极低。
你在做真实项目时,只需要把 update 里的随机数据替换成真实的串口/网络/采集卡数据源,把timer.start(50)改成适合你数据频率的周期即可。
最后聊聊我个人在实际项目里的两个心得
第一个心得是:能用 PyQtGraph 原生 API 解决的,不要自己去拼 OpenGL 或者 SVG。PyQtGraph 的封装已经帮了你一半,自己再往底层钻,工程复杂度会成倍上升,而且未必更快。很多卡顿问题的根源不在渲染管线,而在业务逻辑里是不是把不必要的计算和 IPC 放在 UI 线程里。
第二个心得是:实时可视化调试时,不要把性能优化作为最先考虑的事情。先把功能跑通,数据链路正确,再回头用 profile 工具确认瓶颈在绘图还是算法。我见过很多人在开发前期纠结于是不是要用 OpenGL、是不是要开抗锯齿,结果最后性能瓶颈却是在一个time.sleep(0.1)上。把精力放在真正影响结果的地方,效率才是最高的。
如果这篇文章对你有帮助,建议拿着最后那个完整示例跑一遍,再尝试把波形换成真实数据源,亲手体验一下从传感器到屏幕整个数据链路的构建过程。以后做任何桌面端实时可视化需求,PyQtGraph 大概率都能稳稳接住。