
Штош. В прошлой статье мы сделали дизайн калькулятора. Ну а зачем нам этот голый дизайн без функционала, правильно?
Импортируем библиотеки, следуя стилю PEP 8:
import sys from PySide6.QtWidgets import QApplication, QMainWindow from design import Ui_MainWindow
Напишем дефолтный код для запуска любого Qt приложения с файлом дизайна. Если вам хочется подробнее узнать о работе каждой строчки кода, приглашаю посетить документацию.
class Calculator(QMainWindow): def __init__(self): super(Calculator, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) if __name__ == "__main__": app = QApplication(sys.argv) window = Calculator() window.show() sys.exit(app.exec())
Если у вас не установлен в систему шрифт Rubik, то в вашем приложении шрифт будет дефолтным. Для решения этой проблемы не нужно устанавливать шрифт в систему. Импортируем:
from PySide6.QtGui import QFontDatabase
Теперь используем метод добавления шрифта приложения, в который передадим файл шрифта. Я сделал это в конструкторе класса.
QFontDatabase.addApplicationFont("fonts/Rubik-Regular.ttf")
Добавляем цифры
def add_digit(self): btn = self.sender()
Метод sender() возвращает Qt объект, который посылает сигнал.
def sender(self): # real signature unknown; restored from __doc__ """ sender(self) -> PySide6.QtCore.QObject """ pass
В нашем случае сигнал является нажатием кнопки. Создадим кортеж с именами кнопок-цифр.
digit_buttons = ('btn_0', 'btn_1', 'btn_2', 'btn_3', 'btn_4', 'btn_5', 'btn_6', 'btn_7', 'btn_8', 'btn_9')
По дефолту в поле всегда стоит 0. В этом случае, если нажимается кнопка с цифрой, текст поля заменяется на эту цифру. Получается, что при нажатии на 0 ничего не будет происходить.
if btn.objectName() in digit_buttons: if self.ui.le_entry.text() == '0': self.ui.le_entry.setText(btn.text())
Если же в поле не 0, то просто добавляем текст нажатой цифры в строку поля.
else: self.ui.le_entry.setText(self.ui.le_entry.text() + btn.text())
Полный код метода добавления цифры
def add_digit(self): btn = self.sender() digit_buttons = ('btn_0', 'btn_1', 'btn_2', 'btn_3', 'btn_4', 'btn_5', 'btn_6', 'btn_7', 'btn_8', 'btn_9') if btn.objectName() in digit_buttons: if self.ui.le_entry.text() == '0': self.ui.le_entry.setText(btn.text()) else: self.ui.le_entry.setText(self.ui.le_entry.text() + btn.text())
Теперь нужно соединить нажатия кнопок с этим методом. Напишем в конструкторе класса.
# digits self.ui.btn_0.clicked.connect(self.add_digit) self.ui.btn_1.clicked.connect(self.add_digit) self.ui.btn_2.clicked.connect(self.add_digit) self.ui.btn_3.clicked.connect(self.add_digit) self.ui.btn_4.clicked.connect(self.add_digit) self.ui.btn_5.clicked.connect(self.add_digit) self.ui.btn_6.clicked.connect(self.add_digit) self.ui.btn_7.clicked.connect(self.add_digit) self.ui.btn_8.clicked.connect(self.add_digit) self.ui.btn_9.clicked.connect(self.add_digit)
Изначально был такой код. Но зачем передавать цифру-аргумент, если можно взять её из кнопки?
def add_digit(self, btn_text: str) -> None: if self.ui.le_entry.text() == '0': self.ui.le_entry.setText(btn_text) else: self.ui.le_entry.setText(self.ui.le_entry.text() + btn_text)
Соединения кнопок с методом
# digits self.ui.btn_0.clicked.connect(lambda: self.add_digit('0')) self.ui.btn_1.clicked.connect(lambda: self.add_digit('1')) self.ui.btn_2.clicked.connect(lambda: self.add_digit('2')) self.ui.btn_3.clicked.connect(lambda: self.add_digit('3')) self.ui.btn_4.clicked.connect(lambda: self.add_digit('4')) self.ui.btn_5.clicked.connect(lambda: self.add_digit('5')) self.ui.btn_6.clicked.connect(lambda: self.add_digit('6')) self.ui.btn_7.clicked.connect(lambda: self.add_digit('7')) self.ui.btn_8.clicked.connect(lambda: self.add_digit('8')) self.ui.btn_9.clicked.connect(lambda: self.add_digit('9'))
Посмотрим на результат.

Если вам режет глаз выход цифр за границы поля, потерпите. Мы решим эту проблему в следующей статье.
Очищаем Line Edit и Label
def clear_all(self) -> None: self.ui.le_entry.setText('0') self.ui.lbl_temp.clear()
Сделаем такой же метод для очистки только поля.
def clear_entry(self) -> None: self.ui.le_entry.setText('0')
Соединяем.
# actions self.ui.btn_clear.clicked.connect(self.clear_all) self.ui.btn_ce.clicked.connect(self.clear_entry)

Добавляем точку
Почему вообще точка, а не запятая? Просто число с точкой можно сразу конвертировать в вещественное число, а с запятой придется еще менять знак. Да, мне лень.
Логика проста. Если точки нет в поле, значит добавляем.
def add_point(self) -> None: if '.' not in self.ui.le_entry.text(): self.ui.le_entry.setText(self.ui.le_entry.text() + '.')
Соеди что? Правильно, няем.
self.ui.btn_point.clicked.connect(self.add_point)

Добавляем временное выражение
Что вообще оно из себя представляет? Есть два типа временных выражений:
1) Число и математический знак. Грубо говоря, это память калькулятора.

2) Равенство

def add_temp(self) -> None: btn = self.sender()
Для начала нам нужно убедиться, что в лейбле нет текста. Затем ставим во временное выражение число из поля ввода + текст кнопки btn.
if not self.ui.lbl_temp.text(): self.ui.lbl_temp.setText(self.ui.le_entry.text() + f' {btn.text()} ')
Еще нужно очистить поле ввода. Полный код метода:
def add_temp(self) -> None: btn = self.sender() if not self.ui.lbl_temp.text(): self.ui.lbl_temp.setText(self.ui.le_entry.text() + f' {btn.text()} ') self.ui.le_entry.setText('0')
Прикрутим пока одну кнопку сложения для теста.
self.ui.btn_add.clicked.connect(self.add_temp)

Точка и незначащие конечные нули не обрезаются.
Убираем незначащие конечные нули
Сделаем статический метод для решения этой проблемы. Передавать в функцию мы будем string число, получать то же самое.
@staticmethod def remove_trailing_zeros(num: str) -> str:
Введем переменную n, которая приводит аргумент сначала к типу float, потом к string.
n = str(float(num))
Приведение к float обрезает нули, но не все. В конце остается .0. Мы будем возвращать срез строки без двух последних символов, если они равны .0, иначе будем возвращать просто n.
return n[:-2] if n[-2:] == '.0' else n
Полный код метода:
@staticmethod def remove_trailing_zeros(num: str) -> str: n = str(float(num)) return n[:-2] if n[-2:] == '.0' else n
Теперь добавим обрезку незначащих нулей в метод добавления временного выражения:
def add_temp(self) -> None: btn = self.sender() entry = self.remove_trailing_zeros(self.ui.le_entry.text()) if not self.ui.lbl_temp.text(): self.ui.lbl_temp.setText(entry + f' {btn.text()} ') self.ui.le_entry.setText('0')

Старый код с передачей знака-аргумента
def add_temp(self, math_sign: str): if not self.ui.lbl_temp.text() or self.get_math_sign() == '=': self.ui.lbl_temp.setText( self.remove_trailing_zeros(self.ui.le_entry.text()) + f' {math_sign} ') self.ui.le_entry.setText('0')
Получаем число из Line Edit
Запишем в переменную текст поля, уберем потенциальную точку с помощью strip().
def get_entry_num(self): entry = self.ui.le_entry.text().strip('.') return float(entry) if '.' in entry else int(entry)
Возвращаем float, если точка есть в переменной, иначе возвращаем int, то есть целое число.
Добавим type hint к методу. Он может возвращать только целое или вещественное число. Для этого импортируем:
from typing import Union, Optional
Optional используем позже.
def get_entry_num(self) -> Union[int, float]:
В Python 3.10 не нужно ничего импортировать.
Можно просто написать
def get_entry_num(self) -> int | float:
Получаем число из Label
Если в лейбле есть текст, получаем его, разделяем по пробелам и берем первый элемент, то есть число.
def get_temp_num(self): if self.ui.lbl_temp.text(): temp = self.ui.lbl_temp.text().strip('.').split()[0] return float(temp) if '.' in temp else int(temp)
Type hint здесь - Union[int, float, None].
def get_temp_num(self) -> Union[int, float, None]:
Получаем знак из Label
Чтобы получить знак, нам нужно удостовериться в наличии текста в лейбле, затем получить текст из него, разделить по пробелам и вытащить последний элемент.
def get_math_sign(self): if self.ui.lbl_temp.text(): return self.ui.lbl_temp.text().strip('.').split()[-1]
Type hint здесь - Optional[str]. Это означает, что метод может вернуть либо строку, либо ничего. Как Union[str, None], только компактнее и читабельнее.
def get_math_sign(self) -> Optional[str]:
Вычисляем выражение
Так, калькулятор же считать должен, я правильно понимаю? Ну тогда импортируем сложение, вычитание, умножение и деление из стандартной библиотеки operator.
from operator import add, sub, mul, truediv
Теперь создадим словарь с операциями. Каждому знаку присвоим его логическую функцию.
operations = { '+': add, '−': sub, '×': mul, '/': truediv }
Создадим метод вычисления.
def calculate(self): entry = self.ui.le_entry.text() temp = self.ui.lbl_temp.text()
Если в лейбле есть текст, вводим переменную результата. Обрезаем конечные нули, приводим к строке. Берем операцию из словаря по знаку, в скобках указываем с какими числами провести операцию. Заметьте, что порядок передачи аргументов важен для деления и вычитания. Сначала мы передаем число из временного выражения, а потом из поля ввода.
if temp: result = self.remove_trailing_zeros( str(operations[self.get_math_sign()](self.get_temp_num(), self.get_entry_num())))
Добавляем в лейбл число из поля ввода и знак =
self.ui.lbl_temp.setText(temp + self.remove_trailing_zeros(entry) + ' =')
Ставим результат в поле ввода и возвращаем его.
self.ui.le_entry.setText(result) return result
Type hint - Optional[str].
def calculate(self) -> Optional[str]:
Полный код метода вычисления
def calculate(self) -> Optional[str]: entry = self.ui.le_entry.text() temp = self.ui.lbl_temp.text() if temp: result = self.remove_trailing_zeros( str(operations[self.get_math_sign()](self.get_temp_num(), self.get_entry_num()))) self.ui.lbl_temp.setText(temp + self.remove_trailing_zeros(entry) + ' =') self.ui.le_entry.setText(result) return result
Метод математической операции
def math_operation(self): temp = self.ui.lbl_temp.text() btn = self.sender()
Если в лейбле нет выражения, мы его добавляем, удивительно.
if not temp: self.add_temp()
Если выражение есть, берем знак. Если он не равен знаку нажатой кнопки, то есть два случая. Первый - это равенство. В этом случае просто добавляем временное выражение. Иначе меняем знак выражения на знак нажатой кнопки.
else: if self.get_math_sign() != btn.text(): if self.get_math_sign() == '=': self.add_temp() else: self.ui.lbl_temp.setText(temp[:-2] + f'{btn.text()} ')
Если же знак равен знаку нажатой кнопки, то мы считаем выражение и добавляем в конец лейбла этот знак.
Полный код математической операции
def math_operation(self) -> None: temp = self.ui.lbl_temp.text() btn = self.sender() if not temp: self.add_temp() else: if self.get_math_sign() != btn.text(): if self.get_math_sign() == '=': self.add_temp() else: self.ui.lbl_temp.setText(temp[:-2] + f'{btn.text()} ') else: self.ui.lbl_temp.setText(self.calculate() + f' {btn.text()}')
Полный код математической операции
def math_operation(self) -> None: temp = self.ui.lbl_temp.text() btn = self.sender() if not temp: self.add_temp() else: if self.get_math_sign() != btn.text(): if self.get_math_sign() == '=': self.add_temp() else: self.ui.lbl_temp.setText(temp[:-2] + f'{btn.text()} ') else: self.ui.lbl_temp.setText(self.calculate() + f' {btn.text()}')
Соединяем.
self.ui.btn_add.clicked.connect(self.math_operation) self.ui.btn_sub.clicked.connect(self.math_operation) self.ui.btn_mul.clicked.connect(self.math_operation) self.ui.btn_div.clicked.connect(self.math_operation)
Старый код метода вычисления со знаком-аргументом
def math_operation(self, math_sign: str): temp = self.ui.lbl_temp.text() if not temp: self.add_temp(math_sign) else: if self.get_math_sign() != math_sign: if self.get_math_sign() == '=': self.add_temp(math_sign) else: self.ui.lbl_temp.setText(temp[:-2] + f'{math_sign} ') else: self.ui.lbl_temp.setText(self.calculate() + f' {math_sign}')
self.ui.btn_add.clicked.connect(lambda: self.math_operation('+')) self.ui.btn_sub.clicked.connect(lambda: self.math_operation('−')) self.ui.btn_mul.clicked.connect(lambda: self.math_operation('×')) self.ui.btn_div.clicked.connect(lambda: self.math_operation('/'))
Помолимся за здравие Гвидо Ван Россума и запустим программу.

Почему-то не хочет дальше считать с равенством. А я вам расскажу почему. В методе добавления временного выражения нужно добавить дополнительное условие. В итоге получится "если временного выражения нет или есть равенство".
def add_temp(self) -> None: btn = self.sender() entry = self.remove_trailing_zeros(self.ui.le_entry.text()) if not self.ui.lbl_temp.text() or self.get_math_sign() == '=': self.ui.lbl_temp.setText(entry + f' {btn.text()} ') self.ui.le_entry.setText('0')

И вот еще покажу, как меняется знак, если вы постоянно промахиваетесь по кнопке.

Заключение
Штош, в следующей статье допишем калькулятор. Сделаем отрицание, backspace, несколько шорткатов для одной кнопки и обработаем ошибки. До встречи.
