Как стать автором
Обновить

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

Время на прочтение6 мин
Количество просмотров13K

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

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

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

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 6: ↑6 и ↓0+6
Комментарии11

Публикации

Истории

Работа

Python разработчик
136 вакансий
QT разработчик
8 вакансий
Data Scientist
61 вакансия

Ближайшие события