Prevent PySide2 dialog from closing when QRunnable still running - python

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

Related

With PyQt5, implement two windows looping forever automatically

Using PyQt5, I want to implement a two windows displaying one after another automatically, without the user interacting with any window. Something like this:
While True:
Show Window1
wait 2 seconds
Close Window1
Show Window2
wait 2 seconds
Close Window2
The problem I am having is that the main UI thread is stuck in app.exec_() function, so it cannot implement the opening and closing logic.
import sys
from PyQt5.QtWidgets import *
from PyQt5 import uic
class Win1(QMainWindow):
def __init__(self):
super(Win1, self).__init__()
uic.loadUi('win1.ui', self)
self.show()
class Win2(QMainWindow):
def __init__(self):
super(Win2, self).__init__()
uic.loadUi('win2.ui', self)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
while True:
win = Win1()
time.sleep(1)
win.close()
win = Win2()
time.sleep(1)
win.close()
app.exec_() # <--------- Program blocks here
I would appreciate if someone can share a minimal example for this working without blocking. Or please point to the mechanism that should be used.
If you are going to work with Qt then you should forget about sequential logic but you have to implement the logic using events. For example, in your case you want one window to be shown every time T and another to be hidden, so that can be implemented with a QTimer and a flag:
import sys
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QApplication, QMainWindow
from PyQt5 import uic
class Win1(QMainWindow):
def __init__(self):
super(Win1, self).__init__()
uic.loadUi('win1.ui', self)
self.show()
class Win2(QMainWindow):
def __init__(self):
super(Win2, self).__init__()
uic.loadUi('win2.ui', self)
self.show()
if __name__ == "__main__":
app = QApplication(sys.argv)
timer = QTimer()
timer.setProperty("flag", True)
win1 = Win1()
win2 = Win2()
def on_timeout():
flag = timer.property("flag")
if flag:
win1.show()
win2.close()
else:
win2.show()
win1.close()
timer.setProperty("flag", not flag)
timer.timeout.connect(on_timeout)
timer.start(1 * 1000)
on_timeout()
app.exec_()
You should not use while loop or time.sleep since they block the eventloop in which the GUI lives, that is: they freeze the windows

pyqt5 - closing/terminating application

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.

Close PyQt App via a Modal Dialog

I am currently coding an application using the Qt4 python bindings that requires the user to provide login credentials. At startup of my application I show a modal dialog in which the user can enter their data. Since the application cannot provide useful services when the user is not logged in, I want to close the app if the user clicks the cancel button.
So, if the dialog returns a negative result, I just call the close() method of my QMainWindow. Normally this leads to the exit of the application since there are no interactable windows any more.
However, if the modal Dialog has been shown before, the application simply continues to run and I have to kill it manually.
Below is the code for an minimal example
main.py:
import sys
from PyQt4.QtGui import QApplication
from MyMainWindow import MyMainWindow
app = QApplication(sys.argv)
window = MyMainWindow()
window.show()
app.exec_()
print "Terminated"
MyMainWindow.py:
from PyQt4.QtGui import QMainWindow
from PyQt4 import QtGui
from MyDialog import MyDialog
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle("Close App Test")
self.centralwidget = QtGui.QWidget(self)
self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget)
self.closeButton = QtGui.QPushButton(self.centralwidget)
self.closeButton.setText("Close")
self.closeButton.clicked.connect(self.exit)
def show(self):
QMainWindow.show(self)
myDialog = MyDialog(self)
res = myDialog.exec_()
if res == 0:
self.exit()
else:
print "Continue"
def exit(self):
self.close()
MyDialog.py:
from PyQt4.QtGui import QDialog
from PyQt4 import QtGui
class MyDialog(QDialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setWindowTitle("Modal Dialog")
self.resize(200, 50)
self.closeButton = QtGui.QPushButton(self)
self.closeButton.setText("Close")
self.closeButton.move(10, 10)
self.otherButton = QtGui.QPushButton(self)
self.otherButton.setText("Do Nothing")
self.otherButton.move(100, 10)
self.closeButton.clicked.connect(self.reject)
self.otherButton.clicked.connect(self.accept)
If the Close button on the modal dialog is clicked, the application continues to run even if all windows are closed. If the "Do Nothing" button is clicked, and the application is closed with the close button on the main window, everything works as expected and the application terminates.
I cannot really see the difference between both cases, since each time I just call close on the main window. I can just assume that I have errors in handling the modal dialog. I even tried tinkering around with myDialog.close(), myDialog.destroy(), as well as the quitOnCloseand deleteOnClose window attributes without any positive effects.
Any help is appreciated.
self.exit() is called before app.exec_() if the Close button on the modal dialog is clicked, so the application continues to run. self.exit() should be called when the main event loop is running.
The following is a possible solution for the case:
from PyQt4.QtGui import QMainWindow
from PyQt4 import QtGui
from pyQt4 import QtCore
from MyDialog import MyDialog
class MyMainWindow(QMainWindow):
def __init__(self, parent=None):
QMainWindow.__init__(self, parent)
self.setWindowTitle("Close App Test")
self.centralwidget = QtGui.QWidget(self)
self.verticalLayout = QtGui.QVBoxLayout(self.centralwidget)
self.closeButton = QtGui.QPushButton(self.centralwidget)
self.closeButton.setText("Close")
self.closeButton.clicked.connect(self.exit)
def login(self):
myDialog = MyDialog(self)
res = myDialog.exec_()
if res == 0:
self.exit()
else:
print "Continue"
def show(self):
QMainWindow.show(self)
QtCore.QTimer.singleShot(0, self.login)
def exit(self):
self.close()

PyQt: application unexpectedly leaves main loop

I have a simple PyQt application, which has a tray icon and can be hidden from taskbar by clicking close button or tray icon. Application can be closed from tray icon context menu. After user clicks "exit" in context menu, modal window with confirmation question appears . If user clicks "yes" application closes, if "no" application continues working.
When main window is hidden, application will be closed, even if user click "No" in modal window, but everything correct when window isn't hidden. This also happens with any modal window, for example with some information. I suppose the are some "magic" in parent paarmeter for QtGui.QMessageBox.question, but I don't know how to handle it. Please help to fix this annoying bug.
Here is the code:
import sys
from datetime import datetime
from PyQt4 import QtGui, QtCore
class SampleWindow(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.init_ui()
self.tray_icon.activated.connect(self.tray_click)
self.show_window.triggered.connect(self.show_from_tray)
self.now_button.triggered.connect(self.info)
self.appexit.triggered.connect(self.app_close)
def init_ui(self):
self.setGeometry(300, 300, 250, 150)
self.setWindowTitle('Message box')
self.tray_icon = QtGui.QSystemTrayIcon()
self.tray_icon.setIcon(QtGui.QIcon('clock.ico'))
self.tray_icon.show()
self.iconMenu = QtGui.QMenu()
self.show_window = self.iconMenu.addAction("MyApp")
self.show_window.setDisabled(True)
self.iconMenu.addSeparator()
self.now_button = self.iconMenu.addAction("Now")
self.appexit = self.iconMenu.addAction("Exit")
self.tray_icon.setContextMenu(self.iconMenu)
def info(self):
now = str(datetime.now())
QtGui.QMessageBox.information(self, 'Now', now)
def app_close(self):
info_msg = "Are you sure to quit?"
reply = QtGui.QMessageBox.question(self,
'Exit',
info_msg,
QtGui.QMessageBox.Yes,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
QtGui.QApplication.quit()
def closeEvent(self, event):
self.hide_to_tray()
event.ignore()
def show_from_tray(self):
self.setWindowFlags(QtCore.Qt.Window)
self.showNormal()
self.activateWindow()
self.show_window.setDisabled(True)
def hide_to_tray(self):
self.setWindowFlags(QtCore.Qt.Tool)
self.show_window.setDisabled(False)
def tray_click(self, reason):
if reason != QtGui.QSystemTrayIcon.Context:
if self.isHidden():
self.show_from_tray()
else:
self.hide_to_tray()
def main():
app = QtGui.QApplication(sys.argv)
sample = SampleWindow()
sample.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
By default Qt application is terminated when the last window is closed. I'm not sure why it is closed after invoking context menu. I think it's because you hide your window in a strange way. Usually a window can be hidden using hide().
This behavior can be easily disabled:
app = QtGui.QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)

PyQt4 Minimize to Tray

Is there a way to minimize to tray in PyQt4? I've already worked with the QSystemTrayIcon class, but now I would like to minimize or "hide" my app window, and show only the tray icon.
Has anybody done this? Any direction would be appreciated.
Using Python 2.5.4 and PyQt4 on Window XP Pro
It's pretty straightforward once you remember that there's no way to actually minimize to the system tray.
Instead, you fake it by doing this:
Catch the minimize event on your window
In the minimize event handler, create and show a QSystemTrayIcon
Also in the minimize event handler, call hide() or setVisible(false) on your window
Catch a click/double-click/menu item on your system tray icon
In your system tray icon event handler, call show() or setVisible(true) on your window, and optionally hide your tray icon.
Code helps, so here's something I wrote for an application, except for the closeEvent instead of the minimize event.
Notes:
"closeEvent(event)" is an overridden Qt event, so it must be put in the class that implements the window you want to hide.
"okayToClose()" is a function you might consider implementing (or a boolean flag you might want to store) since sometimes you actually want to exit the application instead of minimizing to systray.
There is also an example of how to show() your window again.
def __init__(self):
traySignal = "activated(QSystemTrayIcon::ActivationReason)"
QtCore.QObject.connect(self.trayIcon, QtCore.SIGNAL(traySignal), self.__icon_activated)
def closeEvent(self, event):
if self.okayToClose():
#user asked for exit
self.trayIcon.hide()
event.accept()
else:
#"minimize"
self.hide()
self.trayIcon.show() #thanks #mojo
event.ignore()
def __icon_activated(self, reason):
if reason == QtGui.QSystemTrayIcon.DoubleClick:
self.show()
Just to add to the example by Chris:
It is crucial that you use the Qt notation when declaring the signal, i.e.
correct:
self.connect(self.icon, SIGNAL("activated(QSystemTrayIcon::ActivationReason)"), self.iconClicked)
and not the PyQt one
incorrect and won't work:
self.connect(self.icon, SIGNAL("activated(QSystemTrayIcon.ActivationReason)"), self.iconClicked)
Note the :: in the signal string. This took me about three hours to figure out.
Here's working code..Thanks Matze for Crucial, the SIGNAL took me more hours of curiosity.. but doing other things. so ta for a #! moment :-)
def create_sys_tray(self):
self.sysTray = QtGui.QSystemTrayIcon(self)
self.sysTray.setIcon( QtGui.QIcon('../images/corp/blip_32.png') )
self.sysTray.setVisible(True)
self.connect(self.sysTray, QtCore.SIGNAL("activated(QSystemTrayIcon::ActivationReason)"), self.on_sys_tray_activated)
self.sysTrayMenu = QtGui.QMenu(self)
act = self.sysTrayMenu.addAction("FOO")
def on_sys_tray_activated(self, reason):
print "reason-=" , reason
This was an edit of vzades response, but it was rejected on a number of grounds. It does the exact same thing as their code but will also obey the minimize event (and run without syntax errors/missing icons).
import sys
from PyQt4 import QtGui, QtCore
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
style = self.style()
# Set the window and tray icon to something
icon = style.standardIcon(QtGui.QStyle.SP_MediaSeekForward)
self.tray_icon = QtGui.QSystemTrayIcon()
self.tray_icon.setIcon(QtGui.QIcon(icon))
self.setWindowIcon(QtGui.QIcon(icon))
# Restore the window when the tray icon is double clicked.
self.tray_icon.activated.connect(self.restore_window)
def event(self, event):
if (event.type() == QtCore.QEvent.WindowStateChange and
self.isMinimized()):
# The window is already minimized at this point. AFAIK,
# there is no hook stop a minimize event. Instead,
# removing the Qt.Tool flag should remove the window
# from the taskbar.
self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.Tool)
self.tray_icon.show()
return True
else:
return super(Example, self).event(event)
def closeEvent(self, event):
reply = QtGui.QMessageBox.question(
self,
'Message',"Are you sure to quit?",
QtGui.QMessageBox.Yes | QtGui.QMessageBox.No,
QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
event.accept()
else:
self.tray_icon.show()
self.hide()
event.ignore()
def restore_window(self, reason):
if reason == QtGui.QSystemTrayIcon.DoubleClick:
self.tray_icon.hide()
# self.showNormal will restore the window even if it was
# minimized.
self.showNormal()
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This is the correct way to handle double click on a tray icon for PyQt5.
def _create_tray(self):
self.tray_icon = QSystemTrayIcon(self)
self.tray_icon.activated.connect(self.__icon_activated)
def __icon_activated(self, reason):
if reason in (QSystemTrayIcon.Trigger, QSystemTrayIcon.DoubleClick):
pass
This is the code and it does help i believe in show me the code
import sys
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import QDialog, QApplication, QPushButton, QLineEdit, QFormLayout, QSystemTrayIcon
class Example(QtGui.QWidget):
def __init__(self):
super(Example, self).__init__()
self.initUI()
def initUI(self):
self.icon = QSystemTrayIcon()
r = self.icon.isSystemTrayAvailable()
print r
self.icon.setIcon(QtGui.QIcon('/home/vzades/Desktop/web.png'))
self.icon.show()
# self.icon.setVisible(True)
self.setGeometry(300, 300, 250, 150)
self.setWindowIcon(QtGui.QIcon('/home/vzades/Desktop/web.png'))
self.setWindowTitle('Message box')
self.show()
self.icon.activated.connect(self.activate)
self.show()
def closeEvent(self, event):
reply = QtGui.QMessageBox.question(self, 'Message', "Are you sure to quit?", QtGui.QMessageBox.Yes |
QtGui.QMessageBox.No, QtGui.QMessageBox.No)
if reply == QtGui.QMessageBox.Yes:
event.accept()
else:
self.icon.show()
self.hide()
event.ignore()
def activate(self, reason):
print reason
if reason == 2:
self.show()
def __icon_activated(self, reason):
if reason == QtGui.QSystemTrayIcon.DoubleClick:
self.show()
def main():
app = QtGui.QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
if __name__ == '__main__':
main()

Categories