-1

I want to queue QProcess in PyQt5 according to the spinBox value, as well as display text in textEdit using readAll (), but whatever value I specify in spinBox, the script runs only 1 time, and its result is not displayed in textEdit. Please tell me the solution. I just recently started learning python. Maybe it's still too difficult for me, but I would like to sort it out.

test.py

from functools import cached_property
from pathlib import Path
from PyQt5 import QtGui, QtWidgets, QtCore, uic
import queue
import os
import sys

CURRENT_DIRECTORY = Path(__file__).resolve().parent


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        QtWidgets.QMainWindow.__init__(self)
        self.ui = uic.loadUi(os.fspath(CURRENT_DIRECTORY / "test.ui"), self)
        self.resize(820, 300)
        self.setFixedSize(self.size())
        managers = TaskManager(self)
        task_list = [os.fspath(CURRENT_DIRECTORY / "test2.py")]
        for task in task_list:
            managers.appendTask(task)
        self.ui.pushButtonStart.clicked.connect(managers.start)
        self.ui.pushButtonStop.clicked.connect(managers.stop)
        managers.messageChanged.connect(self.textEdit.append)


    @cached_property
    def manager(self):
        return TaskManager()


class TaskManager(QtCore.QObject):
    messageChanged = QtCore.pyqtSignal(str)
    numbersTaskRunningChanged = QtCore.pyqtSignal(int)

    def __init__(self, parent=None):
        super(TaskManager, self).__init__(parent)
        self._max_task = 1
        self._queue = queue.Queue()
        self._numbers_task_running = 0
        self._running = False

    @cached_property
    def processes(self):
        return list()

    def execute(self, script, metadata=None):
        process = QtCore.QProcess()
        process.setProperty("metadata", metadata or dict())
        process.finished.connect(self.handle_finished)
        process.setProgram(sys.executable)
        process.setArguments([script])
        process.start()
        self.processes.append(process)

    def handle_finished(self):
        process = self.sender()
        self.processes.remove(process)
        metadata = process.property("metadata")
        print(f"{metadata} finished")

    def setMaxTask(self, max_task):
        self._max_task = max_task
        if self._running:
            self.call_task()

    def maxTask(self):
        return self._max_task

    def appendTask(self, task):
        self._queue.put(task)
        self.call_task()

    def start(self):
        interface = MainWindow()
        interface.pushButtonStart.setEnabled(False)
        interface.pushButtonStop.setEnabled(True)
        self._running = True
        self.call_task()
        number_of_processes = interface.ui.spinBox.value()
        script = os.fspath(CURRENT_DIRECTORY / "test2.py")
        for i in range(number_of_processes):
            interface.manager.execute(script, dict(i=i))

    def stop(self):
        interface = MainWindow()
        interface.pushButtonStart.setEnabled(True)
        interface.pushButtonStop.setEnabled(False)
        self._running = False

    def call_task(self):
        process = QtCore.QProcess(self)
        process.setProcessChannelMode(QtCore.QProcess.MergedChannels)
        process.readyReadStandardOutput.connect(self.on_readyReadStandardOutput)
        process.finished.connect(self.on_finished)
        process.started.connect(self.on_started)
        process.errorOccurred.connect(self.on_errorOccurred)

    def on_readyReadStandardOutput(self):
        codec = QtCore.QTextCodec.codecForLocale()
        decoder_stdout = codec.makeDecoder()
        process = self.sender()
        text = decoder_stdout.toUnicode(process.readAllStandardOutput())
        self.messageChanged.emit(text)

    def on_errorOccurred(self, error):
        process = self.sender()
        print("error: ", error, "-", " ".join([process.program()] + process.arguments()))
        self.call_task()

    def on_finished(self):
        process = self.sender()
        self._numbers_task_running -= 1
        self.numbersTaskRunningChanged.emit(self._numbers_task_running)
        self.call_task()

    def on_started(self):
        process = self.sender()
        print("started: ", " ".join([process.program()] + process.arguments()))
        self._numbers_task_running += 1
        self.numbersTaskRunningChanged.emit(self._numbers_task_running)
        self.call_task()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    w = MainWindow()
    w.show()

    sys.exit(app.exec())

test.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>820</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <property name="styleSheet">
   <string notr="true">background-color: rgb(245, 245, 245);</string>
  </property>
  <widget class="QWidget" name="centralwidget">
   <widget class="QWidget" name="verticalLayoutWidget">
    <property name="geometry">
     <rect>
      <x>0</x>
      <y>0</y>
      <width>121</width>
      <height>271</height>
     </rect>
    </property>
    <layout class="QVBoxLayout" name="verticalLayout">
     <property name="sizeConstraint">
      <enum>QLayout::SetDefaultConstraint</enum>
     </property>
     <item>
      <widget class="QPushButton" name="pushButtonStart">
       <property name="enabled">
        <bool>true</bool>
       </property>
       <property name="font">
        <font>
         <pointsize>10</pointsize>
        </font>
       </property>
       <property name="styleSheet">
        <string notr="true">background-color: rgb(0, 170, 0);</string>
       </property>
       <property name="text">
        <string>Start</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QPushButton" name="pushButtonStop">
       <property name="enabled">
        <bool>false</bool>
       </property>
       <property name="font">
        <font>
         <pointsize>10</pointsize>
        </font>
       </property>
       <property name="styleSheet">
        <string notr="true">background-color: rgb(255, 0, 0);</string>
       </property>
       <property name="text">
        <string>Stop</string>
       </property>
      </widget>
     </item>
     <item>
      <widget class="QFrame" name="frame">
       <property name="frameShape">
        <enum>QFrame::StyledPanel</enum>
       </property>
       <property name="frameShadow">
        <enum>QFrame::Raised</enum>
       </property>
       <widget class="QWidget" name="layoutWidget">
        <property name="geometry">
         <rect>
          <x>10</x>
          <y>10</y>
          <width>106</width>
          <height>20</height>
         </rect>
        </property>
        <layout class="QGridLayout" name="gridLayout_2">
         <item row="0" column="0">
          <widget class="QLabel" name="label_6">
           <property name="text">
            <string>Value</string>
           </property>
          </widget>
         </item>
         <item row="0" column="1">
          <widget class="QSpinBox" name="spinBox">
           <property name="minimum">
            <number>1</number>
           </property>
           <property name="maximum">
            <number>4000</number>
           </property>
           <property name="value">
            <number>1</number>
           </property>
          </widget>
         </item>
        </layout>
       </widget>
      </widget>
     </item>
    </layout>
   </widget>
   <widget class="QWidget" name="verticalLayoutWidget_2">
    <property name="geometry">
     <rect>
      <x>120</x>
      <y>0</y>
      <width>691</width>
      <height>271</height>
     </rect>
    </property>
    <layout class="QVBoxLayout" name="verticalLayout_2">
     <item>
      <widget class="QTextEdit" name="textEdit">
       <property name="readOnly">
        <bool>true</bool>
       </property>
      </widget>
     </item>
    </layout>
   </widget>
  </widget>
  <widget class="QStatusBar" name="statusbar"/>
 </widget>
 <resources/>
 <connections/>
</ui>

test2.py

import random, string

listToPrint = set()
listToPrint.add(''.join(random.choice(string.hexdigits) for i in range(16)))
print(listToPrint)

2 Answers2

1

Your requests are confusing since you talk about a queue but you never use it, it seems that you have patched my codes thinking that they will magically join and work but the reality (unfortunately, fortunately) is not like that.

You have to understand that a good practice is to separate the tasks that each class does. The task of TaskManager is to do the queue processing of the tasks, it is not to interact with the GUI directly. And the task of the GUI is to get the user information and launch the TaskManager.

Using the above as a base I have created a class that if you add tasks they will be executed sequentially, that is, when I finish executing a task, it will just execute a pending task. and what the stop method does is stop the task that is being executed and eliminate the pending tasks. That same class will emit the information of the processes through a signal.

The GUI must get that information and convert it to a string to display it in the QTextEdit. On the other hand, the TaskManager class has a running_changed signal that indicates when the tasks are being executed in order to enable or disable the stop button since it does not make sense to have it enabled when no task is executed.

from functools import cached_property
from pathlib import Path
from PyQt5 import QtGui, QtWidgets, QtCore, uic
from collections import deque
import os
import sys

CURRENT_DIRECTORY = Path(__file__).resolve().parent


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        QtWidgets.QMainWindow.__init__(self)
        self.ui = uic.loadUi(os.fspath(CURRENT_DIRECTORY / "test.ui"), self)
        self.setFixedSize(820, 300)

        self.ui.pushButtonStop.setEnabled

        self.ui.pushButtonStart.clicked.connect(self.handle_start)
        self.ui.pushButtonStop.clicked.connect(self.handle_stop)
        self.manager.standard_output_changed.connect(self.process_data)
        self.manager.standard_error_changed.connect(self.process_data)

        self.manager.running_changed.connect(self.handle_running_changed)

    def handle_start(self):
        number_of_processes = self.ui.spinBox.value()
        for _ in range(number_of_processes):
            self.manager.append(
                sys.executable, [os.fspath(CURRENT_DIRECTORY / "test2.py")]
            )

    def handle_stop(self):
        self.manager.stop()

    def handle_running_changed(self):
        self.ui.pushButtonStop.setEnabled(self.manager.running)

    @cached_property
    def manager(self):
        return TaskManager()

    def process_data(self, data):
        codec = QtCore.QTextCodec.codecForLocale()
        decoder_stdout = codec.makeDecoder()
        text = decoder_stdout.toUnicode(data)
        self.textEdit.append(text)


class TaskManager(QtCore.QObject):
    standard_output_changed = QtCore.pyqtSignal(QtCore.QByteArray)
    standard_error_changed = QtCore.pyqtSignal(QtCore.QByteArray)
    running_changed = QtCore.pyqtSignal()

    _running = False

    @cached_property
    def process(self):
        process = QtCore.QProcess()
        process.readyReadStandardOutput.connect(self.handle_standard_output)
        process.readyReadStandardError.connect(self.handle_standard_error)
        process.finished.connect(self.try_start)
        return process

    @cached_property
    def queue(self):
        return deque()

    @property
    def running(self):
        return self._running

    @running.setter
    def running(self, running):
        if self.running == running:
            return
        self._running = running
        self.running_changed.emit()

    def append(self, program, arguments=()):
        self.queue.append((program, arguments))
        self.try_start()

    def try_start(self):
        if len(self.queue) == 0:
            self.running = False
            return
        if self.process.state() == QtCore.QProcess.NotRunning:
            program, arguments = self.queue.popleft()
            self.process.setProgram(program)
            self.process.setArguments(arguments)
            self.process.start()
            self.running = True

    def handle_standard_output(self):
        self.standard_output_changed.emit(self.process.readAllStandardOutput())

    def handle_standard_error(self):
        self.standard_error_changed.emit(self.process.readAllStandardError())

    def stop(self):
        if self.process.state() != QtCore.QProcess.NotRunning:
            self.process.kill()
        self.queue.clear()
        self.running = False


if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)

    w = MainWindow()
    w.show()

    sys.exit(app.exec())

To test my code I have modified the test2.py to make it more complicated by adding time consuming tasks like time.sleep. So you can finish the tasks that are running. On the other hand, each time the start button is pressed, n more tasks will be added to the ones that are being executed.

import random
import string
import time


listToPrint = set()
for i in range(4):
    listToPrint.add("".join(random.sample(string.hexdigits, 16)))
    print(i, flush=True)
    time.sleep(1)
print(listToPrint, flush=True)
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
0

The problem is that you're continuously creating a new instance of the MainWindow, so the spinbox used for tracking the number of processes always has the default initial value, 1.

You're also doing the same error more than once, and you're not even using the cached_property for the TaskManager.

A possible solution is to pass the main window instance to the task manager, so that it will use the actual value of the spinbox. Consider that this is not a very elegant solution, as the task manager should probably know nothing about the main window, and all communication should happen using more signals and slots.

class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        QtWidgets.QMainWindow.__init__(self)
        uic.loadUi("test.ui", self)
        self.resize(820, 300)
        self.setFixedSize(self.size())
        
        self.manager = TaskManager(self)
        task_list = ["test2.py"]
        for task in task_list:
            self.manager.appendTask(task)
        self.pushButtonStart.clicked.connect(self.manager.start)
        self.pushButtonStop.clicked.connect(self.manager.stop)
        self.manager.messageChanged.connect(self.textEdit.append)


class TaskManager(QtCore.QObject):
    messageChanged = QtCore.pyqtSignal(str)
    numbersTaskRunningChanged = QtCore.pyqtSignal(int)

    def __init__(self, mainWindow):
        super(TaskManager, self).__init__(mainWindow)
        self.mainWindow = mainWindow
        # ...

    def start(self):
        self._running = True
        self.call_task()
        number_of_processes = self.mainWindow.spinBox.value()
        script = "test2.py"
        for i in range(number_of_processes):
            self.execute(script, dict(i=i))

    def stop(self):
        self.mainWindow.pushButtonStart.setEnabled(True)
        self.mainWindow.pushButtonStop.setEnabled(False)
        self._running = False

Please note that the above corrections are very minimal, and for various reasons the code is not optimal under many aspects of Object Oriented Programming.

Considering that, from the look of your code, you probably tried to adapt somebody else's code but without completely understanding the mechanics, I strongly suggest you to carefully study the original and my modifications, and do more patient research about classes/instances and OOP in general.

musicamante
  • 41,230
  • 6
  • 33
  • 58