Getting closeEvent when exiting the application - python

I'm trying to make a small python programs which is able to have several windows. The issue is when I try to implement a menu entry to quit the programs, closing all the windows at once. I've tried to use qApp.close() and qApp.exit() but if those allow to effectively quit the program, there is no close events generated for the windows still opened, which prevent me to save modified data or to prevent leaving the application. What's the best practice for that? I could understand not being able to cancel the exit process, but being able to propose to save modified data is something I really want.
import sys
from PyQt5.QtWidgets import *
opened_windows = set()
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.create_actions()
opened_windows.add(self)
def closeEvent(self, ev):
if QMessageBox.question(self, 'Closing', 'Really close?') == QMessageBox.Yes:
ev.accept()
opened_windows.remove(self)
else:
ev.ignore()
def create_action(self, action_callback, menu, action_name):
action = QAction(action_name, self)
action.triggered.connect(action_callback)
menu.addAction(action)
def create_actions(self):
_file_menu = self.menuBar().addMenu('&File')
self.create_action(self.on_new, _file_menu, '&New')
_file_menu.addSeparator()
self.create_action(self.on_close, _file_menu, '&Close')
self.create_action(self.on_quit, _file_menu, '&Quit')
self.create_action(self.on_exit, _file_menu, '&Exit')
def on_new(self):
win = MainWindow()
win.show()
def on_close(self):
self.close()
def on_quit(self):
qApp.quit()
def on_exit(self):
qApp.exit(1)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
win.show()
status = app.exec()
print(len(opened_windows), ' window(s) opened')
print('status = ', status)
sys.exit(status)
Currently I'm modifying on_close and on_exit like this:
def on_exit(self):
for w in opened_windows.copy():
w.on_close()
if len(opened_windows) == 0:
qApp.exit(1)
but I wonder if I'm missing a better way which would not force me to maintain a set of opened windows.

Cause
It is important to understand, that the app and the main window are related, but are not the same thing. So, when you want to close the program, don't bother closing the app. Close the main window instead. From the documentation of QCloseEvent :
Close events are sent to widgets that the user wants to close, usually by choosing "Close" from the window menu, or by clicking the X title bar button. They are also sent when you call QWidget::close() to close a widget programmatically.
Solution
Connect your exit-action's triggered signal to the close slot of your MainWindow. In your case, instead of:
self.create_action(self.on_exit, _file_menu, '&Exit')
write:
self.create_action(self.close, _file_menu, '&Exit').
Define in MainWindow a signal closed and emit it from your implementation of the closedEvent, e.g. in the place of opened_windows.remove(self)
In on_new connect win.closed to self.close
Example
Here is how I suggest you to change your code in order to implement the proposed solution:
import sys
from PyQt5.QtWidgets import *
class MainWindow(QMainWindow):
closed = pyqtSignal()
def __init__(self):
super().__init__()
self.create_actions()
def closeEvent(self, ev):
if QMessageBox.question(self, 'Closing', 'Really close?') == QMessageBox.Yes:
ev.accept()
self.closed.emit()
else:
ev.ignore()
def create_action(self, action_callback, menu, action_name):
action = QAction(action_name, self)
action.triggered.connect(action_callback)
menu.addAction(action)
def create_actions(self):
_file_menu = self.menuBar().addMenu('&File')
self.create_action(self.on_new, _file_menu, '&New')
_file_menu.addSeparator()
self.create_action(self.close, _file_menu, '&Exit')
def on_new(self):
win = MainWindow()
win.show()
win.closed.connect(self.close)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
win.show()
status = app.exec()
print('status = ', status)
sys.exit(status)

Edit: I wonder how I missed it before. There is the QApplication::closeAllWindows slot which does exactly what I want and whose example is a binding to exit.
There is a way to propose to save modified data on quit and exit, the signal QCoreApplication::aboutToQuit.
Note that although the Qt documentation says that user interaction is not possible, at least with PyQt5 I could use a QMessageBox without apparent issues.

Related

Why is QMessageBox.exec() not blocking my thread

please check below snippet from my application:
requestDialogSignal = QtCore.pyqtSignal()
class MainWindow(QMainWindow):
def __init__(self):
...
self.requestDialogSignal.connect(self.slotRequestDialog, Qt.QueuedConnection)
def slotRequestDialog():
mbox = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "title", "message")
mbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
result = mbox.exec()
def CreateDialogs(self):
self.requestDialogSignal.emit()
time.sleep(1)
self.requestDialogSignal.emit()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
t = threading.Thread(target=window.CreateDialogs)
t.start()
app.exec()
Essentially, what I was trying to achieve is 2nd QMessageBox to appear only after first one gets answered. I was expecting the first QMessageBox to block the receiver thread on exec(), which due to Qt.QueuedConnection should not allow for the slot to be called second time. If I change the connection to Qt.BlockingQueuedConnection it behaves as expected, although this blocks the sending thread, which is not what I want.
Try something like this. I have not tested it, as I do not have PyQt nor PySide on my machine. There may be some little issues but the logic is clear and I hope understandable.
requestDialogSignal = QtCore.pyqtSignal()
class MainWindow(QMainWindow):
def __init__(self):
self.state = 0 # a state variable which can be used to define the dialog opening logic
...
def openDialog(self):
mbox = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "title", "message", self) # use self to give the dialog a parent; not strictly needed but it is a good practice
mbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
mbox.finished.connect(self.onDialogFinished) # or you can connect to accepted or rejected signals to finetune the logic
mbox.open() # open() makes the dialog window-modal but does not block the code
def onDialogFinished(self):
self.state += 1
if self.state == 1: # makes sure we open te dialog only twice, but you can change the logic as you like with the state variable
self.openDialog()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
window.openDialog()
app.exec()
I believe that the following solution should also work but it is ugly for three reasons. 1) Calling dialog's exec() before application's exec(). 2) Using too much exec(), which is not a good practice. exec() should be avoided if possible. 3) Containing an unnecessary recursion... None of these three objections are I believe wrong, they are just ugly and I do not like them.
requestDialogSignal = QtCore.pyqtSignal()
class MainWindow(QMainWindow):
def __init__(self):
self.state = 0 # a state variable which can be used to define the dialog opening logic
...
def execDialog(self):
mbox = QtWidgets.QMessageBox(QtWidgets.QMessageBox.NoIcon, "title", "message", self)
mbox.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
mbox.exec() # blocks the code until closed
self.state += 1
if self.state == 1: # makes sure we open te dialog only twice, but you can change the logic as you like with the state variable
self.execDialog()
app = QApplication(sys.argv)
window = MainWindow()
window.show()
window.execDialog()
app.exec()
What I find strange about you code is that you show the same dialog twice. Is this really what you want? I guess you actually want to show two different dialogs... and in that case the code would look different. But anyway, I have shown you how to avoid threads and how to properly use (or avoid) exec(). I would much vote for using the first of my examples rather than the second.

Issues when attaching and detaching external app from QDockWidget

Consider this little piece of code:
import subprocess
import win32gui
import win32con
import time
import sys
from PyQt5.Qt import * # noqa
class Mcve(QMainWindow):
def __init__(self, path_exe):
super().__init__()
menu = self.menuBar()
attach_action = QAction('Attach', self)
attach_action.triggered.connect(self.attach)
menu.addAction(attach_action)
detach_action = QAction('Detach', self)
detach_action.triggered.connect(self.detach)
menu.addAction(detach_action)
self.dock = QDockWidget("Attach window", self)
self.addDockWidget(Qt.RightDockWidgetArea, self.dock)
p = subprocess.Popen(path_exe)
time.sleep(0.5) # Give enough time so FindWindowEx won't return 0
self.hwnd = win32gui.FindWindowEx(0, 0, "CalcFrame", None)
if self.hwnd == 0:
raise Exception("Process not found")
def detach(self):
try:
self._window.setParent(None)
# win32gui.SetWindowLong(self.hwnd, win32con.GWL_EXSTYLE, self._style)
self._window.show()
self.dock.setWidget(None)
self._widget = None
self._window = None
except Exception as e:
import traceback
traceback.print_exc()
def attach(self):
# self._style = win32gui.GetWindowLong(self.hwnd, win32con.GWL_EXSTYLE)
self._window = QWindow.fromWinId(self.hwnd)
self._widget = self.createWindowContainer(self._window)
self.dock.setWidget(self._widget)
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Mcve("C:\\Windows\\system32\\calc.exe")
w.show()
sys.exit(app.exec_())
The goal here is to fix the code so the window attaching/detaching into a QDockWidget will be made properly. Right now, the code has 2 important issues.
Issue1
Style of the original window is screwed up:
a) Before attaching (the calculator has a menu bar)
b) When attached (the calculator menu bar is gone)
c) When detached (the menu bar hasn't been restored properly)
I've already tried using flags/setFlags qt functions or getWindowLong/setWindowLong but I haven't had luck with all my attempts
Issue2
If you have attached and detached the calculator to the mainwindow, and then you decide to close the mainwindow, you definitely want everything (pyqt process) to be closed and cleaned properly. Right now, that won't be the case, why?
In fact, when you've attached/detached the calculator to the mainwindow, the python process will hold and you'll need to force the termination of the process manually (i.e. ctrl+break conemu, ctrl+c cmd prompt)... which indicates the code is not doing things correctly when parenting/deparenting
Additional notes:
http://doc.qt.io/qt-5/qwindow.html#fromWinId
http://doc.qt.io/qt-5/qwidget.html#createWindowContainer
In the above minimal code I'm spawning calc.exe as a child process but you can assume calc.exe is an existing non-child process spawned by let's say explorer.exe
I found part of the issue wrt to closing. So when you are creating the self._window in the attach function and you close the MainWindow, that other window (thread) is sitting around still. So if you add a self._window = None in the __init__ function and add a __del__ function as below, that part is fixed. Still not sure about the lack of menu. I'd also recommend holding onto the subprocess handle with self.__p instead of just letting that go. Include that in the __del__ as well.
def __del__(self):
self.__p.terminate()
if self._window:
print('terminating window')
self._window.close
Probably better yet would be to include a closeEvent
def closeEvent(self, event):
print('Closing time')
self.__p.terminate()
if self._window is not None:
print('terminating window')
self._window.close

Detect trigger for closeEvent in PyQt

I have a PyQt GUI which has multiple ways of being closed. Two such ways are if they X out of the window, or if they complete a certain task. My class, which extends from QMainWindow, implements the closeEvent method.
In the case when the use X's out of the window, I want to ask them if they're sure they want to quit, then do some cleanup and close if they say yes.
In the case where they complete the certain task, I just want to do the cleanup and quit.
Right now my closeEvent looks like this:
def closeEvent(self, event):
# Ask the user if they actually want to quit using custom message box
msg = MessageBox(self, 'Are you sure you want to quit?', title = 'Quit', buttons = QMessageBox.Yes|QMessageBox.No)
if msg.reply == QMessageBox.Yes:
#
# Do some cleanup here
#
super().close()
else:
event.ignore()
With this certain setup, it always asks if they are sure they want to close, which is necessary for when they X out of the window. When they've completed the specific task, I know they want to close and just want to jump to the cleanup and closing process, without asking this question.
My idea would be that in some close cases, I direct them to a separate function which does it's necessary tasks, then calls the close method. But the problem here is, I don't know how to direct the X out process to a different method before calling the close method. As it is right now, it always asks if they're sure they want to quit because this is needed as part of the X out closing method.
Is there any way to do what I'm trying to do?
A method of solution is to close your application without running closeEvent, for this you can use the function qApp.quit().
For example in the following script can be closed by two methods: the first is a new button called btn, and the second the button X. For the first case we connect it to a slot that calls a dialog, if that is accepted I close the Application with qApp.quit, the second case is similar to the one you implement.
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import sys
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent=parent)
btn = QPushButton("close")
self.setCentralWidget(btn)
btn.clicked.connect(self.closeByButton)
def closeByButton(self):
msgBox = QMessageBox()
msgBox.setText("new Method")
msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Yes | QMessageBox.No);
msgBox.setDefaultButton(QMessageBox.Yes);
if msgBox.exec_() == QMessageBox.Yes:
qApp.quit()
def closeEvent(self, event):
msgBox = QMessageBox()
msgBox.setText("Button X Method")
msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.Yes | QMessageBox.No);
msgBox.setDefaultButton(QMessageBox.Yes);
if msgBox.exec_() == QMessageBox.Yes:
event.accept()
else:
event.ignore()
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())

PyQt5 Interrupt Close Event from Outside the Main Gui Module

I use Qt Designer to build my GUI's and convert them to py files using pyuic5. My end goal here is to interrupt the user from closing the program when a variable == 1 and present them with an 'are you sure you want to close?' type dialog. If said variable == 0 then just close the program normally.
I have seen lots of examples on how to do this, but all of them require editing the code in the GUI module. I import my gui.py file created by pyuic5 into my main script where I do all my connections to buttons, line edits, etc.. I do this so that at anytime I can update the GUI with Qt Designer and not affect the programs functionality.
Is there a way to do this from my main script that has the GUI module from Qt Designer imported?
Example of how my main script is structured:
import philipsControlGui
import sys
def main():
MainWindow.show()
sys.exit(app.exec_())
def test():
print('test')
# Main window setup
app = philipsControlGui.QtWidgets.QApplication(sys.argv)
MainWindow = philipsControlGui.QtWidgets.QMainWindow()
ui = philipsControlGui.Ui_MainWindow()
ui.setupUi(MainWindow)
# Main window bindings
ui.onButton.clicked.connect(test)
### Can I insert something here to do: if user closes the window... do something else instead?
if __name__ == "__main__":
main()
You should create a subclass from your imported gui so you can reimplement the closeEvent method:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
from philipsControlGui import Ui_MainWindow
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.ui = Ui_MainWindow()
self.ui.setUpUi(self)
self.ui.onButton.clicked.connect(self.test)
self._check_close = True
def test(self):
print('test')
self._check_close = not self._check_close
def closeEvent(self, event):
if self._check_close:
result = QtWidgets.QMessageBox.question(
self, 'Confirm Close', 'Are you sure you want to close?',
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if result == QtWidgets.QMessageBox.Yes:
event.accept()
else:
event.ignore()
def main():
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
If there's a specific 'ExitButton' in your design, you should be able to connect it in the main code and create a pop up dialog. You would have to import the QtCore/QtGui components. I always write my GUI directly (QtDesigner is pain when it comes to these things) so I'm assuming something like this:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
[YOUR CODE]
ui.ExitButton.clicked.connect(Exit)
def Exit():
msg = QMessageBox()
msg.setIcon(QMessageBox.Information)
msg.setText("Are you sure you want to close this window?")
msg.setWindowTitle("MessageBox demo")
msg.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
msg.buttonClicked.connect(msgbtn)
retval = msg.exec_()
print "value of pressed message box button:", retval

Popup, non-modal, in-line Dialog in PyQT

It's difficult to describe what I want to do here, so here's what I want:
-Click a button that "pops up" a dialog.
-This dialog, however, should be non-modal.
-I'd also like for it to be sort of 'locked' with the parent app so if I moved it, it too would come with.
It seems like i'm more or less trying to describe an in-line popup, but I'm not sure if that's what it's called, or even how I should go about approaching this problem. So, PyQt gurus, how would you make something like this? Thanks in advance.
To get a modeless dialog, open it with show() rather than exec_().
To ensure that the dialog is "locked with the parent app", set a parent window in the dialog's constructor.
from PyQt4 import QtGui
class Window(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
button = QtGui.QPushButton('Open Dialog', self)
button.clicked.connect(self.handleOpenDialog)
self.resize(300, 200)
self._dialog = None
def handleOpenDialog(self):
if self._dialog is None:
self._dialog = QtGui.QDialog(self)
self._dialog.resize(200, 100)
self._dialog.show()
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())
for Qt5:
I ignore why dialog.setWindowModality(QtCore.Qt.NonModal) won't work, but
dialog.run()
dialog.exec_()
will do. run will make the dialog non modal and exec_ will block it until the user input.

Categories

Resources