![](https://habrastorage.org/getpro/habr/upload_files/bf8/1e6/9c0/bf81e69c0274fd243fa610ab62799505.png)
Штош. Дописываем калькулятор. Если вы не читали прошлую статью, я вам настоятельно рекомендую это сделать.
Добавляем отрицание
def negate(self) -> None:
entry = self.ui.le_entry.text()
Логика проста: если отрицания нет в поле, значит добавляем. Иначе убираем левый символ с помощью среза [1:]. Не забываем ввести дополнительное условие для нуля.
if '-' not in entry:
if entry != '0':
entry = '-' + entry
else:
entry = entry[1:]
self.entry.setText(entry)
Возникает такая проблемка: при максимальной длине отрицание вытесняет последнюю цифру. Так работать, конечно же, не должно.
![](https://habrastorage.org/getpro/habr/upload_files/a88/f2b/678/a88f2b67883320da8a0110ef722045ca.gif)
Давайте добавим в конструктор переменную максимальной длины поля ввода. Взять её можно с помощью метода maxLength.
self.entry_max_len = self.ui.le_entry.maxLength()
Если длина строки больше этой переменной на единицу и в строке есть отрицание, то ставим максимальную длину поля больше на единицу. Иначе ставим обратно дефолтную максимальную длину.
if len(entry) == self.entry_max_len + 1 and '-' in entry:
self.entry.setMaxLength(self.entry_max_len + 1)
else:
self.entry.setMaxLength(self.entry_max_len)
![](https://habrastorage.org/getpro/habr/upload_files/246/ce9/cd1/246ce9cd1e1a819530cc38d4a7d41807.gif)
Backspace
Когда длина поля равна 1
, нажатие на Backspace ставит в поле 0
. Еще он ставит 0
, когда в поле есть одна цифра с отрицанием. Во всех остальных случаях кнопка Backspace обрезает последний правый символ.
def backspace(self) -> None:
entry = self.ui.le_entry.text()
if len(entry) != 1:
if len(entry) == 2 and '-' in entry:
self.ui.le_entry.setText('0')
else:
self.ui.le_entry.setText(entry[:-1])
else:
self.ui.le_entry.setText('0')
![](https://habrastorage.org/getpro/habr/upload_files/546/809/c0c/546809c0c9403bbbd1c4883b53892ff6.gif)
Удаляем равенство из Label
Когда во временном выражении есть равенство, следующая нажатая кнопка должна удалять его из лейбла.
def clear_temp_if_equality(self) -> None:
if self.get_math_sign() == '=':
self.ui.lbl_temp.clear()
Но удалять должна не любая кнопка, а цифра, точка, отрицание, Backspace и очищение поля ввода.
![](https://habrastorage.org/getpro/habr/upload_files/163/59d/869/16359d8699d13fc19c9191e8a829cacf.gif)
Обрабатываем исключения
При нажатии на кнопку "равно", когда во временном выражении уже есть равенство, программа выкидывает KeyError.
Добавим в функцию вычисления конструкцию try-except. Вообще, в калькуляторе Windows оно продолжает считать, но у нас будет лучше - у нас ничего не будет происходить.
if temp:
try:
...
except KeyError:
pass
Куда же без ошибки деления на ноль - ZeroDivisionError. Напишем 2 переменные с текстом для показа ошибки.
error_zero_div = 'Division by zero'
error_undefined = 'Result is undefined'
Напишем метод для показа ошибки, передадим в него текст. Сначала ставим максимальную длину поля, равную длине текста ошибки, а затем уже ставим сам текст.
def show_error(self, text: str) -> None:
self.ui.le_entry.setMaxLength(len(text))
self.ui.le_entry.setText(text)
Если число в лейбле равно нулю, то ставим ошибку "результат не определен". Иначе ставим простое сообщение о делении на ноль.
except KeyError:
pass
except ZeroDivisionError:
if self.get_temp_num() == 0:
self.show_error(error_undefined)
else:
self.show_error(error_zero_div)
![](https://habrastorage.org/getpro/habr/upload_files/4f0/2fe/bea/4f02febea674776cd4b6f9a770d16850.gif)
Когда в лейбле есть 0 /
, деление вызывает TypeError.
def math_operation(self) -> None:
...
else:
try:
self.temp.setText(self.calculate() + f' {btn.text()} ')
except TypeError:
pass
Убираем ошибки
Если текст в поле равен какой-то ошибке, то ставим максимальную длину поля обратно к дефолтному значению и ставим текст 0
def remove_error(self) -> None:
if self.ui.le_entry.text() in (error_undefined, error_zero_div):
self.ui.le_entry.setMaxLength(self.entry_max_len)
self.ui.le_entry.setText('0')
Убирать ошибку нужно в начале методов добавления цифры, backspace и очищения полей. Почему только они? Мы заблокируем кнопки знаков, точки и отрицания.
Блокируем кнопки
Для этого существует метод setDisabled, в который нужно передавать логическую переменную: чтобы заблокировать - True, а чтобы включить - False.
def disable_buttons(self) -> None:
self.ui.btn_calc.setDisabled(True)
self.ui.btn_add.setDisabled(True)
self.ui.btn_sub.setDisabled(True)
self.ui.btn_mul.setDisabled(True)
self.ui.btn_div.setDisabled(True)
self.ui.btn_neg.setDisabled(True)
self.ui.btn_point.setDisabled(True)
Блокируем кнопки в конце метода показа ошибки.
![](https://habrastorage.org/getpro/habr/upload_files/52a/f5c/8df/52af5c8df9c65c760eccc4dbb2e86c6f.gif)
Смотрите, на кнопки нельзя кликнуть, но по интерфейсу так сразу и не скажешь, пока не наведёшь. Нужно сделать текст кнопок серым.
Меняем цвет кнопок
Напишем метод изменения цвета кнопок. Мы будем передавать в него css строку с цветом.
def change_buttons_color(self, css_color: str) -> None:
self.ui.btn_calc.setStyleSheet(css_color)
self.ui.btn_add.setStyleSheet(css_color)
self.ui.btn_sub.setStyleSheet(css_color)
self.ui.btn_mul.setStyleSheet(css_color)
self.ui.btn_div.setStyleSheet(css_color)
self.ui.btn_neg.setStyleSheet(css_color)
self.ui.btn_point.setStyleSheet(css_color)
Для блокировки у нас будет серый цвет #888.
def disable_buttons(self, disable: bool) -> None:
...
self.change_buttons_color('color: #888;')
![](https://habrastorage.org/getpro/habr/upload_files/17b/2f5/aca/17b2f5aca1bf43e25a43cb224a821e37.png)
Включаем кнопки
Передаем логическую переменную в метод блокировки и ставим её же в setDisabled для кнопок.
def disable_buttons(self, disable: bool) -> None:
self.ui.btn_calc.setDisabled(disable)
self.ui.btn_add.setDisabled(disable)
self.ui.btn_sub.setDisabled(disable)
self.ui.btn_mul.setDisabled(disable)
self.ui.btn_div.setDisabled(disable)
self.ui.btn_neg.setDisabled(disable)
self.ui.btn_point.setDisabled(disable)
Еще нужно вернуть кнопкам белый цвет.
color = 'color: #888;' if disable else 'color: white;'
self.change_buttons_color(color)
Проставим в методы показа и удаления ошибки.
def show_error(self, text: str) -> None:
...
self.disable_buttons(True)
def remove_error(self) -> None:
if self.ui.le_entry.text() in (error_undefined, error_zero_div):
...
self.disable_buttons(False)
![](https://habrastorage.org/getpro/habr/upload_files/170/182/179/170182179946fdd279118e00ba7a26d0.gif)
Регулируем размер шрифта
Для начала введем 2 переменные с размерами шрифтов:
default_font_size = 16
default_entry_font_size = 40
Теперь создадим методы получения ширины текста в пикселях для поля и лейбла:
def get_entry_text_width(self) -> int:
return self.ui.le_entry.fontMetrics().boundingRect(
self.ui.le_entry.text()).width()
def get_temp_text_width(self) -> int:
return self.ui.lbl_temp.fontMetrics().boundingRect(
self.ui.lbl_temp.text()).width()
Регулируем размер шрифта в поле ввода. Пока ширина текста больше ширины окна (-15, так будет лучше), мы уменьшаем размер шрифта на единицу.
def adjust_entry_font_size(self) -> None:
font_size = default_entry_font_size
while self.get_entry_text_width() > self.ui.le_entry.width() - 15:
font_size -= 1
self.ui.le_entry.setStyleSheet('font-size: ' + str(font_size) + 'pt; border: none;')
Нужно проставить этот метод после любого изменения длины текста в поле ввода.
Ставим после self.ui.le_entry.setText*
def add_digit(self):
...
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())
self.adjust_entry_font_size()
def add_point(self) -> None:
self.clear_temp_if_equality()
if '.' not in self.ui.le_entry.text():
self.ui.le_entry.setText(self.ui.le_entry.text() + '.')
self.adjust_entry_font_size()
И так далее..
![](https://habrastorage.org/getpro/habr/upload_files/28f/be5/9a7/28fbe59a79a00b43582787718c06cc4d.gif)
Мы только уменьшаем размер шрифта, нужно его еще увеличивать при уменьшении ширины текста и увеличении ширины окна.
Пока ширина текста меньше ширины поля (-60, так будет лучше), увеличиваем размер шрифта, но не больше дефолтного значения.
def adjust_entry_font_size(self) -> None:
...
font_size = 1
while self.get_entry_text_width() < self.ui.le_entry.width() - 60:
font_size += 1
if font_size > default_entry_font_size:
break
self.ui.le_entry.setStyleSheet(
'font-size: ' + str(font_size) + 'pt; border: none;')
![](https://habrastorage.org/getpro/habr/upload_files/f26/c56/366/f26c563665d685cfc86c773215eac003.gif)
Как регулировать размер шрифта при изменении ширины окна приложения? Очень просто, нужно использовать встроенный resizeEvent:
def resizeEvent(self, event) -> None:
self.adjust_entry_font_size()
![](https://habrastorage.org/getpro/habr/upload_files/eab/b22/33e/eabb2233ea10f412d0011ceb738b1cb7.gif)
Регулируем размер шрифта во временном выражении
То же самое проворачиваем для временного выражения.
Полный код метода
def adjust_temp_font_size(self) -> None:
font_size = default_font_size
while self.get_temp_text_width() > self.ui.lbl_temp.width() - 10:
font_size -= 1
self.ui.lbl_temp.setStyleSheet(
'font-size: ' + str(font_size) + 'pt; color: #888;')
font_size = 1
while self.get_temp_text_width() < self.ui.lbl_temp.width() - 60:
font_size += 1
if font_size > default_font_size:
break
self.ui.lbl_temp.setStyleSheet(
'font-size: ' + str(font_size) + 'pt; color: #888;')
Проставляем после изменения длины тексты в лейбле
Ставим после self.ui.lbl_temp.setText*
и self.ui.lbl_temp.clear()
def add_temp(self) -> None:
...
if not self.ui.lbl_temp.text() or self.get_math_sign() == '=':
self.ui.lbl_temp.setText(entry + f' {btn.text()} ')
self.adjust_temp_font_size()
...
def clear_all(self) -> None:
...
self.ui.lbl_temp.clear()
self.adjust_temp_font_size()
И так далее...
Делаем код немного компактнее
Вообще это можно было сделать в самом начале, но мы сделаем в самом конце.
Заменим во всем коде:
self.ui.le_entry
наself.entry
self.ui.lbl_temp
наself.temp
Введем 2 переменные для поля и временного выражения в конструкторе класса:
self.entry = self.ui.le_entry
self.temp = self.ui.lbl_temp
Проблема с вычислениями вещественных чисел
![](https://habrastorage.org/getpro/habr/upload_files/5ca/02a/a79/5ca02aa79372d650648039d43e8db5bb.gif)
Эта проблема не связана конкретно с питоном, она присутствует и в других языках, вот вам пример в JavaScript.
![](https://habrastorage.org/getpro/habr/upload_files/d62/93b/1d2/d6293b1d27408e3582542d89f88cd44a.png)
Все дело в том, что вещественные числа не могут быть точно представлены из-за особенностей их реализации в двоичном виде. Если вы заинтересовались темой, вы можете посмотреть про арифметику вещественных чисел и про стандарт 754.
Ну а как бороться с этой проблемой? Можно использовать модуль decimal, вот пример его работы:
from decimal import *
print('1.2 - 1 =', Decimal('1.2') - Decimal('1'))
print('3.4 + 4.3 =', Decimal('3.4') + Decimal('4.3'))
print('0.2 + 0.1 =', Decimal('0.2') + Decimal('0.1'))
1.2 - 1 = 0.2
3.4 + 4.3 = 7.7
0.2 + 0.1 = 0.3
Вот вам и задача со звездочкой, переделайте вычисления калькулятора с этим модулем.
Заключение
Конечно, можно найти еще уйму изъянов в работе калькулятора, но я и не претендую заменить системный. Зачем? Я думаю, получилось вполне неплохо относительно уже существующих туториалов на калькуляторы в интернетах, и надеюсь, что помог вам с изучением этого змеиного языка.