Events - события

В tkinter с помощью метода bind() между собой связываются виджет, событие и действие. Например, виджет – кнопка, событие – клик по ней левой кнопкой мыши, действие – отправка сообщения. Другой пример: виджет – текстовое поле, событие – нажатие Enter, действие – получение текста из поля методом get() для последующей обработки программой. Действие оформляют как функцию (или метод), которая вызываются при наступлении события.

event_01

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

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()
1
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()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Но это не совсем правильно, так как код тела функций фактически идентичен, а имя шрифта можно передавать как аргумент. Лучше определить одну функцию:

def changeFont(event, font):
    l['font'] = font
1
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))
1
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()
1
2
3
4
5
6
7
8
9
10
11
12

Упражнения

  1. Напишите программу состоящую из текстового поля Entry() и списка Listbox():

    • набранный текст в Entry() при нажатие копки Enter переносит набранный в данном поле текст в список Listbox();
    • при нажатии <Delete> удаляется выбранный элемент спика, или несколько выбранных ранее элементов;
    • при двойном клике <Double-Button-1> по элементу списка, значение элемента должна копироваться в текстовое поле.
  2. Напишите программу состоящую из виджет метки - Lable():

    • виждет растягивается на все окно;
    • добавьте текст "Click mouse button to change color";
    • при нажатии на метке левой кнопкой мыши, меняется цвет метки;
    • при нажатии на метке правой кнопкой мыши, меняется цвет шрифта.

Виды событий

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

Можно выделить три основных типа событий:

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

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

event_02

При вызове метода 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()
1
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()
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

Пример программы, обрабатывающей события:

event_03

Здесь сочетание клавиш 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()
1
2
3
4
5
6
7
8

event_all_keys_01

Подобные общие решения помогают понять и найти решение не прибегая к штудированию документации.

Упражнения

  1. Напишите программу состоящую из списка. При активном окне, и нажатии разных кнопок клавиатуры происходит добавление нажатых клавиш в список.

    event_keys_listbox_01

  2. Для работы со временем можно воспользоваться следующим кодом:

    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" выводит текущее время;

    event_clock_01

    • рамка "Start time" при первом нажатии клавиши пробел выводит время нажатия пробела (время начала отсчета);

    event_clock_01

    • рамка "Finish time" при втором нажатии клавиши пробел выводит время следующего нажатия пробела (время остановки таймера);

    event_clock_01

    • рамка "Spend time" при следующем нажатии клавиши пробел выводит время прошедшее между стартом и финишем.

    event_clock_01

    • следующее нажатие клавиши пробел сбрасывает значения "Start time","Finish time" и "Spend time" на нули.

    event_clock_01

    Если программа написана в статическом стиле, переписать код в динамический вид:

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

    • при нажатии на пробел, запускаются/останавливаются/сбрасываются все таймеры.
    • при нажатии на цифры от 1 до 3 запускается/останавливается/сбрасывается соответствующий таймер.
    • результаты замеров перед сбросом, записываются в файл.
    • когда таймеры работают цвет фона "светло-зеленый", когда остановленый - "розовый".

    event_sport_timer_3

  4. Напишите логику обработки событий программы для написания коротких сочинений:

    • при получении исходных данных: минимального и максимального количества слов, количества слов в предложении и нажатии 'Enter' рассчитывается и выводится количество предложений для каждого абзаца (введения, главной мысли и вывода).
    • при наборе текста (каждом нажатии клавиши) происходит вычисление и заполнение "статистики", и старт таймера.
    • вычисление результата происходит по ходу выполнения: выводится время выполнения задания и показатель готовности текста: "Less", "Ready" или "Much".
    • таймер останавливается, если объем написанных слов более максимально необходимых, но обновляется если работа над текстом продолжается.
    • программа не должна вызывать ошибок при работе.

    event_literary_note

    Код основы:

    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