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_())
Related
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.
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.
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
I have a login screen dialog written using pyqt and python and it shows a dialog pup up when it runs and you can type in a certin username and password to unlock it basicly. It's just something simple I made in learning pyqt. I'm trying to take and use it somewhere else but need to know if there is a way to prevent someone from using the x button and closing it i would like to also have it stay on top of all windows so it cant be moved out of the way? Is this possible? I did some research and couldn't find anything that could help me.
Edit:
as requested here is the code:
from PyQt4 import QtGui
class Test(QtGui.QDialog):
def __init__(self):
QtGui.QDialog.__init__(self)
self.textUsername = QtGui.QLineEdit(self)
self.textPassword = QtGui.QLineEdit(self)
self.loginbuton = QtGui.QPushButton('Test Login', self)
self.loginbuton.clicked.connect(self.Login)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.textUsername)
layout.addWidget(self.textPassword)
layout.addWidget(self.loginbuton)
def Login(self):
if (self.textUsername.text() == 'Test' and
self.textPassword.text() == 'Password'):
self.accept()
else:
QtGui.QMessageBox.warning(
self, 'Wrong', 'Incorrect user or password')
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
if Test().exec_() == QtGui.QDialog.Accepted:
window = Window()
window.show()
sys.exit(app.exec_())
Bad news first, it is not possible to remove the close button from the window, based on the Riverbank mailing system
You can't remove/disable close button because its handled by the
window manager, Qt can't do anything there.
Good news, you can override and ignore, so that when the user sends the event, you can ignore or put a message or something.
Read this article for ignoring the QCloseEvent
Also, take a look at this question, How do I catch a pyqt closeEvent and minimize the dialog instead of exiting?
Which uses this:
class MyDialog(QtGui.QDialog):
# ...
def __init__(self, parent=None):
super(MyDialog, self).__init__(parent)
# when you want to destroy the dialog set this to True
self._want_to_close = False
def closeEvent(self, evnt):
if self._want_to_close:
super(MyDialog, self).closeEvent(evnt)
else:
evnt.ignore()
self.setWindowState(QtCore.Qt.WindowMinimized)
You can disable the window buttons in PyQt5.
The key is to combine it with "CustomizeWindowHint",
and exclude the ones you want to be disabled.
Example:
#exclude "QtCore.Qt.WindowCloseButtonHint" or any other window button
self.setWindowFlags(
QtCore.Qt.Window |
QtCore.Qt.CustomizeWindowHint |
QtCore.Qt.WindowTitleHint |
QtCore.Qt.WindowMinimizeButtonHint
)
Result with QDialog:
Reference: https://doc.qt.io/qt-5/qt.html#WindowType-enum
Tip: if you want to change flags of the current window, use window.show()
after window.setWindowFlags,
because it needs to refresh it, so it calls window.hide().
Tested with QtWidgets.QDialog on:
Windows 10 x32,
Python 3.7.9,
PyQt5 5.15.1
.
I don't know if you want to do this but you can also make your window frameless. To make window frameless you can set the window flag equal to QtCore.Qt.FramelessWindowHint
I am trying to intercept paste() for a specific edit box. After much reading and head scratching I decided to try the big hammer and monkey patch. This didn't work for me either. Anyone know why?
import sys
from PyQt4 import QtGui
def myPaste():
print("paste") # Never gets here
if __name__ == "__main__":
# QtGui.QLineEdit.paste = myPaste # Try #1
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
# QtGui.QLineEdit.paste = myPaste # Try #2
edit.paste = myPaste # Try #3
window.setCentralWidget(centralWidget)
window.show()
app.exec_()
Based on feedback..i was able to use the event filter suggestion to solve my problem. Updated example code follows...
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = QtGui.QMainWindow()
window.setWindowTitle("monkey")
centralWidget = QtGui.QWidget(window)
edit = QtGui.QLineEdit(centralWidget)
window.setCentralWidget(centralWidget)
def eventFilter(obj, e):
if isinstance(obj, QtGui.QLineEdit):
if (e.type() == QtCore.QEvent.KeyPress):
if (e.matches(QtGui.QKeySequence.Paste)):
obj.paste()
t=str(obj.text()).title() # Special handling here...uppercase each word for example
obj.setText(t)
return True
return False
else:
return QtGui.QMainWindow.eventFilter(obj, e)
window.eventFilter = eventFilter
edit.installEventFilter(window)
window.show()
app.exec_()
The reason you can't "monkey-patch" QLineEdit.paste() is because it's not a virtual function. The important thing about virtual functions is that when they are overridden, the reimplemented function will get called internally by Qt; whereas non-virtual overrides will only get called by Python code. So, since QLinedit.paste() isn't virtual, you will instead have to intercept all the events that would normally result in it being called internally by Qt.
That will mean reimplementing QLineEdit.keyPressEvent, so that you can trap the shortcuts for the default key bindings; and also QLineEdit.contextMenuEvent, so that you can modify the default context menu. And, depending on what you're trying to do, you might also need to override the default drag-and-drop handling. (If you prefer not to use a subclass, an event-filter can be used to monitor all the relevant events).
The QClipboard class provides access to the system clipboard, which will allow you to intercept the text before it is pasted. Every application has a single clipboard object, which can be accessed via QApplication.clipboard() or qApp.clipboard().
In order to do what you want you can subclass QLineEdit and create a method that provides the custom paste functionality that you want (paste method isn't virtual so if it is overriden it won't be called from Qt code). In addition you will need an event filter to intercept the shortcut for CTRL+V. Probably you will have to filter the middle mouse button too which is also used to paste the clipboard content. From the event filter you can call your replacement of paste method.
You can use the following code as starting point:
import sys
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class myEditor(QLineEdit):
def __init__(self, parent=None):
super(myEditor, self).__init__(parent)
def myPaste(self):
self.insert("custom text pasted! ")
class myWindow(QMainWindow):
def __init__(self, parent=None):
super(myWindow, self).__init__(parent)
self.customEditor = myEditor(self)
self.setCentralWidget(self.customEditor)
self.customEditor.installEventFilter(self)
def eventFilter(self, obj, e):
if (obj == self.customEditor):
if (e.type() == QEvent.KeyPress):
if (e.matches(QKeySequence.Paste)):
self.customEditor.myPaste()
return True
return False
else:
return QMainWindow.eventFilter(obj, e)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = myWindow()
window.show()
app.exec_()
The event filter here only takes care of the keyboard shortcut for pasting. As I said you need to consider also other sources of the paste operation.