Как стать автором
Обновить

Python. Tkinter. В ожидании релиза 3.13

Уровень сложностиСредний
Время на прочтение7 мин
Количество просмотров6.5K

Работая над проектом svgwidgets я активно использовал функционал tk busy, который появился в релизе Tcl/Tk 8.6.0. Мне стало интересно, а поддерживается ли этот функционал в Python-е, а точнее в Tkinter-е. Каково же было мое удивление узнать, что именно сейчас в Tkinter, который входит в состав Python версии 3.13, добавляется функционал tk busy, который давно включен в tcl/tk. Релиз Python 3.13 ожидается в октябре этого года. Мне показалось, что будет полезно рассказать о функционале tk busy, а точнее о новых методах для виджетов в Tkinter. Вот эти методы - tk_busy_hold(), tk_busy_configure(), tk_busy_cget(), tk_busy_forget() и tk_busy_current().

Команда tk busy предоставляет простой способ блокировки виджета от действий пользователя.

Как работают методы блокировки tk_busy в Tkinter рассмотрим на примере. При этом будем использовать классические виджеты.
Но для начала пришлось собрать из исходных кодов Python-3.13.0rc1.tgz дистрибутив Python-а. Все это было мною проделано в Linux на Mageia release 9.
Итак, создадим некий графический интерфейс, в котором будет главное окно (mwin) размером 10 сантиметров на 6 сантиметров с виджетом панели (frame1), в которой будут размещены поле ввода данных (ent1) и кнопка (but1):

bash-5.2$ /usr/local/bin64/python3.13 Python 3.13.0rc1 (main, Aug 21 2024, 15:48:04) [GCC 12.3.0] on linux Type "help", "copyright", "credits" or "license" for more information.

from tkinter import *
... mwin=Tk()
... #Установим размер главного окна
... w1=mwin.winfo_pixels('10c')
... h1=mwin.winfo_pixels('6c')
... gg=str(w1) +'x'+ str(h1)
... mwin.geometry(gg)
... #Установим желтый фон главного окна
... mwin.configure(bg='yellow')
... #создадим панель/frame на гланом окне с цветом cyan
... fr1=Frame(mwin, bg='cyan')
... #Разместим панель fr1 в главном окне
... fr1.pack(fill='both',expand='1',padx='1c',pady='5m',side='top')
... #Создадим поле для ввода данных на панели fr1
... ent1=Entry(fr1)
... #Расместим поле ent1
... ent1.pack(fill='x', expand='0',padx='1c', pady='5m', anchor='nw')
... #Создадим кновку Ввод на панели fr1
... but1=Button(fr1, text='Ввод')
... #Разместим кнопку but1
... but1.pack(anchor='n')
... but1.pack(anchor='n', pady='0')
... #Определим фнкцию для обработки события , которая будет печатать имя виджета, на котором произошло это
событие
... def on_enter(event):
... print('Виджет=' + str(event.widget) + '\r')
... fr1.unbind('')
... mwin.bind('', on_enter, add=None)
... #Функция для нажатия кнопки
... def put_str (data):
... print ('Кнопка=' + data + '\r')
... #Подключаем вызов функции при нажатии кнопки:
... but1.configure(command=lambda: put_str("but1"))
... #фокус курсора убираем на главное окно
... mwin.focus()
... fr1.tk_busy_hold()
...

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

def on_enter(event):
    print('Виджет=' + str(event.widget) + '\r')

Для того, чтобы эта функция срабатывала, необходимо связать ее с главным окном и событием:

mwin.bind('<Enter>', on_enter, add=None)

Напомним команды для отмены вызова обработчика:

mwin.unbind('<Enter>')

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

Особенность связывания обработки событий <Enter> и <Leave> для главного окна состоит в том, что эта обработка будет вызываться и при наведении курсора мыши на любой виджет в этом окне.

После того, как был подключен обработчик события <Enter>, при наведении курсора на тот или иной виджет будет печататься его идентификатор.
Для полноты картины добавим еще одну функцию, которая будет печатать передаваемую ей строку:

def  put_str (data):
	print ('Кнопка=' + data + '\r') 

Эта функция будет вызываться у нас при нажатии кнопки «Ввод»:

but1.configure(command=lambda: put_str("but1"))

Теперь при попадании курсора на тот или иной виджет будет печататься имя (идентификатор) виджета, а при нажатии на кнопку «Ввод» печататься текст:

Кнопка=but1

Предположим, что мы хотим на какой-то период обезопасить себя и сделать так, чтобы любые события и действия для панели fr1 и расположенных на ней поля ввода ent1 и кнопки but1 были заблокированы. Отметим, что блокировка тех или иных виджетов и операций с ними является неотъемлемой частью безопасности приложения, защиты как от преднамеренных, так и случайных деструктивных действий.
Возьмем сразу быка за рога и применим метод tk_busy_hold() для блокировки панели fr1 и посмотрим, что будет:

fr1.tk_busy_hold() 

Но перед выполнением этой операции переведем фокус курсора мыши на главное окно:

mwin.focus()

Зачем мы это делаем, будет сказан чуть ниже.
Итак, после выполнения команды fr1.tk_busy_hold() в нашем примере появится курсор занятости или блокирования в виде вращающегося круга с сине-красным ободком:

Этот курсор будет на всем пространстве панели, включая поле ввода и кнопку. За пределами панели fr1 курсор примет обычный вид. Кстати, вид курсора занятости можно поменять, задав его его вид как параметр в методе tk_busy_hold(cursor='<имя курсора>'), yапример, fr1.tk_busy_hold(cursor='gumby'):

Стандартный курсор занятости имеет идентификатор watch.
Эффект блокировки впечатляет. Кнопка «Ввод» полностью блокирована, мы не можем нажать на кнопку и она не реагирует на появление курсора мыши на ее поверхности. Аналогичным образом ведет себя и поле ввода. А вот перемещение курсора мыши на поверхность самой панели fr1 вызывает печать следующего текста:

Виджет=.!frame_Busy

До блокировки панели при наведении курсора мыши на нее печатался несколько иной текст:

Виджет=.!frame

Функция блокировки реализована простым и элегантным способом путем создания и отображения прозрачного окна, полностью закрывающего блокируемый виджет. Это окно создается с постфиксом _Busy и оно наследует обработку событий <Enter> и <Leave>, определенных для главного окна. Блокирующее прозрачное окно .!frame_Busy закрывает виджеты ent1 и but1, поэтому курсор мыши не попадает на них и событие для них не наступает.

К сожалению, если наш обработчик события показывет наличие виджета .!frame_Busy, то методы winfo_children() и children.values() не показывают окно блокировки. Может еще не реализовали? Подождем релиза. На вырочку приходит метод call():

mwin.call('winfo', 'children', mwin)

Результат выполнения этой команды будет следующим:

('.!frame', '.!frame_Busy')

Здесь мы видим и виджет, который мы блокируем .!frame и собственно блокирующий виджет .!frame_Busy.

Используя метод tk_busy_current() можно узнать к каким виджетам был применем метод блокирования tk_busy_hold(), т.е. какие виджеты заблокированы, например:

mwin.tk_busy_current()

Результат выполнения будет следующим:

[<tkinter.Frame object .!frame>, <tkinter.Tk object .>]

В данном примере заблокированными являются главное окно (tkinter.Tk) с именем «.» (точка) и панель (tkinter.Frame) с именем «.!frame».
Если мы хотим узнать текущий статус виджета, то можно использовать метод tk_busy_status(), который возвращает либо False либо True:

ent1.tk_busy_status()

Результатом выполнения данной команды будет False, к виджету ent1 метод tk_busy_hold() не применялся.

Узнать какой курсор занятости установлен или сменить его можно, применив метод tk_busy_configure(cursor='<идентификатор курсора>'). Например, установить курсор в виде песочных часов можно следующей командой:

fr1.tk_busy_configure(cursor='clock')

Но не все так радужно, есть и нюансы. Вот о них и пойдет речь ниже.

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

mwin.focus()

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

Предположим, мы в поле ввода ввели текст «Курсор здесь» и сразу же заблокировали панель fr1, оставив курсор в поле ввода:

На верхнем скриншоте показано состояние gui на момент блокирования панели fr1, курсор находился в поле ввода. На нижнем скриншоте показано, что несмотря на то, что панель заблокирована, если главное окно активно и идет ввод с клавиатуры, то он следует за курсором. Вот чтобы этого избежать и требуется установить курсор в нейтральное положение. Лично я ставлю его на главное окно. Можно создать временный виджет (ту же панель), обязательно его разместить (place, pack, grid), установить на него фокус курсора, а после этого временный виджет можно и уничтожить.

Естественно, мы могли не блокировать всю панель fr1, а просто заблокировать отдельно поле ввода ent1 и кнопку but1:

#Разблокируем панель fr1
fr1.tk_busy_forget()
#Курсор  мыши на панель fr1
fr1.focus()
#Блокируем поле ввода ent1
ent1.tk_busy_hold()
# Блокируем кнопку but1
but1.tk_busy_hold()

Теперь при наведении курсора на панель fr1 будет печататься сообщение «Виджет=.!frame», а вот при попадание курсора на поле ввода или кнопку будет печататься идентификатор блокирующего окна «Виджет=.!frame.!entry_Busy» или «Виджет=.!frame.!button_Busy».

А чтобы все было как при блокировании панели fr1, можно установить для нее курсор watch:

fr1.configure(cursor='watch')

предварительно сохранив текущий курсор:

cur=fr1.cget('cursor')

Вернуть курсор в исходное состояние можно так:

fr1.configure(cursor=cur)

Теперь вернемся в исходное состояние, когда заблокирована панель fr1:

Для начала разблокируем поле ввода и кнопку:

ent1.tk_busy_forget()
fr1 tk_busy_forget()

И снова заблокируем панель fr1:

#Прячем курсор
fr1.focus()
#Блокируем панель fr1
fr1.tk_busy_hold()

Все, и поле ввода, и кнопка для нас недоступны.

А теперь попробуйте применить метод lift() к заблокированной панели:

fr1.lift()

И вы увидите, что панель, а следовательно, и поле ввода и кнопки стали доступны!
При этом метод tk_busy_status() показывает, что панель заблокирована:

fr1.tk_busy_status() 
True 

Также как и метод tk_busy_current():

fr1.tk_busy_current() 
[<tkinter.Frame object .!frame>] 

Также как и метод call():

mwin.call('winfo', 'children', mwin) 
('.!frame', '.!frame_Busy'

Все очень просто, блокируемое окно (.!frame) и блоукирующее (.!frame_Busy) находятся на одном уровне иерархии и к ним применимы методы lift() и lower().

Метод lower() позволит опустить панель под блокирующее окно:

fr1.lower('.!frame.!button_Busy')

Методы lift() и lower() открывают широкий простор применения методов семества tk_busy для блокировки одноуровневых виджетов. Но эта тема для отдельной статьи.
А теперь будем ждать выхода Python релиза 3.13.
Всех с началом нового учебного года!

Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+7
Комментарии22

Публикации

Истории

Работа

Ближайшие события

28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
2 – 18 декабря
Yandex DataLens Festival 2024
МоскваОнлайн
11 – 13 декабря
Международная конференция по AI/ML «AI Journey»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань