Простой GUI калькулятор на Python #2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Штош. В прошлой статье мы сделали дизайн калькулятора. Ну а зачем нам этот голый дизайн без функционала, правильно?

Импортируем библиотеки, следуя стилю 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, то просто добавляем текст нажатой цифры в строку поля.

Полный код метода добавления цифры:

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'))

Посмотрим на результат.

Если вам режет глаз выход цифр за границы поля, потерпите. Мы решим эту проблему в следующей статье.

Теперь напишем метод для очистки поля и лейбла.

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')

Создадим метод для получения числа из поля ввода. Запишем в переменную текст поля, уберем потенциальную точку с помощью strip().

Возвращаем float, если точка есть в переменной, иначе возвращаем int, то есть целое число.

def get_entry_num(self):
	entry = self.ui.le_entry.text().strip('.')
	return float(entry) if '.' in entry else int(entry)

Добавим 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:

Создадим метод для получения числа из временного выражения. Если в лейбле есть текст, получаем его, разделяем по пробелам и берем первый элемент, то есть число.

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]:

Создадим метод для получения знака из временного выражения. Чтобы получить знак, нам нужно удостовериться в наличии текста в лейбле, затем получить текст из него, разделить по пробелам и вытащить последний элемент.

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

Присоединяем.

# math
self.ui.btn_calc.clicked.connect(self.calculate)

И наконец-то, напишем функцию математической операции.

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()}')

Соединяем.

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, несколько шорткатов для одной кнопки и обработаем ошибки. До встречи.


Репозиторий на GitHub

Источник: https://habr.com/ru/post/587276/


Интересные статьи

Интересные статьи

В прошлом месяце я создал простой проект, разошедшийся по различным техническим сообществам и социальным сетям. В Github он за 7 дней получил с 0 до 4 тысяч звёзд и более 200 форков. ...
Все делают это. Ну ладно, не все, но большинство. Пишут скрипты, чтобы симулировать свои проекты на Verilog, SystemVerilog и VHDL. Однако, написание и поддержка таких скр...
Приветствую! Сегодня речь снова пойдет о библиотеке KivyMD — наборе виджетов для кроссплатформенной разработки на Python в стиле Material Design. В этой статье я сделаю не обзор виджетов KivyMD...
В наши дни, если вы пишете некое Python-приложение, то вам, скорее всего, придётся оснащать его функционалом HTTP-клиента, который способен общаться с HTTP-серверами. Повсеместное распространение...
Если вы читаете эту статью, вероятно, вы уже знакомы с возможностями, которые открываются при использовании API (Application Programming Interface). Добавив в свое приложение один из мног...