How to add questions into a pyqt thread - python

EDIT: I have attempted to add a slot to use a signal so the question box is handled by the main thread and not the worker, but it does not open the question box it just finishes the thread. I am also unsure of how to make the worker wait for the result of the question and how to return to the worker once I know what the user wants to do.
The code I have is used to test some boards and based on what happens the thread should either continue on to the next test if there was a pass or ask the user a question if there was a failure. The problem is when there is a failure the question box appears but the thread in the background continues, I don't want that to happen as the result of subsequent tests rely on the answers from the tests before.
Below is a example of what I do not want to happen.
import sys
from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow, QPushButton, QVBoxLayout, QWidget, QMessageBox
from PyQt5.QtCore import QObject, pyqtSignal, QThreadPool, QRunnable, pyqtSlot
class WorkerSignals(QObject):
finished = pyqtSignal()
question = pyqtSignal()
class Worker(QRunnable):
def __init__(self, fn, *args, **kwargs):
super(Worker, self).__init__()
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
#pyqtSlot()
def run(self):
try:
result = self.fn(*self.args, **self.kwargs)
except:
pass
else:
self.signals.progress.emit(result)
finally:
self.signals.finished.emit()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
layout = QVBoxLayout()
self.btnStartTest = QPushButton("Start thread")
self.labelOne = QLabel("Test one")
self.labelTwo = QLabel("Test two")
layout.addWidget(self.btnStartTest)
layout.addWidget(self.labelOne)
layout.addWidget(self.labelTwo)
w = QWidget()
w.setLayout(layout)
self.setCentralWidget(w)
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
self.btnStartTest.clicked.connect(self.runTestsThread)
self.show()
def runTestsThread(self):
# Pass the function to execute
worker = Worker(self.execute_this_fn)
worker.signals.question.connect(self.question_asked)
worker.signals.finished.connect(self.thread_complete)
# Execute
self.threadpool.start(worker)
def execute_this_fn(self):
resultOne = "FAIL"
resultTwo = "PASS"
if resultOne == "PASS":
self.labelOne.setStyleSheet("background-color: LightGreen")
else:
self.labelOne.setStyleSheet("background-color: Red")
self.question.emit()
if resultTwo == "PASS":
self.labelTwo.setStyleSheet("background-color: LightGreen")
else:
self.labelTwo.setStyleSheet("background-color: Red")
self.question.emit()
def thread_complete(self):
print("thread complete")
def question_asked(self):
QMessageBox.question(self, "Message Box!!", "Do you want to continue or quit")
if __name__ == "__main__":
app = QApplication(sys.argv)
Main = MainWindow()
Main.show()
sys.exit(app.exec_())
I have tried using a signal to finish the thread if there is a failure, then ask the question but this did not work. I was unsure of how to then start another thread based on the answer to the question.
Any help on how to structure this would be helpful, thanks.

Related

QThread crash when show QMessageBox PyQt5 Python

I'm trying to show MessageBox in QThread, but the program always crash after close the MessageBox. I tried to find answer about "create QThread" and "create QMessageBox in another thread", but none of these solved my problem, and some answers are for PyQt4.
The whole code is from a simple tutorial : https://realpython.com/python-pyqt-qthread/
I only insert some code under function reportProgress to raise an error and show a messagebox.
Please teach me how to code the messagebox part, I already tried to solve this for days, really appreciate.
# Code from: https://realpython.com/python-pyqt-qthread/
import sys
from time import sleep
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QApplication,
QLabel,
QMainWindow,
QPushButton,
QVBoxLayout,
QWidget,
QMessageBox,
)
# Step 1: Create a worker class
class Worker(QObject):
finished = pyqtSignal()
progress = pyqtSignal(int)
def run(self):
"""Long-running task."""
for i in range(5):
sleep(1)
self.progress.emit(i + 1)
self.finished.emit()
class Window(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.clicksCount = 0
self.setupUi()
def setupUi(self):
self.setWindowTitle("Freezing GUI")
self.resize(300, 150)
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget)
# Create and connect widgets
self.clicksLabel = QLabel("Counting: 0 clicks", self)
self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.stepLabel = QLabel("Long-Running Step: 0")
self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
self.countBtn = QPushButton("Click me!", self)
self.countBtn.clicked.connect(self.countClicks)
self.longRunningBtn = QPushButton("Long-Running Task!", self)
self.longRunningBtn.clicked.connect(self.runLongTask)
# Set the layout
layout = QVBoxLayout()
layout.addWidget(self.clicksLabel)
layout.addWidget(self.countBtn)
layout.addStretch()
layout.addWidget(self.stepLabel)
layout.addWidget(self.longRunningBtn)
self.centralWidget.setLayout(layout)
def countClicks(self):
self.clicksCount += 1
self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")
def reportProgress(self, n):
self.stepLabel.setText(f"Long-Running Step: {n}")
# I insert this, trying to show a messagebox when error occurs
if n == 3:
try:
print(int('aaa'))
except:
QMessageBox.information(self, 'Warning', 'Success', QMessageBox.Ok)
def runLongTask(self):
# Step 2: Create a QThread object
self.thread = QThread()
# Step 3: Create a worker object
self.worker = Worker()
# Step 4: Move worker to the thread
self.worker.moveToThread(self.thread)
# Step 5: Connect signals and slots
self.thread.started.connect(self.worker.run)
self.worker.finished.connect(self.thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.thread.finished.connect(self.thread.deleteLater)
self.worker.progress.connect(self.reportProgress)
# Step 6: Start the thread
self.thread.start()
# Final resets
self.longRunningBtn.setEnabled(False)
self.thread.finished.connect(
lambda: self.longRunningBtn.setEnabled(True)
)
self.thread.finished.connect(
lambda: self.stepLabel.setText("Long-Running Step: 0")
)
app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
The code works as-is for me, no crashing. It could be a problem with the version of python you are running or the version of PyQt5 you have installed - I suggest updating everything.
Of course, the program will crash after you click ok on your message box. to only raised a message box you only need to change
try:
print(int('aaa'))
except:
QMessageBox.information(self, 'Warning', 'Success', QMessageBox.Ok)
become
try:
print(int('aaa'))
except:
print("no error")
finally:
QMessageBox.information(
self, 'Warning', 'Success', QMessageBox.Ok)

Threading with QRunnable - Proper manner of sending bi-directional callbacks

From a tutorial, I've seen that signals and slots can be used to make callbacks from a worker thread into the main GUI thread, but I'm uncertain how to establish bi-directional communication using signals and slots. The following is what I am working with:
class RespondedToWorkerSignals(QObject):
callback_from_worker = pyqtSignal()
class RespondedToWorker(QRunnable):
def __init__(self, func, *args, **kwargs):
super(RespondedToWorker, self).__init__()
self._func = func
self.args = args
self.kwargs = kwargs
self.signals = RespondedToWorkerSignals()
self.kwargs['signal'] = self.signals.callback_from_worker
print("Created a responded-to worker")
#pyqtSlot()
def run(self):
self._func(*self.args, **self.kwargs)
#pyqtSlot()
def acknowledge_callback_in_worker(self):
print("Acknowledged Callback in Worker")
class MainWindow(QMainWindow):
# Signal meant to connect to a slot present within a worker
mainthread_callback_to_worker = pyqtSignal()
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
# Quick UI setup
w, lay = QWidget(), QVBoxLayout()
w.setLayout(lay)
self.setCentralWidget(w)
self.timer_label = QLabel("Timer Label")
lay.addWidget(self.timer_label)
self.btn_thread_example = QPushButton("Push Me")
self.btn_thread_example.pressed.connect(self.thread_example)
lay.addWidget(self.btn_thread_example)
self.threadpool = QThreadPool()
self.show()
# Set up QTimer to continue in the background to help demonstrate threading advantage
self.counter = 0
self.timer = QTimer()
self.timer.setInterval(1000)
self.timer.timeout.connect(self.recurring_timer)
self.timer.start()
#pyqtSlot()
def do_something(self, signal):
# signal argument will be the callback_from_worker and it will emit to acknowledge_callback_in_mainthread
print("do_something is sleeping briefly. Try to see if you get a locked widget...")
time.sleep(7)
signal.emit()
#pyqtSlot()
def acknowledge_callback_in_mainthread_and_respond(self):
# this function should respond to callback_from_worker and emit a response
print("Acknowledged Callback in Main")
self.mainthread_callback_to_worker.emit()
def thread_example(self):
print("Beginning thread example")
worker = RespondedToWorker(self.do_something)
worker.signals.callback_from_worker.connect(self.acknowledge_callback_in_mainthread_and_respond)
# self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
def recurring_timer(self):
self.counter += 1
self.timer_label.setText(f"Counter: {self.counter}")
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
app.setStyle("Fusion")
win.show()
sys.exit(app.exec())
At the current moment, the script can make the second thread and send a signal to the main GUI. I'd like the GUI to send a response signal back to the worker thread. I'm also uncertain why connecting the main/GUI's signal mainthread_callback_to_worker causes a crash (see the commented-out line).
I understand that one workaround would be for do_something to return some value and then use it inside the worker as "acknowledgement". But I'd like to know the solution using signals & slots, if possible.
To understand the cause of the error you must run the code in the terminal and you will get the following error message:
QObject::connect: Cannot connect MainWindow::mainthread_callback_to_worker() to (nullptr)::acknowledge_callback_in_worker()
Traceback (most recent call last):
File "main.py", line 72, in thread_example
self.mainthread_callback_to_worker.connect(worker.acknowledge_callback_in_worker) # <-- causes crash
TypeError: connect() failed between MainWindow.mainthread_callback_to_worker[] and acknowledge_callback_in_worker()
Aborted (core dumped)
And the cause of the error is the abuse of the pyqtSlot decorator since it should only be used in the QObject methods but QRunnable is not causing that exception, in addition that in a non-QObject it does not take any advantage so that decorator in the run() method doesn't make sense.
On the other hand, a QRunnable is only an interface that lives in the main thread and only the run method is executed in another thread, so a QRunnable cannot be a worker since that type of objective must execute its methods in a secondary thread.
So with the above QRunnable is not the appropriate option, so for your purpose I recommend using a QObject that lives in a secondary thread and invoking the methods.
import sys
import time
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer, QThread
from PyQt5.QtWidgets import (
QApplication,
QMainWindow,
QWidget,
QVBoxLayout,
QLabel,
QPushButton,
)
class Worker(QObject):
callback_from_worker = pyqtSignal()
def __init__(self, func, *args, **kwargs):
super(Worker, self).__init__()
self._func = func
self.args = args
self.kwargs = kwargs
self.kwargs["signal"] = self.callback_from_worker
def start_task(self):
QTimer.singleShot(0, self.task)
#pyqtSlot()
def task(self):
self._func(*self.args, **self.kwargs)
#pyqtSlot()
def acknowledge_callback_in_worker(self):
print("Acknowledged Callback in Worker")
print(threading.current_thread())
class MainWindow(QMainWindow):
mainthread_callback_to_worker = pyqtSignal()
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
w, lay = QWidget(), QVBoxLayout()
w.setLayout(lay)
self.setCentralWidget(w)
self.timer_label = QLabel("Timer Label")
lay.addWidget(self.timer_label)
self.btn_thread_example = QPushButton("Push Me")
self.btn_thread_example.pressed.connect(self.thread_example)
lay.addWidget(self.btn_thread_example)
self.counter = 0
self.timer = QTimer(interval=1000, timeout=self.recurring_timer)
self.timer.start()
self._worker = Worker(self.do_something)
self._worker.callback_from_worker.connect(
self.acknowledge_callback_in_mainthread_and_respond
)
self.worker_thread = QThread(self)
self.worker_thread.start()
self._worker.moveToThread(self.worker_thread)
#pyqtSlot()
def do_something(self, signal):
print(
"do_something is sleeping briefly. Try to see if you get a locked widget..."
)
time.sleep(7)
signal.emit()
#pyqtSlot()
def acknowledge_callback_in_mainthread_and_respond(self):
print("Acknowledged Callback in Main")
self.mainthread_callback_to_worker.emit()
def thread_example(self):
print("Beginning thread example")
self._worker.start_task()
def recurring_timer(self):
self.counter += 1
self.timer_label.setText(f"Counter: {self.counter}")
if __name__ == "__main__":
app = QApplication(sys.argv)
win = MainWindow()
app.setStyle("Fusion")
win.show()
ret = app.exec_()
win.worker_thread.quit()
win.worker_thread.wait()
sys.exit(ret)

How to get a child thread to close when main GUI window is closed in pyqt5 / python 3?

I am writing a GUI using pyqt5 (Python 3.6). I am trying to run another thread in parallel of the main GUI. I would like this child thread to terminate when I close the main application. In this example, the child thread is a simple counter. When I close the main GUI, the counter still keeps going. How can I get the thread to end when the GUI window is closed? In the real case I may have a thread that is running operations that takes a few minutes to execute. I am reluctant to use a flag within the thread to assess if it should end because it may take minutes for the thread to close after the GUI window has been closed. I would prefer the thread to end right away. Any suggestions?
Thank you.
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton,
QVBoxLayout)
import time, threading, sys
class testScriptApp(QtWidgets.QWidget):
def __init__(self, parent=None):
# initialize th widget
QtWidgets.QWidget.__init__(self, parent)
# set the window title
self.setWindowTitle("Scripting")
# manage the layout
self.mainGrid = QVBoxLayout()
self.button = QPushButton('Start')
self.button.clicked.connect(self.on_click)
self.mainGrid.addWidget(self.button)
self.setLayout(self.mainGrid)
def on_click(self):
self.worker = threading.Thread(target=Worker)
self.worker.daemon = True
self.worker.start()
def Worker(count=1):
while count>0:
print(count)
time.sleep(2)
count+=1
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myapp = testScriptApp()
myapp.show()
app.exec_()
I tried to use the QThread but this locks up the main GUI. I am not sure if I am implementing it correctly.
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton,
QVBoxLayout)
from PyQt5.QtCore import QThread
import time, threading, sys
class testScriptApp(QtWidgets.QWidget):
def __init__(self, parent=None):
# initialize th widget
QtWidgets.QWidget.__init__(self, parent)
# set the window title
self.setWindowTitle("Scripting")
# manage the layout
self.mainGrid = QVBoxLayout()
self.button = QPushButton('Start')
self.button.clicked.connect(self.on_click)
self.mainGrid.addWidget(self.button)
self.setLayout(self.mainGrid)
def on_click(self):
self.worker = Worker()
self.worker.run()
def closeEvent(self,event):
print('Closing')
self.worker.terminate()
event.accept()
class Worker(QThread):
def __init__(self):
QThread.__init__(self)
def run(self):
count=1
while count>0:
print(count)
time.sleep(2)
count+=1
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myapp = testScriptApp()
myapp.show()
app.exec_()
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton,
QVBoxLayout)
from PyQt5.QtCore import QThread,QObject
import time, threading, sys
class testScriptApp(QtWidgets.QWidget):
def __init__(self, parent=None):
# initialize th widget
QtWidgets.QWidget.__init__(self, parent)
# set the window title
self.setWindowTitle("Scripting")
# manage the layout
self.mainGrid = QVBoxLayout()
self.button = QPushButton('Start')
self.button.clicked.connect(self.on_click)
self.mainGrid.addWidget(self.button)
self.setLayout(self.mainGrid)
def on_click(self):
self.my_thread = QThread()
self.worker = Worker()
self.worker.moveToThread(self.my_thread)
self.my_thread.started.connect(self.worker.run)
self.my_thread.start()
def closeEvent(self,event):
print('Closing')
class Worker(QObject):
def __init__(self):
super().__init__()
def run(self):
count=1
while count>0:
print(count)
time.sleep(2)
count+=1
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myapp = testScriptApp()
myapp.show()
app.exec_()
hope self.my_thread.requestInterruption() won't qualify as flag, otherwise is not the sought answer :
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton,
QVBoxLayout)
from PyQt5.QtCore import QThread,QObject
import time, threading, sys
class testScriptApp(QtWidgets.QWidget):
def __init__(self, parent=None):
# initialize th widget
QtWidgets.QWidget.__init__(self, parent)
# set the window title
self.setWindowTitle("Scripting")
# manage the layout
self.mainGrid = QVBoxLayout()
self.button = QPushButton('Start')
self.button.clicked.connect(self.on_click)
self.mainGrid.addWidget(self.button)
self.setLayout(self.mainGrid)
def on_click(self):
self.my_thread = QThread()
self.worker = Worker(self.my_thread)
self.worker.moveToThread(self.my_thread)
self.my_thread.started.connect(self.worker.run)
self.my_thread.start()
def closeEvent(self,event):
self.my_thread.requestInterruption()
print('Closing')
self.my_thread.quit()
self.my_thread.wait()
print('is thread running ?', self.my_thread.isRunning())
class Worker(QObject):
def __init__(self, thread):
super().__init__()
self.thread = thread
def run(self):
count=1
while count>0:
print(count)
time.sleep(2)
count+=1
if self.thread.isInterruptionRequested():
break
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myapp = testScriptApp()
myapp.show()
app.exec_()
see PySide6.QtCore.QThread.isInterruptionRequested()
Using threading.thread like in your question, I used threading.Event() and I set it (= True) to stop your Worker loop, but once again I hope is not a flag:
from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton,
QVBoxLayout)
import time, threading, sys
class testScriptApp(QtWidgets.QWidget):
def __init__(self, parent=None):
# initialize th widget
QtWidgets.QWidget.__init__(self, parent)
# set the window title
self.setWindowTitle("Scripting")
# manage the layout
self.mainGrid = QVBoxLayout()
self.button = QPushButton('Start')
self.button.clicked.connect(self.on_click)
self.mainGrid.addWidget(self.button)
self.setLayout(self.mainGrid)
self.event = threading.Event()
def on_click(self):
self.worker = threading.Thread(target=Worker, args = (self.event,))
self.worker.daemon = True
self.worker.start()
def closeEvent(self, event):
if 'worker' in self.__dict__:
self.event.set() ## sets event: threading.Event() = True
self.worker.join() ## waits self.worker to complete ***
print('is worker alive, self.worker.is_alive() : ', self.worker.is_alive())
pass
def Worker(event, count=1 ):
print(event)
while count>0:
print(count)
time.sleep(2)
count+=1
if event.is_set():
break
if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)
myapp = testScriptApp()
myapp.show()
app.exec_()
try the code commenting and uncommentig out self.event.set() & self.worker.join() in
def closeEvent(self, event):
if 'worker' in self.__dict__:
self.event.set() ## sets event: threading.Event() = True
self.worker.join() ## waits self.worker to complete ***
print('is worker alive, self.worker.is_alive() : ', self.worker.is_alive())
pass
*** Wait until the thread terminates. This blocks the calling thread until the thread whose join() method is called terminates –

What is the proper way of opening a child dialog in a second thread in PyQt when it is previously onknown at what point the dialog will be opened?

I have an application where I run some process in a second thread. At some points during this process, given a certain condition is met, another dialog window opens, which halts the process until you confirm something. This causes the following error message:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)
Interestingly, if you also move your cursor over the MainWindow while the process is running, and before the new dialog pops up, it also produces this error message a couple of times:
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
Very strange. Because it only occurs if you move your cursor over the MainWindow.
Now, in my application, I actually load an interface for the new dialog that pops up using PyQt5.uic.loadUi, and this hasn't caused any problems. However, when I was creating the example for this post, another issue occurred, due to the fact that I was setting the layout of the new dialog during its initialization:
QObject::setParent: Cannot set parent, new parent is in a different thread
Which results in the application crashing:
Process finished with exit code -1073741819 (0xC0000005)
I'm obviously doing something wrong here regarding the threading I would guess, but I don't know what. I am especially baffled by the fact that I cannot set the layout of the new dialog during its initialization, while using loadUi is totally fine. Here is my example code:
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('pass variable')
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
def run(self):
result = None
i = 0
while i < 100 and self._isRunning:
if i == np.random.randint(0, 100):
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('test')
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel('Result:')
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.run)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f'Result: {result}')
self.pbar.setValue(0)
self.handler.stop()
self.handler.progress.disconnect(self.progress)
self.handler.finished.disconnect(self.finisher)
self.handler_thread.started.disconnect(self.handler.run)
self.handler_thread.terminate()
self.handler = None
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
EDIT
I forgot to mention that I already found this post, which may be related to my problem, however, I don't undestand the reasoning of the solution in the top answer, and more importantly, I don't speak what I believe is C++.
If we analyze the case before equality happens it is the same case after that equality happens unless the initial progress is different, with this it indicates that when equality happens a signal must be sent to the GUI to ask for the information, and when you have that information, launch the same task but with an initial progress equal to what you had before the signal was emitted.
from functools import partial
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QPushButton,
QGridLayout,
QProgressBar,
QLabel,
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("pass variable")
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal()
resultChanged = pyqtSignal(int)
requestSignal = pyqtSignal()
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
self.iter = 0
self.result = None
#pyqtSlot()
def start(self):
self._isRunning = True
self._success = False
self.iter = 0
self.result = None
self.task()
#pyqtSlot()
#pyqtSlot(int)
def task(self, value=None):
if value is not None:
self.result = value
while self.iter < 100 and self._isRunning:
if self.iter == np.random.randint(0, 100):
self.requestSignal.emit()
return
time.sleep(0.01)
self.iter += 1
self.progress.emit(self.iter)
if self.iter == 100:
self._success = True
if self.result:
self.resultChanged.emit(self.result)
self.finished.emit()
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("test")
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel("Result:")
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.resultChanged.connect(self.on_result_changed)
self.handler.finished.connect(self.on_finished)
self.handler.requestSignal.connect(self.onRequestSignal)
self.handler_thread.started.connect(self.handler.start)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def onRequestSignal(self):
dialog = SpecialDialog()
dialog.exec_()
wrapper = partial(self.handler.task, dialog.variable)
QTimer.singleShot(0, wrapper)
#pyqtSlot(int)
def on_result_changed(self, result):
self.result = result
self.resultLabel.setText(f"Result: {result}")
#pyqtSlot()
def on_finished(self):
self.pbar.setValue(0)
self.handler.stop()
self.handler_thread.quit()
self.handler_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())

What is the proper way of opening a child dialog in a second thread in PyQt?

I have an application where I run some process in a second thread and at some point, given a certain condition, another dialog window opens, which halts the process until you confirm something. This causes the following error message:
QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)
Interestingly, if you also move your cursor over the MainWindow while the process is running, and before the new dialog pops up, it also produces this error message a couple of times:
QBasicTimer::stop: Failed. Possibly trying to stop from a different thread
Very strange. Because it only occurs if you move your cursor over the MainWindow.
Now, in my application, I actually load an interface for the new dialog that pops up using PyQt5.uic.loadUi, and this hasn't caused any problems. However, when I was creating the example for this post, another issue occurred, due to the fact that I was setting the layout of the new dialog during its initialization:
QObject::setParent: Cannot set parent, new parent is in a different thread
Which results in the application crashing:
Process finished with exit code -1073741819 (0xC0000005)
I'm obviously doing something wrong here regarding the threading I would guess, but I don't know what. I am especially baffled by the fact that I cannot set the layout of the new dialog during its initialization, while using loadUi is totally fine. Here is my example code:
import sys
import time
import numpy as np
from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('pass variable')
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
def run(self):
result = None
i = 0
while i < 100 and self._isRunning:
if i == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton('test')
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel('Result:')
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.run)
self.handler_thread.start()
def progress(self, val):
self.pbar.setValue(val)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f'Result: {result}')
self.pbar.setValue(0)
self.handler.stop()
self.handler.progress.disconnect(self.progress)
self.handler.finished.disconnect(self.finisher)
self.handler_thread.started.disconnect(self.handler.run)
self.handler_thread.terminate()
self.handler = None
if __name__ == '__main__':
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())
EDIT
I forgot to mention that I already found this post, which may be related to my problem, however, I don't undestand the reasoning of the solution in the top answer, and more importantly, I don't speak what I believe is C++.
You cannot create or modify a GUI element from a secondary thread and this is signaled by the error message.
You have to redesign the Handler class, with your requirement you must divide run into 2 methods, the first method will generate progress up to 50% where the GUI will open the dialogue, obtain the result and launch the second method.
import sys
import time
import numpy as np
from functools import partial
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
QDialog,
QApplication,
QPushButton,
QGridLayout,
QProgressBar,
QLabel,
)
class SpecialDialog(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("pass variable")
btn.clicked.connect(self.accept)
layout = QGridLayout()
layout.addWidget(btn)
# self.setLayout(layout)
self.variable = np.random.randint(0, 100)
class Handler(QObject):
progress = pyqtSignal(int)
finished = pyqtSignal(int)
def __init__(self):
super().__init__()
self._isRunning = True
self._success = False
#pyqtSlot()
def task1(self):
i = 0
while i <= 50 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
#pyqtSlot(int)
def task2(self, result):
i = 50
while i < 100 and self._isRunning:
time.sleep(0.01)
i += 1
self.progress.emit(i)
if i == 100:
self._success = True
self.finished.emit(result)
def stop(self):
self._isRunning = False
class MainWindow(QDialog):
def __init__(self):
super().__init__()
btn = QPushButton("test")
btn.clicked.connect(self.run_test)
self.pbar = QProgressBar()
self.resultLabel = QLabel("Result:")
layout = QGridLayout(self)
layout.addWidget(btn)
layout.addWidget(self.pbar)
layout.addWidget(self.resultLabel)
self.setLayout(layout)
self.handler = None
self.handler_thread = QThread()
self.result = None
def run_test(self):
self.handler = Handler()
self.handler.moveToThread(self.handler_thread)
self.handler.progress.connect(self.progress)
self.handler.finished.connect(self.finisher)
self.handler_thread.started.connect(self.handler.task1)
self.handler_thread.start()
#pyqtSlot(int)
def progress(self, val):
self.pbar.setValue(val)
if val == 50:
dialog = SpecialDialog()
dialog.exec_()
result = dialog.variable
wrapper = partial(self.handler.task2, result)
QTimer.singleShot(0, wrapper)
def finisher(self, result):
self.result = result
self.resultLabel.setText(f"Result: {result}")
self.pbar.setValue(0)
self.handler.stop()
self.handler_thread.quit()
self.handler_thread.wait()
if __name__ == "__main__":
app = QApplication(sys.argv)
GUI = MainWindow()
GUI.show()
sys.exit(app.exec_())

Categories