I have a simple script primarily based on QSystemTrayIcon. Everything works find, and there's an option there on right-click on the taskbar icon that exits the program. I would like to add a QMessageBox, and on choosing yes, exit the program; otherwise, do nothing.
I'm familiar with all that, but it doesn't work as it should, and hence the question. I created a minimal example to demonstrate the problem:
import sys
from PyQt5 import QtCore, QtGui, QtWidgets
class SystemTrayIcon(QtWidgets.QSystemTrayIcon):
def __init__(self, icon, parent=None):
QtWidgets.QSystemTrayIcon.__init__(self, icon, parent)
self.menu = QtWidgets.QMenu(parent)
self.exit_action = self.menu.addAction("Exit")
self.setContextMenu(self.menu)
self.exit_action.triggered.connect(self.slot_exit)
self.msg_parent = QtWidgets.QWidget()
def slot_exit(self):
reply = QtWidgets.QMessageBox.question(self.msg_parent, "Confirm exit",
"Are you sure you want to exit Persistent Launcher?",
QtWidgets.QMessageBox.Yes, QtWidgets.QMessageBox.No)
# if reply == QtWidgets.QMessageBox.Yes:
# QtCore.QCoreApplication.exit(0)
def main():
app = QtWidgets.QApplication(sys.argv)
tray_icon = SystemTrayIcon(QtGui.QIcon("TheIcon.png"))
tray_icon.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Now you see, at the slot_exit() function, whether I choose yes or no, the program exits (with code 0, no errors). The commented part is what I expect to be used to determine the action based on the choice. Could you please help me figure out why this behavior is happening and what's the right way to exit only on "yes"?
I'm using Windows 10, 64-bit with Python Anaconda 3.5.2 32-bit, and PyQt 5.7.
The problem is that Qt is made to exit when all Windows are closed. Simply disable that with:
app.setQuitOnLastWindowClosed(False)
in your main().
Related
I'm embedding another window into a Qt widget using PySide2.QtGui.QWindow.fromWinId(windowId). It works well, but it does not fire an event when the the original X11 window destroys it.
If I run the file below with mousepad & python3 embed.py and press Ctrl+Q, no event fires and I'm left with an empty widget.
How can I detect when the X11 window imported by QWindow.fromWinId is destroyed by its creator?
#!/usr/bin/env python
# sudo apt install python3-pip
# pip3 install PySide2
import sys, subprocess, PySide2
from PySide2 import QtGui, QtWidgets, QtCore
class MyApp(QtCore.QObject):
def __init__(self):
super(MyApp, self).__init__()
# Get some external window's windowID
print("Click on a window to embed it")
windowIdStr = subprocess.check_output(['sh', '-c', """xwininfo -int | sed -ne 's/^.*Window id: \\([0-9]\\+\\).*$/\\1/p'"""]).decode('utf-8')
windowId = int(windowIdStr)
print("Embedding window with windowId=" + repr(windowId))
# Create a simple window frame
self.app = QtWidgets.QApplication(sys.argv)
self.mainWindow = QtWidgets.QMainWindow()
self.mainWindow.show()
# Grab the external window and put it inside our window frame
self.externalWindow = QtGui.QWindow.fromWinId(windowId)
self.externalWindow.setFlags(QtGui.Qt.FramelessWindowHint)
self.container = QtWidgets.QWidget.createWindowContainer(self.externalWindow)
self.mainWindow.setCentralWidget(self.container)
# Install event filters on all Qt objects
self.externalWindow.installEventFilter(self)
self.container.installEventFilter(self)
self.mainWindow.installEventFilter(self)
self.app.installEventFilter(self)
self.app.exec_()
def eventFilter(self, obj, event):
# Lots of events fire, but no the Close one
print(str(event.type()))
if event.type() == QtCore.QEvent.Close:
mainWindow.close()
return False
prevent_garbage_collection = MyApp()
Below is a simple demo script that shows how to detect when an embedded external window closes. The script is only intended to work on Linux/X11. To run it, you must have wmctrl installed. The solution itself doesn't rely on wmctrl at all: it's merely used to get the window ID from the process ID; I only used it in my demo script because its output is very easy to parse.
The actual solution relies on QProcess. This is used to start the external program, and its finished signal then notifies the main window that the program has closed. The intention is that this mechanism should replace your current approach of using subprocess and polling. The main limitation of both these approaches is they will not work with programs that run themselves as background tasks. However, I tested my script with a number applications on my Arch Linux system - including Inkscape, GIMP, GPicView, SciTE, Konsole and SMPlayer - and they all behaved as expected (i.e. they closed the container window when exiting).
NB: for the demo script to work properly, it may be necessary to disable splash-screens and such like in some programs so they can embed themselves correctly. For example, GIMP must be run like this:
$ python demo_script.py gimp -s
If the script complains that it can't find the program ID, that probably means the program launched itself as a background task, so you will have to try to find some way to force it into the foreground.
Disclaimer: The above solution may work on other platforms, but I have not tested it there, and so cannot offer any guarantees. I also cannot guarantee that it will work with all programs on Linux/X11.
I should also point out that embedding external, third-party windows is not officially supported by Qt. The createWindowContainer function is only intended to work with Qt window IDs, so the behaviour with foreign window IDs is strictly undefined (see: QTBUG-44404). The various issues are documentented in this wiki article: Qt and foreign windows. In particular, it states:
A larger issue with our current APIs, that hasn't been discussed yet,
is the fact that QWindow::fromWinId() returns a QWindow pointer, which
from an API contract point of view should support any operation that
any other QWindow supports, including using setters to manipulate the
window, and connecting to signals to observe changes to the window.
This contract is not adhered to in practice by any of our platforms,
and the documentation for QWindow::fromWinId() doesn't mention
anything about the situation.
The reasons for this undefined/platform specific behaviour largely
boils down to our platforms relying on having full control of the
native window handle, and the native window handle often being a
subclass of the native window handle type, where we implement
callbacks and other logic. When replacing the native window handle
with an instance we don't control, and which doesn't implement our
callback logic, the behaviour becomes undefined and full of holes
compared to a regular QWindow.
So, please bear all that in mind when designing an application that relies on this functionality, and adjust your expectations accordingly...
The Demo script:
import sys, os, shutil
from PySide2.QtCore import (
Qt, QProcess, QTimer,
)
from PySide2.QtGui import (
QWindow,
)
from PySide2.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QMessageBox,
)
class Window(QWidget):
def __init__(self, program, arguments):
super().__init__()
layout = QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
self.setLayout(layout)
self.external = QProcess(self)
self.external.start(program, arguments)
self.wmctrl = QProcess()
self.wmctrl.setProgram('wmctrl')
self.wmctrl.setArguments(['-lpx'])
self.wmctrl.readyReadStandardOutput.connect(self.handleReadStdOut)
self.timer = QTimer(self)
self.timer.setSingleShot(True)
self.timer.setInterval(25)
self.timer.timeout.connect(self.wmctrl.start)
self.timer.start()
self._tries = 0
def closeEvent(self, event):
for process in self.external, self.wmctrl:
process.terminate()
process.waitForFinished(1000)
def embedWindow(self, wid):
window = QWindow.fromWinId(wid)
widget = QWidget.createWindowContainer(
window, self, Qt.FramelessWindowHint)
self.layout().addWidget(widget)
def handleReadStdOut(self):
pid = self.external.processId()
if pid > 0:
windows = {}
for line in bytes(self.wmctrl.readAll()).decode().splitlines():
columns = line.split(maxsplit=5)
# print(columns)
# wid, desktop, pid, wmclass, client, title
windows[int(columns[2])] = int(columns[0], 16)
if pid in windows:
self.embedWindow(windows[pid])
# this is where the magic happens...
self.external.finished.connect(self.close)
elif self._tries < 100:
self._tries += 1
self.timer.start()
else:
QMessageBox.warning(self, 'Error',
'Could not find WID for PID: %s' % pid)
else:
QMessageBox.warning(self, 'Error',
'Could not find PID for: %r' % self.external.program())
if __name__ == '__main__':
if len(sys.argv) > 1:
if shutil.which(sys.argv[1]):
app = QApplication(sys.argv)
window = Window(sys.argv[1], sys.argv[2:])
window.setGeometry(100, 100, 800, 600)
window.show()
sys.exit(app.exec_())
else:
print('could not find program: %r' % sys.argv[1])
else:
print('usage: python %s <external-program-name> [args]' %
os.path.basename(__file__))
I wrote a GUI in pyqt5 where you can enter two paths (file paths, directory paths, ...). Now my problem is the following:
(1) when I run it with the Anaconda Prompt window, and enter any longer path then it does the word wrap how I want it (and afai can tell correctly):
in this case using setTextElideMode works as well.
(2) when I run it with the windows command prompt (C:\[...]\Desktop>C:\[...]\python\3.8.1.0.0\python-3.8.1.amd64\python.exe C:\[...]\Desktop\cmd_problems.py) it begins wrapping the text directly after "C:" by inserting the normal ellipsis "..." - I have to stretch the column (manually) until the whole path is visible to make it show more than that:
using setTextElideMode does nothing.
Does anyone know how to get the first behavior when running the code from the windows cmd line? (I need this because I use a batch script to launch the program similar to making an .exe file.)
Here is my code:
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QWidget
from PyQt5.QtWidgets import QTableWidget, QVBoxLayout
class MyWindow(QMainWindow):
def __init__(self, parent=None):
super().__init__(parent)
self.setWindowTitle("Very Important Window")
cen = QWidget()
self.layout = QVBoxLayout()
self.setCentralWidget(cen)
cen.setLayout(self.layout)
self.tbl = QTableWidget()
self.tbl.setRowCount(1)
self.tbl.setColumnCount(2)
self.tbl.setTextElideMode(Qt.ElideRight)
col_names = ["FROM", "TO"]
self.tbl.setHorizontalHeaderLabels(col_names)
self.layout.addWidget(self.tbl)
def main():
app = QApplication([])
win = MyWindow()
win.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
I'm trying to create an application that contains a web browser within it, but when I add the web browser my menu bar visually disappears but functionally remains in place. The following are two images, one showing the "self.centralWidget(self.web_widget)" commented out, and the other allows that line to run. If you run the example code, you will also see that while visually the entire web page appears as if the menu bar wasn't present, you have to click slightly below each entry field and button in order to activate it, behaving as if the menu bar was in fact present.
Web Widget Commented Out
Web Widget Active
Example Code
import os
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtWebEngineWidgets import *
class WebPage(QWebEngineView):
def __init__(self, parent=None):
QWebEngineView.__init__(self)
self.current_url = ''
self.load(QUrl("https://facebook.com"))
self.loadFinished.connect(self._on_load_finished)
def _on_load_finished(self):
print("Url Loaded")
class MainWindow(QMainWindow):
def __init__(self, parent=None):
# Initialize the Main Window
super(MainWindow, self).__init__(parent)
self.create_menu()
self.add_web_widet()
self.show()
def create_menu(self):
''' Creates the Main Menu '''
self.main_menu = self.menuBar()
self.main_menu_actions = {}
self.file_menu = self.main_menu.addMenu("Example File Menu")
self.file_menu.addAction(QAction("Testing Testing", self))
def add_web_widet(self):
self.web_widget = WebPage(self)
self.setCentralWidget(self.web_widget)
if __name__ == "__main__":
app = QApplication(sys.argv)
main_window = MainWindow()
main_window.showMaximized()
sys.exit(app.exec_()) # only need one app, one running event loop
Development Environment
Windows 10, PyQt5, pyqt5-5.9
EDIT
The problem doesn't seem to be directly related to the menu bar. Even removing the menu bar the issue still occurs. That said, changing from showMaximized() to showFullScreen() does seem to solve the problem.
I no longer believe this is an issue with PyQt5 specifically but rather a problem with the graphics driver. Specifically, if you look at Atlassian's HipChat application it has a similar problem which is documented here:
https://jira.atlassian.com/browse/HCPUB-3177
Some individuals were able to solve the problem by running the application from the command prompt with the addendum "--disable-gpu" but that didn't work for my python application. On the other hand, rolling back the Intel(R) HD Graphics Driver did solve my problem. Version 21.20.16.4627 is the one that seems to be causing problems.
I'm a newbie to Python and PyQt. I've tried to manage closeEvent to ask before to close mainwindow, but this work well only from 'X' button. From the QMEssageBox created to ask the user, callEvent() it's called two times.
This is the relevant part of the code :
self.ui.actionChiudi.triggered.connect(self.close)
def closeEvent(self, event):
#check presence of data in the table
if self.csvViewer.rowCount() > 0:
print event # only to analyze the caller
#show a warning
Error = QtGui.QMessageBox()
Error.setIcon(QtGui.QMessageBox.Question)
Error.setWindowTitle('ATTENZIONE !!')
Error.setInformativeText(u"Sei sicuro di voler uscire?")
Error.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
ret = Error.exec_()
if ret == QtGui.QMessageBox.Ok:
event.accept()
else:
event.ignore()
else:
#close directly
event.accept()
'actionChiudi' is the menù item in the main menù.
For what I can understand, when use 'X' button, the function close() it's called only one time directly from mainwindow object, then close my app.
When use the menù item, the function create the new object 'QMessageBox' then call 'closeEvent()' one time for this object, then recall the same function for the mainwindow object. If this is correct, I don't know how to manage this.
Thanks in advance for any help!
thankyou for your tips about howto construct a good example of code. Sorry but I'm new there, then I did not succeed in making a good example. But, in the meantime, I was thinking about the problem and, at the end, I've solved this with a little trick.
I've added a global variable that count if you are closing the main window, then at the other calls to closeEvent() avoid to recall the test procedure.
I've tried to create a class for the generation of QMessageBox external to main class, then to override closeEvent() of this object, but don't work. The only way that I've found is with global variable. The program continue to call two times closeEvent (one for the QMessageBox and one for the mainwindow) but now, the second time ignore to recall the test. It's a trick, it's not elegant, but it works for me.
The piece of code now is this :
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import sys
import os
import datetime
import inspect
import csv
import codecs
import pymssql
from PyQt4 import QtGui, QtCore, Qt
import mainwindow
#import _winreg only on Microsoft platforms
import platform
winos = True
if os.name == 'nt' and platform.system() == 'Windows':
import _winreg
#other stuff needed under Windows
import _mssql
import decimal
import uuid
winos = True
else:
winos = False
#other global variables
liccode = False
closemain = False
class MainWindow(QtGui.QMainWindow):
def __init__(self, parent=None):
QtGui.QDialog.__init__(self, parent)
super(MainWindow, self).__init__(parent)
self.ui = mainwindow.Ui_MainWindow()
self.ui.setupUi(self)
# some stuff and widgets to visualize in the main window
#connettors to menù functions; leave only the closing one
self.ui.actionChiudi.triggered.connect(self.close)
#override of closeEvent - there I think, it's better to work to separate
#closeEvent for mainwindow from general closeEvent. But how?
def closeEvent(self, event):
global closemain
#make evaluation test to decide how to work. Close directly or no?
if self.csvViewer.rowCount() > 0 and not closemain:
print event
#show a warning
Error = QtGui.QMessageBox()
Error.setIcon(QtGui.QMessageBox.Question)
Error.setWindowTitle('WARNING !!')
Error.setInformativeText(u"Are you sure to leave?")
Error.setStandardButtons(QtGui.QMessageBox.Ok | QtGui.QMessageBox.Cancel)
ret = Error.exec_()
if ret == QtGui.QMessageBox.Ok:
closemain = True
event.accept()
else:
event.ignore()
else:
#close directly
event.accept()
#start application and create main
app = QtGui.QApplication(sys.argv)
my_mainWindow = MainWindow()
my_mainWindow.show()
sys.exit(app.exec_())
In any case, any advice is well accepted to make a better code. Thank you all!
I'm building a GUI with PyQt, and I'd like to make it possible to stop all the buttons doing anything while code is running. Lets say the user is copying a lot of data from a table - it would be easy for them to click another button while it is happening even if the cursor has changed to the egg timer. Any ideas or workarounds for this without going through all buttons and greying them out one by one? I'd be happy with a workaround too!
Thanks for any ideas,
Pete
You could use a modal QDialog. From the QDialog pyqt documentation:
A modal dialog is a dialog that blocks input
to other visible windows in the same application.
Also, QProgressDialog is a very convenient tool to handle blocking action in a very simple way. Here is an example :
from PyQt4 import QtGui, QtCore
from time import sleep
class Test(QtGui.QDialog):
def __init__(self, parent=None):
super(Test, self).__init__(parent)
button = QtGui.QPushButton("Button")
hbox = QtGui.QHBoxLayout()
hbox.addWidget(button)
self.setLayout(hbox)
button.clicked.connect(self.slot)
def slot(self):
progress = QtGui.QProgressDialog(self)
progress.setWindowModality(QtCore.Qt.WindowModal)
progress.setLabel(QtGui.QLabel("Doing things..."))
progress.setAutoClose(True)
for i in range(101):
progress.setValue(i);
sleep(0.05)
if progress.wasCanceled():
break
if __name__=="__main__":
import sys
app = QtGui.QApplication(sys.argv)
myapp = Test()
myapp.show()
sys.exit(app.exec_())