I'm creating a calculator app using PyQt5, and when I'm assigning an action to my button to update the label text that resides in a different class from the button class. It seems to be doing something, but instead of what I'm expecting to happen. It seems to remove all of my calculator buttons, and rebuild the label without even updating its text. Please see attached screenshots for before and after I clicked a button.
import PyQt5.QtWidgets as qtwidget
import PyQt5.QtGui as qtgui
import PyQt5.QtCore as qtcore
app = qtwidget.QApplication([])
class Button(qtwidget.QPushButton):
def __init__(self, text, parent=None):
super().__init__(text, parent=parent)
self.text = text
button_policy = self.sizePolicy()
button_policy.setVerticalPolicy(button_policy.Policy.Minimum)
self.setSizePolicy(button_policy)
# Set text font style & size
self.setFont(qtgui.QFont('Helvetica', 20))
# assign click action to button_click function
self.clicked.connect(self.button_click)
# The problem seeems to be here
def button_click(self):
# declare MainWindow class
label_obj = MainWindow()
# access label attribute of MainWindow class
label_text = label_obj.label
# Set text of label to button text
label_text.setText(self.text)
btn_percent = Button('%')
btn_ce = Button('CE')
btn_c = Button('C')
btn_back = Button('Back')
btn_sign = Button(' /-')
btn1 = Button('1')
btn2 = Button('2')
btn3 = Button('3')
btn4 = Button('4')
btn5 = Button('5')
btn6 = Button('6')
btn7 = Button('7')
btn8 = Button('8')
btn9 = Button('9')
btn0 = Button('0')
btn_division = Button('/')
btn_multiply = Button('x')
btn_minus = Button('-')
btn_plus = Button(' ')
btn_dot = Button('.')
btn_equal = Button('=')
class MainWindow(qtwidget.QWidget):
def __init__(self):
super().__init__()
# Add a title
self.setWindowIcon(qtgui.QIcon('.\icons\calculator_icon.ico'))
self.setWindowTitle('Calculator')
# Create a label
self.label = qtwidget.QLabel("0")
# Set background color
self.label.setStyleSheet("background-color: #e3e1da;\
border: 1px solid black;\
padding-top: 10px;\
padding-bottom: 10px")
# Set label maximum height
self.label.setMaximumHeight(100)
# Align label text
self.label.setAlignment(qtcore.Qt.AlignmentFlag.AlignVCenter | qtcore.Qt.AlignmentFlag.AlignRight)
# Set label text font and size
self.label.setFont(qtgui.QFont('Helvetica', 40))
# Add grid layout for calculator buttons
main_gridlayout = qtwidget.QGridLayout()
# Add label to main widget
main_gridlayout.addWidget(self.label, 0, 0, 1, 4)
# Add buttons to main widget
main_gridlayout.addWidget(btn_percent, 1, 0)
main_gridlayout.addWidget(btn_ce, 1, 1)
main_gridlayout.addWidget(btn_back, 1, 2)
main_gridlayout.addWidget(btn_division, 1, 3)
main_gridlayout.addWidget(btn7, 2, 0)
main_gridlayout.addWidget(btn8, 2, 1)
main_gridlayout.addWidget(btn9, 2, 2)
main_gridlayout.addWidget(btn_multiply, 2, 3)
main_gridlayout.addWidget(btn4, 3, 0)
main_gridlayout.addWidget(btn5, 3, 1)
main_gridlayout.addWidget(btn6, 3, 2)
main_gridlayout.addWidget(btn_minus, 3, 3)
main_gridlayout.addWidget(btn1, 4, 0)
main_gridlayout.addWidget(btn2, 4, 1)
main_gridlayout.addWidget(btn3, 4, 2)
main_gridlayout.addWidget(btn_plus, 4, 3)
main_gridlayout.addWidget(btn_sign, 5, 0)
main_gridlayout.addWidget(btn0, 5, 1)
main_gridlayout.addWidget(btn_dot, 5, 2)
main_gridlayout.addWidget(btn_equal, 5, 3)
self.setLayout(main_gridlayout)
self.show()
mw = MainWindow()
app.exec_()
Before clicking the button
After clicking the button
CodePudding user response:
Note: this answer is mostly intended for didactic purposes. It somehow goes way beyond (or far away from) the original request, and has conceptual issues both in its resolution and result. Nonetheless, I believe it could be really educational for many (even unrelated) aspects of Python and Qt that are often misunderstood or completely unknown.
Premise (what's wrong with the OP code)
There are many issues with your approach, starting with the peculiar mix of OOP and "pseudo-procedural" paradigm.
The issue you're describing is caused by the fact that you're creating a new instance of the MainWindow
, which in turn reparents all the previously declared widgets. Coincidentally, that main window instance has no reference at all, it's just a local variable, and python will garbage collect it as soon as the button_click
function returns.
The result is that that window will be immediately destroyed after that, and all widgets that have been reparented to that window will be also destroyed along with it.
What you see is the only remaining widgets (the "display").
Not only: from the OOP perspective, a "child" object should never directly act upon its parent object(s) in its hierarchy. It's not the responsibility of the button to change something in its parent.
Hierarchy of objects and their responsibilities
Let me take this from a pseudo-real-life perspective.
As many of us have probably realized, children just behave on their education, no matter of the context. They just do or tell what they've been "taught", bluntly.
Imagine that the "window" is the teacher in a school class, and the buttons are the pupils. The teacher has the responsibility of those students, the lesson is about "solving a math problem" that can be written on the chalkboard. It is the teacher's responsibility to tell which student can write on the chalkboard, not the other way around.
What you're trying to do is that whenever a new student is called to intervene, they will create a new teacher and a new chalkboard, then write whatever they choose, no matter what was written before.
That's clearly wrong: lots of cool doodles, but not very educational.
So. First of all, the button (as the pupil) has should have no direct responsibility, it can only signal that they want to do something.
The window (the teacher) will decide what to do with the student's will, based on a set of rules (the pedagogy they learnt).
Most importantly, the pupils are assigned to the teacher's class (pun intended), they shouldn't be moved at will, and certainly not by direct will of one of those children.
Now, with the above in mind, let's make a "sign" for each pupil, so that they can write their own symbol on it and tell the teacher that they have been "called". In Qt terms, that sign is a signal. We assign them a symbol, and when they're called they will "show" (emit) that symbol (signal):
class Button(QtWidgets.QPushButton):
buttonClicked = QtCore.pyqtSignal(str)
def __init__(self, text, parent=None):
super().__init__(text, parent=parent)
self.setSizePolicy(QtWidgets.QSizePolicy.Preferred,
QtWidgets.QSizePolicy.Preferred)
self.setFont(QtGui.QFont('Helvetica', 20))
self.clicked.connect(lambda: self.buttonClicked.emit(text))
Now, let's make a good class. We have the names of those children (or, better, their "symbols"), let's put them in a proper order, so that the teacher always know the position of each "symbol". For instance:
buttons = [
['%', 'CE', 'Back', '/'],
['7', '8', '9', '×'],
['4', '5', '6', '-'],
['1', '2', '3', ' '],
[' /-', '0', '.', '='],
]
But the class layout is important. The chalkboard is "on top", then the children desks. We use a "main layout" for the whole classroom, on top we put the chalkboard, then the "grid" of desks.
And, now that we're at it, we also define what those "sign[al]s" will do.
Let's see our class:
class MainWindow(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle('Calculator')
# this is our chalkboard! yay!
self.display = QtWidgets.QLabel("0")
self.display.setStyleSheet("background-color: #e3e1da;\
border: 1px solid black;\
padding-top: 10px;\
padding-bottom: 10px")
# the *main* layout of the class:
mainLayout = QtWidgets.QVBoxLayout(self)
# the chalkboard!
self.display.setMaximumHeight(100)
self.display.setAlignment(
QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight)
self.display.setFont(QtGui.QFont('Helvetica', 40))
# the chalkboard is on "top"
mainLayout.addWidget(self.display)
# all student's desks after that
buttonLayout = QtWidgets.QGridLayout()
mainLayout.addLayout(buttonLayout)
# the desk layout...
buttons = [
['%', 'CE', 'Back', '/'],
['7', '8', '9', '×'],
['4', '5', '6', '-'],
['1', '2', '3', ' '],
[' /-', '0', '.', '='],
]
for row, rowButtons in enumerate(buttons):
for column, text in enumerate(rowButtons):
button = Button(text)
button.buttonClicked.connect(self.updateDisplay)
buttonLayout.addWidget(button, row, column)
We only miss our main assignment, common for each of those sign[al]s:
def updateDisplay(self, text):
if self.display.text() == '0':
self.display.clear()
self.display.setText(self.display.text() text)
Oh, wait, there's one child with the "Back" signal. We have to take care of that too! Just ensure that they don't clear the whole chalkboard or write something weird!
def updateDisplay(self, text):
if text == 'Back' and self.display.text() != '0':
self.display.setText(self.display.text()[:-1])
elif self.display.text() == '0':
self.display.clear()
self.display.setText(self.display.text() text)
Ok, now we're pretty set.
But.
No. That's not enough.
Unfortunately, calculators are quite complex: they display something that is logical to us, but might not make a lot of sense mathematically speaking.
But, that's for another time. Class is over!