Unable to exit app properly
-
wrote on 23 Mar 2020, 09:13 last edited by
Hello,
I have 2 issues to close my app, first from closing a QDialog, and second to close app from systray.
I have build an app with a systray, and some overlays (QFrame transparent), there is a QMainWindow as parent for all components, but it is hidden, as I want to the app run in background all the time.- When I start my app, the user needs to select a profile, nothing fancy, just a combobox so far. Unfortunately when I close the QDialog with the dialog the app closes, but the process still runs, and the command line is not released (I am on windows - using gitbash).
- When using the systray I have added an exit button, but it seems to not work either. I call
QCoreApplication.closeAllWindows()
thenQCoreApplication.quit()
but it crashes oncloseAllWindows()
- I also tried withQApplication.quit()
doesnt help, both tries end up like in #1, app hide itself, but process still runs and terminal not released.
Ironically when it crashes withQCoreApplication.closeAllWindows()
it exits the app and finishes the process and frees the terminal :D
Anyone got any ideas what I am missing?
Here are sample of code:
the entrypoint app.py:
def main(): app = QApplication(sys.argv) app.setQuitOnLastWindowClosed(False) main_window = MainWindow() sys.exit(app.exec_()) if __name__ == '__main__': main()
The main window (truncated too long):
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.tray_icon = None self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_TranslucentBackground) self.setAttribute(Qt.WA_NoSystemBackground) self.log_line_queue = queue.Queue() # queues for Internal usage self.threadpool = QThreadPool() # threadpool for Internal usage self.threadpool.setMaxThreadCount(100) # threadpool for Internal usage self.set_profile() # <- will prompt QDialog with combobox self.init_ui() # <- set all the other component, systray and some overlay QFrame def set_profile(self): self.profile_select = ProfileSelectionDialog(self) self.profile_select.show() if not self.profile_select.exec_(): sys.exit(0) # <- Something is missing here to close properly
The systray:
class MySystemTrayIcon(QSystemTrayIcon): def __init__(self, icon, parent=None): super(MySystemTrayIcon, self).__init__(icon, parent) menu = QMenu() settings_action = QAction( 'Settings', self, triggered=self.parent().ex_popup.show) quit_action = QAction( '&Quit', self, triggered=self.exit) menu.addAction(settings_action) menu.addAction(quit_action) self.setContextMenu(menu) def exit(self): self.hide() QCoreApplication.closeAllWindows() # <- Crashes here, if I comment this app closes but doesnt kill process nor release terminal QCoreApplication.quit()
thanks
-
wrote on 23 Mar 2020, 09:59 last edited by
Hi there,
- You are starting your Dialog in the
__init__()
method of the main window, this means that the main event loop for your program has not yet started (as this call is inmain()
beforeQApplication().exec_()
) and calls toquit()
won't work. - You need to call
QApplication().quit()
(or the shortcutqApp.quit()
) rather thanQCoreApplication().quit()
. You don't need to worry about hiding/closing your windows.
See my small working example below:
try: from PyQt5.QtCore import QTimer, pyqtSlot from PyQt5.QtGui import QKeySequence, QIcon from PyQt5.QtWidgets import QMainWindow, QMessageBox, qApp, QMenu, QSystemTrayIcon except ImportError: from PySide2.QtCore import QTimer, Slot as pyqtSlot from PySide2.QtGui import QKeySequence, QIcon from PySide2.QtWidgets import QMainWindow, QMessageBox, qApp, QMenu, QSystemTrayIcon class MainWindow(QMainWindow): def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) self._menu = QMenu() self._menu.addAction("&Quit", qApp.quit, QKeySequence.Quit) self._trayIcon = QSystemTrayIcon(QIcon("./icon.png"), self) self._trayIcon.setContextMenu(self._menu) self._trayIcon.show() # This defers the call to open the dialog after the main event loop has started QTimer.singleShot(0, self.setProfile) @pyqtSlot() def setProfile(self): if QMessageBox.question(self, "Quit?", "Quit?") != QMessageBox.No: qApp.quit() self.hide() if __name__ == "__main__": from sys import exit, argv try: from PyQt5.QtWidgets import QApplication except ImportError: from PySide2.QtWidgets import QApplication a = QApplication(argv) m = MainWindow() m.show() exit(a.exec_())
Hope this helps :o)
- You are starting your Dialog in the
-
wrote on 23 Mar 2020, 11:35 last edited by
Hi @jazzycamel thank you so much, the deferred call indeed solved all my problems.
I have a LOT of things inself.init_ui()
that need to be set after profile is set.
Would it be fine to putself.set_profile()
in theinit_ui
and defer that last one instead? -
Hi @jazzycamel thank you so much, the deferred call indeed solved all my problems.
I have a LOT of things inself.init_ui()
that need to be set after profile is set.
Would it be fine to putself.set_profile()
in theinit_ui
and defer that last one instead?wrote on 23 Mar 2020, 17:08 last edited by@Tyskie
Glad that sorted your issue.There's no reason why you can't defer the call to setup your UI, widgets can be created and destroyed at any time. Creating them in the
__init__()
is just a convention, particularly for persistent UI elements, as it means there will be no delay in their being rendered. -
wrote on 23 Mar 2020, 17:45 last edited by
One last question, please, I see you used a
@pyqtSlot
decorator on thesetProfile
method.
Using it or not doesn't seem to affect code execution.
Could you please explain why you used it then? -
One last question, please, I see you used a
@pyqtSlot
decorator on thesetProfile
method.
Using it or not doesn't seem to affect code execution.
Could you please explain why you used it then?wrote on 24 Mar 2020, 13:36 last edited by jazzycamel@Tyskie
It's not always necessary, any method or function can be connected (including lambdas), but in those cases where the signal has an overloaded signature in C++, the@pyqtSlot
decorator allows you to define which set of arguments you want to respond to and ignore the others.For example,
QComboBox
has two version of thecurrentIndexChanged
signal: one emits aQString
and the other anint
. The example below shows how you can ensure your slot will only receive the signal that has the desired type:from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QWidget, QVBoxLayout, QComboBox class Widget(QWidget): def __init__(self, parent=None, **kwargs): super().__init__(parent, **kwargs) l = QVBoxLayout(self) self._combo = QComboBox(self) self._combo.addItems(["One", "Two", "Three"]) # The default version of this signal has a single int argument, you could select this specifically # by appending [int] to the signal name as we have to to select the [str] version in the # following line. self._combo.currentIndexChanged.connect(self.intCurrentIndexChanged) self._combo.currentIndexChanged[str].connect(self.strCurrentIndexChanged) l.addWidget(self._combo) @pyqtSlot(int) def intCurrentIndexChanged(self, intValue): assert type(intValue) == int print(intValue) @pyqtSlot(str) def strCurrentIndexChanged(self, strValue): assert type(strValue) == str print(strValue) if __name__ == "__main__": from sys import argv, exit from PyQt5.QtWidgets import QApplication a = QApplication(argv) w = Widget() w.show() exit(a.exec_())
When the user selects a different value in the
QComboBox
, both slots are called wih the correct type of arugment (verified by the assert statements).Also, the
@pyqtSlot
decorator is necessary if you wish to expose methods from Python to QML. For example, say we had Python class like that in the following snippet:class Bob(QObject): @pyqtSlot(result=bool) def hello(self): return True
which we expose to QML via
qmlRegisterType
(before we construct ourQApplication
) as follows:qmlRegisterType(Bob, "Bob", 1, 0, "Bob")
We could then instanstiate
Bob
from QML and callhello()
as QML would know what types need to be passed and what to expect to be returned:import Bob 1.0 ApplicationWindow { visible: true width: 640 height: 480 Bob { id: bob } Component.onCompleted: { console.log(bob.hello()); } }
These are just snippets rather than full examples, but hopefully you get the idea and I have answered your question.
Hope this helps :o)
-
wrote on 29 Mar 2020, 19:39 last edited by
Thank you for your answer, that is what I thought too, but it's just not used in your example, I was thinking it may have a trick to it, I guess it is just a habit to put it where it could be used :)
On the other hand I have another issue. In theinit_ui
that is being deferred I have added some threadpool for multi threading purpose and it brings back the problem of closing the app again, hanging, and have to terminate manually process.this is how I instanciate the threadpool:
class MainWindow(): def __init__(...): # [...] QTimer.singleShot(0, self.init_ui) def init_ui(self): self.setprofile() self.threadpool = QThreadPool() self.threadpool.setMaxThreadCount(100) worker = MyWorker() self.threadpool.start(worker)
The workers are similarly based on those ones except that I do a
while True
loop in the function instead.The quit button as the same code as your working example a simple
qApp.quit()
Any ideas ? :D
-
Hi,
You should give your threads an exit point so that you can stop them nicely.
-
wrote on 29 Mar 2020, 19:59 last edited by Tyskie
hi @SGaist thanks for the hint, that make senses, I endup adding a variable
willing_to_exist = False
toMainWindow
and passed it to the functions of workers, exiting loops if set toTrue
and also athreadpool.waitForDone(msecs=X)
before callingqApp.quit()
to avoid their destruction too fast, might not be the sexiest but it does the trick :P
1/9