Модуль tkinter.ttk

В состав пакета tkinter входит модуль ttk, содержащий классы более стилизованных и современных виджет. По умолчанию их внешний вид зависит от операционной системы.

В коде ниже для сравнения в окне размещено несколько пар аналогичных виджетов из модулей tkinter и tkinter.ttk.

import tkinter as tk
import tkinter.ttk as ttk
 
root = tk.Tk()
 
tk.Button(text="Hello").pack()
ttk.Button(text="Hello").pack()
 
tk.Checkbutton(text="Hello").pack()
ttk.Checkbutton(text="Hello").pack()
 
tk.Radiobutton(text="Hello").pack()
ttk.Radiobutton(text="Hello").pack()
 
root.mainloop()

В операционной системе Ubuntu они будут выглядеть так:

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

Если же нам не нужны виджеты модуля tkinter, подобные которым есть в tkinter.ttk, удобнее импортировать всё пространство имен как одного, так и второго модуля.

from tkinter import *
from tkinter.ttk import *
 
root = Tk()
 
b = Button(text="Hello")
b.pack()
 
t = Text(width=10, height=5)
t.pack()
 
print(root.__class__)
print(b.__class__)
print(t.__class__)
 
root.mainloop()

В данном случае пространство имен tkinter.ttk, который импортируется вторым, перекрывает часть имен tkinter. Поэтому выражение Button(text="Hello") вызывает конструктор класса Button, находящийся в модуле tkinter.ttk. В то же время в этом модуле нет классов Tk и Text. Следовательно, экземпляры этих классов создаются от базовых классов tkinter. В консоль будет выведено:

<class 'tkinter.Tk'>
<class 'tkinter.ttk.Button'>
<class 'tkinter.Text'>

Как уже должно быть понятно, наборы виджетов обоих модулей не полностью перекрываются. Есть общие (такие как Label, Button, Entry), есть характерные только для tkinter (например, Listbox и Canvas) и только для ttk (например, Combobox, Notebook).

Сложность заключается в том, что в ttk свойства виджетов программируются не совсем так как в tkinter. Если в приложении сочетаются элементы сразу обоих модулей, код программы становится менее ясным.

Для виджетов из tkinter.ttk многие свойства задаются с помощью экземпляра, созданного от класса ttk.Style. Основная идея ttk – отделить оформление виджета от описания его поведения.

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

from tkinter import *
from tkinter import ttk
 
root = Tk()
 
b_tk = Button(text="Hello Tk")
b_ttk = ttk.Button(text="Hello Ttk")
 
b_tk.config(background="#b00",
            foreground="#fff")
 
style = ttk.Style()
style.configure("TButton",
                background="#0b0",
                foreground="#fff")
 
b_tk.pack(padx=10, pady=10)
b_ttk.pack(padx=10, pady=10)
root.mainloop()

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

b_tk = Button(text="Hello Tk",
              background="#b00",
              foreground="#fff")

Настроить так кнопку из модуля ttk нельзя. Вместо этого мы должны создать экземпляр от класса Style и уже через него изменять свойства по умолчанию.

В коде выше используется метод configure. Здесь первым аргументом в него передается имя стиля (TButton), связанного с классом объектов, для которых производится настройка. В данном случае это кнопки. Для уточнения имени стиля можно воспользоваться методом winfo_class:

print(ttk.Label().winfo_class())
print(ttk.Button().winfo_class())

Вывод:

TLabel
TButton

Что делать, если в программе нужны, например, кнопки разного стиля? Можно создать свой стиль, унаследовав его от исходного, и изменять лишь отдельные свойства.

from tkinter import *
from tkinter.ttk import *
 
root = Tk()
 
style = Style()
style.configure("G.TButton", foreground="green")
 
Button(text="First", style="G.TButton").pack()
Button(text="Second").pack()
 
root.mainloop()

В примере выше созданный стиль называется "G". После точки указывается его родитель (в данном случае это стиль TButton. При создании первой кнопки через опцию style указываем применяемый стиль.

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

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

Кроме того, в ttk есть разные темы. Можно менять не отдельные виджеты, а целиком тему оформления приложения. С помощью методов theme_names и theme_use можно выяснить список тем (который зависит от вашей ОС) и текущую тему:

from tkinter.ttk import *
 
style = Style()
print(style.theme_names())
print(style.theme_use())

Вывод:

('clam', 'alt', 'default', 'classic')
default

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

from tkinter import *
from tkinter.ttk import *
 
root = Tk()
style = Style()
 
e = Entry(justify='center')
e.pack()
Button(text="Hello").pack()
Label(text="Hello").pack()
 
themes = style.theme_names()
i = 0
 
 
def theme_next(event):
    global i
    style.theme_use(themes[i])
    e.delete(0, END)
    e.insert(0, themes[i])
    if i < len(themes)-1:
        i += 1
    else:
        i = 0
 
 
root.bind('<Return>', theme_next)
root.mainloop()

Описание свойств виджетов в ttk не всегда совпадает с тем, как это делается в базовом tkinter. Выше в примере с красной и зеленой кнопками с помощью опций foreground и background мы устанавливаем цвета текста и фона кнопки, когда она не находится под курсором мыши, то есть когда она не активна.

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

… 
b_tk.config(background="red",
            foreground="white",
            activebackground="orange",
            activeforeground="white")
 
style = ttk.Style()
style.configure("TButton",
                background="green",
                foreground="white",
                activebackground="lightgreen",
                activeforeground="black")

Код выше выполнится без ошибок. Однако фон зеленой кнопки при нажатии на нее останется светло-серым, он не станет светло-зеленым.

В данном случае вместо метода configure следует использовать метод map объектов типа Style:

from tkinter import *
from tkinter.ttk import *
 
root = Tk()
 
Button(text="Hello World").pack(
    padx=40, pady=40,
    ipadx=20, ipady=20
)
 
st = Style()
st.map('TButton',
       foreground=[('!active', 'purple'),
                   ('pressed', 'orange'),
                   ('active', 'red')],
       background=[
                   ('pressed', 'brown'),
                   ('active', 'white')]
       )
 
root.mainloop()

Здесь мы описываем для кнопки состояния двух свойств: текста (foreground) и фона (background). При этом у каждого свойства есть три состояния: неактивно, активно и нажато.

Практическая работа

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

Для справки:

Tkinter. Программирование GUI на Python