The example shows how to use signals and slots to implement the functionality of a calculator widget, and how to use
QGridLayout to place child widgets in a grid.
The example consists of two classes:
Calculatoris the calculator widget, with all the calculator functionality.
Buttonis the widget used for each of the calculator button. It derives from
We will start by reviewing
Calculator, then we will take a look at
Calculator Class Definition#
class Calculator(QWidget): Q_OBJECT # public Calculator(QWidget parent = None) # private slots def digitClicked(): def unaryOperatorClicked(): def additiveOperatorClicked(): def multiplicativeOperatorClicked(): def equalClicked(): def pointClicked(): def changeSignClicked(): def backspaceClicked(): def clear(): def clearAll(): def clearMemory(): def readMemory(): def setMemory(): def addToMemory():
Calculator class provides a simple calculator widget. It inherits from
QDialog and has several private slots associated with the calculator’s buttons.
eventFilter() is reimplemented to handle mouse events on the calculator’s display.
Buttons are grouped in categories according to their behavior. For example, all the digit buttons (labeled 0 to 9) append a digit to the current operand. For these, we connect multiple buttons to the same slot (e.g.,
digitClicked()). The categories are digits, unary operators (Sqrt, x², 1/x), additive operators (+, -), and multiplicative operators (×, ÷). The other buttons have their own slots.
createButton() function is used as part of the widget construction.
abortOperation() is called whenever a division by zero occurs or when a square root operation is applied to a negative number.
calculate() applies a binary operator (+, -, ×, or ÷).
sumInMemory = float()
These variables, together with the contents of the calculator display (a
QLineEdit ), encode the state of the calculator:
sumInMemorycontains the value stored in the calculator’s memory (using MS, M+, or MC).
sumSoFarstores the value accumulated so far. When the user clicks =,
sumSoFaris recomputed and shown on the display. Clear All resets
factorSoFarstores a temporary value when doing multiplications and divisions.
pendingAdditiveOperatorstores the last additive operator clicked by the user.
pendingMultiplicativeOperatorstores the last multiplicative operator clicked by the user.
truewhen the calculator is expecting the user to start typing an operand.
Additive and multiplicative operators are treated differently because they have different precedences. For example, 1 + 2 ÷ 3 is interpreted as 1 + (2 ÷ 3) because ÷ has higher precedence than +.
The table below shows the evolution of the calculator state as the user enters a mathematical expression.
Sum so Far
Factor so Far
Waiting for Operand?
1 + 2
1 + 2 ÷
1 + 2 ÷ 3
1 + 2 ÷ 3 -
1 + 2 ÷ 3 - 4
1 + 2 ÷ 3 - 4 =
Unary operators, such as Sqrt, require no special handling; they can be applied immediately since the operand is already known when the operator button is clicked.
display = QLineEdit()
Finally, we declare the variables associated with the display and the buttons used to display numerals.
Calculator Class Implementation#
def __init__(self, parent): super().__init__(parent) self.sumInMemory = 0.0 self.sumSoFar = 0.0 , factorSoFar(0.0), waitingForOperand(True)
In the constructor, we initialize the calculator’s state. The
pendingMultiplicativeOperator variables don’t need to be initialized explicitly, because the
QString constructor initializes them to empty strings. It is also possible to initialize those variable directly in the header. This is called
member-initializaton and avoids a long initialization list.
display = QLineEdit("0")
We create the
QLineEdit representing the calculator’s display and set up some of its properties. In particular, we set it to be read-only.
We also enlarge
display's font by 8 points.
for i in range(0, NumDigitButtons): digitButtons[i] = createButton(QString.number(i), SLOT(digitClicked())) pointButton = createButton(tr("."), SLOT(pointClicked())) changeSignButton = createButton(tr("\302\261"), SLOT(changeSignClicked())) backspaceButton = createButton(tr("Backspace"), SLOT(backspaceClicked())) clearButton = createButton(tr("Clear"), SLOT(clear())) clearAllButton = createButton(tr("Clear All"), SLOT(clearAll())) clearMemoryButton = createButton(tr("MC"), SLOT(clearMemory())) readMemoryButton = createButton(tr("MR"), SLOT(readMemory())) setMemoryButton = createButton(tr("MS"), SLOT(setMemory())) addToMemoryButton = createButton(tr("M+"), SLOT(addToMemory())) divisionButton = createButton(tr("\303\267"), SLOT(multiplicativeOperatorClicked())) timesButton = createButton(tr("\303\227"), SLOT(multiplicativeOperatorClicked())) minusButton = createButton(tr("-"), SLOT(additiveOperatorClicked())) plusButton = createButton(tr("+"), SLOT(additiveOperatorClicked())) squareRootButton = createButton(tr("Sqrt"), SLOT(unaryOperatorClicked())) powerButton = createButton(tr("x\302\262"), SLOT(unaryOperatorClicked())) reciprocalButton = createButton(tr("1/x"), SLOT(unaryOperatorClicked())) equalButton = createButton(tr("="), SLOT(equalClicked()))
For each button, we call the private
createButton() function with the proper text label and a slot to connect to the button.
mainLayout = QGridLayout()
The layout is handled by a single
QGridLayout . The
setSizeConstraint() call ensures that the
Calculator widget is always shown as its optimal size (its
size hint ), preventing the user from resizing the calculator. The size hint is determined by the size and
size policy of the child widgets.
Most child widgets occupy only one cell in the grid layout. For these, we only need to pass a row and a column to
addWidget() . The
clearAllButton widgets occupy more than one column; for these we must also pass a row span and a column span.
def digitClicked(self): clickedButton = Button(sender()) digitValue = clickedButton.text().toInt() if display.text() == "0" and digitValue == 0.0: return if waitingForOperand: display.clear() waitingForOperand = False display.setText(display.text() + QString.number(digitValue))
Pressing one of the calculator’s digit buttons will emit the button’s
clicked() signal, which will trigger the
First, we find out which button sent the signal using
sender() . This function returns the sender as a
QObject pointer. Since we know that the sender is a
Button object, we can safely cast the
QObject . We could have used a C-style cast or a C++
static_cast<>(), but as a defensive programming technique we use a
qobject_cast() . The advantage is that if the object has the wrong type, a null pointer is returned. Crashes due to null pointers are much easier to diagnose than crashes due to unsafe casts. Once we have the button, we extract the operator using
The slot needs to consider two situations in particular. If
display contains “0” and the user clicks the 0 button, it would be silly to show “00”. And if the calculator is in a state where it is waiting for a new operand, the new digit is the first digit of that new operand; in that case, any result of a previous calculation must be cleared first.
At the end, we append the new digit to the value in the display.
unaryOperatorClicked() slot is called whenever one of the unary operator buttons is clicked. Again a pointer to the clicked button is retrieved using
sender() . The operator is extracted from the button’s text and stored in
clickedOperator. The operand is obtained from
Then we perform the operation. If Sqrt is applied to a negative number or 1/x to zero, we call
abortOperation(). If everything goes well, we display the result of the operation in the line edit and we set
true. This ensures that if the user types a new digit, the digit will be considered as a new operand, instead of being appended to the current value.
additiveOperatorClicked() slot is called when the user clicks the + or - button.
Before we can actually do something about the clicked operator, we must handle any pending operations. We start with the multiplicative operators, since these have higher precedence than additive operators:
If × or ÷ has been clicked earlier, without clicking = afterward, the current value in the display is the right operand of the × or ÷ operator and we can finally perform the operation and update the display.
If + or - has been clicked earlier,
sumSoFar is the left operand and the current value in the display is the right operand of the operator. If there is no pending additive operator,
sumSoFar is simply set to be the text in the display.
Finally, we can take care of the operator that was just clicked. Since we don’t have the right-hand operand yet, we store the clicked operator in the
pendingAdditiveOperator variable. We will apply the operation later, when we have a right operand, with
sumSoFar as the left operand.
def multiplicativeOperatorClicked(self): clickedButton = Button(sender()) if not clickedButton: return clickedOperator = clickedButton.text() operand = display.text().toDouble() if not pendingMultiplicativeOperator.isEmpty(): if not calculate(operand, pendingMultiplicativeOperator): abortOperation() return display.setText(QString.number(factorSoFar)) else: factorSoFar = operand pendingMultiplicativeOperator = clickedOperator waitingForOperand = True
multiplicativeOperatorClicked() slot is similar to
additiveOperatorClicked(). We don’t need to worry about pending additive operators here, because multiplicative operators have precedence over additive operators.
def equalClicked(self): operand = display.text().toDouble() if not pendingMultiplicativeOperator.isEmpty(): if not calculate(operand, pendingMultiplicativeOperator): abortOperation() return operand = factorSoFar factorSoFar = 0.0 pendingMultiplicativeOperator.clear() if not pendingAdditiveOperator.isEmpty(): if not calculate(operand, pendingAdditiveOperator): abortOperation() return pendingAdditiveOperator.clear() else: sumSoFar = operand display.setText(QString.number(sumSoFar)) sumSoFar = 0.0 waitingForOperand = True
additiveOperatorClicked(), we start by handling any pending multiplicative and additive operators. Then we display
sumSoFar and reset the variable to zero. Resetting the variable to zero is necessary to avoid counting the value twice.
def pointClicked(self): if waitingForOperand: display.setText("0") if not display.text().contains('.'): display.setText(display.text() + tr(".")) waitingForOperand = False
pointClicked() slot adds a decimal point to the content in
def changeSignClicked(self): text = display.text() value = text.toDouble() if value > 0.0: text.prepend(tr("-")) elif value < 0.0: text.remove(0, 1) display.setText(text)
changeSignClicked() slot changes the sign of the value in
display. If the current value is positive, we prepend a minus sign; if the current value is negative, we remove the first character from the value (the minus sign).
def backspaceClicked(self): if waitingForOperand: return text = display.text() text.chop(1) if text.isEmpty(): text = "0" waitingForOperand = True display.setText(text)
backspaceClicked() removes the rightmost character in the display. If we get an empty string, we show “0” and set
def clear(self): if waitingForOperand: return display.setText("0") waitingForOperand = True
clear() slot resets the current operand to zero. It is equivalent to clicking Backspace enough times to erase the entire operand.
def clearAll(self): sumSoFar = 0.0 factorSoFar = 0.0 pendingAdditiveOperator.clear() pendingMultiplicativeOperator.clear() display.setText("0") waitingForOperand = True
clearAll() slot resets the calculator to its initial state.
def clearMemory(self): sumInMemory = 0.0 def readMemory(self): display.setText(QString.number(sumInMemory)) waitingForOperand = True def setMemory(self): equalClicked() sumInMemory = display.text().toDouble() def addToMemory(self): equalClicked() sumInMemory += display.text().toDouble()
clearMemory() slot erases the sum kept in memory,
readMemory() displays the sum as an operand,
setMemory() replace the sum in memory with the current sum, and
addToMemory() adds the current value to the value in memory. For
addToMemory(), we start by calling
equalClicked() to update
sumSoFar and the value in the display.
Calculator::createButton = Button(QString text, char member) button = Button(text) connect(button, SIGNAL(clicked()), self, member) return button
createButton() function is called from the constructor to create calculator buttons.
def abortOperation(self): clearAll() display.setText(tr("####"))
abortOperation() function is called whenever a calculation fails. It resets the calculator state and displays “####”.
def calculate(self, float rightOperand, QString pendingOperator): if pendingOperator == tr("+"): sumSoFar += rightOperand elif pendingOperator == tr("-"): sumSoFar -= rightOperand elif pendingOperator == tr("\303\227"): = rightOperand elif pendingOperator == tr("\303\267"): if rightOperand == 0.0: return False factorSoFar /= rightOperand return True
calculate() function performs a binary operation. The right operand is given by
rightOperand. For additive operators, the left operand is
sumSoFar; for multiplicative operators, the left operand is
factorSoFar. The function return
false if a division by zero occurs.