Работая над проектом 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.
Всех с началом нового учебного года!