PyQt5 在不冻结 GUI 的情况下绘制数据的最佳方式
PyQt5 Best way to plot data without freezing GUI
我正在努力实现类似于 的目标。具体来说,我有一个队列,里面装满了数据,我想实时绘制这些数据。
我目前有一个使用 QTimer(没有线程)的工作版本,以便每半秒调用一个函数。此函数从队列中取出项目(当它非空时)并绘制它们。虽然这确实有效,但我想知道我是否应该使用更好的方法(涉及线程等)。我主要担心的是,我担心我的方法虽然有效,但会使事情变得比他们需要的更加滞后。我尝试了上面给出的方法,但是(如果我没有搞砸,我可能就是这样):
- 终止线程有问题(特别是:“QObject::killTimer:无法从另一个线程停止计时器”)
- 如最后一条评论所示(在另一个线程中添加睡眠调用会冻结整个 GUI),该进程似乎甚至没有出现在不同的线程中。
关键是,我想知道是否有比我目前的方法“更好”的方法来做到这一点(例如上面给出的方法,如果我能让它在我的情况下正常工作,或者有一个线程它不断地检查队列,只要它非空,就会发出一个值而不是有一个计时器。
我没有给出一个最小的可重现示例,因为这不一定是特定代码的特定问题,而是关于正确方法的问题。
编辑:基于this,是否最好一次只处理队列中的一个事件并将计时器的超时设置为 0 毫秒(而不是循环直到队列不为空半秒)?
这取决于您希望如何绘制数据。 Queue 中的每一项都是一个新图还是您只需要显示最新的数据?如果您只需要显示最新数据,那么这是一个简单的过程。
我创建了一个库 qt_thread_updater 来帮助解决这个问题。 qt_thread_updater
使用一个周期性运行的计时器来调用主线程中的函数。它是线程安全的。但是,qt_thread_updater
将在稍后超时时调用该函数,因此 GUI 更新将延迟几分之一秒。
import multiprocessing as mp
import threading
from qtpy import QtWidgets
from qt_thread_updater import get_updater
app = QtWidgets.QApplication()
fig = Plot() # Your plotting library
fig.show()
def plot_data(data):
# Plot the data here
fig.plot(data)
def read_queue(que, alive):
while alive.is_set():
for _ in range(que.qsize()):
data = que.get()
get_updater().call_latest(plot_data, data)
time.sleep(0.1) # Some delay to let the QEvent loop process events.
# Processed data queue and Event to close down processes
myque = mp.Queue()
alive = mp.Event()
alive.set()
# Read processed data thread and display
pull_data_th = threading.Thread(target=read_queue, args=(myque, alive))
pull_data_th.start()
# Worker process
worker = mp.Process(target=do_work, args=(myque, alive)
worker.start()
app.exec_()
alive.clear()
worker.join()
pull_data_th.join()
如果您需要显示队列中的每个项目,那么您必须注意性能。如果 QEvent 循环没有时间处理事件并且已满,应用程序将崩溃。如果您的应用程序崩溃,那么您需要降低数据速率或获得更快的绘图库(matplotlib 很慢,pyqtgraph 快得多)。
除了将 call_latest
更改为 call_in_main
外,您可以使用与下面相同的代码
def read_queue(que, alive):
while alive.is_set():
for _ in range(que.qsize()):
data = que.get()
get_updater().call_in_main(plot_data, data)
# Some delay to let the QEvent loop process events.
if que.empty():
time.sleep(0.5)
else:
time.sleep(0.001)
我正在努力实现类似于
- 终止线程有问题(特别是:“QObject::killTimer:无法从另一个线程停止计时器”)
- 如最后一条评论所示(在另一个线程中添加睡眠调用会冻结整个 GUI),该进程似乎甚至没有出现在不同的线程中。
关键是,我想知道是否有比我目前的方法“更好”的方法来做到这一点(例如上面给出的方法,如果我能让它在我的情况下正常工作,或者有一个线程它不断地检查队列,只要它非空,就会发出一个值而不是有一个计时器。
我没有给出一个最小的可重现示例,因为这不一定是特定代码的特定问题,而是关于正确方法的问题。
编辑:基于this,是否最好一次只处理队列中的一个事件并将计时器的超时设置为 0 毫秒(而不是循环直到队列不为空半秒)?
这取决于您希望如何绘制数据。 Queue 中的每一项都是一个新图还是您只需要显示最新的数据?如果您只需要显示最新数据,那么这是一个简单的过程。
我创建了一个库 qt_thread_updater 来帮助解决这个问题。 qt_thread_updater
使用一个周期性运行的计时器来调用主线程中的函数。它是线程安全的。但是,qt_thread_updater
将在稍后超时时调用该函数,因此 GUI 更新将延迟几分之一秒。
import multiprocessing as mp
import threading
from qtpy import QtWidgets
from qt_thread_updater import get_updater
app = QtWidgets.QApplication()
fig = Plot() # Your plotting library
fig.show()
def plot_data(data):
# Plot the data here
fig.plot(data)
def read_queue(que, alive):
while alive.is_set():
for _ in range(que.qsize()):
data = que.get()
get_updater().call_latest(plot_data, data)
time.sleep(0.1) # Some delay to let the QEvent loop process events.
# Processed data queue and Event to close down processes
myque = mp.Queue()
alive = mp.Event()
alive.set()
# Read processed data thread and display
pull_data_th = threading.Thread(target=read_queue, args=(myque, alive))
pull_data_th.start()
# Worker process
worker = mp.Process(target=do_work, args=(myque, alive)
worker.start()
app.exec_()
alive.clear()
worker.join()
pull_data_th.join()
如果您需要显示队列中的每个项目,那么您必须注意性能。如果 QEvent 循环没有时间处理事件并且已满,应用程序将崩溃。如果您的应用程序崩溃,那么您需要降低数据速率或获得更快的绘图库(matplotlib 很慢,pyqtgraph 快得多)。
除了将 call_latest
更改为 call_in_main
def read_queue(que, alive):
while alive.is_set():
for _ in range(que.qsize()):
data = que.get()
get_updater().call_in_main(plot_data, data)
# Some delay to let the QEvent loop process events.
if que.empty():
time.sleep(0.5)
else:
time.sleep(0.001)