Введение в проект

В этом проекте мы создадим полнофункциональный калькулятор с графическим интерфейсом пользователя (GUI). Этот проект объединит знания по работе с функциями, обработке исключений, объектно-ориентированным программированием и использованию внешних библиотек. Мы будем использовать библиотеку tkinter для создания интерфейса.

Что вы узнаете

Планирование проекта

Перед началом реализации определим основные компоненты калькулятора:

Функциональные требования
  • Графический интерфейс с цифровой клавиатурой
  • Кнопки для основных математических операций (+, -, *, /)
  • Кнопки для дополнительных функций (очистка, удаление, точка)
  • Дисплей для отображения вводимых данных и результатов
  • Обработка ошибок (деление на ноль, неверный ввод)
  • Поддержка десятичных чисел
  • Возможность выполнения последовательных операций

Реализация калькулятора

Создадим класс Calculator, который будет содержать всю логику работы калькулятора:

Базовая реализация калькулятора
import tkinter as tk
from tkinter import messagebox
import math

class Calculator:
    def __init__(self):
        self.окно = tk.Tk()
        self.окно.title("Калькулятор")
        self.окно.geometry("300x400")
        self.окно.resizable(False, False)
        
        # Переменные для хранения состояния
        self.текущее_значение = "0"
        self.предыдущее_значение = ""
        self.операция = ""
        self.новый_ввод = True
        
        self.создать_интерфейс()
    
    def создать_интерфейс(self):
        # Дисплей
        self.дисплей = tk.Entry(
            self.окно,
            font=("Arial", 20),
            justify="right",
            state="readonly",
            bg="white"
        )
        self.дисплей.grid(row=0, column=0, columnspan=4, padx=10, pady=10, sticky="ew")
        self.обновить_дисплей()
        
        # Кнопки
        кнопки = [
            ('C', 1, 0, 'clear'),
            ('←', 1, 1, 'backspace'),
            ('/', 1, 2, 'операция'),
            ('*', 1, 3, 'операция'),
            ('7', 2, 0, 'цифра'),
            ('8', 2, 1, 'цифра'),
            ('9', 2, 2, 'цифра'),
            ('-', 2, 3, 'операция'),
            ('4', 3, 0, 'цифра'),
            ('5', 3, 1, 'цифра'),
            ('6', 3, 2, 'цифра'),
            ('+', 3, 3, 'операция'),
            ('1', 4, 0, 'цифра'),
            ('2', 4, 1, 'цифра'),
            ('3', 4, 2, 'цифра'),
            ('=', 4, 3, 'равно', 2),
            ('0', 5, 0, 'цифра', 2),
            ('.', 5, 2, 'точка'),
        ]
        
        for (текст, строка, столбец, тип_кнопки, *args) in кнопки:
            if тип_кнопки == 'цифра':
                команда = lambda x=текст: self.ввод_цифры(x)
            elif тип_кнопки == 'операция':
                команда = lambda x=текст: self.ввод_операции(x)
            elif тип_кнопки == 'равно':
                команда = self.вычислить
            elif тип_кнопки == 'clear':
                команда = self.очистить
            elif тип_кнопки == 'backspace':
                команда = self.удалить_символ
            elif тип_кнопки == 'точка':
                команда = self.ввод_точки
            
            кнопка = tk.Button(
                self.окно,
                text=текст,
                font=("Arial", 14),
                command=команда
            )
            
            # Определяем размер кнопки
            columnspan = args[0] if args else 1
            if текст == '=':
                кнопка.grid(row=строка, column=столбец, columnspan=columnspan, 
                           padx=2, pady=2, sticky="nsew")
            elif текст == '0':
                кнопка.grid(row=строка, column=столбец, columnspan=columnspan,
                           padx=2, pady=2, sticky="nsew")
            else:
                кнопка.grid(row=строка, column=столбец, columnspan=columnspan,
                           padx=2, pady=2, sticky="nsew")
        
        # Настройка сетки
        for i in range(6):
            self.окно.grid_rowconfigure(i, weight=1)
        for i in range(4):
            self.окно.grid_columnconfigure(i, weight=1)
    
    def обновить_дисплей(self):
        self.дисплей.config(state="normal")
        self.дисплей.delete(0, tk.END)
        self.дисплей.insert(0, self.текущее_значение)
        self.дисплей.config(state="readonly")
    
    def ввод_цифры(self, цифра):
        if self.новый_ввод:
            self.текущее_значение = цифра
            self.новый_ввод = False
        else:
            if self.текущее_значение == "0":
                self.текущее_значение = цифра
            else:
                self.текущее_значение += цифра
        self.обновить_дисплей()
    
    def ввод_точки(self):
        if self.новый_ввод:
            self.текущее_значение = "0."
            self.новый_ввод = False
        elif "." not in self.текущее_значение:
            self.текущее_значение += "."
        self.обновить_дисплей()
    
    def ввод_операции(self, операция):
        if self.операция and not self.новый_ввод:
            self.вычислить()
        
        self.предыдущее_значение = self.текущее_значение
        self.операция = операция
        self.новый_ввод = True
    
    def вычислить(self):
        try:
            if self.операция and self.предыдущее_значение:
                выражение = f"{self.предыдущее_значение} {self.операция} {self.текущее_значение}"
                # Заменяем символы для eval
                выражение = выражение.replace("×", "*").replace("÷", "/")
                результат = eval(выражение)
                
                # Форматируем результат
                if результат == int(результат):
                    self.текущее_значение = str(int(результат))
                else:
                    self.текущее_значение = str(round(результат, 10))
                
                self.операция = ""
                self.предыдущее_значение = ""
                self.новый_ввод = True
                self.обновить_дисплей()
        except Exception as e:
            messagebox.showerror("Ошибка", "Неверное выражение")
            self.очистить()
    
    def очистить(self):
        self.текущее_значение = "0"
        self.предыдущее_значение = ""
        self.операция = ""
        self.новый_ввод = True
        self.обновить_дисплей()
    
    def удалить_символ(self):
        if not self.новый_ввод:
            if len(self.текущее_значение) > 1:
                self.текущее_значение = self.текущее_значение[:-1]
            else:
                self.текущее_значение = "0"
                self.новый_ввод = True
            self.обновить_дисплей()
    
    def запустить(self):
        self.окно.mainloop()

# Запуск калькулятора
if __name__ == "__main__":
    калькулятор = Calculator()
    калькулятор.запустить()

Улучшенная версия калькулятора

Добавим дополнительные функции и улучшим интерфейс:

Расширенная реализация
import tkinter as tk
from tkinter import messagebox
import math

class AdvancedCalculator:
    def __init__(self):
        self.окно = tk.Tk()
        self.окно.title("Продвинутый Калькулятор")
        self.окно.geometry("400x500")
        self.окно.resizable(False, False)
        self.окно.configure(bg="#f0f0f0")
        
        # Переменные для хранения состояния
        self.текущее_значение = "0"
        self.предыдущее_значение = ""
        self.операция = ""
        self.новый_ввод = True
        self.выражение = ""
        
        self.создать_интерфейс()
    
    def создать_интерфейс(self):
        # Дисплей для выражения
        self.дисплей_выражения = tk.Entry(
            self.окно,
            font=("Arial", 12),
            justify="right",
            state="readonly",
            bg="#e0e0e0",
            relief="flat"
        )
        self.дисплей_выражения.grid(row=0, column=0, columnspan=5, 
                                   padx=10, pady=(10, 0), sticky="ew")
        
        # Основной дисплей
        self.дисплей = tk.Entry(
            self.окно,
            font=("Arial", 20, "bold"),
            justify="right",
            state="readonly",
            bg="white",
            relief="solid",
            bd=2
        )
        self.дисплей.grid(row=1, column=0, columnspan=5, 
                         padx=10, pady=(0, 10), sticky="ew")
        
        self.обновить_дисплей()
        
        # Кнопки
        кнопки = [
            # Первая строка - дополнительные функции
            ('C', 2, 0, 'clear', '#ff6666'),
            ('CE', 2, 1, 'очистить_ввод', '#ff9999'),
            ('⌫', 2, 2, 'backspace', '#ffcc99'),
            ('÷', 2, 3, 'операция', '#ffcc99'),
            ('√', 2, 4, 'функция', '#99ccff'),
            
            # Вторая строка
            ('7', 3, 0, 'цифра', '#ffffff'),
            ('8', 3, 1, 'цифра', '#ffffff'),
            ('9', 3, 2, 'цифра', '#ffffff'),
            ('×', 3, 3, 'операция', '#ffcc99'),
            ('x²', 3, 4, 'функция', '#99ccff'),
            
            # Третья строка
            ('4', 4, 0, 'цифра', '#ffffff'),
            ('5', 4, 1, 'цифра', '#ffffff'),
            ('6', 4, 2, 'цифра', '#ffffff'),
            ('-', 4, 3, 'операция', '#ffcc99'),
            ('1/x', 4, 4, 'функция', '#99ccff'),
            
            # Четвертая строка
            ('1', 5, 0, 'цифра', '#ffffff'),
            ('2', 5, 1, 'цифра', '#ffffff'),
            ('3', 5, 2, 'цифра', '#ffffff'),
            ('+', 5, 3, 'операция', '#ffcc99'),
            ('=', 5, 4, 'равно', '#66cc99', 2),
            
            # Пятая строка
            ('±', 6, 0, 'функция', '#ffffff'),
            ('0', 6, 1, 'цифра', '#ffffff', 2),
            ('.', 6, 3, 'точка', '#ffffff'),
        ]
        
        for элемент in кнопки:
            if len(элемент) == 5:
                текст, строка, столбец, тип_кнопки, цвет = элемент
                rowspan = 1
                columnspan = 1
            elif len(элемент) == 6:
                текст, строка, столбец, тип_кнопки, цвет, размер = элемент
                if размер == 2:
                    rowspan = 1
                    columnspan = 2
                else:
                    rowspan = размер
                    columnspan = 1
            else:
                continue
            
            # Определяем команду для кнопки
            if тип_кнопки == 'цифра':
                команда = lambda x=текст: self.ввод_цифры(x)
            elif тип_кнопки == 'операция':
                команда = lambda x=текст: self.ввод_операции(x)
            elif тип_кнопки == 'равно':
                команда = self.вычислить
            elif тип_кнопки == 'clear':
                команда = self.очистить
            elif тип_кнопки == 'очистить_ввод':
                команда = self.очистить_ввод
            elif тип_кнопки == 'backspace':
                команда = self.удалить_символ
            elif тип_кнопки == 'точка':
                команда = self.ввод_точки
            elif тип_кнопки == 'функция':
                if текст == '√':
                    команда = lambda: self.применить_функцию('sqrt')
                elif текст == 'x²':
                    команда = lambda: self.применить_функцию('square')
                elif текст == '1/x':
                    команда = lambda: self.применить_функцию('reciprocal')
                elif текст == '±':
                    команда = self.сменить_знак
                else:
                    команда = lambda: None
            
            кнопка = tk.Button(
                self.окно,
                text=текст,
                font=("Arial", 12, "bold"),
                command=команда,
                bg=цвет,
                relief="raised",
                bd=1
            )
            
            # Размещаем кнопку
            if текст == '=':
                кнопка.grid(row=строка, column=столбец, rowspan=rowspan, columnspan=columnspan,
                           padx=2, pady=2, sticky="nsew")
            elif текст == '0':
                кнопка.grid(row=строка, column=столбец, columnspan=columnspan,
                           padx=2, pady=2, sticky="nsew")
            else:
                кнопка.grid(row=строка, column=столбец, columnspan=columnspan,
                           padx=2, pady=2, sticky="nsew")
        
        # Настройка сетки
        for i in range(7):
            self.окно.grid_rowconfigure(i, weight=1)
        for i in range(5):
            self.окно.grid_columnconfigure(i, weight=1)
    
    def обновить_дисплей(self):
        self.дисплей.config(state="normal")
        self.дисплей.delete(0, tk.END)
        self.дисплей.insert(0, self.текущее_значение)
        self.дисплей.config(state="readonly")
        
        self.дисплей_выражения.config(state="normal")
        self.дисплей_выражения.delete(0, tk.END)
        self.дисплей_выражения.insert(0, self.выражение)
        self.дисплей_выражения.config(state="readonly")
    
    def ввод_цифры(self, цифра):
        if self.новый_ввод:
            self.текущее_значение = цифра
            self.новый_ввод = False
        else:
            if self.текущее_значение == "0":
                self.текущее_значение = цифра
            else:
                self.текущее_значение += цифра
        self.обновить_дисплей()
    
    def ввод_точки(self):
        if self.новый_ввод:
            self.текущее_значение = "0."
            self.новый_ввод = False
        elif "." not in self.текущее_значение:
            self.текущее_значение += "."
        self.обновить_дисплей()
    
    def ввод_операции(self, операция):
        if self.операция and not self.новый_ввод:
            self.вычислить()
        
        self.предыдущее_значение = self.текущее_значение
        self.операция = операция
        self.выражение = f"{self.предыдущее_значение} {операция}"
        self.новый_ввод = True
        self.обновить_дисплей()
    
    def вычислить(self):
        try:
            if self.операция and self.предыдущее_значение:
                выражение = f"{self.предыдущее_значение} {self.операция} {self.текущее_значение}"
                # Заменяем символы для eval
                выражение = выражение.replace("×", "*").replace("÷", "/")
                результат = eval(выражение)
                
                # Форматируем результат
                if результат == int(результат):
                    self.текущее_значение = str(int(результат))
                else:
                    self.текущее_значение = str(round(результат, 10))
                
                self.выражение = f"{self.предыдущее_значение} {self.операция} {self.текущее_значение} ="
                self.операция = ""
                self.предыдущее_значение = ""
                self.новый_ввод = True
                self.обновить_дисплей()
        except Exception as e:
            messagebox.showerror("Ошибка", "Неверное выражение")
            self.очистить()
    
    def применить_функцию(self, функция):
        try:
            значение = float(self.текущее_значение)
            if функция == 'sqrt':
                if значение < 0:
                    raise ValueError("Невозможно извлечь корень из отрицательного числа")
                результат = math.sqrt(значение)
            elif функция == 'square':
                результат = значение ** 2
            elif функция == 'reciprocal':
                if значение == 0:
                    raise ZeroDivisionError("Деление на ноль")
                результат = 1 / значение
            
            # Форматируем результат
            if результат == int(результат):
                self.текущее_значение = str(int(результат))
            else:
                self.текущее_значение = str(round(результат, 10))
            
            self.новый_ввод = True
            self.обновить_дисплей()
        except ValueError as e:
            messagebox.showerror("Ошибка", str(e))
            self.очистить()
        except ZeroDivisionError as e:
            messagebox.showerror("Ошибка", str(e))
            self.очистить()
    
    def сменить_знак(self):
        if self.текущее_значение != "0":
            if self.текущее_значение.startswith("-"):
                self.текущее_значение = self.текущее_значение[1:]
            else:
                self.текущее_значение = "-" + self.текущее_значение
        self.обновить_дисплей()
    
    def очистить(self):
        self.текущее_значение = "0"
        self.предыдущее_значение = ""
        self.операция = ""
        self.выражение = ""
        self.новый_ввод = True
        self.обновить_дисплей()
    
    def очистить_ввод(self):
        self.текущее_значение = "0"
        self.новый_ввод = True
        self.обновить_дисплей()
    
    def удалить_символ(self):
        if not self.новый_ввод:
            if len(self.текущее_значение) > 1:
                self.текущее_значение = self.текущее_значение[:-1]
            else:
                self.текущее_значение = "0"
                self.новый_ввод = True
            self.обновить_дисплей()
    
    def запустить(self):
        self.окно.mainloop()

# Запуск калькулятора
if __name__ == "__main__":
    калькулятор = AdvancedCalculator()
    калькулятор.запустить()

Упражнения

Упражнение 1: Добавление функции процента

Добавьте в калькулятор функцию процента (%), которая вычисляет процент от текущего значения.

# Добавьте этот метод в класс AdvancedCalculator:
def процент(self):
    try:
        значение = float(self.текущее_значение)
        результат = значение / 100
        if результат == int(результат):
            self.текущее_значение = str(int(результат))
        else:
            self.текущее_значение = str(round(результат, 10))
        self.новый_ввод = True
        self.обновить_дисплей()
    except ValueError:
        messagebox.showerror("Ошибка", "Неверное значение")
        self.очистить()

# Добавьте кнопку в интерфейс:
# В список кнопок добавьте:
('%', 2, 4, 'функция', '#99ccff'),

# В обработчике функций добавьте:
elif текст == '%':
    команда = self.процент
Упражнение 2: История вычислений

Добавьте возможность просмотра истории вычислений в калькуляторе.

# Добавьте в __init__:
self.история = []

# Добавьте метод для сохранения в историю:
def сохранить_в_историю(self, выражение, результат):
    self.история.append(f"{выражение} = {результат}")
    if len(self.история) > 10:  # Ограничиваем историю 10 записями
        self.история.pop(0)

# В методе вычислить() после вычисления результата добавьте:
self.сохранить_в_историю(выражение, self.текущее_значение)

# Добавьте метод для показа истории:
def показать_историю(self):
    if not self.история:
        messagebox.showinfo("История", "История пуста")
        return
    
    история_текст = "\n".join(self.история)
    messagebox.showinfo("История вычислений", история_текст)

# Добавьте кнопку для истории в интерфейс:
('HIST', 1, 4, 'история', '#cccccc'),

# В обработчике кнопок добавьте:
elif тип_кнопки == 'история':
    команда = self.показать_историю
Упражнение 3: Клавиатурный ввод

Добавьте поддержку ввода с клавиатуры в калькулятор.

# Добавьте в метод создания интерфейса:
self.окно.bind("<Key>", self.обработка_клавиши)

# Добавьте метод обработки клавиш:
def обработка_клавиши(self, event):
    клавиша = event.char
    if клавиша.isdigit():
        self.ввод_цифры(клавиша)
    elif клавиша == ".":
        self.ввод_точки()
    elif клавиша in ["+", "-", "*", "/"]:
        операция_отображение = {"*": "×", "/": "÷"}.get(клавиша, клавиша)
        self.ввод_операции(операция_отображение)
    elif клавиша == "\r" or клавиша == "=":  # Enter или =
        self.вычислить()
    elif клавиша == "\x08":  # Backspace
        self.удалить_символ()
    elif клавиша == "\x1b":  # Escape
        self.очистить()
Предыдущий урок Следующий урок