Can signal handlers memory leak in PyQt? [duplicate] - python

This question already has an answer here:
Lifetime of object in lambda connected to pyqtSignal
(1 answer)
Closed 3 years ago.
Short question:
Can signal handlers memory leak.
Long question:
In C#, if I attach a handler to an event
left_object.left_event += right_object.right_handler
Then I need to remove the handler when I get rid of right_object or the garbage collector will never dispose of it (since left_object.left_event retains a pointer to right_object)
Is the same also true of PyQt signals and slots.
left_object.left_signal.connect( right_object.right_handler )
I see from this question that Qt automatically delinks signals and slots, when the destructor of either left_object or right_object is called, but in Python I cannot explicitly call the destructor, and right_handler is a plain-old-function.
Do I need to remove the handler to prevent right_objects memory-leaking, or does PyQt use some sort of weak-referencing?
While one of the answers touches on this, this similar question asks about how PyQt handles objects inside lambda expressions, not how PyQt handles signals.

It is possible to have a memory leak with this design. You need to disconnect the signals and slots if those connections contain references which are keeping objects alive.
Whether or not this actually happens depends on what exactly right_handler is. If right_handler is a closure with a reference to self then you have this problem.

but in Python I cannot explicitly call the destructor,
You can call destructor in python. Check del and __del__
Do I need to remove the handler to prevent right_objects memory-leaking,
No, you don't need to do that. python will handle that. Run below code check your self.
In this code signal of startButton1 and stopButton1 are connected to methods of class object of Hello and this object(hello1) is attribute of Widget class. So hello1 will live until life object of Widget OR we delete it in method Widget.onDelete with Delete button. Once you click Delete button, destructor of hello1 is called and it will goes out of scope. Now signal of startButton1 and stopButton1 don't work as before.
Now in second row there are buttons startButton2 and stopButton2, whose signal are connected with object of hello2. But as soon as construct of Widget is over hello2 life time is finish. So there is not slot connected for signal of those buttons after Widget construct is over.
from PyQt5.QtWidgets import QApplication,QPushButton,QWidget,QHBoxLayout,QVBoxLayout
class Hello():
def __init__(self):
print("init")
def onStart(self):
print("onStart");
def onStop(self):
print("onStop");
def __del__(self):
print("del")
class Widget(QWidget):
def __init__(self,parent=None):
QWidget.__init__(self,parent);
vLayout = QVBoxLayout()
self.setLayout(vLayout)
buttons1 = QWidget()
hLayout1 = QHBoxLayout()
buttons1.setLayout(hLayout1)
vLayout.addWidget(buttons1)
startButton1 = QPushButton("Start");
stopButton1 = QPushButton("Stop");
deleteButton1 = QPushButton("Delete");
self.hello1 = Hello();
startButton1.clicked.connect(self.hello1.onStart)
stopButton1.clicked.connect(self.hello1.onStop)
deleteButton1.clicked.connect(self.onDelete)
hLayout1.addWidget(startButton1);
hLayout1.addWidget(stopButton1);
hLayout1.addWidget(deleteButton1);
buttons2 = QWidget()
hLayout2 = QHBoxLayout()
buttons2.setLayout(hLayout2)
vLayout.addWidget(buttons2)
startButton2 = QPushButton("Start");
stopButton2 = QPushButton("Stop");
deleteButton = QPushButton("Delete");
hello2 = Hello();
startButton2.clicked.connect(hello2.onStart)
stopButton2.clicked.connect(hello2.onStop)
hLayout2.addWidget(startButton2);
hLayout2.addWidget(stopButton2);
def onDelete(self):
if hasattr(self,"hello1"):
del self.hello1
app = QApplication([])
w = Widget();
w.show()
app.exec_()
Hope this will clear you doubt.

Related

Why is QWidget being destroyed? (PyQt)

So I have the main window. When I click a button in the main window, a new widget is created (in a new window):
self.doorButton.clicked.connect(self.open_door)
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
The new QWidget or doorwin has only one widget - tableWidget
I use the object self.doors to populate the table. Now since I have a worker thread (QThread) updating the said object (self.doors), I use QTimer to repopulate the table every 1 second.
class doorwinActions(Ui_doorWin):
def __init__(self,doors):
self.doors = doors
# update setupUi
def setupUi(self, Widget):
super().setupUi(Widget)
Widget.move(self.left, self.top) # set location for window
Widget.setWindowTitle(self.title) # change title
self.timer = QTimer()
self.timer.timeout.connect(lambda:self.popTable())
self.timer.start(1000)
def popTable(self):
mutex.lock()
entries = len(self.doors)
self.tableWidget.setRowCount(entries)
for i in range(entries):
self.tableWidget.setItem(i,0,QtWidgets.QTableWidgetItem(str(self.doors[i][0])))
self.tableWidget.setItem(i,1,QtWidgets.QTableWidgetItem(self.doors[i][1].get_id()))
self.tableWidget.setItem(i,2,QtWidgets.QTableWidgetItem(self.doors[i][1].get_name()))
self.tableWidget.setItem(i,3,QtWidgets.QTableWidgetItem(str(self.doors[i][2])))
mutex.unlock()
When I run the program, it runs smoothly. It opens a new window. If the self.doors object is updated while the window is open, the GUI reflects the change.
BUT, the problem occurs if I reopen the window. If I close the window and then click on the button again, the program crashes with the error:
RuntimeError: wrapped C/C++ object of type QTableWidget has been deleted
From what I understand about my code, when I close the window, the whole widget window (and the table) is deleted. And when I click on the doorButton, new Qwidget/table are created. So, the question is, why would it delete something it just created?
What (sort of) works? - If I move the setup of the door window to the main window's setup, it works. So the open_door function would just be:
def open_door(self):
self.doorwin.show()
The rest would be in the main window setup. But the problem is, then even when I close the window, the QTimer is still going in the background, just eating up processing power.
So, either,
How do I stop the event when the window is closed OR
How do I stop the tableWidget from being deleted?
Your main problem is garbage collection.
When you do this:
def open_door(self):
self.doorwin = QtWidgets.QWidget()
self.doorui = doorwinActions(self.doors)
self.doorui.setupUi(self.doorwin)
self.doorwin.show()
You are creating a new QWidget. It has absolutely no other reference but self.doorwin. This means that if you call open_door again, you will be overwriting self.doorwin, and since the previous widget was not referenced anywhere else, it will get deleted along with all its contents.
Now, QTimers are tricky. You created a QTimer in doorwinActions and QTimers can be persistent even if they have no parent: they keep going on until they're stopped or deleted, and they can only be deleted explicitly or when their parent is deleted (with the exception of timers created with the static QTimer.singleShot() function).
Finally, you must remember that PyQt (like PySide) is a binding. It creates "connections" with the objects created in Qt (let's call them "C++ objects"), and through those bindings we can access those objects, their functions and so on, through python references.
But, and this is of foremost importance, both objects can have a different lifespan:
the python reference can be deleted, and the Qt object can still exist;
the Qt object can be destroyed, yet we still have the python object that referenced (past tense) it;
This is exactly what happens in your case: the self.doorui object is overwritten, but it has an object (the QTimer, self.timer) that is still alive, so the Python garbage collector will not delete it, and the timer is still able to call popTable. But, at that point, the widget (self.doorwin) and its contents have been destroyed on the "C++ side", which causes your crash: while self.tableWidget still exists as a python reference, the actual widget has been destroyed along with its parent widget, and calling its functions causes a fatal error as the binding cannot find the actual object.
Now, how to solve that?
There are various options, and it depends on what you need to do with that window. But, before that, there is something much more important.
You have been manually editing a file generated by pyuic, but those files are not intended for that. They are to be considered like "resource" files, used only for their purpose (the setupUi method), and never, EVER be manually edited. Doing that is considered bad practice and is conceptually wrong for many reasons - and their header clearly warns about that. To read more about the commonly accepted approaches for those files, read the official guidelines about using Designer.
One of those reasons is exactly related to the garbage collection issue explained above.
Note that subclassing the pyuic form class alone is also discouraged (and pointless if you want to extend the widget behavior); the most common, accepted and suggested practice is to create a class that inherits both from the Qt widget class and the UI class. The following code assumes that you have recreated the file with pyuic and named ui_doorwin.py:
# ...
from ui_doorwin import Ui_DoorWin
# ...
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self) # <-- IMPORTANT! Note the "self" argument
self.timer.timeout.connect(self.popTable)
self.timer.start(1000)
def popTable(self):
# ...
With the above code you can be sure that whenever the widget gets deleted for any reason, the timer will be destroyed along with it, so the function will not be called trying to access objects that don't exist anymore.
If you need to keep using an existing instance of the window, the solution is pretty simple: create a None instance (or class) attribute and check if it already exists before creating a new one:
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if not self.doorwin:
self.doorwin = DoorWin()
self.doorwin.show()
The above code will not stop the table from updating, which is something you might not want, so you might choose to start and stop the timer depending on when the window is actually shown:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
super().__init__()
self.doors = doors
self.setupUi(self)
self.timer = QTimer(self)
self.timer.timeout.connect(self.popTable)
def showEvent(self, event):
if not event.spontaneous():
self.timer.start()
def hideEvent(self, event):
if not event.spontaneous():
self.timer.stop()
The event.spontaneous() check above is to prevent stopping the timer if the show/hide event is caused by system calls, like minimizing the window or changing desktop. It's up to you to decide if you want to keep the timer going on and process all data, even if the window is not shown.
Then, if you want to completely destroy the window when it's closed and when a new one is opened, do the following:
class DoorWin(QtWidgets.QWidget, Ui_DoorWin):
def __init__(self, doors):
# ... (as above)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
and then ensure that the widget exists (note that if it's closed by the user the reference still exists):
class SomeParent(QtWidgets.QWidget):
doorwin = None
# ...
def open_door(self):
if self.doorwin:
try:
self.doorwin.close()
except RuntimeError:
pass
self.doorwin = DoorWin()
self.doorwin.show()
I found the solution. My two proposed solutions are the same. If I stop the QTimer on window closing, it no longer gives me the error.
self.exitButton.clicked.connect(self.close_win)
def close_win(self):
self.timer.stop()
self.Widget.close()

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.

How do I open (and close) a PyQt5 application inside a loop, and get that loop running multiple times

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.

Qt canonical way of retrieving values from Wizard / Dialog on accepted / finished signal

I'm using PyQt, but I guess the same questions also applies to Qt C++.
Assume that I have a main window with a button that opens a wizard that collects data and that data needs to be used in the main window after the wizard has closed. standard procedure.
So there are multiple ways to do this. either I can pass a reference to the main window to the Wizard and it does all the work using the main window reference, but I'd say that breaks modularity. I can also wire up a callback to the wizard accepted rejected or finished signal, but in that callback, I don't have a reference to the wizard itself, so I cannot get to the data in the wizards fields. Unless I store a reference to the wizard as instance variable in order to access it again from the callback.
Another option is (even though I haven't fully understood it yet) to get a reference to the emitter of the signal (i.e. the wizard) in the callback using https://doc.qt.io/qt-5/qobject.html#sender. But that seems not recommended.
So whats the canonical way?
Premise: this is a bit of an opinion based question, as there is not one and only "good" way to do that. I just wanted to comment (opinion based answer/questions are discouraged here in SO), but the limited formatting isn't very helpful.
"Passing a reference" doesn't necessarily break modularity.
Instead, that's exactly what QDialog usually are initialized: the parent is the "calling" window, which is also how a QDialog can be "modal" to the parent or the whole application (meaning that no interaction outside the dialog is allowed as long as it is active).
AFAIK, I don't know if this is actually considered canonical, but the following is the most commonly suggested approach.
The idea is that you have a child object (a QDialog, usually) which might or might not be initialized everytime you need it, that's up to you; the important part is that you need a reference to it at least for the time required to update its result, which can even happen within the scope of a single method/slot.
from PyQt5 import QtWidgets
class MyWizard(QtWidgets.QDialog):
def __init__(self, parent=None):
super().__init__(parent)
layout = QtWidgets.QVBoxLayout()
self.setLayout(layout)
self.checkBox = QtWidgets.QCheckBox('check')
layout.addWidget(self.checkBox)
self.input = QtWidgets.QLineEdit()
layout.addWidget(self.input)
buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Cancel)
layout.addWidget(buttonBox)
buttonBox.accepted.connect(self.accept)
buttonBox.rejected.connect(self.reject)
def setData(self, **data):
self.checkBox.setChecked(data.get('check', False))
self.input.setText(data.get('text', ''))
def getData(self):
return {'check': self.checkBox.isChecked(), 'text': self.input.text()}
def exec_(self, **data):
self.setData(**data)
return super().exec_()
class MyWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
centralWidget = QtWidgets.QWidget()
self.setCentralWidget(centralWidget)
layout = QtWidgets.QHBoxLayout()
centralWidget.setLayout(layout)
self.showWizBtn = QtWidgets.QPushButton('Show wizard')
layout.addWidget(self.showWizBtn)
self.showWizBtn.clicked.connect(self.getDataFromWizard)
self.data = {}
def getDataFromWizard(self):
wiz = MyWizard(self)
if wiz.exec_(**self.data):
self.data.update(wiz.getData())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec_())
Another possibility is to create a persistent child dialog (but keep in mind that if the data can be changed by the parent, you'll have to find a way to update it, at least when executed); the concept here is that you can exec the dialog whenever you need, and you have the accepted signal connected to a slot that can get the data from the dialog. This is not a common use (nor very suggested IMHO) and should be used only for very specific scenarios.
As you already found out, using sender is not suggested: signals are asynchronous, and while in normal conditions the sender is reliable it's better to avoid using it unless absolutely necessary.

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.

Categories

Resources