Handling QDialog's events safely in PyQT - python

I'm having troubles with a Qt Application (managed through PyQt5) which used to be reliable, until a bunch of updates (where I ended up with PyQt6). PyQt6 is not the culprit here, as I began to have those problems with later PyQt5 versions.
I'm suspecting my problems are instead linked to an abuse of exec() methods call. The full traceback is not available (I've only a RuntimeError with a cryptic message telling me the Dialog is not available anymore); what (I think) I'm seeing are the parent windows randomly disappearing. This behaviour is indeed mentionned in the Qt documentation:
Note: Avoid using this function; instead, use open(). Unlike exec(), open() is asynchronous, and does not spin an additional event loop. This prevents a series of dangerous bugs from happening (e.g. deleting the dialog's parent while the dialog is open via exec()). When using open() you can connect to the finished() signal of QDialog to be notified when the dialog is closed.
Initially, my app was constructed this way:
QMainWindow A > QDialog B > QDialog C > ...
Each QDialog was launched using .exec() method.
The pseudo code could be summarized this way [edit] :
class A(QMainWindow):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ui = Some_Ui_From_QtDesigner()
self.ui.setupUi(self)
self.ui.some_menu.triggered.connect(self.load_popup)
def load_popup():
B = QDialog(A)
if B.exec() == 1:
# Do some things (extract data from B and update A)
if C.exec() == 1:
# Do some things (extract data from C and update B)
qApp = QtWidgets.QApplication.instance()
a = A()
a.show()
qApp.setQuitOnLastWindowClosed(True)
qApp.exec()
I've tried to use .open() methods instead of .exec() and came up with something like this (using QLoopEvent's and events handling):
def process_C_qdialog(parent):
C = QDialog(parent)
# Construct your QDialog some more ...
loop = QtCore.QEventLoop()
my_result = None
def _accept():
my_result = "blah"
loop.exit(0)
def _reject():
loop.exit(-1)
f.accepted.connect(_accept)
f.rejected.connect(_reject)
f.open()
loop.exec()
return my_result
My troubles are far from over as yet. I suspect that recreating manually a QEventLoop causes the same problems than using .exec() in the first place.
So some questions:
Am I right about the QEventLoop?
Would it be safer/enough to leave the parent out of the QDialog generator?
Any advice on how to handle this the right way (keeping a modal dialog if possible)?
More generally, are they any safe ways to reproduce the static methods of Qt through a custom python function? (ie, a function which would load a customized popup, wait for the user's input and returning it)
Note: as this behaviour happens erratically (and without the full traceback available), I'm not 100% sure of this; but I suspect that even a simple QMessageBox().exec() has the same effects, even if this is not stated in the doc.

Related

pyqt: A correct way to connect multiple signals to the same function in pyqt (QSignalMapper not applicable)

I've ready many posts on how to connect multiple signals to the same event handler in python and pyqt. For example, connecting several buttons or comboboxes to the same function.
Many examples show how to do this with QSignalMapper, but it is not applicable when the signal carries a parameter, as with combobox.currentIndexChanged
Many people suggest it can be made with lambda. It is a clean and pretty solution, I agree, but nobody mentions that lambda creates a closure, which holds a reference - thus the referenced object can not be deleted. Hello memory leak!
Proof:
from PyQt4 import QtGui, QtCore
class Widget(QtGui.QWidget):
def __init__(self):
super(Widget, self).__init__()
# create and set the layout
lay_main = QtGui.QHBoxLayout()
self.setLayout(lay_main)
# create two comboboxes and connect them to a single handler with lambda
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('1', ind))
lay_main.addWidget(combobox)
combobox = QtGui.QComboBox()
combobox.addItems('Nol Adyn Dwa Tri'.split())
combobox.currentIndexChanged.connect(lambda ind: self.on_selected('2', ind))
lay_main.addWidget(combobox)
# let the handler show which combobox was selected with which value
def on_selected(self, cb, index):
print '! combobox ', cb, ' index ', index
def __del__(self):
print 'deleted'
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
wdg = Widget()
wdg.show()
wdg = None
sys.exit(app.exec_())
The widget is NOT deleted though we clear the reference. Remove the connection to lambda - it gets deleted properly.
So, the question is: which is the proper way to connect several signals with parameters to a single handler without leaking memory?
It is simply untrue that an object cannot be deleted because a signal connection holds a reference in a closure. Qt will automatically remove all signal connections when it deletes an object, which will in turn remove the reference to the lambda on the python side.
But this implies that you cannot always rely on Python alone to delete objects. There are two parts to every PyQt object: the Qt C++ part, and the Python wrapper part. Both parts must be deleted - and sometimes in a specific order (depending on whether Qt or Python currently has ownership of the object). In addition to that, there's also the vagaries of the Python garbage-collector to factor in (especially during the short period when the interpreter is shutting down).
Anyway, in your specific example, the easy fix is to simply do:
# wdg = None
wdg.deleteLater()
This schedules the object for deletion, so a running event-loop is required for it have any effect. In your example, this will also automatically quit the application (because the object is the last window closed).
To more clearly see what's happening, you can also try this:
#wdg = None
wdg.deleteLater()
app.exec_()
# Python part is still alive here...
print(wdg)
# but the Qt part has already gone
print(wdg.objectName())
Output:
<__main__.Widget object at 0x7fa953688510>
Traceback (most recent call last):
File "test.py", line 45, in <module>
print(wdg.objectName())
RuntimeError: wrapped C/C++ object of type Widget has been deleted
deleted
EDIT:
Here's another debugging example that hopefully makes it even clearer:
wdg = Widget()
wdg.show()
wdg.deleteLater()
print 'wdg.deleteLater called'
del wdg
print 'del widget executed'
wd2 = Widget()
wd2.show()
print 'starting event-loop'
app.exec_()
Output:
$ python2 test.py
wdg.deleteLater called
del widget executed
starting event-loop
deleted
in many cases the parameter carried by signal can be catched in another way, e.g. if an objectName is set for the sending object, so QSignalMapper can be used:
self.signalMapper = QtCore.QSignalMapper(self)
self.signalMapper.mapped[str].connect(myFunction)
self.combo.currentIndexChanged.connect(self.signalMapper.map)
self.signalMapper.setMapping(self.combo, self.combo.objectName())
def myFunction(self, identifier):
combo = self.findChild(QtGui.QComboBox,identifier)
index = combo.currentIndex()
text = combo.currentText()
data = combo.currentData()

Trouble Understanding Signal Mapper PyQt

So I am generating a menu of options based on some files on my system. I have a list list of objects I need to dynamically generate an option in the menu for and need to be able to let the function that is doing the creation know which object to use. After some research I found the post below. I could not comment as my rep is not high yet: How to pass arguments to callback functions in PyQt
When I run this the signal mapper is not working right. It is not even calling the handleButton correctly. Any ideas as to how to use the signal mapper correctly?
from PyQt4 import QtGui, QtCore
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.mapper = QtCore.QSignalMapper(self)
self.toolbar = self.addToolBar('Foo')
self.toolbar.setToolButtonStyle(QtCore.Qt.ToolButtonTextOnly)
for text in 'One Two Three'.split():
action = QtGui.QAction(text, self)
self.mapper.setMapping(action, text)
action.triggered.connect(self.mapper.map)
self.toolbar.addAction(action)
self.mapper.mapped['QString'].connect(self.handleButton)
self.edit = QtGui.QLineEdit(self)
self.setCentralWidget(self.edit)
def handleButton(self, identifier):
print 'run'
if identifier == 'One':
text = 'Do This'
print 'Do One'
elif identifier == 'Two':
text = 'Do That'
print 'Do Two'
elif identifier == 'Three':
print 'Do Three'
text = 'Do Other'
self.edit.setText(text)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(300, 60)
window.show()
sys.exit(app.exec_())
EDIT:
I've found that by using old-style signal/slot connections this is fixed:
#action.triggered.connect(self.mapper.map)
self.connect(action, QtCore.SIGNAL("triggered()"), self.mapper, QtCore.SLOT("map()"))
and
#self.mapper.mapped['QString'].connect(self.handleButton)
self.connect(self.mapper, QtCore.SIGNAL("mapped(const QString &)"), self.handleButton)
Am I using the new-style connections incorrectly?
Based on this post as well as the original link I posted, I thought I was doing things correctly.
The original example code (which I wrote), works perfectly fine for me using either Python2 or Python3 with several different recent versions of PyQt4. However, if I use a really old version of PyQt4 (4.7), the handler no longer gets called.
The reason (and solution) for this is given in the response to the mailing list post you linked to:
It's actually a problem with QSignalMapper.map() being called from a
proxy rather than new-style connections.
The workaround is to explicitly specify a signal that is compatible
with map()...
self.b1.clicked[()].connect(self.mapper.map)
Tonight's PyQt snapshot will be smarter about finding a usable Qt slot
before deciding that it needs to use a proxy so that the workaround
won't be necessary.
There are some signals (like clicked and triggered) which always send a default value unless you explicitly request otherwise. With the old-style signals, you can specify the no default overload it with SIGNAL("triggered()"), but with new-style signals, you have to do it like this:
action.triggered[()].connect(self.mapper.map)
But that is only necessary with very old versions of PyQt4 - the underlying issue was fixed back in 2010 (don't know the exact version, but 4.8 should be okay).

Overriding/Reimplementing Slots in PySide

I have almost the exact same question as the one found here:
Override shouldInterruptJavaScript in QWebPage with PySide
In my case though I want to override the copy and paste slots on QLineEdit
import sys
from PySide import QtGui, QtCore
class myLineEdit(QtGui.QLineEdit):
# FIXME: This is not working, the slot is not overriden!
#QtCore.Slot()
def copy(self):
print 'overridden copy event'
App.clipboard().setText('customized text')
return False
#QtCore.Slot()
def paste(self):
print 'overridden paste event'
self.setText('customized text')
return False
if __name__ == "__main__":
App = QtGui.QApplication(sys.argv)
Widget = myLineEdit()
Widget.show()
cmenu = Widget.createStandardContextMenu()
sys.exit(App.exec_())
I'm using Python 2.7.3, with PySide 1.2.2
I don't know if these methods are even supposed to be override-able, but I can't find any documentation that says they shouldn't be.
I also found this page
http://qt-project.org/faq/answer/is_it_possible_to_reimplement_non-virtual_slots
The page explains how certain kinds of slots get pointers set to them by functions that get called when the object is initialized (or something along those lines, I'm not as familiar with the C++).
Following this logic I added the createStandardContextMenu() call above in the hopes that it would reinitialize the slots for at least the context menu, but no luck.
Am I doing something wrong? Or should I try filing a bug report?
You cannot override QLineEdit.copy or QLineEdit.paste in such a way that they will be called internally by Qt.
In general, you can only usefully override or reimplement Qt functions that are defined as being virtual. The Qt Docs will always specify whether this is the case, and for QLineEdit, there are no public slots that are defined in that way.
There is also no easy workaround. There are a lot of different ways in which copy and paste operations (or their equivalents) can be invoked, such as keyboard shortcuts, context menu, drag and drop, etc. It can be done: but it's a lot of work to get complete control over all these sorts of operations. So you need to think carefully about what you're trying to achieve before deciding how to proceed.

QTextEdit: Overwrite Ctrl-Z with undo() in PyQt4

I subclassed QTextEdit
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def undo(self):
print("undo")
#my own undo code
In my other class:
self.textedit=Q_My_TextEdit()
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.textedit.undo()
If you type some text into your QTextEdit, and hit CTRL-Z, it gets undo-ed, but without calling the function "undo". So how does it work in detail?
The background is, in a second step I want to set new Text (setText()), so the undo stack is deleted. I already have running code that makes the undo itself, but I can't trigger it on CTRL-Z, because with the "Z" the keyshortcut is somehow reserved. For example, if I call my own undo with event.key()==(Qt.Key_Control and Qt.Key_Y), it works.
Ah, So additionally to the keyPressEvent which I have in my second class, you have to put it also into the Subclass!
class Q_My_TextEdit(QtGui.QTextEdit):
def __init__(self, *args):
QtGui.QTextEdit.__init__(self, *args)
def keyPressEvent(self,event):
if event.key()==(Qt.Key_Control and Qt.Key_Z):
self.undo()
#To get the remaining functionality back (e.g. without this, typing would not work):
QTextEdit.keyPressEvent(self,event)
def undo(self):
print("undo")
#my own undo code
But now I can't type into my textedit any more! How can I solve this?
--> Solved. See Properly handling a keyPressEvent in a Subclassed PyQT LineEdit
In C++ you would have to install an event filter, probably with PyQt this is similar (overwrite virtual bool eventFilter(QObject* pObject, QEvent* pEvent); in your editor class). CTRL-Z gets probably filtered by the QTextEdit event filter thus it never gets to keyPressEvent.

PyQt: updating GUI from a callback

Using Python3 and PyQt4 I have a function (run) that takes as an input a callable to provide status updates.
class Windows(QtGui.QWidget):
# Creates a widget containing:
# - a QLineEdit (status_widget)
# - a button, connected to on_run_clicked
def on_run_clicked(self):
def update(text):
self.widget.setText(text)
threading.Thread(target=run, args=(update, )).start()
This works ok (i.e. the text updates are displayed properly in the widget). However, when I replace QLineEdit by QTextEdit and use the append method to add text, I get:
QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
It still works but point to the fact that I am doing something wrong, and I am not sure that I will keep working when more threads are active. Usually, I do this type of updates using signals and slots, but the run function is not PyQt specific. The questions are:
Why does it work without a warning for QLineEdit and not for
QTextEdit?
What is the right way to deal with a situation like this?
I don't know the specific reason why one class works and the other doesn't - nor do I really know the difference between using Python threading vs. Qt's threading...however, I can tell you that it is very tempremental if you don't set it up properly. Namely, you cannot (or at the very least, should not) modify GUI objects from a thread. Again, not sure the difference of a python vs. a Qt thread on that. But, the safe way to modify your interface from a GUI is by sending signals to your window...easiest way I know to do this is via the Qt threading.
class MyThread(QtCore.QThread):
updated = QtCore.pyqtSignal(str)
def run( self ):
# do some functionality
for i in range(10000):
self.updated.emit(str(i))
class Windows(QtGui.QWidget):
def __init__( self, parent = None ):
super(Windows, self).__init__(parent)
self._thread = MyThread(self)
self._thread.updated.connect(self.updateText)
# create a line edit and a button
self._button.clicked.connect(self._thread.start)
def updateText( self, text ):
self.widget.setText(text)

Categories