pyqt5 - closing/terminating application - python

I'm working though the pyqt5 tutorial found here Zetcode, PyQt5
As an exercise for myself I'm trying to expand on an example so that I am presented with the same dialog message box regardless of method used to close the app:
clicking the 'X' button in the title bar (works as intended)
clicking the 'Close' button (produces attribute error)
pressing the 'escape' key (works but not sure how/why)
The dialog message box is implemented in the closeEvent method, full script provided at the end.
I'm having two issues:
1. When clicking 'Close' button, instead of just quitting, I want to call closeEvent method including message box dialog.
I have replaced a line of the example code for the 'Close' push button:
btn.clicked.connect(QCoreApplication.instance().quit)
And instead am trying to call the closeEvent method which already implements the dialog I want:
btn.clicked.connect(self.closeEvent)
However when i run the script and click the 'Close' button and select the resulting 'Close' option in the dialog i get the following:
Traceback (most recent call last):
File "5-terminator.py", line 41, in closeEvent
event.accept()
AttributeError: 'bool' object has no attribute 'accept'
Aborted
Can anyone advise what I'm doing wrong and what needs to be done here?
2. When hitting the escape key somehow the message box dialog is presented and works just fine.
Ok, it's great that it works, but I'd like to know how and why the message box functionality defined in CloseEvent method is called within the keyPressEvent method.
Full script follows:
import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QToolTip, QPushButton, QMessageBox)
from PyQt5.QtCore import QCoreApplication, Qt
class Window(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn = QPushButton("Close", self)
btn.setToolTip("Close Application")
# btn.clicked.connect(QCoreApplication.instance().quit)
# instead of above button signal, try to call closeEvent method below
btn.clicked.connect(self.closeEvent)
btn.resize(btn.sizeHint())
btn.move(410, 118)
self.setGeometry(30, 450, 500, 150)
self.setWindowTitle("Terminator")
self.show()
def closeEvent(self, event):
"""Generate 'question' dialog on clicking 'X' button in title bar.
Reimplement the closeEvent() event handler to include a 'Question'
dialog with options on how to proceed - Save, Close, Cancel buttons
"""
reply = QMessageBox.question(
self, "Message",
"Are you sure you want to quit? Any unsaved work will be lost.",
QMessageBox.Save | QMessageBox.Close | QMessageBox.Cancel,
QMessageBox.Save)
if reply == QMessageBox.Close:
event.accept()
else:
event.ignore()
def keyPressEvent(self, event):
"""Close application from escape key.
results in QMessageBox dialog from closeEvent, good but how/why?
"""
if event.key() == Qt.Key_Escape:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
sys.exit(app.exec_())
Hope someone can take the time to enlighten me.

Your second question answers the first question.
The reimplemented keyPressEvent method calls close(), which sends a QCloseEvent to the widget. Subsequently, the widget's closeEvent will be called with that event as its argument.
So you just need to connect the button to the widget's close() slot, and everything will work as expected:
btn.clicked.connect(self.close)

Unlike the X button your custom button does not seem to pass an close event just a bool. That's why this exercise should work for the X button but not a normal button. In any case, for your first question you might use destroy() and pass instead (of accept and ignore) just like this:
import sys
from PyQt5.QtWidgets import (
QApplication, QWidget, QToolTip, QPushButton, QMessageBox)
from PyQt5.QtCore import QCoreApplication, Qt
class Window(QWidget):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
btn = QPushButton("Close", self)
btn.setToolTip("Close Application")
# btn.clicked.connect(QCoreApplication.instance().quit)
# instead of above button signal, try to call closeEvent method below
btn.clicked.connect(self.closeEvent)
btn.resize(btn.sizeHint())
btn.move(410, 118)
self.setGeometry(30, 450, 500, 150)
self.setWindowTitle("Terminator")
self.show()
def closeEvent(self, event):
"""Generate 'question' dialog on clicking 'X' button in title bar.
Reimplement the closeEvent() event handler to include a 'Question'
dialog with options on how to proceed - Save, Close, Cancel buttons
"""
reply = QMessageBox.question(
self, "Message",
"Are you sure you want to quit? Any unsaved work will be lost.",
QMessageBox.Save | QMessageBox.Close | QMessageBox.Cancel,
QMessageBox.Save)
if reply == QMessageBox.Close:
app.quit()
else:
pass
def keyPressEvent(self, event):
"""Close application from escape key.
results in QMessageBox dialog from closeEvent, good but how/why?
"""
if event.key() == Qt.Key_Escape:
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
w = Window()
sys.exit(app.exec_())
For your second question Qt has default behaviors depending on the Widget (Dialogs might have another, try pressing the Esc key when your Message Dialog is open just to see). When you do need to override the Esc behavior you might try this:
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Escape:
print("esc")
As you'll eventually see in ZetCode.

closeEvent()
In above right tick mark code please check the closeEvent it is same other wise it's waste of time to research.

Related

Prevent PySide2 dialog from closing when QRunnable still running

I have a PySide2 application where I'm executing a long running process using QRunnable and I don't want the user to accidentally close the dialog until the finished signals is emitted.
While I can use self.setWindowFlag(QtCore.Qt.WindowCloseButtonHint, False) and re-enable it after the QRunnable finished running, I prefer to have a way alert the user that the function is still running if they accidentally close it (despite the dialog showing a progress bar and output log).
I'm thinking of subclassing and override the closeEvent but I wonder if there is other or even better way to approach this problem.
Here's a working example of a closeEvent override that satisfy the need of my original question.
import sys
from PySide2.QtGui import *
from PySide2.QtWidgets import *
from PySide2.QtCore import *
class CloseEventOverrideDialog(QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowFlag(Qt.WindowMinimizeButtonHint, True)
self.setWindowFlag(Qt.WindowMaximizeButtonHint, True)
self.setWindowTitle("closeEvent Override Example")
self.is_busy = False
self.setup_ui()
def closeEvent(self, event: QCloseEvent):
print(f"{self.is_busy=}")
if self.is_busy:
msg = (
f"Something is busy running. Do you wish to abort the progress?"
)
is_cancel = QMessageBox.warning(
self,
"Warning!",
msg,
QMessageBox.Ok,
QMessageBox.Cancel
)
print(f"{is_cancel=}")
if is_cancel == QMessageBox.Cancel:
# Prevent dialog from closing!
event.ignore()
else:
print(f"User explicitly close dialog!")
event.accept()
def setup_ui(self):
self.layout = QVBoxLayout()
self.setLayout(self.layout)
checked_label = QLabel(
"Checked the following checkbox to trigger a QMessageBox Warning dialog "
"when user close the dialog."
)
self.layout.addWidget(checked_label)
self.busy_checkbox = QCheckBox("Busy")
self.busy_checkbox.stateChanged.connect(self.toggle_is_busy)
self.layout.addWidget(self.busy_checkbox)
def toggle_is_busy(self):
self.is_busy = True if self.busy_checkbox.isChecked() else False
print(f"{self.is_busy=}")
def main():
app = QApplication(sys.argv)
dialog = CloseEventOverrideDialog()
dialog.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Screenshot of QMessageBox Warning when user tries to close the dialog with closeEvent override

Emit a signal when any button is pressed

In a pyqt5 app I want to untoggle a button when any other button is clicked.
Is it possible to have a signal emitted when any button inside a layout or a widget is clicked?
I can connect each button click to an untoggle function but that would be hard to maintain if the buttons ever change. Is there a clean way to achieve the same functionality without having to connect the buttons one by one?
How to point out a possible solution is to inherit from the button and implement the indicated logic, but in the case of Qt it can be taken advantage of the fact that the clicked signal is associated with the QEvent.MouseButtonRelease event when a button is pressed. Considering the above, notify can be used to implement the logic:
import sys
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QEvent
from PyQt5.QtWidgets import (
QAbstractButton,
QApplication,
QPushButton,
QVBoxLayout,
QWidget,
)
class Application(QApplication):
buttonClicked = pyqtSignal(QAbstractButton)
def notify(self, receiver, event):
if (
isinstance(receiver, QAbstractButton)
and receiver.isEnabled()
and event.type() == QEvent.MouseButtonRelease
):
self.buttonClicked.emit(receiver)
return super().notify(receiver, event)
def main(argv):
app = Application(argv)
view = QWidget()
lay = QVBoxLayout(view)
special_button = QPushButton("Special")
lay.addWidget(special_button)
for i in range(10):
button = QPushButton(f"button{i}")
lay.addWidget(button)
view.show()
def handle_clicked(button):
if button is not special_button:
print(f"{button} clicked")
QCoreApplication.instance().buttonClicked.connect(handle_clicked)
sys.exit(app.exec_())
if __name__ == "__main__":
main(sys.argv)
What you want is added functionality for an existing type. This suggests that you want to write an extension of the Button class. Write a class that inherits from Button. Write your own function to handle button clicks: this function will call your toggle function and then call the parent's button click handler.

How to work with the "?" (what's this widget) on the title bar of a PyQT Dialog

On the right of the title bar of a PyQt QDialog (see below, next to the "x") there is a "?" that is supposed to help the user query help for any other widget on the Dialog window.
What should I do (programmatically) to get it to work. Once the "?" isClicked, one should be able to capture the next widget clicked and provide a ToolTip or something like that. In PyQt, I do not know how to capture the isClicked event on the "?".
I have seen a couple of posts where the question was how to make the "?" disappear, but the discussion there uses Qt, not PyQt, so I do not understand it, and they are not talking about what I need. I need to make it work as intended. See How can I hide/delete the "?" help button on the "title bar" of a Qt Dialog? and PyQt4 QInputDialog and QMessageBox window flags
You can set the whatsThis property to any widget you want:
self.someWidget.setWhatsThis('hello!')
From that point on, whenever you click on the "?" button and then click on that widget, a tooltip with that text will be shown.
Since the "what's this" mode is individually set to widgets, there's no easy way to capture it globally (as far as I know of) because if the widget has no whatsthis property set that feature won't be available for it.
Also, whenever you enter the "what's this" mode, the cursor will probably change according to the contents of the whatsthis property: if it's not set, the cursor will probably show a "disabled" icon.
I've created a basic workaround for this issue, which automatically enables any child widget's whatsthis (if none is already set) whenever the mode is activated: as soon as the EnterWhatsThisMode is fired, it automatically installs a custom object that acts as an event filter, and emits a signal if the whatsthis event is called; as soon as the mode exits, the filter is removed.
I used a separate object for the event filter because there's no way to know what event filter have been already installed to a widget, and if you already installed the parent's one, removing it automatically would be an issue.
class WhatsThisWatcher(QtCore.QObject):
whatsThisRequest = QtCore.pyqtSignal(QtWidgets.QWidget)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.WhatsThis:
self.whatsThisRequest.emit(source)
return super(WhatsThisWatcher, self).eventFilter(source, event)
class W(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
layout = QtWidgets.QVBoxLayout(self)
hasWhatsThisButton = QtWidgets.QPushButton('Has whatsThis')
layout.addWidget(hasWhatsThisButton)
hasWhatsThisButton.setWhatsThis('I am a button!')
noWhatsThisButton = QtWidgets.QPushButton('No whatsThis')
layout.addWidget(noWhatsThisButton)
self.whatsThisWatchedWidgets = []
self.whatsThisWatcher = WhatsThisWatcher()
self.whatsThisWatcher.whatsThisRequest.connect(self.showCustomWhatsThis)
whatsThisButton = QtWidgets.QPushButton('Set "What\'s this" mode')
layout.addWidget(whatsThisButton)
whatsThisButton.clicked.connect(QtWidgets.QWhatsThis.enterWhatsThisMode)
def event(self, event):
if event.type() == QtCore.QEvent.EnterWhatsThisMode:
for widget in self.findChildren(QtWidgets.QWidget):
if not widget.whatsThis():
# install the custom filter
widget.installEventFilter(self.whatsThisWatcher)
# set an arbitrary string to ensure that the "whatsThis" is
# enabled and the cursor is correctly set
widget.setWhatsThis('whatever')
self.whatsThisWatchedWidgets.append(widget)
elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
while self.whatsThisWatchedWidgets:
widget = self.whatsThisWatchedWidgets.pop()
# reset the whatsThis string to none and uninstall the filter
widget.setWhatsThis('')
widget.removeEventFilter(self.whatsThisWatcher)
return super(W, self).event(event)
def showCustomWhatsThis(self, widget):
widgetPos = widget.mapTo(self, QtCore.QPoint())
QtWidgets.QWhatsThis.showText(
QtGui.QCursor.pos(),
'There is no "what\'s this" for {} widget at coords {}, {}'.format(
widget.__class__.__name__, widgetPos.x(), widgetPos.y()),
widget)
A couple of notes about this:
I used a button to activate the whatsthis mode, as on my window manager on Linux there's no window title button for that;
Some widgets may contain subwidgets, and you'll get those instead of the "main" one (the most common case are QAbstractScrollArea descendands, like QTextEdit or QGraphicsView, which might return the viewport, the inner "widget" or the scrollbars);
By default the task of that button is to enable whatsThis: press "?", then press the widget and you will see the message associated with whatsThis property.
If you want to add other actions(open url, add QToolTip, etc) you can monitor the QEvent::EnterWhatsThisMode and QEvent::LeaveWhatsThisMode events overriding the event() method or using an eventFilter().
from PyQt5 import QtCore, QtGui, QtWidgets
class Dialog(QtWidgets.QDialog):
def event(self, event):
if event.type() == QtCore.QEvent.EnterWhatsThisMode:
print("enter")
QtGui.QDesktopServices.openUrl(QtCore.QUrl("foo_url"))
elif event.type() == QtCore.QEvent.LeaveWhatsThisMode:
print("leave")
return super().event(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = Dialog()
w.setWhatsThis("Whats this")
w.setWindowFlags(
QtCore.Qt.WindowContextHelpButtonHint | QtCore.Qt.WindowCloseButtonHint
)
w.resize(640, 480)
w.show()
sys.exit(app.exec_())

PyQt5 closeEvent method

I'm currently learning how to build an application with pyqt5 and encountered some problem with closeEvent method, overriden so user gets asked for confirmation by QMessageBox object. It seems working well with X button - event gets 'accepted' when action is confirmed and 'canceled' when cancel button is clicked. However, when I use my Quit button from dropdown File menu, no matter which button I click, program gets closed with exit code 1. Seems strange, because I use same closeEvent method in both cases.
import sys
from PyQt5.QtWidgets import QApplication, QMessageBox, QMainWindow, QAction
class window(QMainWindow):
def __init__(self):
super().__init__()
def createUI(self):
self.setGeometry(500, 300, 700, 700)
self.setWindowTitle("window")
quit = QAction("Quit", self)
quit.triggered.connect(self.closeEvent)
menubar = self.menuBar()
fmenu = menubar.addMenu("File")
fmenu.addAction(quit)
def closeEvent(self, event):
close = QMessageBox()
close.setText("You sure?")
close.setStandardButtons(QMessageBox.Yes | QMessageBox.Cancel)
close = close.exec()
if close == QMessageBox.Yes:
event.accept()
else:
event.ignore()
main = QApplication(sys.argv)
window = window()
window.createUI()
window.show()
sys.exit(main.exec_())
Thanks for suggestions!
When you click button then program calls your function but with different event object which doesn't have accept() and ignore() so you get error message and program ends with exit code 1.
You can assign self.close and program will call closeEvent() with correct event object.
quit.triggered.connect(self.close)
The problem is accept is a method while ignore is just an attribute.
This code works for me:
def closeEvent(self, event):
close = QtWidgets.QMessageBox.question(self,
"QUIT",
"Are you sure want to stop process?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if close == QtWidgets.QMessageBox.Yes:
event.accept()
else:
event.ignore()
I had the same problem and fixed with a type-check-hack. It might be an ugly hack, but it works (tested on macOS 10.15 with python 3.8.0 and PyQt 5.14.2).
class Gui(QtWidgets.QMainWindow):
def __init__(self):
super(Gui, self).__init__()
uic.loadUi("gui.ui", self)
# ...
self.actionExit = self.findChild(QtWidgets.QAction, "actionExit")
self.actionExit.triggered.connect(self.closeEvent)
# ...
def closeEvent(self, event):
reply = QMessageBox.question(self, 'Quit?',
'Are you sure you want to quit?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
if not type(event) == bool:
event.accept()
else:
sys.exit()
else:
if not type(event) == bool:
event.ignore()
If you want to close an PyQt5 app from a menu:
When menu event triggered call: self.MainWindow.close() (or what window do you want to close
Add this code before sys.exit(app.exec()): self.MainWindow.closeEvent = lambda event:self.closeEvent(event)
Declare def closeEvent(self,event): method when you really want to close call event.accept() (and perhaps return 1) and if you don't want to close the window call event.ignore() (not event.reject() (it's not working for me))
def exit_window(self, event):
close = QtWidgets.QMessageBox.question(self,
"QUIT?",
"Are you sure want to STOP and EXIT?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
if close == QtWidgets.QMessageBox.Yes:
# event.accept()
sys.exit()
else:
pass

Python Qt How to open a pop up QDialog from a QMainWindow

I'm working on a project where I have a Database linked with a Python interface (I'm using Qt Designer for the design). I want to have a delete button from my main window (QMainWindow) where, when I'm pressing it, it opens a pop up (QDialog) which says
Are you sure you want to delete this item?
But I have no idea how to do it.
Thanks for you help!
def button_click():
dialog = QtGui.QMessageBox.information(self, 'Delete?', 'Are you sure you want to delete this item?', buttons = QtGui.QMessageBox.Ok|QtGui.QMessageBox.Cancel)
Bind this function to the button click event.
Let's say your Qt Designer ui has a main window called "MainWindow" and a button called "buttonDelete".
The first step is to set up your main window class and connect the button's clicked signal to a handler:
from PyQt4 import QtCore, QtGui
from mainwindow_ui import Ui_MainWindow
class MainWindow(QtGui.QMainWindow, Ui_MainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setupUi(self)
self.buttonDelete.clicked.connect(self.handleButtonDelete)
Next you need to add a method to the MainWindow class that handles the signal and opens the dialog:
def handleButtonDelete(self):
answer = QtGui.QMessageBox.question(
self, 'Delete Item', 'Are you sure you want to delete this item?',
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No |
QtGui.QMessageBox.Cancel,
QtGui.QMessageBox.No)
if answer == QtGui.QMessageBox.Yes:
# code to delete the item
print('Yes')
elif answer == QtGui.QMessageBox.No:
# code to carry on without deleting
print('No')
else:
# code to abort the whole operation
print('Cancel')
This uses one of the built-in QMessageBox functions to create the dialog. The first three arguments set the parent, title and text. The next two arguments set the group of buttons that are shown, plus the default button (the one that is initially highlighted). If you want to use different buttons, the available ones can be found here.
To complete the example, you just need some code to start the application and show the window:
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
I got the same error for qt5.6
TypeError: question(QWidget, str, str, buttons: Union[QMessageBox.StandardButtons, QMessageBox.StandardButton] = QMessageBox.StandardButtons(QMessageBox.Yes|QMessageBox.No), defaultButton: QMessageBox.StandardButton = QMessageBox.NoButton): argument 1 has unexpected type 'Ui_MainWindow'
So I changed self to None in the following code, and it works.
def handleButtonDelete(self):
answer = QtGui.QMessageBox.question(
None, 'Delete Item', 'Are you sure you want to delete this item?',
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No |
QtGui.QMessageBox.Cancel,
QtGui.QMessageBox.No)
if answer == QtGui.QMessageBox.Yes:
# code to delete the item
print('Yes')
elif answer == QtGui.QMessageBox.No:
# code to carry on without deleting
print('No')
else:
# code to abort the whole operation
print('Cancel')

Categories