Простой GUI калькулятор на Python #3. Backspace, отрицание и регулировка размера шрифта

от автора

Штош. Дописываем калькулятор. Если вы не читали прошлую статью, я вам настоятельно рекомендую это сделать.

Добавляем отрицание

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

И так далее…

Делаем код немного компактнее

Вообще это можно было сделать в самом начале, но мы сделаем в самом конце.

Заменим во всем коде:

  1. self.ui.le_entry на self.entry

  2. 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

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

Заключение

Конечно, можно найти еще уйму изъянов в работе калькулятора, но я и не претендую заменить системный. Зачем? Я думаю, получилось вполне неплохо относительно уже существующих туториалов на калькуляторы в интернетах, и надеюсь, что помог вам с изучением этого змеиного языка.


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


ссылка на оригинал статьи https://habr.com/ru/articles/589847/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *