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

Работая нал проектом 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 ().

Командаy 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 ()

116c9e81bf8acb4df6a849ad7110ffe4.png

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

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

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

mwin.bind('', on_enter, add=None)

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

mwin.unbind('')

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

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

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

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 () в нашем примере появится курсор занятости или блокирования в виде вращающегося круга с сине-красным ободком:

3874e1b24d611498395107b9abb29aa5.png

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

67502754ec2583e6249d9e0a3b2e2e22.png

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

Виджет=.!frame_Busy

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

Виджет=.!frame

Функция блокировки реализована простым и элегантным способом путем создания и отображения прозрачного окна, полностью закрывающего блокируемый виджет. Это окно создается с постфиксом _Busy и оно наследует обработку событий и , определенных для главного окна. Блокирующее прозрачное окно .! 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.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, оставив курсор в поле ввода:

4efd41462992eb9ada816a5f18017a7e.png

На верхнем скриншоте показано состояние 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() 
[] 

Также как и метод 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.
Всех с началом нового учебного года!

© Habrahabr.ru