Generating a QCloseEvent won't close QMainWindow - python

I'm trying to do something quite simple: add a menu bar with an Exit action that will close a QMainWindow when it is selected. However, when I actually click Exit, it doesn't close the application. A SSCCE:
from PyQt4 import QtGui, QtCore
import sys
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
widget = QtGui.QWidget()
self.setCentralWidget(widget)
self.menu_bar = QtGui.QMenuBar(self)
menu = self.menu_bar.addMenu('File')
exit_action = QtGui.QAction('Exit', self)
exit_action.triggered.connect(lambda:
self.closeEvent(QtGui.QCloseEvent()))
menu.addAction(exit_action)
self.setMenuBar(self.menu_bar)
def closeEvent(self, event):
print('Calling')
print('event: {0}'.format(event))
event.accept()
app = QtGui.QApplication(sys.argv)
form = Window()
form.show()
sys.exit(app.exec_())
What is really confusing me is that when I click Exit from the File menu, I get the following output:
Calling
event: <PyQt4.QtGui.QCloseEvent object at 0x024B7348>
and the application does not exit.
If I click the top-right corner X, I get the same thing (down to the same memory address for the event object):
Calling
event: <PyQt4.QtGui.QCloseEvent object at 0x024B7348>
and the application does exit.
This is on Windows 7 64-bit, Python 2.7.2, PyQt 4.8.6.

Document says,
The QCloseEvent class contains parameters that describe a close event.
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.
Your can call directly with signal close not by QCloseEvent, please call self.close().
from PyQt4 import QtGui, QtCore
import sys
class Window(QtGui.QMainWindow):
def __init__(self, parent=None):
super(Window, self).__init__(parent)
widget = QtGui.QWidget()
self.setCentralWidget(widget)
self.menu_bar = QtGui.QMenuBar(self)
menu = self.menu_bar.addMenu('File')
exit_action = QtGui.QAction('Exit', self)
exit_action.triggered.connect(self.close)
menu.addAction(exit_action)
self.setMenuBar(self.menu_bar)
def closeEvent(self, event):
print('Calling')
print('event: {0}'.format(event))
event.accept()
app = QtGui.QApplication(sys.argv)
form = Window()
form.show()
sys.exit(app.exec_())

The close event doesn't actually make the window close, it's just triggered when the window is already closing. To actually make the window close, you need to call self.close(), which will have the side effect of triggering a QCloseEvent. So simply use this:
exit_action.triggered.connect(self.close)
The documentation of close describes the interaction between close and closeEvent:
bool QWidget.close (self)
This method is also a Qt slot with the C++ signature bool close().
Closes this widget. Returns true if the widget was closed; otherwise
returns false.
First it sends the widget a QCloseEvent. The widget is hidden if it
accepts the close event. If it ignores the event, nothing happens. The
default implementation of QWidget.closeEvent() accepts the close
event.

Related

How to restore focus on a widget after hidding then showing the parent widget?

I'm creating a launcher application (like Spotlight/Albert/Gnome-Do). I'm using Python 2.7 and Pyside. Made and used on Windows 10.
It is running in the background and listening to a shortcut with the keyboard (pip install keyboard). When the shortcut is called, a QObject signal calls the show method of my main widget.
My issue is that when the main widget gets hidden by pressing escape or return, next time the widget is shown, the focus will be in the QlineEdit and the user will be able to type its query straight away.
But when the widget is hidden by clicking outside widget (handled by filtering the QEvent WindowDeactivate), the focus won't be on my QLineEdit at next call, which ruins the user experience.
I've tried playing with activateWindow() or raise_(), but it doesn't change anything.
Heree here a simplified example code that shows my problem:
import sys
import keyboard
from PySide.QtCore import *
from PySide.QtGui import *
SHORTCUT = 'Ctrl+space'
class ShortcutThread(QObject):
signal = Signal()
class Launcher(QMainWindow):
def __init__(self, parent=None):
super(Launcher, self).__init__()
self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Popup)
self.resize(500, 50)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout_ = QHBoxLayout()
self.central_widget.setLayout(self.layout_)
self.search = QLineEdit()
self.layout_.addWidget(self.search)
def eventFilter(self, obj, event):
# Hide dialog when losing focus
if event.type() == QEvent.WindowDeactivate:
self.hide()
return super(Launcher, self).eventFilter(obj, event)
def keyPressEvent(self, e):
# Hide dialog when pressing escape or return
if e.key() in [Qt.Key_Escape, Qt.Key_Return]:
self.hide()
def main():
app = QApplication(sys.argv)
app.setQuitOnLastWindowClosed(False)
launcher = Launcher()
shortcut = ShortcutThread()
shortcut.signal.connect(launcher.show)
keyboard.add_hotkey(SHORTCUT, shortcut.signal.emit, args=[])
launcher.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
When I call the shortcut (Ctrl+Space here) and click elsewhere, next time I'll call the shortcut, the focus won't be set to the QLineEdit widget.
When the launcher is hidden by hitting return or escape, it does work as expected.

PySide application crashes when setting a new widget to QScrollArea

This is a simplification of an application I wrote.
The application's main window has a button and a checkbox.
The checkbox resides inside a QScrollArea (via a widget).
The checkbox has a number stating how many times that checkbox was created.
Clicking on the button will open a dialog with a refresh button.
Clicking on the refresh button will set a new widget to the scroll area with a new checkbox.
The checkbox has a context menu that opens the same dialog.
However, when the widget is created using the dialog triggered by the context menu of the checkbox the application crashes with the following error:
2016-08-03 09:22:00.036 Python[17690:408202] modalSession has been
exited prematurely - check for a reentrant call to endModalSession:
Python(17690,0x7fff76dcb300) malloc: * error for object
0x7fff5fbfe2c0: pointer being freed was not allocated
* set a breakpoint in malloc_error_break to debug
The crash doesn't happen when clicking on the button to open the dialog and clicking refresh from the dialog.
The crash happens on both Mac and Windows.
I am using Python 2.7.10 with PySide 1.2.4
#!/usr/bin/env python
import sys
from itertools import count
from PySide import QtCore, QtGui
class MainWindow(QtGui.QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
self.centralwidget = QtGui.QWidget(parent=self)
self.open_diag_btn = QtGui.QPushButton('Open dialog', parent=self)
self.open_diag_btn.clicked.connect(self.open_dialog)
self.scroll_widget = QtGui.QScrollArea(parent=self)
layout = QtGui.QGridLayout(self.centralwidget)
layout.addWidget(self.scroll_widget)
layout.addWidget(self.open_diag_btn)
self.setCentralWidget(self.centralwidget)
self.set_scroll_widget()
def open_dialog(self):
dialog = Dialog(parent=self)
dialog.refresh.connect(self.set_scroll_widget) # Connecting the signal from the dialog to set a new widget to the scroll area
dialog.exec_()
# Even if I call the function here, after the dialog was closed instead of using the signal above the application crashes, but only via the checkbox
# self.set_scroll_widget()
def set_scroll_widget(self):
"""Replacing the widget of the scroll area with a new one.
The checkbox in the layout of the widget has an class instance counter so you can see how many times the checkbox was created."""
widget = QtGui.QWidget()
layout = QtGui.QVBoxLayout(widget)
widget.setLayout(layout)
open_diag_check = RefreshCheckbox(parent=self)
open_diag_check.do_open_dialog.connect(self.open_dialog) # Connecting the signal to open the dialog window
layout.addWidget(open_diag_check)
self.scroll_widget.setWidget(widget)
class RefreshCheckbox(QtGui.QCheckBox):
"""A checkbox class that has a context menu item which emits a signal that eventually opens a dialog window"""
do_open_dialog = QtCore.Signal()
_instance_counter = count(1)
def __init__(self, *args, **kwargs):
super(RefreshCheckbox, self).__init__(unicode(self._instance_counter.next()), *args, **kwargs)
self.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
action = QtGui.QAction(self)
action.setText("Open dialog")
action.triggered.connect(self.emit_open_dialog)
self.addAction(action)
def emit_open_dialog(self):
self.do_open_dialog.emit()
class Dialog(QtGui.QDialog):
"""A dialog window with a button that emits a refresh signal when clicked.
This signal is used to call MainWindow.set_scroll_widget()"""
refresh = QtCore.Signal()
def __init__(self, *args, **kwargs):
super(Dialog, self).__init__(*args, **kwargs)
self.refresh_btn = QtGui.QPushButton('Refresh')
self.refresh_btn.clicked.connect(self.do_refresh)
layout = QtGui.QVBoxLayout(self)
layout.addWidget(self.refresh_btn)
def do_refresh(self):
self.refresh.emit()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
mySW = MainWindow()
mySW.show()
mySW.raise_()
app.exec_()
It looks like Python is trying to delete an object (or one of its child objects) which is no longer there - but quite what causes that to happen is not completely clear to me. The problematic object is the widget set on the scroll-area. If you excplicitly keep a reference to it:
def set_scroll_widget(self):
self._old_widget = self.scroll_widget.takeWidget()
...
your example will no longer dump core.
I think running the dialog with exec may somehow be the proximal cause of the problem, since this means it will run with its own event-loop (which might have an affect on the order of deletion-related events). I was able to find a better fix for your example by running the dialog with show:
def open_dialog(self):
dialog = Dialog(parent=self)
dialog.setAttribute(QtCore.Qt.WA_DeleteOnClose)
dialog.refresh.connect(self.set_scroll_widget)
dialog.setModal(True)
dialog.show()
Doing things this way means it's no longer necessary to keep an explicit reference to the previous scroll-area widget.

Query regarding Pyside

In the below mentioned example when I click on 'Help' submenu under 'View' menu multiple times its creating multiple windows. Can anyone tell me how to resolve this issue?
import sys
from PySide import Qt Gui
from PySide.QtCore import Qt
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.menu_bar()
def menu_bar(self):
helpAction = QtGui.QAction('&Help', self)
helpAction.setShortcut('Ctrl+H')
helpAction.triggered.connect(self.add_helpWindow)
menu = self.menuBar().addMenu('View')
menu.addAction(helpAction)
def add_helpWindow(self):
window = QtGui.QMainWindow(self)
window.setWindowTitle('New Window')
window.show()
if __name__ == '__main__':
import sys
app=QtGui.QApplication.instance()
if not app:
app = QtGui.QApplication(sys.argv)
window = Window()
window.resize(300, 300)
window.show()
sys.exit(app.exec_())
You help window is just a QMainWindow, which is not modal and there are no restrictions on the number that can exist. Hence why if you select the help option multiple times, you get multiple windows.
You likely want to use a QMessageBox which has its modal property set. While there is nothing forcing only one dialog to exist at a time, being modal means that the use can only interact with that window so long as it is open. Example:
from Pyside.QtGui import QMessageBox
def add_helpWindow(self):
help_dialog = QMessageBox.information(self, 'Help', 'Some Help Text Here')
help_dialog.setModal(True)
return help_dialog.exec_()
You can also get a more generic dialog box using QDialog, which is the parent class of QMessageBox.
If that's not the behavior you want, you'll need to manually track whether the user has opened that window before, and then connect a signal that is emitted when the user closes the help window to a slot that reset the existence tracker. Here is an example using a non-modal QDialog:
from Pyside.QtGui import QDialog
class Window(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.menu_bar()
self.help_open = False # Tracks if the help dialog is already open
def help_closed(self):
self.help_open = False
...
def add_helpWindow(self):
if not self.help_open:
self.help_open = True
help_dialog = QDialog(self)
# Any other setup code here
help_dialog.setModal(False)
help_dialog.accepted.connect(self.help_closed)
help_dialog.rejected.connect(self.help_closed)
help_dialog.show()

Why is my PyQt4 program exiting the main loop for no apparent reason?

I'm (trying to) make a small program that resides in the system tray and checks a list of Twitch channels to see if they're online every once in a while.
I'm currently doing the GUI (in PyQt4), but it's exiting for no reason.
Here's my code so far:
import sys
from PyQt4 import QtGui
from PyQt4 import QtCore
class TwitchWatchTray(QtGui.QSystemTrayIcon):
def __init__(self, icon, parent=None):
super(TwitchWatchTray, self).__init__(icon, parent)
self.menu = QtGui.QMenu(parent)
settings_action = self.menu.addAction("Settings")
settings_action.triggered.connect(self.open_settings)
self.menu.addSeparator()
exit_action = self.menu.addAction("Exit")
exit_action.triggered.connect(QtCore.QCoreApplication.instance().quit)
self.setContextMenu(self.menu)
self.show()
def open_settings(self):
settings = SettingsDialog()
settings.show()
class SettingsDialog(QtGui.QWidget):
def __init__(self):
super(SettingsDialog, self).__init__()
self.resize(300, 300)
self.setWindowTitle('TwitchWatch Settings')
vbox = QtGui.QHBoxLayout()
self.channels_list = QtGui.QListView(self)
vbox.addWidget(self.channels_list)
self.add_box = QtGui.QLineEdit(self)
vbox.addWidget(self.add_box)
self.setLayout(vbox)
self.show()
def main():
app = QtGui.QApplication(sys.argv)
widget = QtGui.QWidget()
tw = TwitchWatchTray(QtGui.QIcon("icon.png"), widget)
app.exec_()
print("Done!")
if __name__ == '__main__':
main()
When I right click the tray icon and click "Settings", it flashes a white box (my dialog), then immediately exits and prints "Done!".
Why is this, and how do I fix it?
There are two reasons why your code exits immediately after you open the settings dialog.
The first problem is with your open_settings method:
def open_settings(self):
settings = SettingsDialog()
settings.show()
This creates a dialog and makes it visible. show() returns immediately after showing the window; it doesn't wait for the window to be closed. The settings variable goes out of scope at the end of the method, and this causes the reference count of your SettingsDialog to drop to zero and hence become eligible for garbage collection. When Python deletes the SettingsDialog object, PyQt will delete the underlying C++ object, and this is what causes the dialog to close again.
I would recommend having your settings dialog subclass QDialog rather than QWidget (it is a dialog, after all). Instead of calling settings.show() you can then call settings.exec_(). settings.exec_() does wait for the dialog to be closed before it returns. It also returns QDialog.Accepted or QDialog.Rejected depending on whether the user clicked OK or Cancel. I'd also recommend getting rid of the call to self.show() in your SettingsDialog constructor.
The second problem is that your QApplication is set to quit when the last window is closed. This is the default behaviour, which is what a lot of applications need, but not yours. Even if your dialog stayed open and you could close it, you wouldn't want your application to exit immediately after you close the settings dialog. Call app.setQuitOnLastWindowClosed(False) to fix this.

Hide window from taskbar

I'm trying to minimize a window to the tray, but it seems it refuses to hide from the taskbar. I've spent a little time and distilled the problem code down to this. It's not mcuh so I'm wondering if I need something else to hide my app to tray in Windows 7.
import sys, os
from PyQt4 import uic
from PyQt4.QtGui import QMainWindow, QApplication
class MyClass(QMainWindow):
def __init__(self, parent = None):
QMainWindow.__init__(self, parent)
self.ui = uic.loadUi(os.path.join("gui", "timeTrackerClientGUI.ui"), self)
def hideEvent(self, event):
self.hide()
def showEvent(self, event):
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
wnd = MyClass()
wnd.show()
app.exec_()
It seems the application icon does hide but then another one pops up, If I click the taskbar icon multiple times I can get these two icons flickering, looks kind of like this for a splitsecond before the first one hides:
It took a quite ugly hack to get it working but here's the final code if anybody is interested, ph is my platform-specific module, you can use platform.name or similar function instead:
def hideEvent(self, event):
self.hide()
if ph.is_windows():
self.hidden = True
self.setWindowFlags(Qt.ToolTip)
def showEvent(self, event):
if ph.is_windows() and self.hidden:
self.setWindowFlags(Qt.Window)
self.hidden = False
self.show()
calling show/hide in showEvent()/hideEvent() doesn't make sense - those events are the result of show()/hide() calls (and the like), not the trigger. If you want to toggle the window visiblity by clicking the tray icon, try setVisible(!isVisible()) on the widget, if you want to hide the window when the user clicks the window close button try reimplementing closeEvent():
MyMainWindow::closeEvent( QCloseEvent* e ) {
hide();
e->accept();
}
In Python, that is
def closeEvent(self, event):
self.hide()
event.accept()

Categories

Resources