Штош. Дописываем калькулятор. Если вы не читали прошлую статью, я вам настоятельно рекомендую это сделать.
Добавляем отрицание
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)
Возникает такая проблемка: при максимальной длине отрицание вытесняет последнюю цифру. Так работать, конечно же, не должно.
Давайте добавим в конструктор переменную максимальной длины поля ввода. Взять её можно с помощью метода 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)
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')
Удаляем равенство из Label
Когда во временном выражении есть равенство, следующая нажатая кнопка должна удалять его из лейбла.
def clear_temp_if_equality(self) -> None:
if self.get_math_sign() == '=':
self.ui.lbl_temp.clear()
Но удалять должна не любая кнопка, а цифра, точка, отрицание, Backspace и очищение поля ввода.
Обрабатываем исключения
При нажатии на кнопку "равно", когда во временном выражении уже есть равенство, программа выкидывает 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)
Когда в лейбле есть 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)
Блокируем кнопки в конце метода показа ошибки.
Смотрите, на кнопки нельзя кликнуть, но по интерфейсу так сразу и не скажешь, пока не наведёшь. Нужно сделать текст кнопок серым.
Меняем цвет кнопок
Напишем метод изменения цвета кнопок. Мы будем передавать в него 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;')
Включаем кнопки
Передаем логическую переменную в метод блокировки и ставим её же в 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)
Регулируем размер шрифта
Для начала введем 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()
И так далее..
Мы только уменьшаем размер шрифта, нужно его еще увеличивать при уменьшении ширины текста и увеличении ширины окна.
Пока ширина текста меньше ширины поля (-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;')
Как регулировать размер шрифта при изменении ширины окна приложения? Очень просто, нужно использовать встроенный resizeEvent:
def resizeEvent(self, event) -> None:
self.adjust_entry_font_size()
Регулируем размер шрифта во временном выражении
То же самое проворачиваем для временного выражения.
Полный код метода
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
Проблема с вычислениями вещественных чисел
Эта проблема не связана конкретно с питоном, она присутствует и в других языках, вот вам пример в JavaScript.
Все дело в том, что вещественные числа не могут быть точно представлены из-за особенностей их реализации в двоичном виде. Если вы заинтересовались темой, вы можете посмотреть про арифметику вещественных чисел и про стандарт 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
Вот вам и задача со звездочкой, переделайте вычисления калькулятора с этим модулем.
Заключение
Конечно, можно найти еще уйму изъянов в работе калькулятора, но я и не претендую заменить системный. Зачем? Я думаю, получилось вполне неплохо относительно уже существующих туториалов на калькуляторы в интернетах, и надеюсь, что помог вам с изучением этого змеиного языка.