PySide2/QT for Python : Widget not updating / GUI Freezing - python

I'm using pyside2 with python. On a Qmainwindow, I've created a QpushButton and a Qlabel. The label is hidden at the init of the mainWindow, and the pushButton is connected to the following function, defined in mainWindow :
def buttonPushed(self):
self.label.show()
self.doStuff()
self.label.hide()
The "doStuff()" function takes 15sec of exec time and works as intended so the label should be visible for this time, but it isn't. If I delete the "self.label.hide()", the label does show (and never hide anymore, of course).
The "doStuff()" function calls pyside2's functions.
I also tried to add a self.label.update() just after the self.label.show(), but it makes no difference.
My guess is that is has something to do with how QT schedules tasks : when I call self.label.show(), QT only does it after buttonPushed() has ended.
What should I do ?

If a task is executed for a long time, it will block the Qt event loop causing certain ones to not work properly since the events cannot be transmitted causing the window to freeze, generating as an effect what you indicate.
So in those cases you must execute that task in another thread (assuming that task does not modify the GUI directly) and send the information to the GUI thread through signals. In your case, a signal must be emitted before executing the heavy task and another after they are connected to the show and hide method of the QLabel, respectively.
import threading
from PySide2.QtCore import Signal
class MainWindow(QMainWindow):
started = Signal()
finished = Signal()
def __init__(self, parent=None):
super().__init__(parent)
# ...
self.started.connect(self.label.show)
self.finished.connect(self.label.hide)
self.button.clicked.connect(self.buttonPushed)
def buttonPushed(self):
threading.Thread(target=self.doStuff, daemon=True).start()
def doStuff(self):
self.started.emit()
# heavy task
self.finished.emit()

#eyllanesc 's solution works when the task in the thread doesn't involve QT. One should use QThread and Qthreadpool otherwise. I used this tutorial to understand how to do it, but here's a small example :
from PySide2.QtCore import Signal, QRunnable, QThreadPool, QObject
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
# [...]
self.threadpool = QThreadPool()
self.button.clicked.connect(self.buttonPushed)
def buttonPushed(self):
builder = Builder()
builder.signals.started.connect(self.label.show)
builder.signals.finished.connect(self.label.hide)
builder.signals.result.connect(self.doStuff)
self.threadpool.start(builder)
# a function that use the heavy task's results
def doStuff(self, *args):
# [...]
#defining the signals that our thread will use
class BuilderSignals(QObject):
started = Signal()
finished = Signal()
result = Signal(*args)
#defining our thread
class Builder(QRunnable):
def __init__(self,parent=None):
super(Builder, self).__init__()
self.signals = BuilderSignals()
def run(self):
self.signals.started.emit()
#heavy task involving PySide2
self.signals.result.emit(*args)
self.signals.finished.emit()

Related

How to structure large pyqt5 GUI without subclassing QThread and using QPushButtons to do long-running tasks

I'm looking at creating a program with a PyQt5 GUI. The program will start with a UI with numerous buttons. These buttons will be used to open other programs/completed long running tasks. I know I need to use QThread, but I am unsure how to structure the programs so that it scales properly.
I've been at this for ages and have read numerous posts/tutorials. Most lean down the subclassing route. In the past, I have managed to create a working program subclassing QThread, but I have since read that this metholodogy is not preferred.
I have a feeling I should be creating a generic worker and passing in a function with *args and **kwargs, but that is not in my skillset yet.
I originally created a thread for each button during the GUI init, but that seemed like it was going to get out of hand quickly.
I am currently at the stage of creating a thread under the slot connected to the button.clicked signal. I am not sure if I then have to have a worker for each button or if I can/should make a generic worker and pass in a function. Note: I have tried to do this but have not been able to do it.
#Import standard modules
import sys
#Import third-party modles
from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QMainWindow, QApplication, QPushButton, QVBoxLayout, QWidget
class Worker(QObject):
#Custom signals?? or built-in QThread signals?
started = pyqtSignal()
finished = pyqtSignal()
def __init__(self):
super().__init__()
self.started.emit()
#pyqtSlot()
def do_something(self):
for _ in range(3):
print('Threading...')
QThread.sleep(1)
self.finished.emit()
class Window(QMainWindow):
def __init__(self):
super().__init__()
self.initUi()
def initUi(self):
#Create GUI
self.centralWidget = QWidget()
self.setCentralWidget(self.centralWidget )
self.vertical_layout = QVBoxLayout(self.centralWidget)
self.setWindowTitle('QThread Test')
self.setGeometry(300, 300, 300, 50)
self.button1=QPushButton("Task 1", self, clicked=self._task1_clicked)
self.button2=QPushButton("Task 2", self, clicked=self._task2_clicked)
self.vertical_layout.addWidget(self.button1)
self.vertical_layout.addWidget(self.button2)
self.vertical_layout.addStretch()
def _task1_clicked(self):
print('task1 clicked')
#Create the worker
self.my_worker = Worker()
#Create thread; needs to be done before connecting signals/slots
self.task1_thread = QThread()
#Move the worker to the thread
self.my_worker.moveToThread(self.task1_thread)
#Connect worker and thread signals to slots
self.task1_thread.started.connect(self._thread_started)
self.task1_thread.started.connect(self.my_worker.do_something)
self.my_worker.finished.connect(self._thread_finished)
#Start thread
self.task1_thread.start()
def _task2_clicked(self):
print('task2 clicked')
def _thread_started(self):
print('thread started')
def _thread_finished(self):
print('thread finished')
self.my_worker.isRunning = False
self.task1_thread.quit()
self.task1_thread.wait()
print('The thread is running: ' + str(self.task1_thread.isRunning()))
if __name__ == '__main__':
app = QApplication(sys.argv)
form = Window()
form.show()
app.exec_()
The above seems to work, but I feel like I have stumbled on to it and it is not the correct way of doing this. I do not want this to be my 'go-to' method if it is completely wrong. I'd like to be able to generate more complicated (more buttons doing things) programs compared to a one button/one task program.
In addition, I can't seem to get the QThread started and finished signals to fire without basically making them custom built signals. This is one reason I think I am going about this wrong.
from PyQt5 import QtCore
class AsyncTask(QtCore.QThread):
taskDone = QtCore.pyqtSignal(dict)
def __init__(self, *, task, callback=None, parent = None):
super().__init__(parent)
self.task = task
if callback != None:
self.taskDone.connect(callback)
if callback == None:
callback = self.callback
self.start()
def run(self):
try:
result = self.task()
print(result)
self.taskDone.emit(result)
except Exception as ex:
print(ex)
def callback(self):
print('callback')
Please try code above, call like this:
AsyncTask(task=yourTaskFunction, callback=yourCallbackFunction)

pyqt QLabel not rendered while another thread update its text

I have a GUI program built on PyQt5, which constantly receive messages. The GUI has a QLabel showing the number of messages received, and a QThread trying to receive messages and update counter in an infinite loop in the run(). Here is the code:
class ReceiveThread(QtCore.QThread):
def __init__(self, parent, dialog, config):
super(BufRecvThread, self).__init__(parent)
#here dialog is the QDialog which contains the QLabel showing the message counter
self.dialog=dialog
self.toStop=False
def run(self):
bufRecvCnt=0
while not self.toStop:
recv_buff=sock.recvMessage()
bufRecvCnt=bufRecvCnt+1
#self.dialog.lbBufRecvCnt is the QLabel showing the message counter
self.dialog.lbBufRecvCnt.setText(str(bufRecvCnt))
QApplication.processEvents() #this statement has no effect
However, most of the time, I find that the QLabel in the GUI does NOT render the counter properly, say, the ReceiveThread has received 10000 messages and block at sock.recvMessage(), the QLabel still shows "500" until I manually resize the GUI causing the GUI to rerender.
I tried the suggestions of this thread pyqt QtGraphicsView does not get updated within loop, and added QApplication.processEvents() into the loop, but still NOT work.
So, is it proper to directly update the GUI from another thread? PyQt - Modify GUI from another thread suggests to emit signals. As I'm not familiar with signal & slot, should I use a predefined signal of the QLabel, or May I define whatever signal as i need, as long as the corresponding slot will update the text of the QLabel with setText().
Only the main GUI thread can update the GUI. You should not directly interact with GUI objects outside the main thread. If you want to communicate with the main thread from worker threads, you need to use Signals and Slots
class Dialog(QtGui.QDialog):
def __init__(self, parent):
super(Dialog, self).__init__(parent)
...
self.thread = ReceiveThread(self)
self.thread.message_received.connect(self.handle_message)
#QtCore.pyqtSlot(str)
def handle_message(self, message):
self.lbBufRecvCnt.setText(message)
class ReceiveThread(QtCore.QThread):
message_received = QtCore.pyqtSignal(str)
def __init__(self, parent, config):
super(ReceiveThread, self).__init__(parent)
self.toStop = False
def run(self):
bufRecvCnt=0
while not self.toStop:
recv_buff = sock.recvMessage()
bufRecvCnt = bufRecvCnt + 1
self.message_received.emit(str(bufRecvCnt))

PySide: Emiting signal from QThread in new syntax

I'm trying (and researching) with little success to emit a signal from a working Qthread to the main window. I don't seem to understand how I should go about this in the new syntax.
Here's a simple example.
from PySide.QtCore import *
from PySide.QtGui import *
import sys
import time
class Dialog(QDialog):
def __init__(self, parent=None):
super(Dialog, self).__init__(parent)
button = QPushButton("Test me!")
layout = QVBoxLayout()
layout.addWidget(button)
self.setLayout(layout)
#self.button.clicked.connect(self.test) ----> 'Dialog' object has no attribute 'button'
self.connect(button, SIGNAL('clicked()'), self.test)
self.workerThread = WorkerThread()
def test(self):
self.workerThread.start()
QMessageBox.information(self, 'Done!', 'Done.')
class WorkerThread(QThread):
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
def run(self):
time.sleep(5)
print "Thread done!"
app = QApplication(sys.argv)
dialog = Dialog()
dialog.show()
app.exec_()
I understand that if I didn't have another thread I'd create the signal inside the Dialog class and connect it in the __init__ but how can I create a custom signal that can be emitted from WorkerThread and be used test()?
As a side question. You can see it commented out of the code that the new syntax for connecting the signal errors out. Is it something in my configurations?
I'm on OsX El Capitan, Python 2.7
Any help is highly appreciated! Thanks a lot
TL:DR: I'd like to emmit a signal from the WorkerThread after 5 seconds so that the test function displays the QMessageBox only after WorkingThread is done using the new syntax.
Ok, it's been a long day trying to figure this out. My main resource was this: http://www.matteomattei.com/pyside-signals-and-slots-with-qthread-example/
In the new syntax, in order to handle signals from different threads, you have to create a class for your signal like so:
class WorkerThreadSignal(QObject):
workerThreadDone = Signal()
This is how the WorkerThread end up looking like:
class WorkerThread(QThread):
def __init__(self, parent=None):
super(WorkerThread, self).__init__(parent)
self.workerThreadSignal = WorkerThreadSignal()
def run(self):
time.sleep(3)
self.workerThreadSignal.workerThreadDone.emit()
And for the connections on the Dialog class:
self.workerThread = WorkerThread()
self.buttonn.clicked.connect(self.test)
and:
self.workerThreadSignal = WorkerThreadSignal()
self.workerThread.workerThreadSignal.workerThreadDone.connect(self.success)
def success(self):
QMessageBox.warning(self, 'Warning!', 'Thread executed to completion!')
So the success method is called once the signal is emitted.
What took me the longest to figure out was this last line of code. I originally thought I could connect directly to the WorkerThreadSignal class but, at least in this case, it only worked once I backtracked it's location. From the Dialog init to WorkerThread init back to the WorkerThreadSignal. I took this hint from the website mentioned above.
I find strange that I have to create the same local variables on both classes, maybe there's a way to create one global variable I can refer to instead all the current solution but it works for now.
I hope this helps someone also stuck in this process!
PS: The syntax problem for the connection was also solved. So everything is written with the new syntax, which is great.

PyQt5 - Used QThread to avoid ui hang but it still happens

I'm trying to make something parses an website and displays its content on PyQt widget.
I've finished logic part, but IO task hangs the ui for few seconds, making progressbar useless.
So I tried to use threads but it didn't helped.
Here is simplified version of what I'm doing (I'm using PyQt5 on Python 3.4.4, Windows 10)
MainWindow is Qt Designer generated class.
from PyQt5 import QtWidgets
from PyQt5.QtCore import QThread, pyqtSignal
class WorkerThread(QThread):
signal = pyqtSignal(str)
def __init__(self, data):
QThread.__init__(self)
self.data = data
def __del__(self):
self.wait()
def run(self):
result = dosomeIOjob(self.data) # IO and parsing job, takes about 3 seconds
sendsomething(result)
def sendsomething(self, value):
self.signal.emit(value)
class UIClass(QtWidgets.QMainWindow, MainWindow.Ui_MainWindow):
def __init__(self):
super(self.__class__, self).__init__()
self.setupUi(self)
self.button.clicked.connect(self.dosomejob)
def dosomejob(self):
data = self.lineedit_search.text()
self.runworker(self, data)
def settext(value):
self.text.setText(value)
def runworker(self, data):
worker = WorkerThread(data)
worker.signal.connect(self.settext)
worker.run()
There's no errors; it just hangs till the job is done.
Am I using pyqtSignal wrong? Sorry for my bad english.
You need to pass the type (str) along so that settext will get it and be able to use it. I've also always used start() as in worker.start() instead of run() to start the thread. Not sure that makes a difference, just how I've read/learned it.
here's the code you need to change.
worker.signal[str].connect(self.settext)

Python PyQt: Is it possible to use QThread with a non-GUI program?

I have a Python PyQt application that displays a simple UI.
When the user clicks a button in the UI it triggers a QThread.
The use of a thread prevents the UI from "freezing" while the thread runs.
I emit signals to pass information from the run thread back to the UI for status updates and to indicate completion. Everything works fine as described and I've created a simple class for my UI to call which creates the thread and runs my generic processing.
However, I would also like to create a command line version (No GUI) of my program and use the same processing QThread class. However, I get the following error when I try to connect my signals. It would seem that QThread was only meant for GUI programs?
AttributeError: MyClass instance has no attribute 'connect'
Is it possible to use QThread with a non-GUI program?
from PyQt4 import QtCore
from PyQt4.QtCore import *
#======================================
class MyProcess(QThread):
def __init__(self):
QThread.__init__(self)
def __del__(self):
self.quit()
self.wait()
def run(self):
print "do time intensive process here"
self.emit( SIGNAL('processdone'), "emitting signal processdone")
return
#======================================
class MyClass(QObject):
def __init__(self, parent=None): # All QObjects receive a parent argument (default to None)
super(MyClass, self).__init__(parent) # Call parent initializer.
thread1 = MyProcess() # uses QThread and emits signal 'processdone'
self.connect( thread1, SIGNAL("processdone"), self.thread1done)
thread1.start()
def thread1done(self):
print "done"
#======================================
if __name__ == "__main__":
MyClass()
The problem isn't QThread, the problem is that you're calling the connect method from a class which doesn't have it. You need to make MyClass inherits from QObject.
In the GUI this works because whatever widget you're working with (QDialog, QMainWindow, QWidget ...) it inherits (directly or indirectly) from QObject.
To make MyClass inherit from QObject you only have to:
class MyClass(QObject): # Specify the class your are specializing.
def __init__(self, parent=None): # All QObjects receive a parent argument (default to None)
super(MyClass, self).__init__(parent) # Call parent initializer.
# And countinue your code here...
I also recommend you to use the New-style Signal and Slot Support.
Everything works except the processdone signal is called but apparently it never triggers call to thread1done.
The problem I can spot is you don't have defined a processdone signal. That signal does not exist for Qt. Check the link I left you to learn about custom signals. Meanwhile you can add:
class MyProcess(QThread):
processdone = QtCore.pyqtSignal("QString")
at the beginning of the class.
And one last thing, but very important. You aren't working with GUI, but you are still using QObjects and the Qt singal mechanisms, which depends on the main Qt loop to work. Hence, you still need a QApplication object despite of your application is a non-gui program.
Here is your code, now working:
from PyQt4 import QtCore
from PyQt4 import QtGui
from PyQt4.QtCore import *
class MyProcess(QThread):
processdone = QtCore.pyqtSignal("QString") # Define custom signal.
def __init__(self, parent = None):
QThread.__init__(self, parent)
def run(self):
print("do time intensive process here")
self.emit( SIGNAL('processdone'), "emitting signal processdone")
return
class MyClass(QObject):
def __init__(self, parent=None): # All QObjects receive a parent argument (default to None)
super(MyClass, self).__init__(parent) # Call parent initializer.
thread1 = MyProcess(self)
self.connect( thread1, SIGNAL("processdone"), self.thread1done)
thread1.start()
#QtCore.pyqtSlot("QString") # Tell Python this is a QTSLOT an receives a string
def thread1done(self, text):
print(text) # Print the text from the signal.
if __name__ == "__main__":
import sys
app = QtGui.QApplication(sys.argv) # You still need a QApplication object.
a = MyClass()
sys.exit(app.exec())

Categories

Resources