如何更新 pyqtgraph 中的绘图?

How to update a plot in pyqtgraph?

我正在尝试使用 PyQt5 和 pyqtgraph 创建一个用户界面。我制作了两个复选框,每当我 select 它们时,我想绘制代码中可用的两个数据集之一,每当我 deselect 一个按钮时,我希望它清除相应的曲线。有两个带有文本 A1A2 的复选框,每个复选框绘制一组数据。

我有两个问题:

1- 如果我 select A1 它会绘制与 A1 相关的数据,只要我不 select A2,由 de selecting A1 我可以清除与 A1 关联的数据。 但是,如果我选中 A1 框然后选中 A2 框,则 deselecting A1 不会清除相关的绘图。在这种情况下,如果我选择绘制随机数据,而不是 sin 等确定性曲线,我会看到通过 select 任一按钮都会添加新数据,但无法将其删除。

2- 实际应用程序有 96 个按钮,每个按钮都应与一个数据集相关联。我认为我编写代码的方式效率低下,因为我需要为一个按钮和数据集复制相同的代码 96 次。有没有办法将我在下面展示的玩具代码推广到任意数量的复选框?或者,using/copying每个按钮几乎相同的代码是执行此操作的通常且正确的方法吗?

密码是:

from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

#This function is called whenever the state of checkboxes changes
def todo():
    if cbx1.isChecked():
        global curve1
        curve1 = plot.plot(x, y1, pen = 'r')
    else:
        try:
            plot.removeItem(curve1)
        except NameError:
            pass
    if cbx2.isChecked():
        global curve2
        curve2 = plot.plot(x, y2, pen = 'y')
    else:
        try:
            plot.removeItem(curve2)
        except NameError:
            pass  
#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)

cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())


    

在下文中,我采用更蛮力的方法,同时假设绘制 所有 曲线所花费的时间可以忽略不计:

import numpy as np
import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtWidgets

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

curves = [y1, y2]
pens = ["r", "y"]

#This function is called whenever the state of checkboxes changes
def plot_curves(state):
    plot.clear()
    for checkbox, curve, pen in zip(checkboxes, curves, pens):
        if checkbox.isChecked():
            plot.plot(x, curve, pen=pen)

#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

checkboxes = [QtWidgets.QCheckBox() for i in range(2)]
for i, checkbox in enumerate(checkboxes):
    checkbox.setText(f"A{i+1}")
    checkbox.stateChanged.connect(plot_curves)
    layout.addWidget(checkbox, 0, i)

#Adding the widgets to the layout
layout.addWidget(plot, 1, 0, len(checkboxes), 0)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())

现在你有一个复选框列表,索引为 0 的复选框对应于 curves-列表中索引为 0 的数据。我每次都绘制所有曲线,这会产生更易读的代码.但是,如果这确实会影响性能,则需要稍微复杂一些。

我也尝试添加另一条曲线,结果似乎非常好:

下面是我做的一个很好用的例子。 可以复用做更多的plot,不需要增加代码,只需要改变self.num的值,使用函数add_data(x,y,ind)添加相应的数据即可,其中xy是数据的值,ind 是框的索引(从 0n-1)。

import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui

class MyApp(QtGui.QWidget):
    def __init__(self):
        QtGui.QWidget.__init__(self)
        self.central_layout = QtGui.QVBoxLayout()
        self.plot_boxes_layout = QtGui.QHBoxLayout()
        self.boxes_layout = QtGui.QVBoxLayout()
        self.setLayout(self.central_layout)
        
        # Lets create some widgets inside
        self.label = QtGui.QLabel('Plots and Checkbox bellow:')
        
        # Here is the plot widget from pyqtgraph
        self.plot_widget = pg.PlotWidget()
        
        # Now the Check Boxes (lets make 3 of them)
        self.num = 6
        self.check_boxes = [QtGui.QCheckBox(f"Box {i+1}") for i in range(self.num)]
        
        # Here will be the data of the plot
        self.plot_data = [None for _ in range(self.num)]
        
        # Now we build the entire GUI
        self.central_layout.addWidget(self.label)
        self.central_layout.addLayout(self.plot_boxes_layout)
        self.plot_boxes_layout.addWidget(self.plot_widget)
        self.plot_boxes_layout.addLayout(self.boxes_layout)
        for i in range(self.num):
            self.boxes_layout.addWidget(self.check_boxes[i])
            # This will conect each box to the same action
            self.check_boxes[i].stateChanged.connect(self.box_changed)
            
        # For optimization let's create a list with the states of the boxes
        self.state = [False for _ in range(self.num)]
        
        # Make a list to save the data of each box
        self.box_data = [[[0], [0]] for _ in range(self.num)] 
        x = np.linspace(0, 3.14, 100)
        self.add_data(x, np.sin(x), 0)
        self.add_data(x, np.cos(x), 1)
        self.add_data(x, np.sin(x)+np.cos(x), 2)
        self.add_data(x, np.sin(x)**2, 3)
        self.add_data(x, np.cos(x)**2, 4)
        self.add_data(x, x*0.2, 5)
        

    def add_data(self, x, y, ind):
        self.box_data[ind] = [x, y]
        if self.plot_data[ind] is not None:
            self.plot_data[ind].setData(x, y)

    def box_changed(self):
        for i in range(self.num):
            if self.check_boxes[i].isChecked() != self.state[i]:
                self.state[i] = self.check_boxes[i].isChecked()
                if self.state[i]:
                    if self.plot_data[i] is not None:
                        self.plot_widget.addItem(self.plot_data[i])
                    else:
                        self.plot_data[i] = self.plot_widget.plot(*self.box_data[i])
                else:
                    self.plot_widget.removeItem(self.plot_data[i])
                break
        
if __name__ == "__main__":
    app = QtGui.QApplication(sys.argv)
    window = MyApp()
    window.show()
    sys.exit(app.exec_())

请注意,在 de PlotWidget 中,我使用 plot() 方法添加绘图,它 returns 保存在调用 [= 之前​​创建的列表中的 PlotDataItem 22=]。 有了这个,您可以轻松地将其从 Plot Widget 中删除并再次添加。此外,如果您的目标是一个更复杂的程序,例如,您可以更改 运行 上每个框的数据的程序,如果您使用 setData() 方法,绘图将更新而不会出现重大问题PlotDataItem

正如我一开始所说,这应该可以很好地处理很多复选框,因为复选框时调用的函数是 Checked/Unchecked,首先将每个框的实际状态与前一个进行比较(存储在 self.state 中)并且只对与该特定框相对应的绘图进行更改。有了这个,你就可以避免为每个复选框执行一个功能,并且每次 check/uncheck 一个框时都重新绘制所有 de 框(就像 user8408080 那样)。我不是说它不好,但是如果你增加复选框的数量 and/or 数据的复杂性,重新绘制所有数据的工作量将急剧增加。

唯一的问题是当 window 太小而无法支持大量的复选框(例如 96 个)时,您将不得不在另一个小部件而不是布局中组织复选框。

现在是上面代码的一些截图:

然后将 self.num 的值更改为 6 并向它们添加一些随机数据:

self.add_data(x, np.sin(x)**2, 3)
self.add_data(x, np.cos(x)**2, 4)
self.add_data(x, x*0.2, 5)

我在您的代码中发现了问题。让我们看看您的代码做了什么:

  1. 当您将第一个绘图添加到小部件(A1A2)时,您会得到 PlotDataItem 并将其存储在 curve1 中或 curve2。假设您首先检查 A1,然后您的 todo 函数首先检查复选框 1 是否已选中,因此绘制数据并将其存储在 curve1 中,然后相同的函数检查复选框 2。复选框 2 未选中,因此函数执行 else 语句,从绘图小部件中删除 curve2,此变量不存在,因此可能会引发错误,但是,您使用 try 语句,错误永远不会出现。

  2. 现在,您选中 A2 框,您的函数首先检查复选框 1,它已被选中,因此该函数将再次添加相同的绘图,但作为另一个 PlotDataItem,存入curve1。到目前为止,您有两个 PlotDataItem 相同的数据(这意味着两个图),但只有最后一个存储在 curve1 中。该函数接下来要做的是检查复选框 2,它被选中,因此它将绘制第二个数据并将其 PlotDataItem 保存在 curve2

  3. 因此,当您现在取消选中复选框 1 时,您的函数首先检查复选框 1(抱歉,如果重复),它未选中,因此该函数将删除存储在 PlotDataItem curve1 它做到了,但请记住,您有两个相同数据的图,所以对我们(观众)来说,图不会消失。这就是问题所在,但它并没有就此结束,该函数现在检查复选框 2,它被选中,因此该函数将添加第二个数据的另一个 PlotDataItem 并将其存储在 curve2 中。我们将再次遇到与第一个数据相同的问题。

通过这个分析,我也学到了一些东西,如果你“覆盖”存储它的变量,PlotDataItem 不会消失,当它从 [=36= 中删除时也不会消失].考虑到这一点,我对之前答案的代码做了一些更改,因为每次我们选中一个之前已选中但未选中的框时,旧代码都会创建另一个项目。现在,如果该项目已创建,我的函数将再次添加它,而不是创建另一个。

我有一些建议:

  • 尝试使用对象,生成您自己的小部件class。您可以避免调用全局变量,将它们作为 class 的属性传递。 (就像我之前的回答一样)

  • 如果你想保持你的代码原样(不使用 classes),为了让它工作,你可以添加另外两个变量,“状态”为你的复选框,所以当你第一次调用你的函数时,它会检查状态是否没有改变并忽略那个复选框。另外,检查之前是否生成了PlotDataItem,只有重新添加它,以避免生成更多项。

  • 你的 objective 是用一堆框或按钮来做的,尝试对所有这些框或按钮只使用一个变量:例如,一个列表,包含所有 boxes/buttons(对象)。然后你可以通过索引管理它们中的任何一个。此外,您可以对该变量进行循环,以将内部对象连接到同一函数。

    my_buttons = [ QtGui.QPushButton() for _ in range(number_of_buttons) ]
    my_boxes= [ QtGui.QCheckBox() for _ in range(number_of_boxes) ]
    my_boxes[0].setText('Box 1 Here')
    my_boxes[2].setChecked(True)
    for i in range(number_of_boxes):
        my_boxes[i].stateChanged.connect(some_function)
    
  • 做对象列表也可以帮助您轻松地自动命名:

    my_boxes= [ QtGui.QCheckBox(f"Box number {i+1}") for i in range(number_of_boxes) ]
    my_boxes= [ QtGui.QCheckBox(str(i+1)) for i in range(number_of_boxes) ]
    my_boxes= [ QtGui.QCheckBox('Box {:d}'.format(i+1)) for i in range(number_of_boxes) ]
    

最后,这是您的代码,经过一些小改动以使其正常工作:

from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore

app = QtWidgets.QApplication(sys.argv)

x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2

#This function is called whenever the state of checkboxes changes
def todo():
    global b1st, b2st, curve1, curve2
    if cbx1.isChecked() != b1st:
        b1st = cbx1.isChecked()
        if cbx1.isChecked():
            if curve1 is None:
                curve1 = plot.plot(x, y1, pen = 'r')
            else:
                plot.addItem(curve1)
        else:
            plot.removeItem(curve1)

    if cbx2.isChecked() != b2st:
        b2st = cbx2.isChecked()
        if cbx2.isChecked():
            if curve2 is None:
                curve2 = plot.plot(x, y2, pen = 'y')
            else:
                plot.addItem(curve2)
        else:
            plot.removeItem(curve2)

#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()

#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)
b1st = False
curve1 = None

cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)
b2st = False
curve2 = None

#Making a pyqtgraph plot widget
plot = pg.PlotWidget()

#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)

#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)

widget_holder.adjustSize()
widget_holder.show()

sys.exit(app.exec_())