Программируем калькулятор

Что будем делать?

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

Что в нем должно быть? Как минимум - клавиатура и стандартные математические операции - сложение, вычитание, деление и умножение.

Но как только минимальный вариант заработает - можно добавить возведение в степень, квадратный корень, тригонометрические функции, в общем, весь цвет модуля math.

Приступим?

Используем сетку для размещения виджетов

Итак, в самом верху у нас, очевидно, поле для ввода, а дальше 10 кнопок с цифрами. Попробуем же разместить все это богатство в окне...

from tkinter import *

window = Tk()
window.title("Калькулятор")

# Объект для хранения текста
input_var = StringVar()

# Поле для ввода
Entry(window, textvariable=input_var, width=25).pack()

# Добавляем кнопки
Button(window, text="1", width=10, height=5).pack()
Button(window, text="2", width=10, height=5).pack()
Button(window, text="3", width=10, height=5).pack()
Button(window, text="4", width=10, height=5).pack()
Button(window, text="5", width=10, height=5).pack()
Button(window, text="6", width=10, height=5).pack()
Button(window, text="7", width=10, height=5).pack()
Button(window, text="8", width=10, height=5).pack()
Button(window, text="9", width=10, height=5).pack()
Button(window, text="0", width=10, height=5).pack()

mainloop()

Пробуем запустить и... Беда! Все кнопки выстроились в башню.

Как заставить кнопки расположится по... сетке! Представь, что ты играешь в морской бой и располагаешь корабли на поле. У каждой клетки на поле будет номер (или координата) по горизонтали и вертикали. Если корабль будет в левом верхнем углу, его координаты - ноль, ноль. А если отступить на 3 клетки вправо, то координата будет уже "три, ноль". Первое число - отступ от края вправо, а второе - вниз.

Обрати внимание, что нумерация в сетке начинается с нуля. Чаще всего в программировании элемент любого множества имеет номер 0.

Точно также сетка или, по-английски, "grid", работает и в питоне. Можно располагать виджеты прямо как корабли в игре в "морской бой". Для того чтобы поместить кнопку в нужную ячейку просто используй метод .grid(column=X, row=Y) вместо pack(), в качестве параметров передай номер столбца и строки. Например...

from tkinter import *

window = Tk()
window.title("Калькулятор")

# Добавляем кнопки
Button(window, text="1", width=10, height=5).grid(row=0, column=0)
Button(window, text="2", width=10, height=5).grid(row=0, column=1)
Button(window, text="3", width=10, height=5).grid(row=0, column=2)
Button(window, text="4", width=10, height=5).grid(row=1, column=0)
Button(window, text="5", width=10, height=5).grid(row=1, column=1)
Button(window, text="6", width=10, height=5).grid(row=1, column=2)
Button(window, text="7", width=10, height=5).grid(row=2, column=0)
Button(window, text="8", width=10, height=5).grid(row=2, column=1)
Button(window, text="9", width=10, height=5).grid(row=2, column=2)
Button(window, text="0", width=10, height=5).grid(row=3, column=1)

mainloop()

Ну-ка кнопки встаньте в ряд!

Теперь клавиатура выглядит как надо, вот только как разместить поле для ввода сверху, ведь оно должно занимать сразу три колонки. Для это достаточно просто сказать об этом методу grid с помощью параметра columnspan. Если нужно взять три колонки, начиная с нулевой - просто говорим, что column=0, а columnspan=3.

from tkinter import *

window = Tk()
window.title("Калькулятор")

# Поле для ввода
Entry(window, width=30).grid(row=0, columnspan=3)

# Добавляем кнопки
Button(window, text="1", width=10, height=5).grid(row=1, column=0)
Button(window, text="2", width=10, height=5).grid(row=1, column=1)
Button(window, text="3", width=10, height=5).grid(row=1, column=2)
Button(window, text="4", width=10, height=5).grid(row=2, column=0)
Button(window, text="5", width=10, height=5).grid(row=2, column=1)
Button(window, text="6", width=10, height=5).grid(row=2, column=2)
Button(window, text="7", width=10, height=5).grid(row=3, column=0)
Button(window, text="8", width=10, height=5).grid(row=3, column=1)
Button(window, text="9", width=10, height=5).grid(row=3, column=2)
Button(window, text="0", width=10, height=5).grid(row=4, column=1)

mainloop()

Если ширина кнопки 10, а кнопок три, то поля для ввода должно быть ширины 30, чтобы занять всю ширину окна. Поскольку сверху добавилась строка с полем ввода, то номера строк всех кнопок уехали на один вниз.

Программируем кнопки

Итак, теперь мы можем расположить в окне все нужные кнопки для калькулятора.

from tkinter import *

window = Tk()
window.title("Калькулятор")

Button(window, text='1', height=5, width=10).grid(row=1, column=0)
Button(window, text='2', height=5, width=10).grid(row=1, column=1)
Button(window, text='3', height=5, width=10).grid(row=1, column=2)
Button(window, text='4', height=5, width=10).grid(row=2, column=0)
Button(window, text='5', height=5, width=10).grid(row=2, column=1)
Button(window, text='6', height=5, width=10).grid(row=2, column=2)
Button(window, text='7', height=5, width=10).grid(row=3, column=0)
Button(window, text='8', height=5, width=10).grid(row=3, column=1)
Button(window, text='9', height=5, width=10).grid(row=3, column=2)
Button(window, text='=', height=5, width=10).grid(row=4, column=0)
Button(window, text='0', height=5, width=10).grid(row=4, column=1)
Button(window, text='AC', height=5, width=10).grid(row=4, column=2)

Button(window, text='+', height=5, width=10).grid(row=1, column=3)
Button(window, text='-', height=5, width=10).grid(row=2, column=3)
Button(window, text='*', height=5, width=10).grid(row=3, column=3)
Button(window, text='/', height=5, width=10).grid(row=4, column=3)

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Единственное что остается, это оживить его!

Начнем с кнопок с цифрами, при нажатии на каждую из них сверху должна появлятся нужная цифра. Причем, если в поле ввода уже что то есть, цифра должна дописываться справа. Как ты помнишь, для того, чтобы обработать нажатие на кнопку, нужно создать функцию и передать ее имя в параметр command у нужной кнопки.

Давай попробуем сделать это для кнопки 1.

from tkinter import *

window = Tk()
window.title("Калькулятор")

def button_press_1():
    entry_text.set(1)

Button(window, text='1', height=5, width=10, command=button_press_1).grid(row=1, column=0)

# тут описание всех остальных кнопок...

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Теперь при нажатии на кнопку один в поле вверху экрана правда появляется единица. Но вот если нажимать ее много раз, то ничего не меняется, каждый раз в поле записывается одна новая единица. Что делать? Можно завести переменную number и записывать в нее текущее число.

from tkinter import *

window = Tk()
window.title("Калькулятор")

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0

def button_press_1():
    # чтобы менять глобальную переменную, пишем global
    global number

    # прибавим единицу
    number += 1

    # запишем число в поле ввода
    entry_text.set(number)

Button(window, text='1', height=5, width=10, command=button_press_1).grid(row=1, column=0)

# тут описание всех остальных кнопок...

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Теперь число меняется, но не так, как надо - вместо дописывания справа, мы просто прибавляем единицу. А как дописать? Довольно легко - нужно умножить на 10 и прибавить нужную цифру. Т.е. если в поле было 2, а пользователь нажал 3, мы сначала умножим на 10 и получим 20, а затем прибавим 3 и вуаля! В поле для ввода как и нужно 23.

from tkinter import *

window = Tk()
window.title("Калькулятор")

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0

def button_press_1():
    # чтобы менять глобальную переменную, пишем global
    global number

    # допишем 1 справа
    number = number * 10 + 1

    # запишем число в поле ввода
    entry_text.set(number)

Button(window, text='1', height=5, width=10, command=button_press_1).grid(row=1, column=0)

# тут описание всех остальных кнопок...

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Выходит теперь нам надо полность скопировать функцию button_press_1() для каждой кнопки? Можно, но мы немного упростим себе задачу! Для этого нам придется освоить создание функцию с параметрами.

Создание функций с аргументами

Возможность не повторять одинаковый код с помощью функций - это круто! Однако, что делать, если код не совсем одинаковый?

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

Аргументы функции - значения переменных, которые мы можем передать функции для работы.

Ниже приведен простейший пример функции с аргументом.

def hello(name):
    print('Hello', name)

Теперь мы можем использовать функцию hello для того, чтобы поздороваться с кем угодно, не меняя код самой функции.

hello("Маша")
hello("Петя")
hello("Витя")

Далеко, не всегда хочется указывать все аргументы функции, особенно если их много и значения у некоторых из них часто повторяются. Например, если мы пишем игру (этим мы займемся позже), в нашей игре может быть функция, рисующая игровое поле заданного размера. Это, конечно, хорошо, когда функция полностью под контролем пользователя, однако пользователь - существо не менее линивое, чем программист (обычно в сотни раз ленивей). Ему приятней, когда он может просто взять и вызвать функцию с минимальными усилиями. Чтобы оказать пользователю такую услугу мы можем, прописать для функции значения аргументов по умолчанию.

Например, так:

def create_playground(width=100, height=100):
    ...
    print('Отрисовано поле ', width, ' в ширину и ', height, 'в высоту')

create_playground(200, 300)
create_playground(200)
create_playground()
create_playground(height=200, width=300)

Результат:

Отрисовано поле 200 в ширину и 300 в высоту
Отрисовано поле 200 в ширину и 100 в высоту
Отрисовано поле 100 в ширину и 100 в высоту
Отрисовано поле 300 в ширину и 200 в высоту

Как мы видим из примера, мы можем совершенно спокойно опускать все или несколько аргументов, которых заданы значения по-умолчанию. Последний вызов функции create_playground выглядит необычно по сравнению с остальными. Как это так, описании функции я задал первое значение, как ширину, а второе, как высоту, а с в последней строке входные данные считались наоборот. Как так?

Все очень просто. Дело в том, что у нас на вооружение есть два способа задать значения аргументов: порядковый и именованный. Когда мы просто перечисляем значения аргументов, которые мы передаем функции, такие аргументы называют порядковыми. Причина очевидна, ведь значения аргументов считываются по порядку, а если передаваемые значения кончились, то остальным аргументам присваивается значение по-умолчанию. Так у нас и получилось при втором вызове функции create_playground. Там мы передали всего одно значение 200, которое записалось в первый аргумент width. Для аргумента height значения не нашлось, поэтому в него было записано значение по-умолчанию - 100.

Когда мы в явном виде указываем имя аргумента, которому мы хотим присвоить значение, такой аргумент называют именованным. Таким способом можно задать значение любому аргументу в любому порядке. Поэтому мы при четвертом вызове функции. Там мы сначала задали для аргумента height, а потом для width, потому они и считались соответствующе. Кстати, именно таким образом и работают всем функции из tkinter, которые мы использовали.

Вы можете свободно использовать оба способа обращения к аргументам, если не забыть об одном маленьком нюансе: сначала идут порядковые аргументы, а потом именованные и никак не наоборот. В противном случае получите ошибку:

create_playground(200, height=100) # сработает
create_playground(width=200, 100) # ошибка (`SyntaxError: positional argument follows keyword argument`)

Создаем работающую клавиатуру

Итак, как мы можем использовать аргументы, чтобы уменьшить число повторений в коде? К сожалению, без лямбда функций (а о них мы поговорим еще нескоро) совсем от повторенией нам не избавиться. Но мы можем выделить всю работу с полем для ввода в отдельную функцию.

# функция для ввода цифры
def add_digit(digit):
    # чтобы менять глобальную переменную, пишем global
    global number

    # допишем нужную цифру справа
    number = number * 10 + digit

    # запишем число в поле ввода
    entry_text.set(number)

Теперь мы можем с функциях, которые будут срабатывать по нажатию на кнопки, просто вызывать add_digit с нужным параметром. Например...

def button_press_1():
    add_digit(1)

def button_press_2():
    add_digit(2)

Теперь мы можем полностью описать работу цифровой клавиатуры. Ох, приготовься, будет длинно!

from tkinter import *

window = Tk()
window.title("Калькулятор")

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0

# функция для ввода цифры
def add_digit(digit):
    # чтобы менять глобальную переменную, пишем global
    global number

    # допишем нужную цифру справа
    number = number * 10 + digit

    # запишем число в поле ввода
    entry_text.set(number)

def button_press_1():
    add_digit(1)

def button_press_2():
    add_digit(2)

def button_press_3():
    add_digit(3)

def button_press_4():
    add_digit(4)

def button_press_5():
    add_digit(5)

def button_press_6():
    add_digit(6)

def button_press_7():
    add_digit(7)

def button_press_8():
    add_digit(8)

def button_press_9():
    add_digit(9)

def button_press_0():
    add_digit(0)

Button(window, text='1', height=5, width=10, command=button_press_1).grid(row=1, column=0)
Button(window, text='2', height=5, width=10, command=button_press_2).grid(row=1, column=1)
Button(window, text='3', height=5, width=10, command=button_press_3).grid(row=1, column=2)
Button(window, text='4', height=5, width=10, command=button_press_4).grid(row=2, column=0)
Button(window, text='5', height=5, width=10, command=button_press_5).grid(row=2, column=1)
Button(window, text='6', height=5, width=10, command=button_press_6).grid(row=2, column=2)
Button(window, text='7', height=5, width=10, command=button_press_7).grid(row=3, column=0)
Button(window, text='8', height=5, width=10, command=button_press_8).grid(row=3, column=1)
Button(window, text='9', height=5, width=10, command=button_press_9).grid(row=3, column=2)
Button(window, text='=', height=5, width=10).grid(row=4, column=0)
Button(window, text='0', height=5, width=10, command=button_press_0).grid(row=4, column=1)
Button(window, text='AC', height=5, width=10).grid(row=4, column=2)

Button(window, text='+', height=5, width=10).grid(row=1, column=3)
Button(window, text='-', height=5, width=10).grid(row=2, column=3)
Button(window, text='*', height=5, width=10).grid(row=3, column=3)
Button(window, text='/', height=5, width=10).grid(row=4, column=3)

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Налаживаем работу арифметических действий

Отлично, теперь цифры работают, но посчитать пока еще ничего нельзя. Давай это исправим! Но сначала надо понять, как это сделать.

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

Из этого всего следует, что нам как минимум нужно запоминать выбранное действие и прошлое число при нажатии на +, -, * или /. А если запоминать, то нам нужны две дополнительные переменные. Назовем их previous_number и action.

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0
# а это прошлое число в памяти - и тоже сначала ноль
previous_number = 0

# выбранное действие
# пускай по-умолчанию это будет плюс
action = '+'

# функция для сложения
def button_press_add():
    global number
    global previous_number
    global action

    # запоминаем действие
    action = '+'

    # запоминаем текущее число
    previous_number = number

    # и кладем ноль в текущее - пользователю нужно его ввести
    number = 0

    # показываем текущее на экране
    entry_text.set(number)

Аналогично мы поступим для всех остальных действий. Кстати, если поступим так же, почему не выделить одинаковую часть в отдельную функцию?

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0
# а это прошлое число в памяти - и тоже сначала ноль
previous_number = 0

# выбранное действие
# пускай по-умолчанию это будет плюс
action = '+'

# параметр - выбранное действие
def make_action(chosen_action):
    global number
    global previous_number
    global action

    # запоминаем выбранное действие
    action = chosen_action

    # запоминаем текущее число
    previous_number = number

    # и кладем ноль в текущее - пользователю нужно его ввести
    number = 0

    # показываем текущее на экране
    entry_text.set(number)

# функция для сложения
def button_press_add():
    # выбранное действие - это сложение
    make_action('+')

# функция для вычитания
def button_press_substract():
    # выбранное действие - это сложение
    make_action('-')

Теперь осталось только реализовать подсчет результата (нажатие на =) и очистку поля. С очисткой все просто - обнуляем текущее и предыдущее число. А при подсчете нужно в зависимости от действия выполнить операцию над текущим и предыдущим числом, а затем вывести ответ. Чтобы с ответом можно было дальше работать, можно положить его в текущее число.

# функция для вычисления результата (=)
def button_press_result():
    global number
    global previous_number
    global action

    # в зависимости от действия - делаем дело!
    if action == '+':
        number = previous_number + number
    elif action == '-':
        number = previous_number - number
    elif action == '*':
        number = previous_number * number
    elif action == '/':
        number = previous_number / number

    # записываем ответ в поле ввода
    entry_text.set(number)

Собираем все вместе

А вот теперь действительно длинно!

from tkinter import *

window = Tk()
window.title("Калькулятор")

# запомним, какое число записано в поле ввода
# изначально это ноль
number = 0
# а это прошлое число в памяти - и тоже сначала ноль
previous_number = 0

# выбранное действие
# пускай по-умолчанию это будет плюс
action = '+'

# параметр - выбранное действие
def make_action(chosen_action):
    global number
    global previous_number
    global action

    # запоминаем выбранное действие
    action = chosen_action

    # запоминаем текущее число
    previous_number = number

    # и кладем ноль в текущее - пользователю нужно его ввести
    number = 0

    # показываем текущее на экране
    entry_text.set(str(number))

# функция для сложения
def button_press_add():
    # выбранное действие - это сложение
    make_action('+')

# функция для вычитания
def button_press_substract():
    # выбранное действие - это сложение
    make_action('-')

# функция для умножения
def button_press_multiply():
    make_action('*')

# функция для деления
def button_press_divide():
    make_action('/')

# функция для очистки (AC)
def button_press_clear():
    global number
    global previous_number

    number = 0
    previous_number = 0

    entry_text.set(number)

# функция для вычисления результата (=)
def button_press_result():
    global number
    global previous_number
    global action

    # в зависимости от действия - делаем дело!
    if action == '+':
        number = previous_number + number
    elif action == '-':
        number = previous_number - number
    elif action == '*':
        number = previous_number * number
    elif action == '/':
        number = previous_number / number

    # записываем ответ в поле ввода
    entry_text.set(number)

# функция для ввода цифры
def add_digit(digit):
    # чтобы менять глобальную переменную, пишем global
    global number

    # допишем нужную цифру справа
    number = number * 10 + digit

    # запишем число в поле ввода
    entry_text.set(number)

def button_press_1():
    add_digit(1)

def button_press_2():
    add_digit(2)

def button_press_3():
    add_digit(3)

def button_press_4():
    add_digit(4)

def button_press_5():
    add_digit(5)

def button_press_6():
    add_digit(6)

def button_press_7():
    add_digit(7)

def button_press_8():
    add_digit(8)

def button_press_9():
    add_digit(9)

def button_press_0():
    add_digit(0)

Button(window, text='1', height=5, width=10, command=button_press_1).grid(row=1, column=0)
Button(window, text='2', height=5, width=10, command=button_press_2).grid(row=1, column=1)
Button(window, text='3', height=5, width=10, command=button_press_3).grid(row=1, column=2)
Button(window, text='4', height=5, width=10, command=button_press_4).grid(row=2, column=0)
Button(window, text='5', height=5, width=10, command=button_press_5).grid(row=2, column=1)
Button(window, text='6', height=5, width=10, command=button_press_6).grid(row=2, column=2)
Button(window, text='7', height=5, width=10, command=button_press_7).grid(row=3, column=0)
Button(window, text='8', height=5, width=10, command=button_press_8).grid(row=3, column=1)
Button(window, text='9', height=5, width=10, command=button_press_9).grid(row=3, column=2)
Button(window, text='=', height=5, width=10, command=button_press_result).grid(row=4, column=0)
Button(window, text='0', height=5, width=10, command=button_press_0).grid(row=4, column=1)
Button(window, text='AC', height=5, width=10, command=button_press_clear).grid(row=4, column=2)

Button(window, text='+', height=5, width=10, command=button_press_add).grid(row=1, column=3)
Button(window, text='-', height=5, width=10, command=button_press_substract).grid(row=2, column=3)
Button(window, text='*', height=5, width=10, command=button_press_multiply).grid(row=3, column=3)
Button(window, text='/', height=5, width=10, command=button_press_divide).grid(row=4, column=3)

entry_text = StringVar()
Entry(window, width=40, textvariable=entry_text).grid(row=0, column=0, columnspan=4)

mainloop()

Что остается доделать? Сейчас у нас работают только самые обычные операции, а значит калькулятор совсем не инженерный! Обязательно добавь еще один ряд кнопок как минимум с возведением в степень, взятием корня, логарифмом (чтобы это не было!) и синусом.

Когда все это будет сделано, второй проект (да еще какой!) будет у тебя в копилке! 🙌