Как создать графическое приложение?

Кто такой GUI и зачем он нужен?

В прошлой главе (фух, это было сложно!) мы с тобой создали текстовый квест, и, в целом, ты теперь умеешь создавать достаточно большие программы в текстовым интерфейсом. Но вот не задача: большинство программ, с которыми мы работаем на ПК - текстовые редакторы, браузеры, проигрыватели фильмов - выглядят совсем по-другому: у них есть кнопки, поля ввода, графика, иными словами - графический интерфейс пользователя (GUI).

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

Можно ли создавать такие приложения на питоне? Конечно, да! И более того, для этого есть множество разных библиотек, например, PyQt и Kivy (последняя, кстати, позволяет в том числе создавать мобильные приложения), однако устроены они довольно сложно. Поскольку мы с тобой только начинаем, мы возьмем самый простой вариант - библиотеку TkInter которая, как и random, time, winsound уже встроена в дистрибутив питона.

Я должен заранее признаться, что TkInter - местами достаточно топорный и запутанный инструмент, но зато - максимально простой для освоения. В этой главе я не смогу объяснить все досконально (обычно эту тему проходят чуть позже), зато смогу показать, как уже сейчас ты сможешь добавлять графический интерфейс в свои программы. А постепенно, в дальнейшем, ты будешь понимать что же такое мы написали с технической точки зрения.

В качестве практического проекта в этот раз я предлагаю тебе создать инженерный калькулятор, который ты сможешь использовать для математических расчетов в школе. Но перед этим нам предстоить освоить создание окон, кнопок, текстовых полей и полей для ввода. Поехали! 🚀

Создаем окно приложения

Для начала работы с TkInter, необходимо импортировать модуль:

import tkinter

Однако в этот раз, чтобы не писать постоянно tkinter. перед каждой командой, давайте импортируем из модуля сразу все, что в нем есть. Делается это следующим образом:

from tkinter import *

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

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

window = Tk()

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

from tkinter import *
window = Tk()

mainloop()

Вот теперь-то все запустилось!

При желании окошку можно задать название:

from tkinter import *

window = Tk()
window.title("Графическое приложение")

mainloop()

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

from tkinter import *

window1 = Tk()
window2 = Tk()
window3 = Tk()
window4 = Tk()

mainloop()

Добавляем кнопку

Итак, мы научились создавать окна. Только как-то в этих окнах пока пустовато. TkInter позволяет добавлять на окно различные элементы пользовательского интерфейса - виджеты. Давайте что-нибудь добавим, например, кнопку!

from tkinter import *

# создаем окно
window = Tk()
window.title("Графическое приложение")

# создаем кнопку внутри окна window с текстом "Нажми меня!"
Button(window, text="Нажми меня!")

mainloop()

Запускаем... Кажется, кнопки нет. На самом деле, мы просто забыли попросить ее показаться с помощью метода pack().

from tkinter import *

# создаем окно
window = Tk()
window.title("Графическое приложение")

# создаем кнопку внутри окна window с текстом "Нажми меня!"
button = Button(window, text="Нажми меня!")
# просим кнопку "нарисоваться"
button.pack()

mainloop()

Вот теперь, конечно, топорно, но кнопка появилась.

В коде выше есть два момента, которые нужно отдельно обсудить. Во-первых, мы с тобой уже видели, что функции могут принимать один или несколько параметров, причем в зависимости от ситуации их может быть разное количество. Например, в print(...) мы можем передать сколько угодно строк через запятую, а можем не передавать вообще. А вот random.randint(from, to) хочет от нас ровно два параметра - самое маленькое и самое большое число, которые можно сгенерировать.

Иногда бывает, что необязательных параметров у функции так много, что нужно указать, какие именно мы передаем. В частости, в функцию (сейчас немного лукавлю, но это во благо!) Button(window, text="Нажми меня!") мы передали окно и текст для кнопки. Первый параметр - это всегда окно, поэтому его имя можно не указывать, а вот чтобы уточнить, что за ним мы передаем именно текст, мы и добавили text=.

Тогда какие могут быть еще параметры? Их много, например, еще есть ширина и высота.

from tkinter import *

# создаем окно
window = Tk()
window.title("Графическое приложение")

# создаем кнопку внутри окна window с текстом "Нажми меня!"
button = Button(window, text="Нажми меня!", width=30, height=5)
# просим кнопку "нарисоваться"
button.pack()

mainloop()

Хорошо, с этим понятно. А что тогда во-вторых? Возможно ты заметил, что мы как-то странно вызываем функцию button.pack()? Раньше мы использовали такой синтаксис с точкой для вызова функции из модуля, но ведь button - это не модуль, а кнопка (что-бы это не было)?

Здесь я должен заметить две вещи: во-первых, в переменных могут лежать не только строки и числа, как было в прошлой главе, но и более сложные объекты, например, кнопки и окна, как мы уже видели в коде выше. А во-вторых, иногда объект из переменной можно не только передать в функцию в качестве параметра, как мы передаем строки в print и окно в Button, но и попросить у объекта, чтобы он сделал что-то сделал сам.

Если мы просим объект что-то сделать, то команду для него мы запускаем через точку, такая команда называется методом объекта. Например, у строк есть метод lower(), который приводит их к нижнему регистру, а у кнопок и других виджетов из TkInter метод pack(), который просит виджет нарисоваться.

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

s = "HaHaHaHa"
print(s)
print(s.lower())

Вывод:

HaHaHaHa
hahahaha

По аналогии работает и отрисовка виджетов. pack - это метод кнопки, т.е. действие которое мы просим выполнить саму кнопку.

# создаем кнопку внутри окна window с текстом "Нажми меня!"
button = Button(window, text="Нажми меня!", width=30, height=5)
# просим кнопку "нарисоваться"
button.pack()

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

from tkinter import *

# создаем окно
window = Tk()
window.title("Графическое приложение")

# создаем кнопку внутри окна window с текстом "Нажми меня!" и сразу рисуем ее
Button(window, text="Нажми меня!", width=30, height=5).pack()

mainloop()

Обрабатываем нажатие на кнопку

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

То есть алгоритм действий следующий:

  1. написать функцию, которую мы хотим вызвать после нажатия кнопки;
  2. указать название этой функции про создании кнопке в параметре command.
from tkinter import *

# описываем функцию my_action
def my_action():
    print("Пользователь нажал на кнопку!")

window = Tk()
window.title("Графическое приложение")

# сообщаем имя функции (только имя, без скобочек!) в параметре command
Button(window, text="Нажми меня!", width=30, height=5, command=my_action).pack()

mainloop()

Обрати внимание, что имя функции мы передаем без скобочек, иначе она вызовется только в момент создания кнопки (и больше работать не будет). А мы хотим, чтобы TkInter вызывал ее при каждом нажатии кнопки. После запуска программы в самом окне ничего по-прежнему меняться не будет (а мы ничего и не меняем), а вот в консоли питона будет выводится сообщение при каждом нажатии.

Кстати, ничто не мешает нам добавить вторую кнопку, например, для закрытия окна. Для этого добавим еще одну функцию-обработчик и создадим еще одну кнопку.

from tkinter import *

# описываем функцию my_action
def my_action():
    print("Пользователь нажал на кнопку!")

# описываем функцию для закрытия окна
def close_window():
    # закрыть окно
    quit()

window = Tk()
window.title("Графическое приложение")

# сообщаем имя функции (только имя, без скобочек!) в параметре command
Button(window, text="Нажми меня!", width=30, height=5, command=my_action).pack()

# добавляем вторую кнопку - для закрытия окна, передаем в command нашу функцию close_window
Button(window, text="Закрыть окно", width=30, height=5, command=close_window).pack()

mainloop()

Добавляем текстовое поле

Давайте сделаем приложение, которое при нажатии на кнопку будет показывать пользователю случайную пословицу. Мы уже умеем создавать окно и можем добавить в него кнопку, а вот выводить у нас пока получалось только в консоль питона. Чтобы вывести какой то текст в окно нам потребуется текстовое поле - Label.

Создается текстовое поле так:

from tkinter import *

window = Tk()
window.title("Графическое приложение")

# создаем текстовое поле в окне window с текстом "Привет, мир"
Label(window, text="Привет, мир!", width=25, height=5).pack()

mainloop()

Для примера, давайте напишем рандомный генератор курса акций Tesla 🚗 . Говорят, они часто делают крутое "пике"!

import random
from tkinter import *

# 1. Описываем функцию, которая будет срабатывать при нажатии на кнопку
def print_price():
    # Генерируем случайное число от 100 до 3000
    price = random.randint(100, 3000)
    # Превращаем число в строку с помощью str и добавляем знак доллара
    # Результат отправляем в text_label
    text_label.config(text=str(price) + "$")

# 2. Создаем окно
window = Tk()
window.title("Стоимость акций Tesla")

# 3. Добавляем текстовое поле text_label
# и привязываем к нему price_var
text_label = Label(window, width=25, height=5)
text_label.pack()

# 4. Добавляем кнопку
# При нажатии вызывается функция print_price
Button(window, text="Узнать стоимость", width=25, height=5, command=print_price).pack()

mainloop()

Теперь при каждом нажатии наша рандомная стоимость будет меняться.