Events - события
В tkinter
с помощью метода bind()
между собой связываются виджет, событие и действие. Например, виджет – кнопка, событие – клик по ней левой кнопкой мыши, действие – отправка сообщения. Другой пример: виджет – текстовое поле, событие – нажатие Enter
, действие – получение текста из поля методом get()
для последующей обработки программой. Действие оформляют как функцию (или метод), которая вызываются при наступлении события.
Один и тот же виджет можно связать с несколькими событиями. В примере ниже используется одна и та же функция-обработчик, однако могут быть и разные:
from tkinter import *
from tkinter.ttk import *
def change(event):
b['text'] = 'Thanks for your click. ;)'
root = Tk()
b = Button(text='Click me!')
b.bind('<Button-1>', change)
b.bind('<Return>', change)
b.pack()
root.mainloop()
2
3
4
5
6
7
8
9
10
11
12
13
14
Здесь текст кнопки меняется как при клике по ней (событие <Button-1>
), так и при нажатии клавиши Enter
(событие <Return>
). Однако Enter
сработает, только если кнопка предварительно получила фокус. В данном случае для этого надо один раз нажать клавишу Tab. Иначе нажатие Enter
будет относиться к окну, но не к кнопке.
У функций-обработчиков, которые вызываются через bind()
, а не через свойство command, должен быть обязательный параметр event
, через который передается событие. Имя event
– соглашение, идентификатор может иметь другое имя, но обязательно должен стоять на первом месте в функции, или может быть вторым в методе.
Что делать, если в функцию надо передать дополнительные аргументы? Например, клик левой кнопкой мыши по метке устанавливает для нее один шрифт, а клик правой кнопкой мыши – другой. Можно написать две разные функции:
from tkinter import *
from tkinter.ttk import *
def font1(event):
l['font'] = "Verdana"
def font2(event):
l['font'] = "Times"
root = Tk()
l = Label(text="Hello World")
l.bind('<Button-1>', font1) # ЛКМ
l.bind('<Button-3>', font2) # ПКМ
l.pack()
root.mainloop()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Но это не совсем правильно, так как код тела функций фактически идентичен, а имя шрифта можно передавать как аргумент. Лучше определить одну функцию:
def changeFont(event, font):
l['font'] = font
2
Однако возникает проблема, как передать дополнительный аргумент функции в метод bind()? Ведь в этот метод мы передаем объект-функцию, но не вызываем ее. Нельзя написать l.bind("<Button-1>", changeFont(event, "Verdana"))
. Потому что как только вы поставили после имени функции скобки, то значит вызвали ее, то есть заставили тело функции выполниться. Если в функции нет оператора return, то она возвращает None. Поэтому получается, что даже если правильно передать аргументы, то в метод bind() попадет None, но не объект-функция.
На помощь приходят так называемые анонимные объекты-функции Python, которые создаются инструкцией lambda. Применительно к нашей программе выглядеть это будет так:
l.bind('<Button-1>', lambda event, f="Verdana": changeFont(event, f))
l.bind('<Button-3>', lambda event, f="Times": changeFont(event, f))
2
Лямбда-функции можно использовать не только с методом bind()
, но и опцией command
, имеющейся у ряда виджет. Если функция передается через command
, ей не нужен параметр event
. Здесь обрабатывается только одно основное событие для виджета – клик левой кнопкой мыши.
У меток нет command, однако это свойство есть у кнопок:
from tkinter import *
def changeFont(font):
l['font'] = font
root = Tk()
l = Label(text="Hello World")
l.pack()
Button(text="Verdana", command=lambda f="Verdana": changeFont(f)).pack()
Button(text="Times", command=lambda f="Times": changeFont(f)).pack()
root.mainloop()
2
3
4
5
6
7
8
9
10
11
12
Упражнения
Напишите программу состоящую из текстового поля
Entry()
и спискаListbox()
:- набранный текст в
Entry()
при нажатие копкиEnter
переносит набранный в данном поле текст в списокListbox()
; - при нажатии
<Delete>
удаляется выбранный элемент спика, или несколько выбранных ранее элементов; - при двойном клике
<Double-Button-1>
по элементу списка, значение элемента должна копироваться в текстовое поле.
- набранный текст в
Напишите программу состоящую из виджет метки -
Lable()
:- виждет растягивается на все окно;
- добавьте текст "Click mouse button to change color";
- при нажатии на метке левой кнопкой мыши, меняется цвет метки;
- при нажатии на метке правой кнопкой мыши, меняется цвет шрифта.
Виды событий
Обычно, чтобы приложение с графическим интерфейсом что-то делало, должны происходить те или иные события, чаще всего представляющие собой воздействие человека на элементы GUI.
Можно выделить три основных типа событий:
- производимые мышью,
- нажатиями клавиш на клавиатуре,
- события, возникающие в результате изменения виджетов.
Нередко обрабатываются сочетания. Например, клик мышью с зажатой клавишей на клавиатуре.
При вызове метода bind()
событие передается в качестве первого аргумента: widget.bind(event, function)
Название события заключается в кавычки, а также в угловые скобки <
и >
. События описывается с помощью зарезервированных последовательностей ключевых слов.
Часто используемые события, производимые мышью:
<Button-1>
– клик левой кнопкой мыши<Button-2>
– клик средней кнопкой мыши<Button-3>
– клик правой кнопкой мыши<Double-Button-1>
– двойной клик левой кнопкой мыши<Motion>
– движение мыши- и т. д.
Пример:
from tkinter import *
def b1(event):
root.title("Левая кнопка мыши")
def b3(event):
root.title("Правая кнопка мыши")
def move(event):
x = event.x
y = event.y
s = "Движение мышью {}x{}".format(x, y)
root.title(s)
root = Tk()
root.minsize(width = 500, height=400)
root.bind('<Button-1>', b1)
root.bind('<Button-3>', b3)
root.bind('<Motion>', move)
root.mainloop()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
В этой программе меняется надпись в заголовке главного окна в зависимости от того, двигается мышь, щелкают левой или правой кнопкой.
Событие (event) – это один из объектов tkinter
. У событий есть атрибуты, как и у многих других объектов. В примере в функции move()
извлекаются значения атрибутов x
и y
объекта event, в которых хранятся координаты местоположения курсора мыши в пределах виджета, по отношению к которому было сгенерировано событие. В данном случае виджетом является главное окно, а событием – <Motion>
, т.е. перемещение мыши.
Для событий с клавиатуры буквенные клавиши можно записывать без угловых скобок (например, „a“).
Для неалфавитных клавиш существуют специальные зарезервированные слова. Например, <Return>
- нажатие клавиши Enter, <space>
- пробел. (Заметим, что есть событие <Enter>
, которое не имеет отношения к нажатию клавиши Enter
, а происходит, когда курсор заходит в пределы виджета.)
Сочетания пишутся через тире. В случае использования так называемого модификатора, он указывается первым, детали на третьем месте. Например, <Shift-Up>
- одновременное нажатие клавиш Shift
и стрелки вверх, <Control-B1-Motion>
– движение мышью с зажатой левой кнопкой и клавишей Ctrl
:
from tkinter import *
def exitWin(event):
root.destroy()
def inLabel(event):
t = ent.get()
label.configure(text = t)
def selectAll(event):
root.after(10, select_all, event.widget)
def select_all(widget):
widget.selection_range(0, END)
widget.icursor(END) # курсор в конец
root = Tk()
ent = Entry(width=40)
ent.focus_set()
ent.pack()
label = Label(height=3, fg='orange', bg='darkgreen', font="Verdana 24")
label.pack(fill=X)
ent.bind('<Return>',inLabel)
ent.bind('<Control-a>',selectAll)
root.bind('<Control-q>',exitWin)
root.mainloop()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Пример программы, обрабатывающей события:
Здесь сочетание клавиш Ctrl+a
выделяет текст в поле. Без root.after()
выделение не работает. Метод after()
выполняет функцию, указанную во втором аргументе, через промежуток времени, указанный в первом аргументе. В третьем аргументе передается значение атрибута widget
объекта event
. В данном случае им будет поле ent
. Именно оно будет передано как аргумент в функцию select_all()
и присвоено параметру widget
.
События мыши
<Button-1>
- самая левая кнопка;<Button-2>
- средняя кнопка (где доступно);<Button-3>
- самая правая кнопка.<Button-4>
- прокрутка вверх колесика мыши в Linux;<Button-5>
- прокрутка вниз колесика мыши в Linux;<Button-1>
,<ButtonPress-1>
и<1>
являются синонимами.<Motion>
– движение курсора мыши.<B1-Motion>
- мышь перемещается с нажатой кнопкой B1 (используйте B2 для средней кнопки, B3 для правой кнопки).<ButtonRelease-1>
- кнопка была отпущена. Это, вероятно, лучший выбор в большинстве случаев, чем событиеButton()
, потому что если пользователь случайно нажимает кнопку, они могут отодвинуть мышь от виджета, чтобы избежать отключения события.<Double-Button-1>
- кнопка-1 была дважды нажата. Вы можете использовать Double или Triple как префиксы.<Enter>
- Указатель мыши вошел в виджет (это событие не означает что пользователь нажал клавишу Enter!).<Leave>
- Указатель мыши покинул виджет.
События клавиатуры
<FocusIn>
- Фокус клавиатуры был перемещен на этот виджет или на дочерний элемент этот виджет.<FocusOut>
- Фокус клавиатуры был перемещен из этого виджета в другой виджет.<Return>
Пользователь нажал клавишу Enter. Для обычного 102-клавишного.Клавиатура в стиле ПК, специальные клавиши: Cancel (the Break key), BackSpace, Tab, Return(the Enter key), space Shift_L (any Shift key), Control_L (any Control key), Alt_L (any Alt key), Pause, Caps_Lock, Escape, Prior (Page Up), Next (Page Down), End, Home, Left, Up, Right, Down, Print, Insert, Delete, F1, F2, F3, F4, F5, F6, F7, F8, F9, F10, F11, F12, Num_Lock, and Scroll_Lock.
<Key>
Пользователь нажал любую клавишу. Ключ предоставляется в символе объекта события, переданного в обратный вызов (это пустая строка для специальных ключей).<a>
Пользователь нажал "а". Можно использовать большинство печатных символов как есть. Исключения составляют пробел (<space>
) и меньше (<Меньше>). Обратите внимание, что 1 - это привязка клавиатуры, а<1>
- это привязка мыши.<Shift-Up>
- пользователь нажал стрелку вверх, удерживая клавишу Shift нажат. Вы можете использовать префиксы, такие какAlt
,Shift
иControl
.<Configure>
Виджет изменил размер (или местоположение на некоторых платформах) новый размер предоставляется в атрибутах ширины и высоты объект события передан обратному вызову.<Activate>
Виджет меняется с неактивного на активный. Это относится к изменениям в параметре состояния виджета, таких как кнопка меняется с неактивного (неактивного) на активное.<Deactivate>
Виджет меняется с активного на неактивный. Это относится к изменениям в параметре состояния виджета, таких как радиокнопка, изменяющийся с активного на неактивный (выделен серым цветом).<Destroy>
Это событие происходит, когда виджет уничтожается.<Expose>
Это событие происходит всякий раз, когда хотя бы какая-то часть вашего приложения или виджета становится видимым после того, как ,было прикрыто другим окном.<KeyRelease>
Пользователь отжал клавишу.<Map>
Виджет отображается, то есть делается видимым в приложении. Это произойдет, например, при вызове метода виджета.grid()
.<Motion>
Пользователь полностью переместил указатель мыши внутри виджета.<MouseWheel>
Пользователь перемещал колесико мыши вверх или вниз. В настоящее время эта привязка работает на Windows и MacOS, но не на Linux.<Unmap>
Виджет не отображается и больше не виден.<Visibility>
Происходит, когда хотя бы некоторая часть окна приложения становится видимой на экране.
Используя следующий пример кода можно узнать информацию о клавишах клавиаты которые нажаты:
from tkinter import *
def show_key(event):
root.title(str(event))
root = Tk()
root.bind('<Key>', show_key)
root.mainloop()
2
3
4
5
6
7
8
Подобные общие решения помогают понять и найти решение не прибегая к штудированию документации.
Упражнения
Напишите программу состоящую из списка. При активном окне, и нажатии разных кнопок клавиатуры происходит добавление нажатых клавиш в список.
Для работы со временем можно воспользоваться следующим кодом:
import time for _ in range(5): print(int(time.time())) # общее количество секунд print(time.strftime("%H:%M:%S")) # время ввиде строки time.sleep(1) # задержка 1 сек.
1
2
3
4
5
6Расчет можно основывать на получаемых секундах или строчного времени. В реальных проектах задержку не используйте, т.к. это тормозит выполнение всей программы.
Напишите программу таймер состоящую из четырех рамок:
- рамка "Moscow clock" выводит текущее время;
- рамка "Start time" при первом нажатии клавиши
пробел
выводит время нажатия пробела (время начала отсчета);
- рамка "Finish time" при втором нажатии клавиши
пробел
выводит время следующего нажатия пробела (время остановки таймера);
- рамка "Spend time" при следующем нажатии клавиши
пробел
выводит время прошедшее между стартом и финишем.
- следующее нажатие клавиши пробел сбрасывает значения "Start time","Finish time" и "Spend time" на нули.
Если программа написана в статическом стиле, переписать код в динамический вид:
- создать спиcки для хранения виджетов,
- динамичесики отрисовывать повторяющиеся фреймы,
- использовать функции с параметрами для работы и изменения содержания феймов.
Напишите программу с тремя таймерами:
- при нажатии на пробел, запускаются/останавливаются/сбрасываются все таймеры.
- при нажатии на цифры от 1 до 3 запускается/останавливается/сбрасывается соответствующий таймер.
- результаты замеров перед сбросом, записываются в файл.
- когда таймеры работают цвет фона "светло-зеленый", когда остановленый - "розовый".
Напишите логику обработки событий программы для написания коротких сочинений:
- при получении исходных данных: минимального и максимального количества слов, количества слов в предложении и нажатии 'Enter' рассчитывается и выводится количество предложений для каждого абзаца (введения, главной мысли и вывода).
- при наборе текста (каждом нажатии клавиши) происходит вычисление и заполнение "статистики", и старт таймера.
- вычисление результата происходит по ходу выполнения: выводится время выполнения задания и показатель готовности текста: "Less", "Ready" или "Much".
- таймер останавливается, если объем написанных слов более максимально необходимых, но обновляется если работа над текстом продолжается.
- программа не должна вызывать ошибок при работе.
Код основы:
from tkinter import * from tkinter.ttk import * def get_status(): pass def get_task(): pass def set_result(): pass def set_time(): pass root = Tk() root.title('Literary note') text = Text(wrap=WORD, width=30) text.pack(side=LEFT, expand=1, fill=BOTH) scroll = Scrollbar(command=text.yview) scroll.pack(side=LEFT, fill=Y) text.config(yscrollcommand=scroll.set) frame_right = Frame() frame_right.pack(side=LEFT, fill=Y) frame_task = LabelFrame(frame_right, text='Task:') frame_task.pack(fill=X) Label(frame_task, text='Min words:').pack() entry_min_words = Entry(frame_task, justify=RIGHT) entry_min_words.insert(END, 70) entry_min_words.pack(fill=X) Label(frame_task, text='Max words:').pack() entry_max_words = Entry(frame_task, justify=RIGHT) entry_max_words.insert(END, 90) entry_max_words.pack(fill=X) Label(frame_task, text='Number of words in a sentence:').pack() entry_sentence_len = Entry(frame_task, justify=RIGHT) entry_sentence_len.insert(END, 7) entry_sentence_len.pack(fill=X) label_sentence_start = Label(frame_task, text="Number of start sentences: 2 - 3") label_sentence_start.pack(fill=X) label_sentence_main = Label(frame_task, text="Number of main sentences: 5 - 7") label_sentence_main.pack(fill=X) label_sentence_end = Label(frame_task, text="Number of end sentences: 2 - 3") label_sentence_end.pack(fill=X) frame_status = LabelFrame(frame_right, text='Status:') frame_status.pack(fill=X) label_written_words = Label(frame_status, text='Written words: 0') label_written_words.pack(fill=X) label_written_sentences = Label(frame_status, text='Written sentences: 0') label_written_sentences.pack(fill=X) label_written_sentence_start = Label(frame_status, text="Written start sentences: 0") label_written_sentence_start.pack(fill=X) label_written_sentence_main = Label(frame_status, text="Written main sentences: 0") label_written_sentence_main.pack(fill=X) label_written_sentence_end = Label(frame_status, text="Written end sentences: 0") label_written_sentence_end.pack(fill=X) frame_result = LabelFrame(frame_right, text='Result:') frame_result.pack(fill=X) label_time_spent = Label(frame_result, text='Spent time: 00:00:00') label_time_spent.pack(fill=X) label_result = Label(frame_result, text="Result: Less.../Ready!/Much...") label_result.pack(fill=X) root.mainloop()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78