Рубрики
Без рубрики

Разработка графического интерфейса Python с помощью Tkinter: Часть 2

Автор оригинала: Mateusz Dobrychlop.

Разработка графического интерфейса Python с помощью Tkinter: Часть 2

Это вторая часть нашей многосерийной серии по разработке графических интерфейсов на Python с использованием Tkinter. Проверьте ссылки ниже для других частей этой серии:

  • Разработка графического интерфейса Python с помощью Tkinter
  • Разработка графического интерфейса Python с помощью Tkinter: Часть 2
  • Разработка графического интерфейса Python с помощью Tkinter: Часть 3

Вступление

В первой части обучающей серии Stack Abuse Tkinter мы узнали, как быстро создавать простые графические интерфейсы с помощью Python. В статье объяснялось, как создать несколько различных виджетов и расположить их на экране с помощью двух различных методов, предлагаемых Tkinter, – но все же мы едва поцарапали поверхность возможностей модуля.

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

Расширенные Параметры Сетки

В предыдущей статье мы познакомились с методом grid () , который позволяет нам ориентировать виджеты в строках и столбцах, что позволяет получать гораздо более упорядоченные результаты, чем при использовании метода pack () . Однако традиционные сетки имеют свои недостатки, которые можно проиллюстрировать следующим примером:

import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew")

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Выход:

Приведенный выше код должен быть легко понятен вам, если вы прошли первую часть нашего урока Tkinter, но давайте все равно сделаем краткий обзор. В строке 3 мы создаем наше главное корневое окно. В строках 5-7 мы создаем три кадра: мы определяем, что корень является их родительским виджетом и что их краям будет придан тонкий 3D-эффект. В строках 9-11 кадры распределяются внутри окна с помощью метода grid () . Мы указываем ячейки сетки, которые должны быть заняты каждым виджетом, и используем опцию sticky , чтобы растянуть их по горизонтали и вертикали.

В строках 13-15 мы создаем три простых виджета: метку, кнопку, которая ничего не делает, и еще одну кнопку, которая закрывает (уничтожает) главное окно – по одному виджету на кадр. Затем в строках 17-19 мы используем метод pack() для размещения виджетов внутри соответствующих родительских фреймов.

Как вы можете видеть, три виджета, распределенные по двум строкам и двум столбцам, не создают эстетически приятного результата. Несмотря на то, что frame 3 имеет всю свою строку для себя, а параметр sticky заставляет ее растягиваться горизонтально, она может растягиваться только в пределах границ своей отдельной ячейки сетки. В тот момент, когда мы смотрим на окно, мы инстинктивно знаем, что рамка, содержащая button2 , должна охватывать два столбца – особенно учитывая важную функцию, которую выполняет кнопка.

Ну, к счастью, создатели метода grid() предсказали такой сценарий и предложили опцию columnspan. После применения крошечной модификации к строке 11:

import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Мы можем сделать так, чтобы ваша рамка 3 растянулась по всей ширине нашего окна.

Выход:

Метод place()

Обычно при построении хороших и упорядоченных интерфейсов на основе Tkinter методы place() и grid() должны удовлетворять всем вашим потребностям. Тем не менее, пакет предлагает еще один geometry manager – метод place () .

Метод place() основан на простейших принципах из всех трех менеджеров геометрии Tkinter. Используя place () , вы можете явно указать положение виджета внутри окна, либо напрямую указав его точные координаты, либо сделав его положение относительно размера окна. Взгляните на следующий пример:

import tkinter

root = tkinter.Tk()

root.minsize(width=300, height=300)
root.maxsize(width=300, height=300)

button1 = tkinter.Button(root, text="B")
button1.place(x=30, y=30, anchor="center")

root.mainloop()

Выход:

В строках 5 и 6 мы указываем, что хотим, чтобы размеры нашего окна были точно 300 на 300 пикселей. В строке 8 мы создаем кнопку. Наконец, в строке 9 мы используем метод place() для размещения кнопки внутри нашего корневого окна.

Мы предлагаем три значения. Используя параметры x и y , мы определяем точные координаты кнопки внутри окна. Третий вариант, anchor , позволяет нам определить, какая часть виджета окажется в точке (x,y). В этом случае мы хотим, чтобы он был центральным пикселем нашего виджета. Аналогично sticky опции grid() , мы можем использовать различные комбинации n , s , e и w для привязки виджета по его краям или углам.

Метод place() не заботится о том, допустим ли мы здесь ошибку. Если координаты указывают на место за пределами нашего окна, кнопка не будет отображаться. Более безопасный способ использования этого менеджера геометрии – использование координат относительно размера окна.

import tkinter

root = tkinter.Tk()

root.minsize(width=300, height=300)
root.maxsize(width=300, height=300)

button1 = tkinter.Button(root, text="B")
button1.place(relx=0.5, rely=0.5, anchor="center")

root.mainloop()

Выход

В приведенном выше примере мы изменили строку 9. Вместо абсолютных координат x и y мы теперь используем относительные координаты. Установив relx и rely на 0,5, мы убедимся, что независимо от размера окна наша кнопка будет помещена в его центр.

Хорошо, есть еще одна вещь о методе place () , которая вам, вероятно, покажется интересной. Теперь давайте объединим примеры 2 и 4 из этого урока:

import tkinter

root = tkinter.Tk()

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Simple button")
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

button1 = tkinter.Button(root, text="B")
button1.place(relx=0.5, rely=0.5, anchor="center")

root.mainloop()

Выход:

В приведенном выше примере мы просто взяли код из примера 2, а затем в строках 21 и 22 создали и поместили нашу маленькую кнопку из примера 4 внутри того же окна. Вы можете быть удивлены, что этот код не вызывает исключения, хотя мы явно смешиваем методы grid() и place() в корневом окне. Ну, из-за простой и абсолютной природы place () вы действительно можете смешать его с pack() и grid() . Но только если это действительно необходимо .

Результат в данном случае, очевидно, довольно уродлив. Если центрированная кнопка была больше, это повлияет на удобство использования интерфейса. О, и в качестве упражнения вы можете попробовать переместить линии 21 и 22 выше определений фреймов и посмотреть, что произойдет.

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

Настройка виджетов

Внешний вид наших виджетов может быть изменен во время работы программы. Большинство косметических аспектов элементов наших окон могут быть изменены в нашем коде с помощью опции configure . Давайте рассмотрим следующий пример:

import tkinter

root = tkinter.Tk()

def color_label():
    label1.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=1, row=0, sticky="nsew")
frame3.grid(column=0, row=1, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label")
button1 = tkinter.Button(frame2, text="Configure button", command=color_label)
button2 = tkinter.Button(frame3, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')

root.mainloop()

Выход:

В строках 5 и 6 мы добавили простое определение новой функции. Наша новая функция color_label() настраивает состояние label1 . Параметры, которые принимает метод configure () , – это те же параметры, которые мы используем при создании новых объектов виджета и определении начальных визуальных аспектов их внешнего вида.

В этом случае нажатие только что переименованной кнопки “Настроить” изменяет текст, цвет фона (bg) и цвет переднего плана (fg – в данном случае это цвет текста) нашего уже существующего label1 .

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

import tkinter

root = tkinter.Tk()

def color_label():
    label1.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=color_label)
button2 = tkinter.Button(frame4, text="Configure button 2", command=color_label)

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')

root.mainloop()

Выход:

Итак, теперь у нас есть две этикетки и три кнопки. Допустим, мы хотим, чтобы “Configure button 1” настроила “Simple label 1”, а “Configure button 2” настроила “Simple label 2” точно таким же образом. Конечно, приведенный выше код работает не так – обе кнопки выполняют функцию color_label () , которая по-прежнему изменяет только одну из меток.

Вероятно, первое решение, которое приходит вам на ум, – это изменение функции color_label() таким образом, чтобы она принимала объект виджета в качестве аргумента и настраивала его. Затем мы могли бы изменить определение кнопки так, чтобы каждая из них передавала свою индивидуальную метку в командной опции:

# ...

def color_label(any_label):
    any_label.configure(text="Changed label", bg="green", fg="white")

# ...

button1 = tkinter.Button(frame3, text="Configure button 1", command=color_label(label1))
button2 = tkinter.Button(frame4, text="Configure button 2", command=color_label(label2))

# ...

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

Так как же заставить его работать должным образом?

Передача аргументов через лямбда-выражения

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

import tkinter

root = tkinter.Tk()

def color_label(any_label):
    any_label.configure(text="Changed label", bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=lambda: color_label(label1))
button2 = tkinter.Button(frame4, text="Configure button 2", command=lambda: color_label(label2))

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')

root.mainloop()

Выход:

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

Самое интересное-это строки 22 и 23. Здесь мы фактически определяем две новые лямбда-функции, которые передают различные аргументы функции color_label() и выполняют ее. Таким образом, мы можем избежать вызова функции color_label() в момент инициализации кнопок.

Получение Пользовательского Ввода

Мы приближаемся к концу второй статьи нашей серии учебников Tkinter, поэтому на этом этапе было бы неплохо показать вам способ получения информации от пользователя вашей программы. Для этого может быть полезен виджет Entry . Посмотрите на следующий сценарий:

import tkinter

root = tkinter.Tk()

def color_label(any_label, user_input):
    any_label.configure(text=user_input, bg="green", fg="white")

frame1 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame2 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame3 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame4 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame5 = tkinter.Frame(root, borderwidth=2, relief='ridge')
frame6 = tkinter.Frame(root, borderwidth=2, relief='ridge')

frame1.grid(column=0, row=0, sticky="nsew")
frame2.grid(column=0, row=1, sticky="nsew")
frame3.grid(column=1, row=0, sticky="nsew")
frame4.grid(column=1, row=1, sticky="nsew")
frame5.grid(column=0, row=2, sticky="nsew", columnspan=2)
frame6.grid(column=0, row=3, sticky="nsew", columnspan=2)

label1 = tkinter.Label(frame1, text="Simple label 1")
label2 = tkinter.Label(frame2, text="Simple label 2")
button1 = tkinter.Button(frame3, text="Configure button 1", command=lambda: color_label(label1, entry.get()))
button2 = tkinter.Button(frame4, text="Configure button 2", command=lambda: color_label(label2, entry.get()))

button3 = tkinter.Button(frame5, text="Apply and close", command=root.destroy)

entry = tkinter.Entry(frame6)

label1.pack(fill='x')
label2.pack(fill='x')
button1.pack(fill='x')
button2.pack(fill='x')
button3.pack(fill='x')
entry.pack(fill='x')

root.mainloop()

Выход:

Взгляните на строки 5 и 6. Как вы можете видеть, метод color_label() теперь принимает новый аргумент. Этот аргумент – строка – затем используется для изменения настроенного параметра метки text . Кроме того, в строке 29 мы создаем новый виджет Entry (а в строке 36 мы упаковываем его в новый фрейм, созданный в строке 13).

В строках 24 и 25 мы видим, что каждая из наших лямбда-функций также передает один дополнительный аргумент. Метод get() класса Entry возвращает строку, которую пользователь ввел в поле ввода. Итак, как вы, вероятно, уже подозреваете, после нажатия кнопок “Настроить” текст назначенных им меток изменяется на любой текст, введенный пользователем в наше новое поле ввода.

Вывод

Я надеюсь, что эта часть урока заполнила некоторые пробелы в вашем понимании модуля Tkinter. Хотя некоторые расширенные функции Tkinter на первый взгляд могут показаться немного сложными, общая философия построения интерфейсов с использованием самого популярного графического пакета для Python очень проста и интуитивно понятна.

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