different ways of calling class method with button clicks - python

I am exploring how to call class methods in PyQt applications. As a test, I created a widget that initializes a worker, puts it on a separate thread, and starts the thread. I also created two buttons:
The left button connects to the run function of the worker directly
The right button connects to a widget method that calls the worker run function
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *
import sys
from time import sleep
class Worker(QObject):
worker_finished = pyqtSignal()
def __init__(self):
super().__init__()
#pyqtSlot()
def run(self):
print("Sleeping")
for ii in range(5):
print(ii)
sleep(1)
print("Finished sleeping")
class MainApp(QWidget):
def __init__(self):
super().__init__()
self._threads = []
#Create the worker
self.worker = Worker()
layout = QHBoxLayout(self)
#Move the worker on the thread and start
self.put_on_thread_and_start(self.worker)
#This button calls worker.run() directly
button = QPushButton("Call \"\"run\"\" method directly")
button.clicked.connect(self.worker.run)
layout.addWidget(button)
#This button calls a widget method, which calls worker.run()
button = QPushButton("Call \"\"run\"\" method through this widget class method")
button.clicked.connect(self.make_the_worker_run)
layout.addWidget(button)
def put_on_thread_and_start(self, worker_class):
myThread = QThread()
self._threads.append(myThread)
worker_class.moveToThread(myThread)
print("Starting thread...")
myThread.start()
def make_the_worker_run(self):
self.worker.run()
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainApp()
window.show()
sys.exit(app.exec_())
When I click the left button, the worker executes in the background, as expected. However, when I click the right button the widget freezes until the worker has finished running. What's the difference between the two approaches?

When a signal is emitted to a function (slot) and that signal is emitted, Qt detects if the signal emission and the receiving objects are in the same thread. If they are not, the slot is executed in the receiver's thread and control is immediately returned in the sender instead. This is what allows to use threads on Qt without blocking the "main Qt thread" (which is responsible of keeping the UI responsive).
A very important thing to understand is that Qt is able to detect what thread emitted the signal and on what thread the slot is, then it decides if the slot can be directly executed or not.
In your first case, the button (which is going to emit the signal) and the self.worker.run are on different threads and Qt knows that when it tries to call run; the result is that the function will be executed in the other thread.
In the second case, Qt only knows about make_the_worker_run, which from its perspective is in the same thread of the button: Qt doesn't know anything about what you actually do in that function. The fact that run is in a method of an object that has been moved to another thread doesn't mean anything, and the result is that the function will be executed in the main thread, hence the blocking.
Read more about this topic on the Qt docs about Threads and QObjects.

There is an incorrect conception of how threads, QThread, signals, etc. work.
When a QObject is moved to a QThread it only tells the Qt eventloop that if it invokes a slot of that QObject it must be executed in the thread that manages the QThread (if the Qt::QueuedConnection or Qt::AutoConnection flag is used in the connection). And how does the Qt eventloop invoke the functions? Well, for this use the signals, timers, QMetaObject::invokedMethod, QEvents, etc.
The above explains why the first method works: By using the clicked signal to invoke "run" it notifies the eventloop that it should invoke the method, and using the previous rule it will invoke the thread that manages the QThread.
In the second method instead you are invoking it directly in the main thread which blocks the eventloop of the main thread freezing the GUI.

Related

PyQt5. Controlling GUI from separate thread [duplicate]

This question already has an answer here:
In PyQt, what is the best way to share data between the main window and a thread
(1 answer)
Closed 5 months ago.
I want to produce simple GUI, a label and a push button. When I press the push button it should invoke a separate thread (the GUI unfreezes ). In the thread I have simple loop
for i in range(4):
self.label.setText(str(i))
time.sleep(1)
the loop must change the label text each second. So I expect to see the GUI how the label changes 0, 1, 2, 3 each second. This is my code:
self.pushButton.clicked.connect(self.clicked_1)
self.label.setText("Lubo1")
def clicked_1(self):
for i in range(4):
self.label.setText(str(i))
app.processEvents()
print("Sleep!")
time.sleep(1)
print("Weak up!")
x = threading.Thread(target=clicked_1, args=(1,))
x.start()
The code works exactly as I expect when I comment x = threading.Thread(target=clicked_1, args=(1,)) and uncomment app.processEvents().
However when app.processEvents() is commented and x = threading.Thread(target=clicked_1, args=(1,)) is uncommented the code gives an error:
AttributeError: 'int' object has no attribute 'label'
I don't want to use app.processEvents() since I know this is not the right way. The correct way is to use threading, but I don't know how to overcome the AttributeError: 'int' object has no attribute 'label' error. Please help.
Best Regards, thank you in advance.
Lubomir Valkov
You should never do such a process, Qt does not support gui operations of any kind outside the main thread. Sooner or later, if you call your GUI out of your main thread your app will crash because it calls methods which are not thread-safe. You should always use signals and slots to communicate between threads and main gui.
# your thread class
class ThreadClass(QtCore.QThread):
any_signal = QtCore.pyqtSignal(int)
def __init__(self, time_to_wait, parent=None):
super(ThreadClass, self).__init__(parent)
self.time_wait = time_to_wait
def run(self):
for val in range(self.time_wait):
time.sleep(1)
self.any_signal.emit(val+1)
your GUI
self.pushButton.clicked.connect(self.clicked_1)
self.label.setText("Lubo1")
def clicked_1(self):
self.thread = ThreadClass(time_to_wait=4, parent=None)
self.thread.any_signal.connect(self.update_label)
self.thread.start()
def update_label(self, value):
self.label.setText(str(i))
As you can see you your button start a thread that emit a signal. Your gui captures this signal and do what ever it need to do in the main GUI, but your thread is never changing anything of the main thread

PyQt5: Closing / relaunching application causes seg fault [duplicate]

The following is a loop that I created:
import mainui
import loginui
from PyQt5 import QtWidgets
import sys
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
ui = loginui.Ui_MainWindow()
ui.setupUi()
ui.MainWindow.show()
app.exec_()
username=ui.username
app2 = QtWidgets.QApplication(sys.argv)
ui2 = mainui.Ui_MainWindow(username)
ui2.setupUi()
ui2.MainWindow.show()
app2.exec_()
if ui2.exitFlag=='repeat':#Repeat Condition
continue
else: #Exit Condition
sys.exit()
This is a loop containing a couple of PyQt5 windows, which are displayed in order. The windows run normally when they are not contained within a loop, and they also run pretty well in the first iteration of the loop.
But, when the repeat condition is satisfied, even though the loop does iterate (prints 'test' again) - the ui and ui2 windows do not get displayed again, and subsequently the program hits the exit condition and stops.
Any suggestions about why the windows do not get displayed, and how I can get them displayed would be very much appreciated.
An important premise: usually you need only one QApplication instance.
Proposed solutions
In the following examples I'm using a single QApplication instance, and switch between windows using signals.
Since you probably need to wait for the window to be closed in some way, you might prefer to use a QDialog instead of a QMainWindow, but if for some reason you need the features provided by QMainWindow (menus, dockbars, etc) this is a possible solution:
class First(QtWidgets.QMainWindow):
closed = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.close)
def closeEvent(self, event):
self.closed.emit()
class Last(QtWidgets.QMainWindow):
shouldRestart = QtCore.pyqtSignal()
def __init__(self):
super().__init__()
central = QtWidgets.QWidget()
self.setCentralWidget(central)
layout = QtWidgets.QHBoxLayout(central)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.restart)
closeButton.clicked.connect(self.close)
def restart(self):
self.exitFlag = True
self.close()
def showEvent(self, event):
# ensure that the flag is always false as soon as the window is shown
self.exitFlag = False
def closeEvent(self, event):
if self.exitFlag:
self.shouldRestart.emit()
app = QtWidgets.QApplication(sys.argv)
first = First()
last = Last()
first.closed.connect(last.show)
last.shouldRestart.connect(first.show)
first.show()
sys.exit(app.exec_())
Note that you can add menubars to a QWidget too, by using setMenuBar(menuBar) on their layout.
On the other hand, QDialogs are more indicated for these cases, as they provide their exec_() method which has its own event loop and blocks everything else until the dialog is closed.
class First(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
button = QtWidgets.QPushButton('Continue')
layout.addWidget(button)
button.clicked.connect(self.accept)
class Last(QtWidgets.QDialog):
def __init__(self):
super().__init__()
layout = QtWidgets.QHBoxLayout(self)
restartButton = QtWidgets.QPushButton('Restart')
layout.addWidget(restartButton)
closeButton = QtWidgets.QPushButton('Quit')
layout.addWidget(closeButton)
restartButton.clicked.connect(self.accept)
closeButton.clicked.connect(self.reject)
def start():
QtCore.QTimer.singleShot(0, first.exec_)
app = QtWidgets.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
first = First()
last = Last()
first.finished.connect(last.exec_)
last.accepted.connect(start)
last.rejected.connect(app.quit)
start()
sys.exit(app.exec_())
Note that in this case I had to use a QTimer to launch the first dialog. This is due to the fact that in normal conditions signals wait for theirs slot to be completed before returning control to the emitter (the dialog). Since we're constantly recalling the same dialog, this leads to recursion:
First is executed
First is closed, emitting the finished signal, which causes the following:
Second is executed
at this point the finished signal has not returned yet
Second is accepted, emitting the accepted signal, which causes:
First hasn't returned its exec_() yet, but we're trying to exec it again
Qt crashes showing the error StdErr: QDialog::exec: Recursive call detected
Using a QTimer.singleShot ensures that the signal returns instantly, avoiding any recursion for exec_().
Ok, but why doesn't it work?
As said, only one Q[*]Application instance should usually exists for each process. This doesn't actually prevent to create more instances subsequently: in fact, your code works while it's in the first cycle of the loop.
The problem is related to python garbage collection and how PyQt and Qt deals with memory access to the C++ Qt objects, most importantly the application instance.
When you create the second QApplication, you're assigning it to a new variable (app2). At that point, the first one still exists, and will be finally deleted (by Qt) as soon as the process is completed with sys.exit.
When the cycle restarts, instead, you're overwriting app, which would normally cause python to garbage collect the previous object as soon as possible.
This represents a problem, as Python and Qt need to do "their stuff" to correctly delete an existing QApplication object and the python reference.
If you put the following line at the beginning, you'll see that the first time the instance is returned correctly, while the second returns None:
app = QtWidgets.QApplication(sys.argv)
print('Instance: ', QtWidgets.QApplication.instance())
There's a related question here on StackOverflow, and an important comment to its answer:
In principle, I don't see any reason why multiple instances of QApplication cannot be created, so long as no more than one exists at the same time. In fact, it may often be a requirement in unit-testing that a new application instance is created for each test. The important thing is to ensure that each instance gets deleted properly, and, perhaps more importantly, that it gets deleted at the right time.
A workaround to avoid the garbage collection is to add a persistent reference to the app:
apps = []
while True:
print('test')
app = QtWidgets.QApplication(sys.argv)
apps.append(app)
# ...
app2 = QtWidgets.QApplication(sys.argv)
apps.append(app2)
But, as said, you should not create a new QApplication instance if you don't really need that (which is almost never the case).
As already noted in the comments to the question, you should never modify the files generated with pyuic (nor try to mimic their behavior). Read more about using Designer.

QTimer runs after Dialog has been closed

I have a QDialog, where i create a QTimer Object, which triggers every n seconds a function. After closing the Dialog (hitting the x button), the timer is still firing and seems to be not destroyed. How can i stop it? Currently as a workaround i am explicitly calling Qtimer.stop() when entering the closeEvent()?
I would expect that every class-member is deleted, when the Window is closed, even when i call the Deconstructor explicitly, the Qtimer object persists.
from PyQt4 import QtGui, QtCore
import sys
def update():
print "tick..."
class Main(QtGui.QDialog):
def __init__(self, parent = None):
super(Main, self).__init__(parent)
self.timer = QtCore.QTimer()
self.timer.timeout.connect(update)
self.timer.start(2000)
# scroll area
self.scrollArea = QtGui.QScrollArea()
self.scrollArea.setWidgetResizable(True)
# main layout
self.mainLayout = QtGui.QVBoxLayout()
self.setLayout(self.mainLayout)
def closeEvent(self, evt):
print "class event called"
self.timer.stop()
myWidget = Main()
myWidget.show()
http://doc.qt.io/qt-5/timers.html
The main API for the timer functionality is QTimer. That class
provides regular timers that emit a signal when the timer fires, and
inherits QObject so that it fits well into the ownership structure of
most GUI programs. The normal way of using it is like this:
QTimer *timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(updateCaption()));
timer->start(1000);
The QTimer object is made into a child of this widget so that, when
this widget is deleted, the timer is deleted too. Next, its timeout()
signal is connected to the slot that will do the work, it is started
with a value of 1000 milliseconds, indicating that it will time out
every second.
In C++, the timers are parented to the widget or another QObject, and then their lifespan is tied to the lifespan of the QObject, but it is still good practice to stop a timer when you don't need it. The layouts get parented when you call setLayout. The timer doesn't know its parent so it doesn't get destroyed when the widget gets destroyed. It just sits on the heap, still getting ran by the QApplication event loop.
http://doc.qt.io/qt-5/qobject.html#setParent
So either pass in self to the constructor of the QTimer, or call setParent on the QTimer to set it into the object tree properly.
http://doc.qt.io/qt-5/objecttrees.html
UPDATE: Apparently setParent isn't working in PyQt. Just pass in self in the QTimer constructor.
Hope that helps.

Signals and Slots PyQt clarification

I have noticed that there are a lot of users, myself included, who don't quite grasp the concept of signals and slots in Qt. I was hoping to get some clarification on the following:
#I have a function that runs as soon as the GUI is built, this takes the information from
#a list and puts it into a string which is then uploaded to a texbox. At the bottom of this
#loop, I want it to call a function in the parent thread via signals and slots, as
#recommended by other users.
class MainWindow(QtGui.QMainWindow):
#all the code needed to build the GUI
thread_mythread = threading.Thread(target = self.updateText, args = ())
thread_mythread.start()
def clearText(self):
self.TextEdit.clear()
def updateText(self):
self.trigger.connect(self.clearText)
while True:
self.trigger.emit()
NewString = list.pop(0)
#I think I may have to use append, as setText() is not safe outside of the parent thread
self.TextEdit.append(NewString)
Although probably terribly incorrect, I attempt to use signals. Is this the proper way to do it? I also get an error that says that the Main Window object has no attribute "trigger",why is this?
thank you.
The reason you get that error is exactly the reason described by the error message - the signal trigger has not been defined anywhere in your class. You need to define it before you can emit it.
Signals and slots are used to communicate between different objects. In your example you are trying to do everything from within your MainWindow class and there is no interaction with other objects. You also only need to make the call to connect() once. You would typically call this either in the class constructor or from your main function after instantiating the objects you want to connect together.
Take a look at http://pyqt.sourceforge.net/Docs/PyQt4/new_style_signals_slots.html for some examples of how to use signals and slots properly in PyQt.
For threading, use QThread rather than threading.Thread as it is better integrated with the Qt framework. This post shows some simple examples of how to use QThread in PyQt. The second method (using moveToThread()) is considered to be the most correct way to create new threads.
The basic idea for your kind of problem is:
handle GUI operations from the main thread
handle blocking operations (in your case the while loop) in a separate thread
emit signals from the worker thread to call functions (slots) in the main thread and vice versa
Also note that:
You cannot call any methods of QWidget its descendents from a secondary thread
Signals can also send data if you need to pass it between threads
To add to #user3419537 good answer. A very quick threading example:
from PyQt4.QtCore import QObject, pyqtSlot, pyqtSignal, QThread, \
Q_ARG, Qt, QMetaObject
class MyWorker(QObject):
# define signal
clear = pyqtSignal()
update_text_signal = pyqtSignal(str) # passes a string back
finished = pyqtSignal()
def __init__(self, parent=None):
super(MyWorker, self).__init__(parent)
# Add functions etc.
#pyqtSlot(list)
def update_text(self, string_list):
#Intensive operation
self.clear.emit() # moved outside of while
while(True):
#This is infinite loop so thread runs forever
new_string = self.string_list.pop(0)
self.update_text_signal.emit(new_string) # Fixed this line
#Finished
self.finished.emit()
Then in your MainWindow class
self.my_thread = QThread()
self.handler = MyWorker()
self.handler.moveToThread(self.my_thread)
self.handler.clear.connect(self.clearText)
self.handler.update_text_signal.connect(self.update_line_edit)
self.handler.finished.connect(self.my_thread.quit)
# Start Thread
self.my_thread.start()
#pyqtSlot(str)
def update_line_edit(self, text):
self.TextEdit.append(text)
QMetaObject.invokeMethod(self.handler, 'update_text',
Qt.QueuedConnection,
Q_ARG(list, string_list))
You will need to call self.my_thread.quit() before your application closes to stop the thread and avoid the error: QThread: Destroyed while thread is still running
Please read docs for QMetaObject.invokeMethod.

How to move an object back and forth between QThreads in Pyqt

In my program (using Python 2.7), I create an object containing some important data and methods. Some of the methods are CPU hungry, so in certain cases I move the object to a new QThread for the duration of the CPU intensive methods, then have them come back to the main thread. At a later point, when a CPU intensive method is called, I would like to move the object to another QThread again, however this fails saying "Current thread is not the object's thread".
Here is a trivial example which reproduces the problem:
import sys
from PyQt4 import QtCore, QtGui
from time import sleep
class ClassA(QtGui.QDialog):
def __init__(self):
super(ClassA, self).__init__()
mainLayout=QtGui.QVBoxLayout()
self.lineEdit=QtGui.QLineEdit()
mainLayout.addWidget(self.lineEdit)
self.setLayout(mainLayout)
self.show()
self.obj=ClassC(self)
self.executeProgram()
def executeProgram(self):
self.lineEdit.setText("Starting new thread...")
self.thread=QtCore.QThread()
self.obj.moveToThread(self.thread)
self.thread.started.connect(self.obj.doWork)
self.obj.doingWork.connect(self.updateGui)
self.obj.finished.connect(self.killThread)
self.thread.start()
def updateGui(self,message):
self.lineEdit.setText(message)
def killThread(self):
self.thread.quit()
self.thread.wait()
self.obj.finished.disconnect()
self.executeProgram()
class ClassC(QtCore.QObject):
finished=QtCore.pyqtSignal()
doingWork=QtCore.pyqtSignal(str)
def __init__(self,parent=None):
super(ClassC, self).__init__()
def doWork(self):
for i in range(5):
self.doingWork.emit("doing work: iteration "+str(i))
sleep(1)
self.finished.emit()
if __name__=="__main__":
app=QtGui.QApplication(sys.argv)
obj=ClassA()
app.exec_()
Is it possible to move an object to a different QThread multiple times? If so, how would I fix my code to do this?
Note moveToThread must be called on the thread that the object currently belongs to, so you may need to move the object back to the main thread before moving it to yet another thread.
Add the line mainThread = QtCore.QThread.currentThread() somewhere at the top, and put self.moveToThread(mainThread) right before emitting the finished signal.

Categories

Resources