Home > front end >  PyQt5 - Assigning action to button class to update a label attribute from a different class
PyQt5 - Assigning action to button class to update a label attribute from a different class

Time:07-07

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

Before_button_clicked

After clicking the button

AFter_button_clicked

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!

  • Related