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)
Related
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.
I'm trying to build a cool app, but it seems I lack some knowledge. Read lots of infos and examples in internet, but it doesn't help:
Understanding the "underlying C/C++ object has been deleted" error
Ok, here what I do:
I create central widget from my main.py, which works fine and I don't post it here fully:
self.rw = ReportWidget()
self.setCentralWidget(self.rw)
And here is my central widget - report.py:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
class ReportWidget(QtGui.QWidget):
def __init__(self, parent=None):
super(ReportWidget, self).__init__(parent)
self.setup_ui()
def setup_ui(self):
report = QtGui.QVBoxLayout(self)
report.setAlignment(QtCore.Qt.AlignTop)
head = QtGui.QHBoxLayout()
add_label = QtGui.QLabel(u"Add")
head.addWidget(add_label)
report.addLayout(head)
area = QtGui.QScrollArea()
area.setWidgetResizable(True)
area.setEnabled(True)
layout = QtGui.QVBoxLayout()
layout.setAlignment(QtCore.Qt.AlignTop)
widget = QtGui.QWidget()
widget.setLayout(layout)
area.setWidget(widget)
report.addWidget(area)
self.s = layout
# trying to create first line:
first_line = Line(self)
self.s.addWidget(first_line)
first_line.set_controls(True, False)
head = QtGui.QHBoxLayout()
ok = QtGui.QPushButton(u"Calculate")
head.addWidget(ok)
report.addLayout(head)
Continued from the same file report.py:
class Line(QtGui.QWidget):
def __init__(self, parent=None):
super(Line, self).__init__(parent)
self.setup_ui(parent)
def setup_ui(self, parent):
add_button = QtGui.QPushButton()
add_button.setObjectName("add_button")
self.add_button = add_button
self.layout = QtGui.QHBoxLayout(line)
self.layout.addWidget(add_button)
def set_controls(self, add_button=True, remove_button=True):
self.add_button.setEnabled(add_button)
Thus, running main.py raises RuntimeError: underlying C/C++ object has been deleted error on the last piece of code where I try to setEnabled parameter to new button, as if it was never created or bound anywhere.
It seems I have some design flaw. Maybe it's wrong idea to have different classes in one file or else? Or maybe I don't quite control which widget has which parent and how layouts work.
Thank you for reading. Have a nice day!
Thanks to everyone who tried to answer! Unfortunately no one said what a bunch of crap I wrote! *smile*
My line is already a widget and I don't need to create itself inside itself. All I had to do is to create layout inside setup_ui and add widgets to it. Finally it looks like:
class Line(QtGui.QWidget):
def __init__(self, parent=None):
super(Line, self).__init__(parent)
self.setup_ui(parent)
def setup_ui(self, parent):
line = QtGui.QHBoxLayout(self)
add_button = QtGui.QPushButton()
add_button.setObjectName("add_button")
line.addWidget(add_button)
# to get reference from outside
self.add_button = add_button
def set_controls(self, add_button=True, remove_button=True):
self.add_button.setEnabled(add_button)
Special thanks to nymk and Avaris!
I could not reproduce an error with the code you showed us (apart from an error about the variable line not being defined in Line.setup_ui). If I replaced line with self, I got no error.
However, I could get a crash if I set line to a QWidget that I created and didn't keep a reference to. In other words, I added
line = QtGui.QWidget()
to Line.setup_ui, and found that this crashed on the same line of code you reported, complaining that the wrapped C/C++ object had been deleted.
I have a MainWindow that looks like this:
def __init__(self, parent = None):
QMainWindow.__init__(self, parent)
self.setupUi(self)
self.showMaximized()
menu=mainMenu.MainMenu()
classification=classificationMain.ClassificationMain()
self.stackedWidget.addWidget(menu)
self.stackedWidget.addWidget(classification)
self.stackedWidget.setCurrentWidget(menu)
self.stackedWidget.showFullScreen()
#connections
menu.pushButton.clicked.connect(self.showClassification)
classification.backButton.clicked.connect(self.showMainWindow)
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
def showMainWindow(self):
self.stackedWidget.setCurrentIndex(2)
The MainWindows waits for signal from the rest of the dialogs. Now, the Classification dialog has another StackedWidget in it, since it works as a main window for an important part of the application. It looks like:
class ClassificationMain(QDialog, Ui_Dialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setupUi(self)
choose=choosePatient.ChoosePatient()
self.stackedWidget.addWidget(choose)
self.stackedWidget.setCurrentWidget(choose)
Now, I want to reload the data inside ChoosePatient every time the button "Show Classification" from MainMenu is clicked, but now the data is loaded only once in the line classification=classificationMain.ClassificationMain() of MainWindow.
I was thinking I had to connect a slot inside ChoosePatient with the click of "Show Classification" button inside MainMenu, but I would need an instance of MainMenu, which is not possible.
How can a method of ChoosePatient can be execute every time the button in the "parent" window is clicked? (also, please tell me if this is not the right way to work with pyqt windows)
You need to save references to your composed widgets, and also to expose some public methods to the parents:
class ClassificationMain(QDialog, Ui_Dialog):
def __init__(self, parent = None):
QDialog.__init__(self, parent)
self.setupUi(self)
self.chooseWidget=choosePatient.ChoosePatient()
self.stackedWidget.addWidget(self.chooseWidget)
self.stackedWidget.setCurrentWidget(self.chooseWidget)
def reloadPatients(self):
# whatever your operation should be on the ChoosePatient
self.chooseWidget.reload()
# MAIN WINDOW
def __init__(self, parent = None):
...
self.classification=classificationMain.ClassificationMain()
self.stackedWidget.addWidget(self.classification)
...
#connections
menu.pushButton.clicked.connect(self.showClassification)
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
self.classification.reloadPatients()
You could also just skip the reloadPatients method and connect to the ChoosePatient directly if you want:
def showClassification(self ):
self.stackedWidget.setCurrentIndex(3)
self.classification.chooseWidget.reload()
My personal opinion is to make your custom classes wrap up the internal functionality nicely so that you only need to interface with it over the custom class, and not dig into its internals. That way you can change how it works inside without breaking the main window.
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).