Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Unable to exit app properly



  • 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.

    1. 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).
    2. When using the systray I have added an exit button, but it seems to not work either. I call QCoreApplication.closeAllWindows() then QCoreApplication.quit() but it crashes on closeAllWindows() - I also tried with QApplication.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 with QCoreApplication.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



  • Hi there,

    1. 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 in main() before QApplication().exec_()) and calls to quit() won't work.
    2. You need to call QApplication().quit() (or the shortcut qApp.quit()) rather than QCoreApplication().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)



  • Hi @jazzycamel thank you so much, the deferred call indeed solved all my problems.
    I have a LOT of things in self.init_ui() that need to be set after profile is set.
    Would it be fine to put self.set_profile() in the init_ui and defer that last one instead?



  • @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.



  • One last question, please, I see you used a @pyqtSlot decorator on the setProfile method.
    Using it or not doesn't seem to affect code execution.
    Could you please explain why you used it then?



  • @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 the currentIndexChanged signal: one emits a QString and the other an int. 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 our QApplication) as follows:

    qmlRegisterType(Bob, "Bob", 1, 0, "Bob")
    

    We could then instanstiate Bob from QML and call hello() 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)



  • 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 the init_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


  • Lifetime Qt Champion

    Hi,

    You should give your threads an exit point so that you can stop them nicely.



  • hi @SGaist thanks for the hint, that make senses, I endup adding a variable willing_to_exist = False to MainWindow and passed it to the functions of workers, exiting loops if set to True and also a threadpool.waitForDone(msecs=X) before calling qApp.quit() to avoid their destruction too fast, might not be the sexiest but it does the trick :P


Log in to reply