Carcass of QProgressDialog lingers - sometimes - python

progress = QtGui.QProgressDialog("Parsing Log", "Stop", 0,numberOfLinesInFile , self)
progress.setWindowModality(QtCore.Qt.WindowModal)
for lineNumber, line in enumerate(file):
# yield a bit to the Qt UI handler
QtGui.QApplication.processEvents()
progress.setValue(lineNumber + 1) # lineNumber is zero-based so need the plus one to match the more literal numberOfLinesInFile
if progress.wasCanceled():
progressWasCancelled = True
break
# ...read and parse lines from file (20mb takes ~10 seconds)
# crank the progress bar through to completion to get rid of it
# this seems to forgo the opportunity to use progress.wasCanceled() subsequently?
progress.setValue(numberOfLinesInFile)
if not progressWasCancelled:
self.updateTable(self.requestRoster)
After this, and regardless of the progress dialogue being cancelled or not, the progress dialogue is hidden (it slides back up into the toolbar). But if I switch application ('command tab' on the Mac) then switch back to my application, a ghost of the QProgressDialog is in front of the main application window! Its progress bar is at 100% and the stop button is blue but not pulsing. It is unresponsive. If I move the application window it disappears.
If I call progress.destroy() after progress.setValue(numberOfLinesInFile) that seems to help. But it seems worrying to copy the example from the docs and get bitten, and I don't know the ramifications of destroy().
I was using PySide, I switched to PyQt and same thing.
Also, sometimes progress.setValue(numberOfLinesInFile) causes subsequent reads of progress.wasCancelled() to return false (but sometimes it returns true!) which is why I set my own progressWasCancelled. Its randomness is disturbing.
I'm on Mac 10.6.8, Qt 4.8.2, Python 2.7. Tried with PySide 1.1.0 and PyQt 4.9.4.
Am I doing this all wrong?

I can't test on a Mac, but I'll try to make a few suggestions which could help solve your issues.
Firstly, if you use a modal progress dialog, there's no need to call processEvents(), as the dialog will handle this itself.
Secondly, this line in your code:
progress.setValue(lineNumber + 1)
is problematic, because to quote the Qt docs:
For the progress dialog to work as expected, you should initially set this property to 0 and finally set it to QProgressDialog::maximum(); you can call setValue() any number of times in-between.
so you should either call progress.setValue(0) before the loop, or just avoid adding the offset altogether. Also, on the final iteration, lineNumber + 1 will equal the maximum, which will reset the dialog at that point (unless autoReset has been set to False). It is for this reason that the Qt example calls setValue(maximum) after the loop has completed.
Finally, there is no problem with calling destroy() or deleteLater() after you've finished with the progress dialog - in fact, it's a good idea. When you pass self to the QProgressDialog constructor, it will become the parent of the dialog and keep a reference to it. So, unless you explicitly delete it, a new child dialog (plus all it's child objects) will be added every time you call the function that uses it (which could potentially waste a lot of memory).
Here's a demo script that may be improvement:
import sys, time
from PyQt4 import QtGui, QtCore
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.button = QtGui.QPushButton('Test', self)
self.button.clicked.connect(self.handleButton)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.button)
def handleButton(self):
file = range(30)
numberOfLinesInFile = len(file)
progressWasCancelled = False
progress = QtGui.QProgressDialog(
"Parsing Log", "Stop", 0, numberOfLinesInFile, self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setMinimumDuration(0)
for lineNumber, line in enumerate(file):
progress.setValue(lineNumber)
if progress.wasCanceled():
progressWasCancelled = True
break
time.sleep(0.05)
progress.setValue(numberOfLinesInFile)
print 'cancelled', progress.wasCanceled(), progressWasCancelled
progress.deleteLater()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec_())

Related

Unreal Pyside6 widgets wont get removed by garbage collector

I am unable to remove PySide6 widgets in Unreal 5 python. Will describe the code first:
I have my ArtToolsUI class inheriting from QMainWindow. In this class I set some basic stuff like groupboxes, layouts etc.:
class ArtToolsUI(QMainWindow):
def __init__(self):
super().__init__()
self.setAttribute(Qt.WA_DeleteOnClose, True)
self._app = self._get_qt_app()
self.resize(600,400)
self.editor_tools_buttons = [] #for showcase this has no widgets in it
self._main_widget = self._set_layouts()
def _get_qt_app(self):
qt_app = QApplication.instance()
qt_app.setQuitOnLastWindowClosed(True)
return qt_app
def get_v_layout(widgets):
widgets = make_list(widgets)
layout = QVBoxLayout()
layout.setObjectName("vert_layout")
for widget in widgets:
layout.addWidget(widget)
return layout
def _set_layouts(self):
default_grpbox_names = {"tools":self.editor_tools_buttons}
all_grp_boxes = []
for key in default_grpbox_names:
layout = get_v_layout(default_grpbox_names[key]) # right now this just returns empty QVBoxLayout
new_grp_box = QGroupBox(title=key)
new_grp_box.setAttribute(Qt.WA_DeleteOnClose, True)
new_grp_box.setLayout(layout)
all_grp_boxes.append(new_grp_box)
main_layout = get_v_layout(all_grp_boxes)
main_widget = QWidget()
main_widget.setObjectName("tat_main_widget")
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
return main_widget
I also implemented function for removing all widgets in this unreal python session. This method is called from different module.
def _remove_widgets():
qt_app = QApplication.instance()
if qt_app != None:
all_qt_widgets = qt_app.allWidgets()
for widget in all_qt_widgets:
widget.setParent(None)
widget.close()
widget.deleteLater()
This _remove_widgets() method goes through all existing widgets, which should be destroyed, but for some reason only my ArtToolsUI main window is getting destroyed and all other widgets are still hanging around in the memory.
On the other side when I manually click on the X button to close the window it closes everything.
Does anyone know what might be the problem?
Ok I think I figured it out.
When running PySide6 on unreal you actualy cannot start your own QT event loop ( if you do, Unreal Editor will freeze and wait till you end your window). QT somehow finds unreal GUI event loop, which is responsible for everything .. except answering deleteLater().
Only two ways ( I found) for deleting UI are:
When user clicks on X button to end the Window -> This automatically ends and removes all widgets in that window.
When calling QApplication.closeAllWindows() -> This calls all windows, but definitelly better than if they stay in memory.
Both ways seem to bypass deleteLater() functionality, as documentation for deleteLater() says:
The object will be deleted when control returns to the event loop.
At the end I ended up with this functionality:
I am not deleting dialogs, I just hide them on close().
Then when creating them I ask if there are dialogs with my specific object name (object name is class property at every window I have)
This will prevent spawning multiple widgets ( 10k widgets is aprox. 1.2GB RAM)
ADD: There is also possibility to use self.setAttribute(Qt.WA_DeleteOnClose, True) on closing. This will delete dialog from memory as well.

PyQt5: Using DeleteOnClose when switching to new window

I am currently creating a GUI in Python 3.7, using PyQt5 and Qt Designer in the Spyder environment. The GUI has many different windows. Basically I am starting with the UI_Start window and then open the next window when a button is pressed. The GUI is working kind of fine, however after approximately 50 windows the program suddenly doesn't show the next window anymore but also doesn't stop the execution. The weird thing about this issue is that:
the exact same window class has been called a lot of times beforehand and there have never been any issues
the problem does not only occur for one window but it can also occur for another window class (but after the same amount of windows being shown)
I tried to figure out why the .show() command is suddenly not working anymore. I used print statements to see where the program "breaks down". I saw that even the print statements after the .show() command are working but then as the window isn't shown I can't press any button to trigger the next event. So basically the program is hanging.
I am relatively new to programming in Python and creating GUIs but I thought that maybe the problem occurs due to memory leak. This is why I am now trying to open memory space when closing a window by using self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True). However, now I am facing the problem that the next window doesn't show up anymore. So how can I use DeleteOnClose if I want to show a new window afterwards?
Also if anyone has a suggestion for the original problem, please let me know. I am trying to figure out the problem since like a week but have not come any further.
Thank you already!
Some part of my code to work with:
class UI_Start(QtWidgets.QMainWindow):
def __init__(self):
super(UI_Start, self).__init__() # Call the inherited classes __init__ method
uic.loadUi('Screen_Start.ui', self) # Load the .ui file
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True) # added newly
self.Start_pushButton_Start.clicked.connect(self.openKommiScreen)
def openKommiScreen(self):
self.close()
self.KommiScreen = UI_Kommi(self)
class UI_Kommi(QtWidgets.QMainWindow):
def __init__(self, parent = None):
super(UI_Kommi, self).__init__(parent)
uic.loadUi('Screen_Kommi.ui', self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose, True)
global sheetNo
sheetNo = 1
self.WeiterButton = self.findChild(QtWidgets.QPushButton,'pushButton_Weiter')
self.WeiterButton.clicked.connect(self.openScanScreen)
self.show()
def openScanScreen(self):
self.close()
self.ScanScreen = UI_Scan(self)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = UI_Start()
window.show()
sys.exit(app.exec_())
At first I would guess it's a garbage collection problem. The only reference to your new window is stored in your previous one. Which is deleted, so there is no more reference to your window object and python may delete it automatically.
In these cases I often goes for a global variable to store the current windows references.

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()

time .sleep() taking place at incorrect order in commands; always at the beginning of the function

The code below is a stripped down version (for clarity reasons) of a small application I am working on; an application for spelling words for children.
The problem
The problem I am having is in the function flash_correct(); its purpose is to show a word for 5 seconds, then hide again.
I must have a silly blind spot, but no matter where I put the time.sleep(5), the function starts with the break of 5 seconds, while the entry: self.entry2 never shows up:
Without the time.sleep(5) however, it shows up correctly:
Where is my blind spot?
The (stripped down) code:
#!/usr/bin/env python3
from gi.repository import Gtk, Pango, Gdk
import subprocess
import time
class InterFace(Gtk.Window):
def __init__(self):
Gtk.Window.__init__(self, title="Woorden raden")
maingrid = Gtk.Grid()
self.add(maingrid)
maingrid.set_border_width(10)
self.entry2 = Gtk.Entry()
self.entry2.set_size_request(500,60)
self.entry2.set_child_visible(False)
self.entry2.modify_font(Pango.FontDescription('Ubuntu 30'))
maingrid.attach(self.entry2, 0, 4, 4, 1)
quitbutton = Gtk.Button("Stop", use_underline=True)
quitbutton.modify_font(Pango.FontDescription('Ubuntu 20'))
quitbutton.connect("clicked", self.on_close_clicked)
maingrid.attach(quitbutton, 3, 7, 1, 1)
showword_button = Gtk.Button("↺", use_underline=True)
showword_button.modify_font(Pango.FontDescription('Ubuntu 25'))
showword_button.connect("clicked", self.flash_correct)
showword_button.set_size_request(60,20)
maingrid.attach(showword_button, 0, 6, 1, 1)
def flash_correct(self, button):
# the time.sleep(5) seems to take place at the beginning
# no matter in which order I set the commands
self.entry2.set_text("Monkey")
self.entry2.set_child_visible(True)
time.sleep(5)
self.entry2.set_child_visible(False)
def on_close_clicked(self, button):
Gtk.main_quit()
window = InterFace()
window.connect("delete-event", Gtk.main_quit)
window.set_default_size(330, 330)
window.set_resizable(False)
window.show_all()
Gtk.main()
You can use time.time to hide for roughly 5 seconds calling Gtk.main_iteration() in the loop to avoid your app becoming unresponsive.
def hide(self, time_lapse):
start = time.time()
end = start + time_lapse
while end > time.time():
Gtk.main_iteration()
def flash_correct(self, button):
# the time.sleep(5) seems to take place at the beginning
# no matter in which order I set the commands
self.entry2.set_text("Monkey")
self.entry2.set_child_visible(True)
self.hide(5)
self.entry2.set_child_visible(False)
There is a good explanation in the pygtk faq 7. How can I force updates to the application windows during a long callback or other internal operation?
If you have a long-running callback or internal operation that tries to modify the application windows incrementally during its execution, you will notice that this doesn't happen; the windows of your app freeze for the duration.
This is by design: all gtk events (including window refreshing and updates) are handled in the mainloop, and while your application or callback code is running the mainloop can't handle window update events. Therefore nothing will happen in the application windows.
The trick here is to realize where your operation can take a while to return, or where it is dynamically changing the window contents, and add a code fragment like this wherever you want an update forced out:
while gtk.events_pending():
gtk.main_iteration(False)
This tells gtk to process any window events that have been left pending. If your handler has a long loop, for instance, inserting this snippet as part of the loop will avoid it hanging the window till the callback has finished.
More eloquently, in the words of the great Malcolm Tredinnick, 'this requires using what should be called "Secret Technique #1 For Making Your Application Look Responsive"(tm):
Adding while gtk.events_pending(): may be no harm also.
It would be better to use a timer that integrates with the main loop, rather than busy-waiting until the time has elapsed. Luckily there is just such a facility in GLib:
def flash_correct(self, button):
self.entry2.set_text("Monkey")
self.entry2.set_child_visible(True)
GLib.timeout_add_seconds(5, self.flash_end)
def flash_end(self):
self.entry2.set_child_visible(False)
return GLib.SOURCE_REMOVE

Error "QObject::startTimer: QTimer can only be used with threads started with QThread" many times when closing application

I know this has been asked many times before. I read all of those threads, and my case seems different. Everybody else who has this trouble has a few straightforward causes that I think I’ve ruled out, such as:
Starting a timer with no event loop running
Starting/stopping a timer from a thread other than the one that created the timer
Failing to set the parent property of a widget, leading to problems with the order of destruction
Below I have a minimal code sample that demonstrates the problem. Notice that I’ve started no threads or timers. I also have set the parent of every widget. If I remove the graph widgets, the problem goes away, so one is tempted to blame pyQtGraph, however, if I include the plot widgets but exclude all the blank tabs (i.e. every tab except tabCatchaTiger), the problem also goes away, and that seems to vindicate pyQtGraph.
Versions:
Windows 7
Python 2.7.8
Wing IDE 5.0.9-1
PyQt 4.11.1
PyQwt 5.2.1
PyQtGraph 0.9.8
Test case:
from PyQt4 import Qt, QtGui, QtCore
import PyQt4.Qwt5 as Qwt
import pyqtgraph as pg
pg.functions.USE_WEAVE = False # Lets pyqtgraph plot without gcc
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
# GUI for visualizing data from database
class crashyGUI(QtGui.QWidget) :
def __init__(self) :
# Make the window
QtGui.QWidget.__init__(self)
self.resize(700, QtGui.QDesktopWidget().screenGeometry(self).height()*.85)
self.setWindowTitle('Data Visualization')
# Create tab interface
tabWidget = QtGui.QTabWidget(self)
# define the tab objects
self.tabEeny = QtGui.QWidget(tabWidget)
self.tabMeeny = QtGui.QWidget(tabWidget)
self.tabMiney = QtGui.QWidget(tabWidget)
self.tabMoe = QtGui.QWidget(tabWidget)
self.tabCatchaTiger = QtGui.QWidget(tabWidget)
self.tabByThe = QtGui.QWidget(tabWidget)
self.tabToe = QtGui.QWidget(tabWidget)
# Initialize the tab objects
self.initTabCatchaTiger()
###########################################
############### Main Layout ###############
###########################################
tabWidget.addTab(self.tabEeny, 'Eeny')
tabWidget.addTab(self.tabMeeny, 'Meeny')
tabWidget.addTab(self.tabMiney, 'Miney')
tabWidget.addTab(self.tabMoe, 'Moe')
tabWidget.addTab(self.tabCatchaTiger, 'Catch a Tiger')
tabWidget.addTab(self.tabByThe, 'By The')
tabWidget.addTab(self.tabToe, 'Toe')
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.addWidget(tabWidget)
self.setLayout(self.mainLayout)
def initTabCatchaTiger(self):
###########################################
############# ADC Capture Tab #############
###########################################
# define tab layout
grid = QtGui.QGridLayout(self.tabCatchaTiger)
# create copy of adc plot and add to row 3 of the grid
self.catchaTigerPlot1 = pg.PlotWidget(name = 'Catch a Tiger 1', parent = self.tabCatchaTiger)
self.catchaTigerPlot1.setTitle('Catch a Tiger 1')
grid.addWidget(self.catchaTigerPlot1, 2, 0, 1, 8)
self.catchaTigerPlot2 = pg.PlotWidget(name = 'Catch a Tiger 2', parent = self.tabCatchaTiger)
self.catchaTigerPlot2.setTitle('Catch a Tiger 2')
grid.addWidget(self.catchaTigerPlot2, 3, 0, 1, 8)
# set layout for tab
self.tabCatchaTiger.setLayout(grid)
def closeEvent(self, event) :
pass
def main() :
# open a QApplication and dialog() GUI
app = QtGui.QApplication([])
windowCrashy = crashyGUI()
windowCrashy.show()
app.exec_()
main()
There seem to be two closely-related issues in the example.
The first one causes Qt to print the QObject::startTimer: QTimer can only be used with threads started with QThread messages on exit.
The second one (which may not affect all users) causes Qt to print QPixmap: Must construct a QApplication before a QPaintDevice, and then dump core on exit.
Both of these issues are caused by python deleting objects in an unpredicable order when it exits.
In the example, the second issue can be fixed by adding the following line to the __init__ of the top-level window:
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
Unless QApplication.setQuitOnLastWindowClosed has been changed to False, this will ensure that the application quits at the right time, and that Qt has a chance to automatically delete all the children of the top-level window before the python garbage-collector gets to work.
However, for this to be completely successful, all the relevant objects must be linked together in a parent-child hierarchy. The example code does this where it can, but there seem to be some critical places in the initialization of the PlotWidget class where it is not done.
In particular, there is nothing to ensure that the central item of the PlotWidget has a parent set when it is created. If the relevant part of the code is changed to this:
class PlotWidget(GraphicsView):
...
def __init__(self, parent=None, background='default', **kargs):
GraphicsView.__init__(self, parent, background=background)
...
self.plotItem = PlotItem(**kargs)
# make sure the item gets a parent
self.plotItem.setParent(self)
self.setCentralItem(self.plotItem)
then the first issue with the QTimer messages also goes away.
Here's a better answer:
You are allowing the QApplication to be collected before python exits. This causes two different issues:
The QTimer error messages are caused by pyqtgraph trying to track its ViewBoxes after the QApplication has been destroyed.
The crash appears to be intrinsic to Qt / PyQt. The following crashes in the same way:
from PyQt4 import Qt, QtGui, QtCore
def main() :
app = QtGui.QApplication([])
x = QtGui.QGraphicsView()
s = QtGui.QGraphicsScene()
x.setScene(s)
x.show()
app.exec_()
main()
You can fix it by adding global app to your main function, or by creating the QApplication at the module level.
Try to write this in block of __init__:
self.setAttribute(Qt.WA_DeleteOnClose)
Personally, I don't put any effort into chasing exit crashes anymore--just use pg.exit() and be done with it.
(but if you do happen to find a bug in pyqtgraph, don't hesitate to open an issue on github)
I had this happen as well and in my case it was caused by a call to deleteLater() on the aboutToQuit-Signal of the application, like so:
def closeEvent(self, *args, **kwargs):
self.deleteLater()
if __name__ == "__main__":
application = QtWidgets.QApplication(sys.argv)
window = testApplication()
# Handle application exit
application.aboutToQuit.connect(window.closeEvent)
# System exit
sys.exit(application.exec_())
Getting rid of the deleteLater on the whole window seemed to solve it.

Categories