首页 > 解决方案 > pyqt5中pyqtgraph的平移/缩放即使使用线程也没有响应

问题描述

我制作了一个 PyQt5 GUI,它绘制了来自 RGA 的几次扫描中的最后一次。我希望用户能够在获取下一次扫描时缩放/平移现有图,并尝试在串行通信完成时使用线程来保持 GUI 响应。那不太行。我制作了一个更简单的代码版本,带有一个 while 循环来模拟数据采集期间的暂停。同样,我发现我必须将 QApplication.processevents() 放在我的线程中,以便在线程内的每个循环结束时更新绘图 - 否则它只会在线程完成后更新最终绘图。这似乎违背了目的。

我还有一个停止按钮,我想中断所有似乎滞后的响应,类似于图表上的平移/缩放。

任何帮助,将不胜感激!

添加为 PlotWidget 的 pyqtgraph 的 UI 文件是(道歉 - 比必要的复杂,因为它是我原来的精简版本) ThreadingLayout.ui :

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>1126</width>
    <height>738</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <layout class="QGridLayout" name="gridLayout_2">
    <item row="0" column="0">
     <layout class="QVBoxLayout" name="verticalLayout_6">
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout_16">
        <property name="sizeConstraint">
         <enum>QLayout::SetFixedSize</enum>
        </property>
        <item>
         <layout class="QVBoxLayout" name="verticalLayout">
          <item>
           <layout class="QHBoxLayout" name="horizontalLayout_4">
            <item>
             <widget class="QPushButton" name="startbutton">
              <property name="text">
               <string>Start</string>
              </property>
             </widget>
            </item>
            <item>
             <widget class="QPushButton" name="stopbutton">
              <property name="text">
               <string>Stop</string>
              </property>
             </widget>
            </item>
           </layout>
          </item>
         </layout>
        </item>
       </layout>
      </item>
      <item>
       <layout class="QVBoxLayout" name="verticalLayout_5">
        <item>
         <widget class="PlotWidget" name="plotWidget" native="true">
          <property name="sizePolicy">
           <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
            <horstretch>0</horstretch>
            <verstretch>0</verstretch>
           </sizepolicy>
          </property>
          <property name="minimumSize">
           <size>
            <width>0</width>
            <height>300</height>
           </size>
          </property>
         </widget>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_3"/>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_17">
          <property name="spacing">
           <number>0</number>
          </property>
          <item>
           <widget class="QPushButton" name="exitbutton">
            <property name="sizePolicy">
             <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
              <horstretch>0</horstretch>
              <verstretch>0</verstretch>
             </sizepolicy>
            </property>
            <property name="text">
             <string>Exit</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
       </layout>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <widget class="QMenuBar" name="menubar">
   <property name="geometry">
    <rect>
     <x>0</x>
     <y>0</y>
     <width>1126</width>
     <height>21</height>
    </rect>
   </property>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <customwidgets>
  <customwidget>
   <class>PlotWidget</class>
   <extends>QWidget</extends>
   <header location="global">pyqtgraph</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
 <buttongroups>
  <buttongroup name="buttonGroup_2"/>
  <buttongroup name="buttonGroup"/>
 </buttongroups>
</ui>

我的python3代码是:

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from PyQt5.QtCore import *
import pyqtgraph as pg
from random import random
import numpy as np
import traceback, sys


class WorkerSignals(QObject):
    finished = pyqtSignal()
    error = pyqtSignal(tuple)
    result = pyqtSignal(object)
    progress = pyqtSignal(int)


class Worker(QRunnable):

    def __init__(self, fn, *args, **kwargs):
        super(Worker, self).__init__()

        # Store constructor arguments (re-used for processing)
        self.fn = fn
        self.args = args
        self.kwargs = kwargs
        self.signals = WorkerSignals()

        # Add the callback to our kwargs
        self.kwargs['progress_callback'] = self.signals.progress

    @pyqtSlot()
    def run(self):
        try:
            result = self.fn(*self.args, **self.kwargs)
        except:
            traceback.print_exc()
            exctype, value = sys.exc_info()[:2]
            self.signals.error.emit((exctype, value, traceback.format_exc()))
        else:
            self.signals.result.emit(result)  # Return the result of the processing
        finally:
            self.signals.finished.emit()  # Done


class UI(QMainWindow):
    def __init__(self):
        super(UI, self).__init__()

        uic.loadUi("ThreadingLayout.ui", self)

        self.startButton = self.findChild(QPushButton, "startbutton")
        self.stopButton = self.findChild(QPushButton, "stopbutton")
        self.exitButton = self.findChild(QPushButton, "exitbutton")
        self.startButton.clicked.connect(self.clickedstartButton)
        self.stopButton.clicked.connect(self.clickedstopButton)
        self.exitButton.clicked.connect(self.clickedexitButton)

        self.numberstartclicks=0
        self.numberstopclicks=0

        self.show()

        self.threadpool = QThreadPool()


    def clickedstopButton(self):
        self.numberstopclicks = self.numberstopclicks+1


    def plotanddelay(self):
        self.plotWidget.clear()

        try:
            self.legend.scene().removeItem(self.legend)
        except Exception as e:
            pass
        self.legend = self.plotWidget.addLegend()

        self.singlerunx = list(range(100))
        self.singleruny = []
        randomscaling = random()
        for i in range(100):
            self.singleruny.append(randomscaling * self.singlerunx[i] + random())

        self.allrunsy = np.append(self.allrunsy, self.singleruny)

        # only plot last 4 plots
        if self.plotrunindex < self.numberplotlines:
            for self.plotlineindex in reversed(range(self.plotrunindex)):
                pen = pg.mkPen(color=pg.intColor(self.plotlineindex), width=2)
                ploty = self.allrunsy[len(self.allrunsy) - len(self.singlerunx) * (self.plotlineindex + 1):
                                      len(self.allrunsy) - len(self.singlerunx) * self.plotlineindex]
                self.plotWidget.plot(self.singlerunx, ploty, pen=pen,
                                     name='Run ' + str(self.plotrunindex - self.plotlineindex))
        else:
            for plotlineindex in reversed(range(self.numberplotlines)):
                pen = pg.mkPen(color=pg.intColor(plotlineindex), width=2)
                ploty = self.allrunsy[len(self.allrunsy) - len(self.singlerunx) * (plotlineindex + 1):
                                      len(self.allrunsy) - len(self.singlerunx) * plotlineindex]
                self.plotWidget.plot(self.singlerunx, ploty, pen=pen,
                                     name='Run ' + str(self.plotrunindex - plotlineindex))

        self.plotWidget.showGrid(True, True)

        QApplication.processEvents()
        # put in time delay
        while i < 2E7:
             i = i+1


    def clickedstartButton(self):
        self.numberstartclicks = self.numberstartclicks+1

        if (self.numberstartclicks==1):
            self.plotWidget.clear()

            try:
                self.legend.scene().removeItem(self.legend)
            except Exception as e:
                pass
            self.legend = self.plotWidget.addLegend()

            self.allrusnx = []
            self.allrunsy = []
            self.numberruns = 10
            self.numberplotlines = 3

            for self.plotrunindex in range(self.numberruns):
                if self.numberstopclicks > 0:
                    print('Run stopped by user')
                    self.numberstopclicks = 0
                    break

                worker = Worker(self.plotanddelay())
                # Execute
                self.threadpool.start(worker)

        else:
            self.numberstartclicks = 0


    def clickedexitButton(self):
        self.close()


app=QApplication([])
UIWindow=UI()
app.exec()

只是一个更新 - 根据卢克的有用信息,我发现以下代码可以解决问题:

from PyQt5 import uic
from PyQt5.QtWidgets import QApplication, QMainWindow, QPushButton
from PyQt5.QtCore import *
import pyqtgraph as pg
from random import random
import numpy as np


class ConfWorker(QThread):
    threadSignal = pyqtSignal(list)
    finishSignal = pyqtSignal(str)

    def __init__(self, startParm):
        super().__init__()
        self.startParm = startParm   # Initialize the parameters passed to the task


    def run(self):
        for i in range(self.startParm):
            if (self.isInterruptionRequested()):
                return
            self.singlerunx = list(range(100))
            returndata=[]
            singleruny = []
            randomscaling = random()
            for j in range(100):
                 singleruny.append(randomscaling * self.singlerunx[j] + random())
            returndata.append(i)  #gives the run number
            returndata.append(singleruny)
            self.threadSignal.emit(returndata)
            QThread.msleep(2000)
        self.finishSignal.emit('finished')


class UI(QMainWindow):
    def __init__(self):
        super(UI, self).__init__()

        uic.loadUi("ThreadingLayout.ui", self)

        self.startButton = self.findChild(QPushButton, "startbutton")
        self.stopButton = self.findChild(QPushButton, "stopbutton")
        self.exitButton = self.findChild(QPushButton, "exitbutton")
        self.startButton.clicked.connect(self.workerStart)
        self.stopButton.clicked.connect(self.clickedstopButton)
        self.exitButton.clicked.connect(self.clickedexitButton)

        self.numberstartclicks=0
        self.numberstopclicks=0
        self.singlerunx=[]
        self.singleruny=[]
        self.allrunsx=[]
        self.allrunsy=[]
        self.threadrunindex=0
        self.startParm=0

        self.show()


    def clickedstopButton(self):
        self.worker.requestInterruption()
        print('stopped')

    def updateplot(self, returndata):
        self.plotWidget.clear()

        self.plotrunindex=returndata[0]
        self.allrunsy = np.append(self.allrunsy, returndata[1:])
        self.singlerunx = list(range(100))

        try:
            self.legend.scene().removeItem(self.legend)
        except Exception as e:
            pass
        self.legend = self.plotWidget.addLegend()

        # only plot last 4 plots
        if self.plotrunindex < self.numberplotlines:
            for self.plotlineindex in reversed(range(self.plotrunindex)):
                pen = pg.mkPen(color=pg.intColor(self.plotlineindex), width=2)
                ploty = self.allrunsy[len(self.allrunsy) - len(self.singlerunx) * (self.plotlineindex + 1):
                                       len(self.allrunsy) - len(self.singlerunx) * self.plotlineindex]
                self.plotWidget.plot(self.singlerunx, ploty, pen=pen,
                                     name='Run ' + str(self.plotrunindex - self.plotlineindex))
        else:
            for self.plotlineindex in reversed(range(self.numberplotlines)):
                pen = pg.mkPen(color=pg.intColor(self.plotlineindex), width=2)
                ploty = self.allrunsy[len(self.allrunsy) - len(self.singlerunx) * (self.plotlineindex + 1):
                                       len(self.allrunsy) - len(self.singlerunx) * self.plotlineindex]
                self.plotWidget.plot(self.singlerunx, ploty, pen=pen,
                                     name='Run ' + str(self.plotrunindex - self.plotlineindex))

        self.plotWidget.showGrid(True, True)


    def clickedexitButton(self):
        self.close()


    def workerStart(self):
        self.numberstartclicks = self.numberstartclicks + 1

        if (self.numberstartclicks == 1):
            self.plotWidget.clear()

            try:
                self.legend.scene().removeItem(self.legend)
            except Exception as e:
                pass
            self.legend = self.plotWidget.addLegend()

            self.allrusnx = []
            self.allrunsy = []
            self.numberruns = 10
            self.numberplotlines = 3

            self.plotrunindex = 0
            self.startParm = self.numberruns

            self.worker = ConfWorker(self.startParm)
            self.worker.threadSignal.connect(self.updateplot)
            self.worker.finishSignal.connect(self.on_finishSignal)
            self.worker.start()

        else:
            self.numberstartclicks = 0


    def on_finishSignal(self, text):
        print(text)


app=QApplication([])
UIWindow=UI()
app.exec()

标签: python-3.xpyqt5threadpoolpyqtgraph

解决方案


这一行有几个错误:

worker = Worker(self.plotanddelay())
  1. 这一行直接调用plotanddelay(),意思是当点击开始按钮时,立即进入这个循环,plotanddelay重复调用,不回到Qt事件循环。这就是 GUI 锁定的原因,以及为什么需要添加processEvents(); 这允许您手动处理这些事件(但不是必需的)。

  2. 您的 QRunnable 类期望一个函数作为其第一个参数运行,但您正在向它发送 的输出plotanddelay(),即None. 所以这意味着你的后台线程根本没有做这项工作。

  3. 无论如何,Qt 中的后台线程都不允许做任何 GUI 工作;因此,即使您让后台线程按plotanddelay预期运行,该程序也可能会崩溃。

所以要解决这个问题,你需要你的后台线程完成所有缓慢的、非 GUI 的工作(比如从你的 RGA 获取数据),然后你的主 GUI 线程完成所有的绘图工作。这可以在计时器上,以便无论数据是否到达,绘图都会定期更新,或者您可以让后台线程发送一个信号,指示新数据何时到达。


推荐阅读