pan / zoom pyqtgraph в pyqt5 не отвечает даже при многопоточности - PullRequest
0 голосов
/ 21 апреля 2020

Я сделал PyQt5 GUI, который отображает последний из нескольких сканов из RGA. Я бы хотел, чтобы пользователь мог масштабировать / панорамировать существующие графики во время получения следующего сканирования, и я пытаюсь использовать многопоточность, чтобы обеспечить реагирование GUI во время выполнения последовательной связи. Это не совсем работает. Я сделал более простую версию кода с некоторой l oop, чтобы имитировать c паузу во время сбора данных. Кроме того, я обнаружил, что мне нужно поместить QApplication.processevents () в мой поток, чтобы график обновлялся в конце каждого l oop в потоке - в противном случае он обновляется только с последними графиками после потока сделано. Который, кажется, победить цель.

У меня также есть кнопка «Стоп». Я хотел бы прервать все, что кажется запаздывающим в ответ, аналогично панорамированию / масштабированию на графике.

Любая помощь будет принята!

Файл пользовательского интерфейса с pyqtgraph, добавленным как PlotWidget, (извинения - сложнее, чем необходимо, поскольку это урезанная версия моего оригинала) 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()

1 Ответ

0 голосов
/ 22 апреля 2020

В этой строке есть несколько ошибок:

worker = Worker(self.plotanddelay())
  1. Эта строка напрямую вызывает plotanddelay(), что означает, что при нажатии кнопки запуска она сразу переходит в эту l oop и повторно вызывает plotanddelay, не возвращаясь к событию Qt l oop. Вот почему GUI блокируется, и поэтому вам необходимо было добавить processEvents(); это позволяет вам обрабатывать эти события вручную (но не обязательно).

  2. Ваш класс QRunnable ожидает, что функция будет выполняться в качестве первого аргумента, но вы отправляете ей вывод plotanddelay(), что None. Так что это означает, что фоновые потоки вообще не выполняют эту работу.

  3. Фоновые потоки в Qt в любом случае не могут выполнять какую-либо работу GUI; так что даже если у вас есть фоновый поток для запуска plotanddelay, как вы и предполагали, вероятно, эта программа просто взломает sh.

Так что, чтобы исправить это, вам нужны фоновые потоки, чтобы выполняйте всю медленную, не GUI работу (например, получение данных из вашей RGA), а затем ваш основной поток GUI выполняет всю работу по построению графика. Это может происходить по таймеру, чтобы графики регулярно обновлялись независимо от того, поступили ли данные, или вы могли бы, чтобы фоновый поток отправлял сигнал, указывающий, когда поступили новые данные.

...