Why is QMessageBox.exec() not blocking my thread - python

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.

Related

Getting closeEvent when exiting the application

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.

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

In PyQt, how does one get a shared menu and toolbar to talk to the currently active subwindow?

I have an application which has a main window, which can have multiple subwindows. I would like to have one set of QActions in the main window that interact with the currently selected window. For example, the application might be a text editor, and clicking file->save should save the text file the user is currently working on. Additionally, some QActions are checkable, so their checked state should reflect the state of the currently active window.
Here is a minimum working example that has the basic functionality I want, but I suspect there is a better way to do it (further discussion below the code).
import sys
import PyQt4.QtGui as QtGui
class DisplayWindow(QtGui.QWidget):
def __init__(self, parent=None, name="Main Window"):
# run the initializer of the class inherited from
super(DisplayWindow, self).__init__()
self.myLayout = QtGui.QFormLayout()
self.FooLabel = QtGui.QLabel(self)
self.FooLabel.setText(name)
self.myLayout.addWidget(self.FooLabel)
self.setLayout(self.myLayout)
self.is_foo = False
def toggle_foo(self):
self.is_foo = not self.is_foo
if self.is_foo:
self.FooLabel.setText('foo')
else:
self.FooLabel.setText('bar')
class WindowActionMain(QtGui.QMainWindow):
def __init__(self):
super(WindowActionMain, self).__init__()
self.fooAction = QtGui.QAction('Foo', self)
self.fooAction.triggered.connect(self.set_foo)
self.fooAction.setCheckable(True)
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(self.fooAction)
self.toolbar = self.addToolBar('File')
self.toolbar.addAction(self.fooAction)
self.centralZone = QtGui.QMdiArea()
self.centralZone.subWindowActivated.connect(
self.update_current_window)
self.setCentralWidget(self.centralZone)
self.create_dw("Window 1")
self.create_dw("Window 2")
def create_dw(self, name):
dw = DisplayWindow(name=name)
self.centralZone.addSubWindow(dw)
dw.show()
def update_current_window(self):
""" redirect future actions to affect the newly selected window,
and update checked statuses to reflect state of selected window"""
current_window = self.centralZone.activeSubWindow()
if current_window:
self.current_dw = self.centralZone.activeSubWindow().widget()
self.fooAction.setChecked(self.current_dw.is_foo)
def set_foo(self):
self.current_dw.toggle_foo()
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
ex = WindowActionMain()
ex.show()
sys.exit(app.exec_())
My actual version of DisplayWindow could be useful in many different projects, and I want to package it up so that you don't have to add a lot of code to the main window to use it. Therefore, DisplayWindow, all of its functionality and a list of available actions should be in one module, which would be imported in WindowActionMain's module. I should then be able to add more actions for DisplayWindow without changing any code in WindowActionMain. In particular, I don't want to have to write a little function like WindowActionMain.set_foo(self) just to redirect each action to the right place.
Yes, this is possible by handling the QMenu's aboutToShow signal
and considering the QGuiApplication's focusWindow (or however you get that in Qt4).
Example below shows a generic 'Window' menu acting on the frontmost window.
http://doc.qt.io/qt-4.8/qmenu.html#aboutToShow
http://doc.qt.io/qt-5/qguiapplication.html#focusWindow
def on_windowMenu_aboutToShow(self):
self.windowMenu.clear()
self.newWindowAction = QtWidgets.QAction(self)
self.newWindowAction.setShortcut("Ctrl+n")
self.newWindowAction.triggered.connect(self.on_newWindowAction)
self.newWindowAction.setText("New Window")
self.windowMenu.addAction(self.newWindowAction)
self.windowMenu.addSeparator()
playerWindows = [w for w in self.topLevelWindows() if w.type()==QtCore.Qt.Window and w.isVisible()]
for i, w in enumerate(playerWindows):
def action(i,w):
a = QtWidgets.QAction(self)
a.setText("Show Window {num} - {title}".format(num=i+1, title=w.title()))
a.triggered.connect(lambda : w.requestActivate())
a.triggered.connect(lambda : w.raise_())
self.windowMenu.addAction(a)
action(i,w)
self.windowMenu.addSeparator()
self.closeWindowAction = QtWidgets.QAction(self)
self.closeWindowAction.setShortcut("Ctrl+w")
self.closeWindowAction.triggered.connect(lambda : self.focusWindow().close())
self.closeWindowAction.setText("Close")
self.windowMenu.addAction(self.closeWindowAction)

PyQt - open only one child window and minimize it with parent window

The idea is to open a child window from parent window menu and when I minimize the parent window, the child window must be minimized also and only one child window can be opened.
I have the solution for minimizing the child when parent is minimized, but I can open child window multiple-times (although the child is already opened) and I would like to disable opening of multiple child windows.
The parent window is MainWindow.py:
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle('Parent window')
self.flags = QtCore.Qt.Window
self.ControlPanel = Control_panel_window()
self.createActions()
self.createMenus()
def createActions(self):
# window - menu
self.windowShowControlPanelAction = QtGui.QAction(self.tr("&Control panel"), self, statusTip='Control panel')
self.connect(self.windowShowControlPanelAction, QtCore.SIGNAL("triggered()"), self.ShowControlPanel)
def createMenus(self):
# window
self.windowMenu = QtGui.QMenu(self.tr("&Window"), self)
self.windowMenu.addAction(self.windowShowControlPanelAction)
self.menuBar().addMenu(self.windowMenu)
def ShowControlPanel(self):
self.ControlPanel = Control_panel_window(self)
self.ControlPanel.setWindowFlags(QtCore.Qt.Window)
self.ControlPanel.show()
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = MainWindow()
win.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
win.show()
sys.exit(app.exec_())
The child window is ChildWindow.py:
class Control_panel_window(QWidget):
def __init__(self, parent = None):
super(Control_panel_window, self).__init__(parent)
self.setFixedSize(200, 300)
def setWindowFlags(self, flags):
print "flags value in setWindowFlags"
print flags
super(Control_panel_window, self).setWindowFlags(flags)
The problem is: how can I set that only one child window is opened?
In your ShowControlPanel function you are creating a new control panel each time the signal is triggered. Since you already have an instance available why don't you use that instead?
class MainWindow(QtGui.QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowTitle('Parent window')
self.flags = QtCore.Qt.Window
self.control_panel = ControlPanelWindow(self)
self.control_panel.setWindowFlags(self.flags)
#...
def create_actions(self):
self.show_control_panel_action = QtGui.QAction(
self.tr("&Control panel"),
self,
statusTip='Control panel'
)
self.show_control_panel_action.triggered.connect(self.show_control_panel)
#...
def show_control_panel(self):
self.control_panel.show()
Some stylistic notes:
Try to follow PEP8 official python coding-style guide. This include using CamelCase for classes, lowercase_with_underscore for almost everything else. In this case, since Qt uses halfCamelCase for methods etc you may use it too for consistency.
Use the new-style signal syntax:
the_object.signal_name.connect(function)
instead of:
self.connect(the_object, QtCore.SIGNAL('signal_name'), function)
not only it reads nicer, but it also provides better debugging information. Using QtCore.SIGNAL you will not receive an error if the signal doesn't exist (e.g. you wrote a typo like trigered() instead of triggered()). The new-style syntax does raise an exception in that case you will be able to correct the mistake earlier, without having to guess why something is not working right and searching the whole codebase for the typo.

PyQt monkey patching QLineEdit.paste?

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.

Categories

Resources