Работая над проектом 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.
Всех с началом нового учебного года!
Комментарии (22)
JHD
02.09.2024 12:46По субъективному любительскому мнению - веб интерфейс, даже с питоном под капотом, легче сделать красиво и самое главное распространить, нежели скрипты с GUI.
kAIST
02.09.2024 12:46+1Самодостаточная инсталяшка python+tkinter под windows в пределах 10 мегабайт занимает. В чем сложность?
JHD
02.09.2024 12:46Это уже инсталяшка которую надо всем клиентам пихнуть и уговорить поставить, да еще и обновлять при выходе новых версий для совместимости. На мак вообще удачи что-то в виде инсталяшки расшерить. Веб браузер уже есть у всего и везде, и у планшета и у телефона.
kAIST
02.09.2024 12:46+1А вы не думали, что есть немалое количество приложений, которые в браузере (+ на сервере), просто не могут работать?
Те, чем пользуюсь я, например, перемалывают десятки гигабайт медиа, используя при этом ресурсы оперативки, gpu и cpu на полную катушку.
JHD
02.09.2024 12:46А я где писал про функционал? Хотя и там уже есть и webgl и webasm. ну и уж сервер то локальной машине ни по какому параметру не уступит. Мой комментарий был про простоту сделать красивый/удобный гуи и легко его другим пользователям донести.
kAIST
02.09.2024 12:46+1Хотите сказать, локальный сервер распространять проще чем просто GUI приложение? Вам в любом случае придется делать инсталяшку какую то, плюс еще костыль в виде окна консоли + открытие в браузере. Ну и ограничений немало при таком подходе. Как мне, например, из браузера вызвать диалог выбора каталога?
saipr Автор
02.09.2024 12:46+1про простоту сделать красивый/удобный гуи и легко его другим пользователям донести
Простота и красивость гуи зависит прежде всего от проектировщика этого гуи. Дайте проект этого гуи и без проблем он будет написан на том же tcl/tk и Tkinter-е. А насчет донести, есть, например, такие разработки как freewrap, tclexecomp и другие которые это прекрасно делают. Ссылки нам них можно найти здесь.
Tikhvinskiy
02.09.2024 12:46+1Начал изучать tkinter - интересная штука, как обертка для маленьких приложений очень даже, плюс она мультиплатформеная.
Вот тоже наваял утилиту. Часто слушаю большие аудиофайлы(книги, подкасты) что неудобно. Наколдил себе быстрый удобный резатель по паузам.
Интерфейс tkinter:
https://github.com/Tikhvinskiy/Smart-audio-splitter/blob/main/screen2.jpg
Сама прога, кому интересно:
efi
Много лет назад писал на pyqt, интересно, как много сегодня пишут на tkinter
saipr Автор
Честно скажу - не знаю. Но кто-то пишет...
economist75
Красивого на Tk вообще ничего не встречал. Очень сильно продвинулись за последнее время web-инструменты (streamlit, gradio итд) и консольные утилиты для имитации GUI.
saipr Автор
Взгляните все же на svgwiidgets
lrdprdx
Честное слово -- не хочу обидеть, но Вы про эти кислотные цвета и градиентные заливки ? Если Вы про скрины в Ваших статьях, то это ужасно просто. Я не вникал в Ваши статьи, но предполагаю, что визуальная составляющая в них не главное. Ещё раз -- без негатива, личное мнение.
saipr Автор
Нет, конечно! Я об инструменте, который позволяет создавать современный GUI или GUI, который бы устроил вас.
lrdprdx
Принял.
OrAnGeFoXL
Назовите какие именно консольные утилиты для имитации GUI очень интересно. Встречал только Textual и отдельные элементы по типу меню и прогрессбаров.
lrdprdx
Посмотри тут: тут
lrdprdx
Сейчас пишу систему мониторинга температуры. Только это tkinter :
GamePad64
Довольно симпатично. Это какой-токастомный стиль для tk?
lrdprdx
ttkbootstrap
Ну и matplotlib я соответственно настроил цвета.
saipr Автор
Оказывается не только пишут, но и учат школьников программированию с использованием Tkinter:
Как создать часы на Python: уроки программирования для школьников