I have a window that contains two main widgets, both of which are containers of other widgets, which in turn may contain further widgets.
Each Widget is a separate class ( to maintain code readability ), and child widgets are instantiated directly inside this class, which is similar to something like React components structure.
I would like to call some methods from any widget to a top level one, or send signals from a deeply nested widget to one near top level without having to use something akin to "self.parent().parent().parent().doStuff(args)", it works but if hierarchy changes this will raise bugs, it gets harder to maintain the more complex my GUI gets.
Edit : Here is a simplified example of what I'm trying to achieve :
from PySide2.QtWidgets import *
import sys
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.central = QWidget()
self.central.setObjectName("Central_Widget")
self.setCentralWidget(self.central)
mainLayout = QHBoxLayout(self.central)
self.central.setStyleSheet("""
#Central_Widget{
background-color: #2489FF;
}""")
#init mainView widget
self.leftSide = leftWindow(self.central)
self.rightSide = rightWindow(self.central)
mainLayout.addWidget(self.leftSide)
mainLayout.addWidget(self.rightSide)
class leftWindow(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setObjectName("leftWindow")
layout = QVBoxLayout()
self.setLayout(layout)
self.label = QLabel(" Left ")
self.leftButton = customBtn(" Left Button ", "someSVGHere", self)
layout.addWidget(self.label)
layout.addWidget(self.leftButton)
self.setStyleSheet("border : 2px solid white;")
class rightWindow(QWidget):
def __init__(self, parent):
super().__init__(parent)
self.setObjectName("rightWindow")
layout = QVBoxLayout()
self.setLayout(layout)
self.label = QLabel(" Right ")
self.rightButton = customBtn(" Right Button ", "someSVGHere", self)
layout.addWidget(self.label)
layout.addWidget(self.rightButton)
self.setStyleSheet("border : 2px solid black;")
class customBtn(QWidget):
def __init__(self, text, svgIcon, parent):
super().__init__(parent)
layout = QVBoxLayout()
self.setLayout(layout)
self.btnText = QLabel(text)
self.Icon = svgIcon
layout.addWidget(self.btnText)
# setup the icon and the stylesheet etc ....
def mouseReleaseEvent(self, event):
# if it's the right button getting clicked, change the stylesheet of the central widget in main window
# How do I do that here ???
return super().mouseReleaseEvent(event)
# Launcher
if __name__ == '__main__':
app = QApplication(sys.argv)
win = MainWindow()
win.setWindowTitle("nesting test")
win.show()
sys.exit(app.exec_())
In the above example, I want to change the background of the central widget in the MainWindow class using the right button inside the rightWindow widget, in this example it's only 2 layers of nesting so it's not really deep, but in my App I would have up to 5 or 6 layers.
ideally, each class would be in a separate file but that doesn't change this example.
I unfortunately cannot use QT designer because I'm building a modern GUI app, so every single widget will be a custom one, and I would like to keep my code fragmented instead of instantiating everything in a single class like what QT designer generates.
Generally speaking, when dealing with object hierarchy, child objects should not attempt to directly interact their parents. In fact, they should always be able to "work" on their own, no matter of their parent, and even when they have no parent at all.
Considering this, instead of connecting a signal to a slot of a [great-][grand]parent, it should be that parent that connects the [great-][grand]child signal to its own slot. This perspective is important, because it's also what allows us to connect sibling objects that are (nor could) be aware of each other.
class customBtn(QWidget):
customSignal = pyqtSignal(Qt.MouseButton)
def mouseReleaseEvent(self, event):
self.customSignal.emit(event.button())
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.leftSide = leftWindow(self.central)
self.leftSide.leftButton.customSignal.connect(self.something)
def something(self):
# whatever
If the structure has many levels, it's also good practice to create custom signals for the intermediate classes, since signals can also be chained as long as they have a compatible signature (the target signal must have the same argument types or fewer arguments with same types).
class leftWindow(QWidget):
customSignal = pyqtSignal()
def __init__(self, parent):
# ...
self.leftButton = customBtn(" Left Button ", "someSVGHere", self)
self.leftButton.customSignal.connect(self.customSignal)
class MainWindow(QMainWindow):
def __init__(self):
# ...
self.leftSide = leftWindow(self.central)
self.leftSide.customSignal.connect(self.something)
In the case above, the signal of leftWindow doesn't have any arguments, so it's compatible with the button signal, as those arguments will be automatically discarded.
Related
I am learning Python by creating an MDI application in PyQt5. This application contains a class derived from QMdiSubWindow. These sub-windows need their own menu to be added to the main menu-bar. Where is the 'correct' place to create and show/hide that part of the menu which is only relevant to the sub-window when it's in focus? And where should the menu be destroyed (if it doesn't happen automatically because ownership is taken by the parent)? My attempt at detecting when the sub-window gains/loses focus causes infinite recursion, presumably because the newly visible menu steals the focus back from the sub-window.
This is probably such a common requirement that it's not mentioned in the tutorials, but the only reference in the docs to sub-window menus seems to just refer to the system menu, and not the main menu-bar. Most other Q&A's just refer to activating other sub-windows from the main menu. Several hours of searching haven't quite got what I need, so thank you for your help in either pointing me to the right place in the docs, or by improving my code ... or even both!
A minimal app to illustrate the problem:
#!/usr/bin/python3
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class TestSubWin(QMdiSubWindow):
def __init__(self, parent=None):
super().__init__()
self.setWidget(QLabel("Hello world"))
self.own_menu = QMenu("Sub win menu")
parent.menuBar().addMenu(self.own_menu)
# Add sub-window actions to the menu here
## Causes infinite recursion
# def focusOutEvent(self, event):
# self.own_menu.setVisible(False)
#
# def focusInEvent(self, event):
# self.own_menu.setVisible(True)
class MainWindow(QMainWindow):
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
file = bar.addMenu("File")
file.addAction("New")
file.triggered[QAction].connect(self.windowaction)
self.setWindowTitle("MDI demo")
def windowaction(self, q):
if q.text() == "New":
sub = TestSubWin(self)
self.mdi.addSubWindow(sub)
sub.show()
def main():
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
QMenu and QMenuBar don't take ownership of QActions (and QMenus), unless when created with the functions that accepts icon/title arguments.
This also means that you shall not need to destroy the menus, but only remove them from the menu bar.
The solution is to connect to the subWindowActivated signal, remove the previously added menu, retrieve the menu for the newly active sub window (if any) and add it.
Note that in order to remove a menu from QMenuBar you have to use removeAction() along with the menuAction(), which is the action associated with the menu and shown as menubar title for the menu (or item in a menu for sub menus).
In the following example I'm creating a base subclass for any mdi subwindows that will support menubar menus, and further subclasses for different window types.
import sys
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
class MenuSubWin(QMdiSubWindow):
own_menu = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setAttribute(Qt.WA_DeleteOnClose)
def menu(self):
return self.own_menu
class TestSubWin1(MenuSubWin):
def __init__(self):
super().__init__()
self.setWidget(QLabel("Hello world"))
self.own_menu = QMenu("Sub win menu 1")
self.own_menu.addAction('Test 1')
class TestSubWin2(MenuSubWin):
def __init__(self):
super().__init__()
self.setWidget(QLabel("How are you?"))
self.own_menu = QMenu("Sub win menu 2")
self.own_menu.addAction('Test 2')
class MainWindow(QMainWindow):
def __init__(self, parent = None):
super(MainWindow, self).__init__(parent)
self.mdi = QMdiArea()
self.setCentralWidget(self.mdi)
bar = self.menuBar()
fileMenu = bar.addMenu("File")
new1Action = fileMenu.addAction("New 1")
new1Action.setData(TestSubWin1)
new2Action = fileMenu.addAction("New 2")
new2Action.setData(TestSubWin2)
fileMenu.triggered.connect(self.newWindow)
self.setWindowTitle("MDI demo")
self.subWinMenu = None
self.mdi.subWindowActivated.connect(self.subWindowActivated)
def subWindowActivated(self, subWindow):
if self.subWinMenu:
self.menuBar().removeAction(self.subWinMenu.menuAction())
self.subWinMenu = None
if subWindow is None or not hasattr(subWindow, 'menu'):
return
self.subWinMenu = subWindow.menu()
if self.subWinMenu:
self.menuBar().addMenu(self.subWinMenu)
def newWindow(self, action):
cls = action.data()
if not cls:
return
sub = cls()
self.mdi.addSubWindow(sub)
sub.show()
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = MainWindow()
ex.show()
sys.exit(app.exec_())
Notes:
you shall always set the WA_DeleteOnClose attribute when directly adding a QMdiSubWindow (as opposed to adding a QWidget), otherwise the window will still exist for the MDI area and listed in the subWindowList(), thus preventing proper focus switching (and menu removal) upon closure;
for simplicity, I used the setData() feature of QAction with the class of the window that has to be created;
specifying the signature of signals is only required when signals do have overrides, which is unnecessary for triggered() since it has no overrides; note that Qt is gradually removing signal overrides, preferring explicit and unique signals instead;
I've used QtDesigner to make ui files that I then use to make classes, like
class MyPopup1(MyBaseClass, MyClass):
def __init__(self, parent=None):
super(MyPopup1, self).__init__(parent)
self.setupUi(self)
...
Granted I used some tutorial for this so I'm not actually sure what all of that does. But now I have written code that generates a popup that uses the QMessageBox class and I would like to move this code to a separate class so I can call it from multiple places.
How do I move this code to make a MyPopup2 class?
MyPopup2 = QtWidgets.QMessageBox(parent = self.central_widget)
MyPopup2.setWindowTitle("My Popup 2")
MyPopup2.setText("Some text")
MyPopup2.setIcon(QtWidgets.QMessageBox.Question)
MyPopup2.addButton("Btn1", QtWidgets.QMessageBox.RejectRole)
MyPopup2.addButton("Btn2", QtWidgets.QMessageBox.ActionRole)
choice = MyPopup2.exec_()
I know I probably need to connect the button signals to functions and use self.done() to send the result back to a call.
I am mostly confused on what to put as MyBaseClass and MyClass for the second popup.
Qt Designer provides a class that serves to fill a widget, so a recommended way is to inherit a widget and inherit from the generated class of Qt Designer, for example the structure that Qt Designer provides has the following structure:
class MyClass(object):
def setupUi(self, AAA):
...
self.retranslateUi(AAA)
QtCore.QMetaObject.connectSlotsByName(AAA)
def retranslateUi(self, AAA):
...
Then depending on the template you should choose as MyBaseClass to QMainWindow, QDialog or QWidget and call setupUi() which is the method that you add the other widget to the window as you point out:
class MyPopup1(MyBaseClass, MyClass):
def __init__(self, parent=None):
super(MyPopup1, self).__init__(parent)
self.setupUi(self)
...
But in the case that you are going to create the widget, MyClass is not necessary, so in your case the solution is the following:
from PyQt5 import QtWidgets
class MyPopup2(QtWidgets.QMessageBox):
def __init__(self, parent=None):
super(MyPopup2, self).__init__(parent)
self.setWindowTitle("My Popup 2")
self.setText("Some text")
self.setIcon(QtWidgets.QMessageBox.Question)
self.addButton("Btn1", QtWidgets.QMessageBox.RejectRole)
self.addButton("Btn2", QtWidgets.QMessageBox.ActionRole)
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
popup = MyPopup2()
if popup.exec_() == QtWidgets.QMessageBox.Accepted:
print("Btn2")
else:
print("Btn1")
The full solution for the example is
class MyPopup2(QtWidgets.QMessageBox):
def __init__(self, parent=None):
super(NoMatch, self).__init__(parent)
self.setWindowTitle("My Popup 2")
self.setText("Some text")
self.setIcon(QtWidgets.QMessageBox.Question)
self.Btn1 = self.addButton("Btn1", QtWidgets.QMessageBox.RejectRole)
self.Btn2 = self.addButton("Btn2", QtWidgets.QMessageBox.ActionRole)
self.Btn1.clicked.connect(lambda: self.done(QtWidgets.QMessageBox.RejectRole))
self.Btn2.clicked.connect(lambda: self.done(QtWidgets.QMessageBox.ActionRole))
Which can be called with choice = MyPopup2.exec_() from anywhere
I am working on developing a GUI with PyQt5. This is my first step into OOP, and I'm trying to teach myself as I go. I'm struggling with understanding when classes inherit methods/attributes etc and what methods they have available -- I guess it is a scope-related question? I have produced a MWE to my GUI below. In total, there will be many more pages and signals/slots.
What I want:
The stack should initialize with the "MainMenu" widget/object showing (left image below). Clicking on "Next Page" button should switch the stack order to put the "OtherPage" widget/object on top (right image below). I am creating each page as a class, thinking this would be a good way to organize my project. Is this good or bad practice?
What happens now:
The GUI works (initializes) if the line nextPg.clicked.connect(self.drawOtherPage()) is commented out, but of course then clicking on the button does nothing. I can switch the initial stack order so that "other" widget is on top of the stack and it shows up fine, so I think that class is also working. When the above line is included in the code, the following error is thrown:
in __init__
nextPg.clicked.connect(self.drawOtherPage())
AttributeError: 'MainMenu' object has no attribute 'drawOtherPage'
What I've tried
I thought that the call to super() was supposed to allow the child class (in this case MainMenu) to inherit the methods from the parent class (RootInit). Therefore, I would think this should make the drawOtherPage method available to the button connect signal. Obviously, the error isa result of the method not being available.
What am I doing wrong? Should I be creating these "page" widgets in methods instead? Do they need to be under the RootInit class or can they live in the top level of the .py file? I'm trying to follow best practices as the project will become pretty large in the end. Fortunately, most of it should be pages with variations based on what buttons were clicked to get there -- I therefore thought classes would be helpful. Please be harsh on the code and my python/PyQt vernacular, trying to learn -- thanks!
import sys, os
from PyQt5.QtWidgets import *
from PyQt5 import QtGui, QtCore
class RootInit(QMainWindow):
# root window
def __init__(self, parent=None):
QMainWindow.__init__(self)
self.root = QWidget()
self.stack = QStackedWidget()
rootLayout = QVBoxLayout()
rootLayout.addWidget(self.stack)
self.root.setLayout(rootLayout)
self.setCentralWidget(self.root)
self.initializeGUI()
def initializeGUI(self):
self.main = MainMenu(self) # build MainMenu (class)
self.other = OtherPage(self) # build OtherPage (class)
self.stack.addWidget(self.main)
self.stack.addWidget(self.other)
def drawMain(self):
self.stack.setCurrentIndex(0)
def drawOtherPage(self):
self.stack.setCurrentIndex(1)
class MainMenu(QWidget):
# class for main menu
def __init__(self, parent=None):
QWidget.__init__(self, parent)
super().__init__()
mainLayout = QGridLayout() # layout for entire main menu
quitBtn = QPushButton("Quit")
quitBtn.clicked.connect(QtCore.QCoreApplication.instance().quit)
nextPg = QPushButton("Next page")
nextPg.clicked.connect(self.drawOtherPage())
mainLayout.addWidget(quitBtn, 0, 0)
mainLayout.addWidget(nextPg, 0, 1)
self.setLayout(mainLayout)
class OtherPage(QWidget):
# class for another menu
def __init__(self, parent=None):
QWidget.__init__(self, parent)
label = QLabel("test label")
layout = QGridLayout() #
layout.addWidget(label, 0, 0)
self.setLayout(layout)
if __name__ == '__main__':
app = QApplication(sys.argv)
root = RootInit()
root.setWindowTitle("Title")
root.show()
sys.exit(app.exec_())
Your code has the following errors:
The variable self refers to the same instance of the class, in your case self refers to an instance of MainMenu, and if we observe MainMenu it does not have any drawOtherPage() method.
Another mistake in your case is to call the parent's constructor twice:
class MainMenu(QWidget):
# class for main menu
def __init__(self, parent=None):
QWidget.__init__(self, parent)
super().__init__()
In the first constructor you are assigning a parent, and in the second, you are not. To clarify in python there are several ways to call the parent's constructor:
QWidget.__init__(self, parent)
super(MainMenu, self).__init__(parent)
super().__init__(parent)
so you should only use one of them.
Another error is that a signal is connected through the name of a function, the function must not be evaluated using parentheses
and for the last use of functions or methods that involve several objects should be done in a place where both objects can access, in your case you can take advantage of what you are going to RootInit as parent of MainMenu: self.main = MainMenu(self), and access the connection to that element through the method parent().
All of the above entails modifying the MainMenu class to the following:
class MainMenu(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
mainLayout = QGridLayout() # layout for entire main menu
quitBtn = QPushButton("Quit")
quitBtn.clicked.connect(QtCore.QCoreApplication.instance().quit)
nextPg = QPushButton("Next page")
nextPg.clicked.connect(self.parent().drawOtherPage)
mainLayout.addWidget(quitBtn, 0, 0)
mainLayout.addWidget(nextPg, 0, 1)
self.setLayout(mainLayout)
I've created a custom widget in pyqt4 that I've worked on and tested and now am ready to load it into my main window. Since it doesn't show up in designer, I need to manually add it to my main window manually.
My widget uses uic to load the ui file instead of converting it to a py file (it's been quicker less hassle so far) so it looks something like this:
class widgetWindow(QtGui.QWidget):
def __init__(self, parent = None):
super(widgetWindow, self).__init__(parent)
self.ui = uic.loadUi("widget.ui")
#everything else
now in my main class (example for brevity) I create the layout, add the widget to the layout and then add it to the main widget
class main(QtGui.QMainWindow):
def __init__(self, parent = None):
super(main, self).__init__(parent)
self.ui = uic.loadUi("testWindow.ui")
mainlayout = QtGui.QVBoxLayout()
window = widgetWindow(self)
mainlayout.addWidget(window)
centerWidget = QtGui.QWidget()
centerWidget.setLayout(mainlayout)
self.ui.setCentralWidget(centerWidget)
There are no errors thrown, and it will make space for the widget, but it simply won't show anything.
adding in the line window.ui.show() will just pop open a new window overtop the space that it should be occupying on the main window. What am I missing?
Doing some more research into the uic loader, there are two ways to load a ui file. The way I'm using it in the question is one way, the other way is with the uic.loadUiType(). This creates both the base class and the form class to be inherited by the class object instead of just the QtGui.QWidget class object.
widgetForm, baseClass= uic.loadUiType("addTilesWidget.ui")
class windowTest(baseClass, widgetForm):
def __init__(self, parent = None):
super(windowTest, self).__init__(parent)
self.setupUi(self)
This way, the widget can be loaded into another form as expected. As for exactly why, I haven't found that answer yet.
Some more info on the different setup types: http://bitesofcode.blogspot.com/2011/10/comparison-of-loading-techniques.html
Try to add the parent argument into the loadUi statements:
self.ui = uic.loadUi("widget.ui",parent)
self.ui = uic.loadUi("testWindow.ui",self)
And try the following line at the end of your main class.
self.setCentralWidget(centerWidget)
You need to specify that 'centerWidget' is the central widget of the main window.
i.e your code for class main should be have a line like:
self.setCentralWidget(centerWidget)
class main(QMainWindow):
def __init__(self, parent = None):
super(main, self).__init__(parent)
....
self.setCentralWidget(centerWidget)
I'm trying to make a class that extends qwidget, that pops up a new window, I must be missing something fundamental,
class NewQuery(QtGui.QWidget):
def __init__(self, parent):
QtGui.QMainWindow.__init__(self,parent)
self.setWindowTitle('Add New Query')
grid = QtGui.QGridLayout()
label = QtGui.QLabel('blah')
grid.addWidget(label,0,0)
self.setLayout(grid)
self.resize(300,200)
when a new instance of this is made in main window's class, and show() called, the content is overlaid on the main window, how can I make it display in a new window?
follow the advice that #ChristopheD gave you and try this instead
from PyQt4 import QtGui
class NewQuery(QtGui.QWidget):
def __init__(self, parent=None):
super(NewQuery, self).__init__(parent)
self.setWindowTitle('Add New Query')
grid = QtGui.QGridLayout()
label = QtGui.QLabel('blah')
grid.addWidget(label,0,0)
self.setLayout(grid)
self.resize(300,200)
app = QtGui.QApplication([])
mainform = NewQuery()
mainform.show()
newchildform = NewQuery()
newchildform.show()
app.exec_()
Your superclass initialiser is wrong, you probably meant:
class NewQuery(QtGui.QWidget):
def __init__(self, parent):
QtGui.QWidget.__init__(self, parent)
(a reason to use super):
class NewQuery(QtGui.QWidget):
def __init__(self, parent):
super(NewQuery, self).__init__(parent)
But maybe you want inherit from QtGui.QDialog instead (that could be appropriate - hard to tell with the current context).
Also note that the indentation in your code example is wrong (a single space will work but 4 spaces or a single tab are considered nicer).