Python3 PyQt5 setEnabled of a QAction causing crashes - python

For a project I'm creating a GUI using Python 3 and PyQt5. Because it has to be usable by people outside of my immediate team, I want to disable actions on the menu until they've already filled out some forms in other parts of the program (e.g. disabling the final solution view when they haven't set up the initial data connection). The issue is that when I try to call the QAction's setEnabled function outside of the function that created it (but still inside the overall class), it's causing my script to crash with no error code, so I'm having trouble understanding the issue. In the snipit below, I'm trying to set the "View Solution" menu option as true. There are some more options in that menu, but I deleted them here to make it more easy to read.
The code is structured something like this:
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication, QMessageBox, QStackedLayout
class MediaPlanner(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
# Menu bar example from: zetcode.com/gui/pyqt5/
exitAction = QAction('&Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newProject = QAction('&New Project', self)
newProject.setShortcut('Ctrl+N')
newProject.setStatusTip('Start A New Project')
newProject.triggered.connect(self.createNewProject)
openProject = QAction('&Open Project',self)
openProject.setShortcut('Ctrl+O')
openProject.setStatusTip('Open A Saved Project')
openProject.setEnabled(False)
viewSolution = QAction('&View Solution',self)
viewSolution.setStatusTip('View the Current Solution (If Built)')
viewSolution.setEnabled(False)
self.statusBar()
menubar = self.menuBar()
filemenu = menubar.addMenu('&File')
filemenu.addAction(newProject)
filemenu.addAction(openProject)
filemenu.addAction(exitAction)
viewmenu = menubar.addMenu('&View')
viewmenu.addAction(viewSolution)
self.setGeometry(300,300,700,300)
self.setWindowTitle('Menubar')
self.show()
def createNewProject(self):
print('Project Created')
self.viewSolution.setEnabled(True)
if __name__ == '__main__':
app = QApplication(sys.argv)
gui = MediaPlanner()
sys.exit(app.exec_())

The problem is that viewSolution is a variable, but it is not a member of the class so you will not be able to access it through the self instance. One possible solution is to make viewSolution member of the class as shown below:
self.viewSolution = QAction('&View Solution',self)
self.viewSolution.setStatusTip('View the Current Solution (If Built)')
self.viewSolution.setEnabled(False)
...
viewmenu.addAction(self.viewSolution)
Another possible solution is to use the sender() function, this function returns the object that emits the signal, using the following:
def createNewProject(self):
print('Project Created')
self.sender().setEnabled(True)

Related

Python PyQt5 how to show the full QMenuBar with a QWidget

I'm getting this weird result when using QMenuBar I've used this exact code before for the QMenuBar and it worked perfectly. But it doesn't show more than 1 QMenu
This is my code:
from PyQt5.QtWidgets import *
from PyQt5.QtCore import *
from PyQt5.QtGui import *
import sys
from functools import partial
class MainMenu(QWidget):
def __init__(self, parent = None):
super(MainMenu, self).__init__(parent)
# background = QWidget(self)
lay = QVBoxLayout(self)
lay.setContentsMargins(5, 35, 5, 5)
self.menu()
self.setWindowTitle('Control Panel')
self.setWindowIcon(self.style().standardIcon(getattr(QStyle, 'SP_DialogNoButton')))
self.grid = QGridLayout()
lay.addLayout(self.grid)
self.setLayout(lay)
self.setMinimumSize(400, 320)
def menu(self):
menubar = QMenuBar(self)
viewMenu = menubar.addMenu('View')
viewStatAct = QAction('Dark mode', self, checkable=True)
viewStatAct.setStatusTip('enable/disable Dark mode')
viewMenu.addAction(viewStatAct)
settingsMenu = menubar.addMenu('Configuration')
email = QAction('Set Email', self)
settingsMenu.addAction(email)
if __name__ == '__main__':
app = QApplication(sys.argv)
main = MainMenu()
main.show()
sys.exit(app.exec_())
Result:
I am aware that I am using QWidget when I should be using QMainWindow But is there a workaround???
(I apologize in advance for the terrible quality of the image, there is no good way to take a picture of a QMenuBar)
The problem is that with a QWidget you are not using the "private" layout that a QMainWindow has, which automatically resizes specific children widgets (including the menubar, the statusbar, the dock widgets, the toolbars and, obviously, the "centralWidget").
Remember that a QMainWindow has its own layout (which can't and shouldn't be changed), because it needs that specific custom layout to lay out the aforementioned widgets. If you want to set a layout for the main window, you'll need to apply it to its centralWidget.
Read carefully how the Main Window Framework behaves; as the documentation reports:
Note: Creating a main window without a central widget is not supported. You must have a central widget even if it is just a placeholder.
In order to work around that when using a basic QWidget, you'll have to manually resize the children widgets accordingly. In your case, you only need to resize the menubar, as long as you have a reference to it:
def menu(self):
self.menubar = QMenuBar(self)
# any other function has to be run against the *self.menubar* object
viewMenu = self.menubar.addMenu('View')
# etcetera...
def resizeEvent(self, event):
# calling the base class resizeEvent function is not usually
# required, but it is for certain widgets (especially item views
# or scroll areas), so just call it anyway, just to be sure, as
# it's a good habit to do that for most widget classes
super(MainMenu, self).resizeEvent(event)
# now that we have a direct reference to the menubar widget, we are
# also able to resize it, allowing all actions to be shown (as long
# as they are within the provided size
self.menubar.resize(self.width(), self.menubar.height())
Note: you can also "find" the menubar by means of self.findChild(QtWidgets.QMenuBar) or using the objectName, but using an instance attribute is usually an easier and better solution.
Set minimum width
self.setMinimumSize(320,240)

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)

Missing menuBar in PyQt5

I've been developing a GUI using PyQt5 and wanted to include a menu bar. When I went to code this feature, however, my menu wouldn't appear. Figuring my understanding on how to implement menu bars in PyQt5 was off, I looked for a pre-existing example online. With some tweaking I developed the following test case:
import sys
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QMainWindow, QMenuBar, QAction, qApp
class Example(QMainWindow):
def __init__(self):
super().__init__()
exitAction = QAction(QIcon('exit.png'), '&Exit', self)
exitAction.triggered.connect(qApp.quit)
menubar = self.menuBar()
fileMenu = menubar.addMenu('&Testmenu')
fileMenu.addAction(exitAction)
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Example()
sys.exit(app.exec_())
When I run this, however, Testmenu is nowhere to be found.
I have also tried creating the menu bar (and the rest of my GUI layout) in QTCreator before converting the .ui file to an importable .py using pyuic5. I thought this would eliminate some programming mistake on my end, but the menubar still won't show. Any thoughts?
Edit:
Im running this code using Python 3.5 (Anaconda 4.1) from within a Jupyter notebook, version 4.1. I'm also using a Macbook running os 10.1l, PyQt 5.7 and Qt version 5.7.0.
I've realized that the menu bar will become responsive if I click off the application window and then click back onto the window - effectively unfocusing and the focusing the application. Armed with this information I realized that I am not the first to notice this problem (see https://github.com/robotology/yarp/issues/457). Unfortunately, I'm still not sure how to resolve the issue.
It's not a Qt and PyQt5 Bug.
I think your code is zetcode pyqt5 menubar tutorial. I experienced the exact same problem on Mac OS.
First solution is a trick. Use ' &Exit' instead of '&Exit'. Insert a space at the beginning of '&Exit' like this:
...
# exitAction = QAction(QIcon('exit.png'), '&Exit', self) # Not shown
exitAction = QAction(QIcon('exit.png'), ' &Exit', self)
...
The system-wide menubar of macOS reserves keywords such as "Exit", "Quit", and etc. For the same reason, yurisnm's example code shows only the menu items except "Quit" on Mac OS. Actually "Quit" has TextHeuristicRole, so overrides "Quit " behavior in the Application menu. When you click "Quit python" in "Python" menu, it does not quit and just print "quit triggered".
If you must use that name in other menu(e.g. File, Edit), you need to change the action name like above or use QAction::setMenuRole(...) like this:
...
exitAction = QAction(QIcon('exit.png'), '&Exit', self)
print(exitAction.menuRole()) # It prints "1". QAction::TextHeuristicRole
exitAction.setMenuRole(QAction.NoRole)
...
Please read the following, it will be help you.
https://stackoverflow.com/a/11387977/5362866
http://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
Menubar isn't visible in PyQt5
bar = self.menuBar()
bar.setNativeMenuBar(False)
file = bar.addMenu("File")
file.addAction("New")
NativeMenuBar property specifies whether or not the menubar should be used as a native menubar on platforms that support it. If this property is true, the menubar is used in the native menubar and is not in the window of its parent, if false the menubar remains in the window.
Sample program
import sys
from PyQt5.QtWidgets import QMainWindow, QAction, qApp, QApplication
from PyQt5.QtGui import QIcon
class Menu(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
def initUI(self):
exitAct = QAction(QIcon('exit.png'), ' &Quit', self)
exitAct.setShortcut('Ctrl+Q')
exitAct.setStatusTip('Exit application')
exitAct.triggered.connect(qApp.quit)
self.statusBar()
menubar = self.menuBar()
menubar.setNativeMenuBar(False)
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(exitAct)
bar = self.menuBar()
file = bar.addMenu("Edit")
file.addAction("New")
self.setGeometry(300, 300, 300, 200)
self.setWindowTitle('Simple menu')
self.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = Menu()
sys.exit(app.exec_())
If your program is running on Ubuntu, you may find your menu bar on the top of the screen.
If you want to move menu bar to window's title bar, you can toggle the setting at "System setting / Appearance / Behavior / Show the menus for a window / In the window's title bar".
try this:
menubar = QMenuBar()
self.setMenuBar(menubar)
instead of menubar = self.menuBar()
Have you tried the most simple example in the following link tutorialspoint.
Here is the most simple example.
import sys
from PyQt5.QtWidgets import QHBoxLayout, QAction, QApplication, QMainWindow
class menudemo(QMainWindow):
def __init__(self, parent = None):
super(menudemo, self).__init__(parent)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
save = QAction("Save",self)
save.setShortcut("Ctrl+S")
file.addAction(save)
edit = file.addMenu("Edit")
edit.addAction("copy")
edit.addAction("paste")
quit = QAction("Quit",self)
file.addAction(quit)
file.triggered[QAction].connect(self.processtrigger)
self.setWindowTitle("menu demo")
def processtrigger(self, q):
print(q.text()+" is triggered")
def main():
app = QApplication(sys.argv)
ex = menudemo()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
Under MacOS, an application menu appears at the top of the screen. I managed to get the Quit menu option to appear in the above example by prepending the string "Quit" with a null character as follows:
close = QAction("\0Quit",self)
close.setShortcut("Ctrl+Q")
file.addAction(close)
It seems macOS somehow intercepts the menu items called "Quit" or "Exit".
You can happily use "Close" or "Leave" without the workaround.

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()

Using signal to pop-up a screen

I'm having a little trouble using a signal to make a little screen appear.
Shortening all i have so far, this following code should show my problem.
import sys
from PyQt4 import QtGui, QtCore
qApp = QtGui.QApplication(sys.argv)
class InformatieVenster(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('Informatie')
self.setGeometry(100,100,300,200)
informatie = InformatieVenster()
class MenuKlasse(QtGui.QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
about = QtGui.QAction('About...', self)
about.setShortcut('Ctrl+A')
about.setStatusTip('Some text, haha')
self.connect(about, QtCore.SIGNAL('clicked()'), QtCore.SIGNAL(informatie.show()))
menubar = self.menuBar()
self.Menu1 = menubar.addMenu('&File')
self.Menu1.addAction(about)
Menu = MenuKlasse()
Venster = QtGui.QMainWindow()
Venster.menuBar().addMenu(Menu.Menu1)
Venster.setGeometry(200, 200, 300, 300);
size = Venster.geometry()
Venster.show()
qApp.exec_()
When this program is runned, the 'informatie' window automatically pops-up.
However... i only want this to happen every time I click on 'about...' in the menu, or when i use the assigned shortcut.
How may i improve my code such that my problem will be made history?
Greets!
The window is shown, because you are actually calling .show() during your connect. You have to pass a function object, not the result of a function invocation, as argument to .connect(). Moreover the function to be invoked, if a signal is emitted, is called "slot", the second SIGNAL() is completely misplaced.
Replace the connect line with:
self.connect(about, QtCore.SIGNAL('triggered()') informatie.show)
Even better, use the modern connection syntax:
about.triggered.connect(informatie.show)
Btw, do not use absolute sizes in GUI programs. Instead use layout management.

Categories

Resources