Штош. Дописываем калькулятор. Если вы не читали прошлую статью, я вам настоятельно рекомендую это сделать.
Добавляем отрицание
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
Вот вам и задача со звездочкой, переделайте вычисления калькулятора с этим модулем.
Заключение
Конечно, можно найти еще уйму изъянов в работе калькулятора, но я и не претендую заменить системный. Зачем? Я думаю, получилось вполне неплохо относительно уже существующих туториалов на калькуляторы в интернетах, и надеюсь, что помог вам с изучением этого змеиного языка.
Комментарии (11)
zoldaten
18.11.2021 09:56+1почему взяли pyside,а не qt6 ?
lesskop Автор
18.11.2021 14:49+1Не хочу распинаться про лицензии, быстродействие и остальное, поэтому отвечу просто - захотел PySide, сделал на PySide.
Хотя сейчас прихожу к выводу, что для open-source проектов лучше использовать PyQt.
zoldaten
18.11.2021 15:50у меня были сложности при совмещении двух фреймворков. например, из окна, написанного на pyqt5 открыть окно, написанное на pyside5. пришлось переписать на pyqt5. было у вас что-то похожее ?
lesskop Автор
18.11.2021 16:03Пока что не приходилось совмещать, да я и не придумаю такой ситуации, где мне это понадобилось бы.
zoldaten
18.11.2021 16:25допустим, есть окно-родитель на pyqt5 с кнопкой, при нажатии на которою, открывается дочернее окно на pyside5:
окно
# app.py import sys from PySide2.QtWidgets import ( QWidget, QLineEdit, QScrollArea, QMainWindow, QApplication, QVBoxLayout, QSpacerItem, QSizePolicy, QCompleter ) from PySide2.QtCore import Qt from customwidgets2 import OnOffWidget class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super().__init__() self.controls = QWidget() # Controls container widget. self.controlsLayout = QVBoxLayout() # Controls container layout. # List of names, widgets are stored in a dictionary by these keys. #ряды a = map(str, range(0, 10)) #print(a) widget_names = a self.widgets = [] # Iterate the names, creating a new OnOffWidget for # each one, adding it to the layout and # and storing a reference in the self.widgets dict for name in widget_names: item = OnOffWidget(name) self.controlsLayout.addWidget(item) self.widgets.append(item) spacer = QSpacerItem(1, 1, QSizePolicy.Minimum, QSizePolicy.Expanding) self.controlsLayout.addItem(spacer) self.controls.setLayout(self.controlsLayout) # Scroll Area Properties. self.scroll = QScrollArea() self.scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setWidgetResizable(True) self.scroll.setWidget(self.controls) # Search bar. self.searchbar = QLineEdit() self.searchbar.textChanged.connect(self.update_display) # Adding Completer. self.completer = QCompleter(widget_names) self.completer.setCaseSensitivity(Qt.CaseInsensitive) self.searchbar.setCompleter(self.completer) # Add the items to VBoxLayout (applied to container widget) # which encompasses the whole window. container = QWidget() containerLayout = QVBoxLayout() containerLayout.addWidget(self.searchbar) containerLayout.addWidget(self.scroll) container.setLayout(containerLayout) self.setCentralWidget(container) self.setGeometry(600, 100, 800, 600) self.setWindowTitle('Инвентаризация. Работа.') def update_display(self, text): for widget in self.widgets: if text.lower() in widget.name.lower(): widget.show() else: widget.hide() app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec_())
# customwidgets.py from PySide2.QtWidgets import (QWidget, QLabel, QPushButton, QHBoxLayout) class OnOffWidget(QWidget): def __init__(self, name): super(OnOffWidget, self).__init__() self.name = name self.is_on = False self.is_pause = True self.is_off = False self.lbl = QLabel(self.name) self.lbl2 = QLabel('') self.btn_on = QPushButton("Старт") self.btn_pause = QPushButton("Пауза") self.btn_off = QPushButton("Завершить ряд") self.hbox = QHBoxLayout() self.hbox.addWidget(self.lbl) self.hbox.addWidget(self.lbl2) self.hbox.addWidget(self.btn_on) self.hbox.addWidget(self.btn_pause) self.hbox.addWidget(self.btn_off) self.btn_on.clicked.connect(self.on) self.btn_pause.clicked.connect(self.on_pause) self.btn_off.clicked.connect(self.off) self.setLayout(self.hbox) self.update_button_state() def show(self): """ Show this widget, and all child widgets. """ for w in [self, self.lbl, self.btn_on, self.btn_off]: w.setVisible(True) def hide(self): """ Hide this widget, and all child widgets. """ for w in [self, self.lbl, self.btn_on, self.btn_off]: w.setVisible(False) #действия кнопок def on(self): #старт self.is_on = True self.is_pause = False self.is_off = False self.update_button_state() def off(self): #завершить ряд self.is_on = False self.is_pause = False self.update_button_state() def on_pause(self): #пауза self.is_pause = True self.is_on = False self.is_off = False self.update_button_state() #обновление цвета кнопок согласно статуса def update_button_state(self): if self.is_on == True: self.btn_on.setStyleSheet("background-color: #4CAF50; color: #fff;") self.btn_pause.setStyleSheet("background-color: none; color: none;") self.btn_off.setStyleSheet("background-color: none; color: none;") self.lbl2.setText("В процессе") # self.name - № ряда elif self.is_pause == True: self.btn_on.setStyleSheet("background-color: none; color: none;") self.btn_pause.setStyleSheet("background-color: #316ad3; color: #fff;") self.btn_off.setStyleSheet("background-color: none; color: none;") self.lbl2.setText("На паузе") else: self.btn_on.setStyleSheet("background-color: none; color: none;") self.btn_pause.setStyleSheet("background-color: none; color: none;") self.btn_off.setStyleSheet("background-color: #D32F2F; color: #fff;") self.lbl2.setText("Ряд завершен")
pyside5 приложение также использует class MainWindow(QMainWindow). А должно быть что-то вроде class Ui_object(object): для целей pyqt5. Попытка склеить эти два фреймворка.
Mavsent
18.11.2021 12:02Статейка очень классная, но у меня сразу возникает вопрос, как работать с длинными числами Прошу прощения если скажу не правильно, но имел ввиду числа больше BigInteger.
lesskop Автор
18.11.2021 15:10+4В Python нет таких понятий, как big integer, long integer, есть только integer, и он поддерживает настолько длинные числа, насколько вы захотите.
К сожалению, в калькуляторе из статьи есть проблемы с вычислениями длинных чисел, не реализовал правильно. Еще есть очевиднейшая проблема с обрезанием чисел до максимальной длины поля в 16 символов. Ее можно решить округлением, но тогда возникнет проблема с экспоненциальной записью числа. Да и вообще лучше переделать нативные вычисления.
Посчитал, что уже и так достаточно сделал. Да и до сих пор так считаю :)
Mavsent
19.11.2021 03:02+2Вообще этого с головой хватит для написания подобного калькулятора новичку. Кто захочет сам запотеет и сделает остальное.
sci_nov
Несмотря на простоту калькулятора, его реализация не совсем простая: фактически это state machine.