Границы и рельефность
Параметр bd=N виджета можно использовать для установки ширины границы, а параметр relief=S - ее стиля. S может принимать значения FLAT, SUNKEN, RAISED, GROOVE, SOLID или RIDGE - все эти константы экспортирует модуль tkinter.
Курсор
Параметр cursor позволяет определять внешний вид указателя мыши при наведении его на виджет. Например, cursor=’gumby’ изменяет стрелку на фигурку зеленого человечка. В число других имен указателей, часто используемых в этой книге, входят watch, pencil, cross и hand2.
Состояние
Некоторые виджеты поддерживают понятие состояния, влияющее на их внешний вид. Например, виджет с параметром state=DISABLED обычно рисуется на экране закрашенным (окрашивается в серый цвет) и делается неактивным. Значение NORMAL делает его обычным. Некоторые виджеты поддерживают также состояние READONLY, когда сам виджет отображается, как обычно, но он никак не откликается на попытки изменения.
Отступы (padding)
Вокруг многих виджетов (кнопок, меток и текста) можно добавить дополнительное пустое пространство с помощью параметров padx=N и pady=N. Интересно, что эти параметры можно определять и в вызовах метода pack (тогда пустое пространство добавляется вокруг виджета в целом), и в самом объекте виджета (в результате увеличивается сам графический элемент).
Чтобы проиллюстрировать некоторые из этих дополнительных настроек, в примере 8.2 создается кнопка, изображенная на рис. 8.2, и изменяется форма указателя мыши, когда он помещается над кнопкой.
Пример 8.2. PP4E\Gui\Tour\config-button.py
from tkinter import *
widget = Button(text=’Spam’, padx=10, pady=10) widget.pack(padx=20, pady=20) widget.config(cursor=’gumby’)
widget.config(bd=8, relief=RAISED) widget.config(bg=’dark green’, fg=’white’) widget.config(font=(‘helvetica’, 20, ‘underline italic’)) mainloop()
Рис. 8.2. Параметры кнопки в действии
Чтобы увидеть эффект, создаваемый этими двумя параметрами, попробуйте поиграть с ними на своем компьютере. Большинству виджетов можно придать новый внешний вид таким же способом, и в этой книге мы будем неоднократно встречаться с такими параметрами. Мы встретимся также с операционными параметрами, такими как focus (передает фокуса ввода), и другими. У виджетов могут быть десятки параметров, большинство из которых имеет разумные значения по умолчанию, создающие принятый на каждой оконной платформе внешний вид, что является одной из причин простоты использования tkinter. Но при необходимости tkinter позволяет создавать значительно более индивидуальные изображения.
Дополнительные способы применения параметров настройки, обеспечивающих типичный внешний вид виджетов, можно увидеть в разделе «Настройка виджетов с помощью классов» в предыдущей главе и особенно в примерах ThemedButton. Теперь, когда вы больше знаете о настройках, вам будет проще понять, как настройки в этих примерах, выполняемые в подклассах виджетов, наследуются всеми экземплярами и подклассами. Новое расширение ttk, описываемое в главе 7, также реализует дополнительные способы настройки виджетов, вводя понятие тем оформления. Больше подробностей и ссылки на ресурсы, посвященные ttk, вы найдете в предыдущей главе.
Окна верхнего уровня
Графические интерфейсы, построенные на базе tkinter, всегда имеют корневое окно, которое создается по умолчанию или явно с помощью конструктора объекта Tk. Это главное корневое окно открывается при запуске программы и обычно служит для размещения наиболее важных виджетов. Помимо этого окна сценарии, использующие библиотеку tkinter, могут порождать любое число независимых окон, которые создаются и открываются по требованию, в результате создания объектов виджетов Toplevel.
Каждый объект Toplevel порождает на экране новое окно и автоматически добавляет его в поток обработки цикла событий программы (для активации новых окон не нужно вызывать метод mainloop). В примере 8.3 создается корневое окно и два дополнительных окна.
Пример 8.3. PP4E\Gui\Tour\toplevel0.py
import sys
from tkinter import Toplevel, Button, Label win1 = Toplevel() # два независимых окна
win2 = Toplevel() # являющихся частью одного и того же процесса
Button(win1, text=’Spam’, command=sys.exit).pack()
Button(win2, text=’SPAM’, command=sys.exit).pack()
Label(text=’Popups’).pack() # по умолчанию добавляется в корневое окно Tk() win1.mainloop()
Сценарий toplevel0 получает корневое окно по умолчанию (к которому прикрепляется метка Label, потому что для нее не указан родитель) и создает два самостоятельных окна Toplevel, которые появляются и действуют независимо от корневого окна, как показано на рис. 8.3.
Рис. 8.3. Два окна Toplevel и корневое окно
Два окна Toplevel в правой части являются полноценными окнами - они могут независимо сворачиваться, распахиваться на весь экран и так далее. Обычно окна Toplevel используются для реализации многооконных интерфейсов, а также модальных и немодальных диалогов (подробнее о диалогах рассказывается в следующем разделе). Они сохраняются до тех пор, пока не будут явно закрыты или пока создавшее их приложение не завершит работу.
Согласно реализации примера щелчок на кнопке с крестиком в правом верхнем углу любого из окон Toplevel закрывает только это окно. С другой стороны, щелчок на любой из кнопок или на крестике главного окна закроет все остальные и завершит программу (подробнее о протоколе завершения рассказывается чуть ниже).
Важно знать, что, хотя окна Toplevels и действуют независимо, они не являются независимыми процессами - если программа завершится, автоматически будут закрыты все ее окна, включая все окна Toplevel, которые она могла создать. Позднее будет показано, как обойти это правило путем запуска независимых программ с графическим интерфейсом.
Виджеты Toplevel и Tk
Окно Toplevel напоминает Frame тем, что отщепляется в самостоятельное окно и обладает дополнительными методами, позволяющими работать со свойствами окна верхнего уровня. Виджет Tk в общем похож на виджет Toplevel, но используется для представления корневого окна приложения. Окна Toplevel имеют родителя, тогда как окно Tk - нет. Оно является настоящим корнем иерархии виджетов, создаваемых при конструировании графических интерфейсов с помощью библиотеки tkinter.
В примере 8.3 корневое окно Tk было получено даром, потому что для виджета Label был использован родитель по умолчанию, назначаемый при отсутствии первого аргумента в вызове конструктора:
Label(text=’Popups’).pack() # корневое окно Tk() по умолчанию
Передача значения None в первом аргументе конструктора виджета (или в именованном аргументе master) также приводит к назначению родителя по умолчанию. В других сценариях корневое окно Tk создается явно, например:
root = Tk()
Label(root, text=’Popups’).pack() # явное создание корневого окна Tk() root.mainloop()
В действительности, из-за того, что графические интерфейсы tkinter строятся в виде иерархии, по умолчанию всегда создается хотя бы одно корневое окно Tk, явно, как в данном примере, или нет. Хотя это и не типично, тем не менее в приложении вручную может создаваться несколько корневых окон Tk, при этом программа завершается только после закрытия всех окон Tk. Первое созданное корневое окно Tk - явно, в программном коде, или автоматически, интерпретатором, - используется как родитель по умолчанию для виджетов и других окон, при создании которых родитель не указывается.
В целом корневое окно Tk должно использоваться для отображения какой-либо информации верхнего уровня. Если не прикрепить графические элементы к корневому окну, при запуске сценария оно будет выведено как странное пустое окно (часто это происходит из-за забывчивости, когда программист создает виджеты, использующие родителя по умолчанию, но забывает вызвать метод компоновщика, выполняющий размещение виджетов). Технически можно подавить создание корневого окна по умолчанию и создать несколько корневых окон с помощью виджета Tk, как показано в примере 8.4.
Пример 8.4. PP4E\Gui\Tour\toplevel1.py
import tkinter
from tkinter import Tk, Button tkinter.NoDefaultRoot()
win1 = Tk() # два независимых корневых окна
win2 = Tk()
Button(win1, text=’Spam’, command=win1.destroy).pack()
Button(win2, text=’SPAM’, command=win2.destroy).pack() win1.mainloop()
Если запустить этот сценарий, он создаст только два окна, изображенные на рис. 8.3 (третье корневое окно не будет создано). Но чаще корневой объект Tk используется как главное окно, а виджеты Toplevel - как всплывающие окна приложения.
Обратите внимание: чтобы закрыть только одно окно, вместо функции sys.exit, которая завершает работу всей программы, вызывается метод destroy этого окна - чтобы понять, как действует этот метод, перейдем к изучению протоколов окна.
Протоколы окна верхнего уровня
Виджеты Tk и Toplevel экспортируют дополнительные методы и свойства, предназначенные для той роли, которую они играют на верхнем уровне, что иллюстрируется примером 8.5.
Пример 8.5. PP4E\Gui\Tour\toplevel2.py
Открывает три новых окна со стилями
метод destroy() закрывает одно окно, метод quit() закрывает все окна и завершает
приложение (прерывает работу функции mainloop);
окна верхнего уровня имеют заголовки, значки, могут сворачиваться
и восстанавливаться и поддерживают протокол событий wm;
приложение всегда имеет корневое окно, создаваемое по умолчанию или явно,
вызовом конструктора Tk(); все окна верхнего уровня являются контейнерами,
но они никогда не размещаются с помощью менеджера компоновки; объект Toplevel
напоминает фрейм Frame, но в действительности является новым окном и может иметь
собственное меню;
from tkinter import * root = Tk() # explicit root
trees = [(‘The Larch!’, ‘light blue’),
(‘The Pine!’, ‘light green’),
(‘The Giant Redwood!’, ‘red’)]
for (tree, color) in trees:
win = Toplevel(root) # новое окно
win.title(‘Sing...’) # установка границ
win.protocol(‘WM_DELETE_WINDOW’, lambda:None) # игнорировать закрытие
win.iconbitmap(‘py-blue-trans-out.ico’) # вместо значка Tk
msg = Button(win, text=tree, command=win.destroy) # закрывает одно окно
msg.pack(expand=YES, fill=BOTH) msg.config(padx=10, pady=10, bd=10, relief=RAISED) msg.config(bg=’black’, fg=color, font=(‘times’, 30, ‘bold italic’))
root.title(‘Lumberjack demo’)
Label(root, text=’Main window’, width=30).pack()
Button(root, text=’Quit All’, command=root.quit).pack() # завершает программу root.mainloop()
Эта программа добавляет виджеты в корневое окно Tk, сразу выводит три окна Toplevel с прикрепленными к ним кнопками и использует специальные протоколы верхнего уровня. Если запустить этот пример, он создаст картинку, переданную в черно-белом изображении на рис. 8.4 (на мониторе текст кнопок отображается синим, зеленым и красным цветом).
Рис. 8.4. Три настроенных окна Toplevel
Здесь следует отметить несколько деталей, касающихся функционирования, которые станут более заметными, если вы запустите сценарий на своем компьютере:
Перехват закрытия: protocol
Этот сценарий перехватывает событие закрытия окна менеджера окон с помощью метода виджета верхнего уровня protocol, поэтому при нажатии кнопки X в правом верхнем углу какого-либо из трех окон Toplevel ничего не происходит. Строка WM_DELETE_WINDOW обозначает операцию закрытия. С помощью этого метода можно запретить закрытие окон, кроме как из создаваемых в сценарии виджетов. Создаваемая этим сценарием функция lambda: None лишь возвращает значение None и больше ничего не делает.
Закрытие одного окна (и его дочерних окон): destroy
При нажатии на большую черную кнопку в любом из трех дополнительных окон закрывается только это окно, потому что это действие вызывает метод destroy виджета. Остальные окна продолжают существовать, как это свойственно диалоговым окнам. Технически вызов этого метода приводит к уничтожению соответствующего виджета и всех остальных виджетов, для которых он является родителем. В случае окон под этим подразумевается все их содержимое. В случае более простых виджетов метод destroy уничтожает сам виджет.
Вследствие того, что окна Toplevel имеют родителя, эти их отношения могут иметь последствия при применении метода destroy. Уничтожение окна, даже первого корневого окна Tk, созданного автоматически или явно, - которое является родителем по умолчанию, -приводит к уничтожению всех его дочерних окон. Так как корневые окна Tk не имеют родителя, на них никак не действует уничтожение других окон. При этом уничтожение последнего (или единственного) корневого окна Tk приводит к завершению программы. Окна Toplevel всегда уничтожаются при уничтожении родителя, но их уничтожение никак не влияет на другие окна, для которых они не являются предками. Это делает их идеальными для создания диалогов. Технически виджет Toplevel может быть дочерним по отношению к любому виджету и автоматически будет уничтожен вместе с родителем, однако обычно они создаются как потомки окна Tk, созданного явно или автоматически.
Закрытие всех окон: quit
Чтобы закрыть сразу все окна и завершить приложение с графическим интерфейсом (в действительности - активный вызов mainloop), кнопка корневого окна вызывает метод quit. То есть нажатие кнопки в корневом окне завершает работу приложения. Метод quit немедленно завершает приложение в целом и закрывает все его окна. Он может быть вызван относительно любого виджета tkinter, а не только относительно окна верхнего уровня - этот метод имеется также у фреймов, кнопок и других виджетов. Дополнительные подробности о методах quit и destroy вы найдете в обсуждении метода bind и его события
Заголовки окон: title
В главе 7 говорилось о методе title виджетов окон верхнего уровня (Tk и Toplevel), позволяющем изменять текст, выводимый в области верхней кромки окна. В данном случае в качестве текста заголовка окна устанавливается строка ‘Si...’, замещающая текст по умолчанию ‘tk’.
Значки окон: iconbitmap
Метод iconbitmap изменяет значок окна верхнего уровня. Он принимает значок или файл с растровым изображением и использует его в качестве графического значка окна, когда оно сворачивается и открывается. Если в Windows передать имя файла с расширением .ico (в данном примере используется такой файл, находящийся в текущем каталоге), он заменит значок по умолчанию с красными буквами «Tk», который обычно появляется в левом верхнем углу окна, а также в панели задач Windows. На других платформах вам может потребоваться использовать иные соглашения о файлах со значками, если вызов этого метода в наших примерах не дает желаемого результата (или просто закомментируйте вызов этого метода, если он приводит к аварийному завершению сценариев) - значки обычно являются платформозависимой особенностью, работа с ними зависит от используемого менеджера окон.
Управление компоновкой
Окна верхнего уровня служат контейнерами для других виджетов, подобно отдельному фрейму Frame. Однако в отличие от фреймов, виджеты - окна верхнего уровня - сами не компонуются (и не размещаются каким-либо другим менеджером компоновки). Для встраивания виджетов этот сценарий передает свои окна в аргументах, определяющих родительское окно, конструкторам меток и кнопок.
Имеется также возможность определять максимальный размер окна (физические размеры экрана в виде кортежа [ширина, высота]) с помощью метода maxsize() и устанавливать начальные размеры окна с помощью высокоуровневого метода geometry(" width x height + x + y "). На практике гораздо проще и удобнее позволить библиотеке tkinter (или вашим пользователям) самой устанавливать размер окон, тем не менее размер экрана может пригодиться при выборе масштаба отображения изображений (смотрите обсуждение PyPhoto в главе 11, например).
Кроме того, графические элементы окон верхнего уровня поддерживают другие типы протоколов, которые будут позднее использованы в данном обзоре:
Состояние
Методы iconify и withdraw объектов окон верхнего уровня позволяют сценариям сворачивать или удалять окна на лету; метод deiconify перерисовывает свернутое или удаленное окно. Метод state возвращает или изменяет состояние окна - допустимыми значениями, которые могут устанавливаться или возвращаться, являются: iconic, withdrawn, zoomed (в Windows: распахнутое на весь экран с помощью geometry или какого-либо другого метода) и normal (достаточно большого размера, чтобы вместить все содержимое). Методы lift и lower поднимают или опускают окно относительно других (метод lift является аналогом команды raise библиотеки Tk, которое является зарезервированным словом в языке Python). Их использование демонстрируется в сценариях будильника в конце главы 9.
Меню
Каждое окно верхнего уровня может иметь собственное меню - виджеты Tk и Toplevel принимают параметр menu, который используется для подключения горизонтальной строки меню с открывающимися списками элементов выбора. Эта строка меню выглядит соответствующим образом на каждой платформе, где выполняется сценарий. Меню будут изучаться в начале главы 9.
Большую часть методов окна верхнего уровня, используемых для взаимодействия с менеджером окон, можно также вызывать под именами с префиксом «wm_». Например, методы state и protocol можно также вызвать как wm_state и wm_protocol.
Обратите внимание, что в примере 8.3 при вызове конструктора Toplevel ему явно передается родительский виджет - корневое окно Tk (то есть Toplevel(root)). Окна Toplevel можно связывать с родительскими, как и любые другие виджеты, хотя зрительно они не встраиваются в родительские окна. Такой способ написания сценария имел целью избежать одной, на первый взгляд странной, особенности. Если бы окно создавалось так:
win = Toplevel() # новое окно
и корневое окно Tk при этом еще не существовало бы, этот вызов создал бы корневое окно Tk по умолчанию, которое стало бы родителем для окон Toplevel, как при всяком другом вызове графического элемента без передачи аргумента со ссылкой на родителя. Проблема в том, это делает принципиальным местоположение следующей строки:
root = Tk() # явное создание корня
Если поместить эту строку выше вызовов конструктора Toplevel, она создаст одно корневое окно, как и предполагается. Но если поставить эту строку ниже вызовов Toplevel, то tkinter создаст корневое окно Tk, которое будет отлично от созданного сценарием при явном вызове Tk. Это приведет к созданию двух корневых окон Tk, как в примере 8.4. Переместите вызов Tk под вызовы Toplevel, перезапустите сценарий, и вы увидите, что я имею в виду - вы получите четвертое, совершенно пустое окно! Чтобы избежать таких странностей, возьмите за правило создавать корневые окна Tk в начале сценариев и явным образом.
Все интерфейсы протоколов верхнего уровня доступны только в виджетах окон верхнего уровня, но часто доступ к ним можно получить через атрибут master виджета, хранящего ссылку на родительское окно. Например, изменение заголовка окна, в котором содержится фрейм, можно реализовать так:
theframe.master.title(‘Spam demo’) # master является окном-контейнером
Естественно, делать так можно только при уверенности, что фрейм будет использован только в одном типе окна. Например, прикрепляемые компоненты общего назначения, реализуемые в виде классов, должны оставить право на установку свойств окон за своим приложениями-клиентами.
Для виджетов верхнего уровня существуют другие инструменты, некоторые из которых могут не встретиться в этой книге. Например, в менеджерах окон Unix можно также устанавливать имя значка окна (iconname). Поскольку некоторые параметры значков можно применять только в сценариях, выполняемых в Unix, подробности, касающиеся этой темы, смотрите в других ресурсах по библиотекам Tk и tkinter. А сейчас нас ожидает следующая запланированная остановка в нашей экскурсии, где будет рассказано об одном из наиболее частых применений окон верхнего уровня.
Диалоги
Диалоги - это окна, выводимые сценарием с целью показать или запросить дополнительную информацию. Существует два вида диалогов: модальные и немодальные:
Модальные
Эти диалоги блокируют остальную часть интерфейса, пока окно диалога не будет закрыто - выполнение программы будет продолжено после получения диалогом ответа пользователя.
Немодальные
Эти диалоги могут оставаться на экране неопределенное время, не создавая помех другим окнам интерфейса, - обычно они в любой момент могут принимать входные данные.
Независимо от модальности диалоги обычно реализуются с помощью объекта окна Toplevel, с которым мы познакомились в предыдущем разделе, создаете вы Toplevel или нет. Существует три основных способа вывести диалог с помощью библиотеки tkinter: вызовом стандартных диалогов, обращением к современному объекту Dialog и путем создания пользовательских диалоговых окон с помощью Toplevel и других типов виджетов. Рассмотрим основы использования всех трех схем.
Стандартные (типичные) диалоги
Вызовы стандартных диалогов проще, поэтому начнем с них. В составе библиотеки tkinter поставляется набор готовых диалогов, реализующих многие из наиболее часто встречающихся окон, генерируемых программами, - диалоги выбора файла, диалоги с сообщениями об ошибках и предупреждениями и диалоги, позволяющие запросить ввод данных. Они называются стандартными диалогами, поскольку входят в состав библиотеки tkinter и используют библиотечные вызовы для конкретных платформ, чтобы принять вид, свойственный данной платформе. Например, диалог открытия файла в библиотеке tkinter выглядит как любой другой подобный диалог в Windows.
Все стандартные диалоги являются модальными (они не возвращают управление, пока пользователь не закроет диалог) и блокируют главное окно программы. Сценарии могут настраивать окна этих диалогов, передавая текст сообщения, заголовки и тому подобное. Они очень просты в использовании, поэтому сразу перейдем к примеру 8.6 (который хранится в файле с расширением .pyw, чтобы подавить вывод окна консоли в Windows при запуске сценария щелчком мыши):
Пример 8.6. PP4E\Gui\Tour\dlg1.pyw
from tkinter import *
from tkinter.messagebox import *
def callback():
if askyesno(‘Verify’, ‘Do you really want to quit?’): showwarning(‘Yes’, ‘Quit not yet implemented’) else:
showinfo(‘No’, ‘Quit has been cancelled’)
errmsg = ‘Sorry, no Spam allowed!’
Button(text=’Quit’, command=callback).pack(fill=X)
Button(text=’Spam’, command=(lambda: showerror(‘Spam’, errmsg))).pack(fill=X) mainloop()
Анонимная lambda-функция использована здесь в качестве оболочки вызова showerror, для передачи двух жестко определенных аргументов (напомню, что обработчики событий не получают аргументов от самой библиотеки tkinter). Если запустить этот сценарий, он создаст главное окно, изображенное на рис. 8.5.
Нажатие кнопки Quit в этом окне выводит диалог (рис. 8.6) - вызовом стандартной функции askyesno из модуля messagebox, входящего в состав пакета tkinter. В Unix и Macintosh этот диалог выглядит иначе, а в Windows выглядит, как показано на рисунке (на практике внешний вид диалога зависит от версии и настроек Windows - в моей системе Window 7 с настройками по умолчанию он выглядит несколько иначе, чем в Windows XP, как было показано в предыдущем издании).
Рис. 8.5. Главное окно dlg1: кнопки вызывают появление дополнительных окон
Диалог на рис. 8.6 блокирует программу, пока пользователь не щелкнет по одной из кнопок - при выборе кнопки Yes (или нажатии клавиши Enter) вызов диалога возвращает значение True, и сценарий выводит стандартный диалог showwarning (рис. 8.7), вызывая функцию showwarning.
Рис. 8.6. Диалог askyesno, выводимый сценарием dlg1 (в Windows 7)
Рис. 8.7. Диалог showwarning, выводимый сценарием dlg1
В диалоге на рис. 8.7 пользователь может только нажать кнопку OK. Если щелкнуть на кнопке No в диалоге на рис. 8.6, вызов showinfo создаст соответствующее окно диалога (рис. 8.8). Наконец, если в главном окне щелкнуть по кнопке Spam, то с помощью стандартного вызова show-error будет создан стандартный диалог showerror (рис. 8.9).
Рис. 8.8. Диалог showinfo, выводимый сценарием dlg1
Рис. 8.9. Диалог showerror, выводимый сценарием dlg1
Конечно, в результате создается множество всплывающих окон, и не следует злоупотреблять этими диалогами (обычно лучше применять окна с полями ввода, остающиеся на экране длительное время, а не отвлекать пользователя всплывающими окнами). Но в нужных случаях такие всплывающие диалоги сокращают время разработки и обеспечивают привычный внешний вид.
«Умная» и многократно используемая кнопка Quit
Для некоторых из этих готовых диалогов можно найти лучшее применение. В примере 8.7 реализована прикрепляемая кнопка Quit, которая с помощью стандартных диалогов получает подтверждение в ответ на запрос о завершении. Поскольку она реализована в виде класса, ее можно прикреплять и повторно использовать в любом приложении, где требуется кнопка Quit с запросом на подтверждение. Так как в этой кнопке использованы стандартные диалоги, она должным образом выглядит на любой платформе.
Пример 8.7. PP4E\Gui\Tour\quitter.py
кнопка Quit, которая запрашивает подтверждение на завершение;
для повторного использования достаточно прикрепить экземпляр к другому
графическому интерфейсу и скомпоновать с желаемыми параметрами
from tkinter import * # импортировать классы виджетов
from tkinter.messagebox import askokcancel # импортировать стандартный диалог
class Quitter(Frame): # подкласс графич. интерфейса
def __init__(self, parent=None): # метод конструктора
Frame.__init__(self, parent)
self.pack()
widget = Button(self, text=’Quit’, command=self.quit) widget.pack(side=LEFT, expand=YES, fill=BOTH)
def quit(self):
ans = askokcancel(‘Verify exit’, "Really quit?”) if ans: Frame.quit(self)
if __name__ == ‘__main__’: Quitter().mainloop()
Вообще этот модуль предназначен для использования в других программах, но может запускаться самостоятельно и тогда выводит кнопку, которая в нем реализована. На рис. 8.10 слева вверху показана сама кнопка Quit и диалог askokcancel запроса подтверждения, выведенный при нажатии кнопки Quit.
Рис. 8.10. Модуль Quitter с диалогом askokcancel
Если нажать кнопку OK в этом окне, модуль Quitter вызовет метод quit элемента Frame и закроет графический интерфейс, к которому прикреплена кнопка (на самом деле завершит работу функции mainloop). Но чтобы действительно оценить пользу, приносимую такими подпружиненными кнопками, рассмотрим графический интерфейс клиента, приведенный в следующем разделе.
Панель запуска демонстрации диалогов
Пока мы увидели лишь несколько стандартных диалогов, но их число значительно больше. Не станем показывать их на серых снимках с экрана, а напишем на языке Python демонстрационный сценарий, который будет генерировать их по требованию. Ниже приводится один из способов сделать это. Во-первых, напишем модуль, приведенный в примере 8.8, который определяет таблицу соответствий между именами демонстрационных программ и вызовами стандартных диалогов (будем использовать lambda-выражения для обертывания вызовов вызова, если функции диалога нужно передать дополнительные аргументы).
Пример 8.8. PP4E\Gui\Tour\dialogTable.py
# определяет таблицу имя:обработчик с демонстрационными примерами
from tkinter.filedialog import askopenfilename # импортировать стандартные from tkinter.colorchooser import askcolor # диалоги из Lib\tkinter
from tkinter.messagebox import askquestion, showerror from tkinter.simpledialog import askfloat
demos = {
‘Open’: askopenfilename,
‘Color’: askcolor,
‘Query’: lambda: askquestion(‘Warning’, ‘You typed "rm *”\nConfirm?’), ‘Error’: lambda: showerror(‘Error!’, "He’s dead, Jim”),
‘Input’: lambda: askfloat(‘Entry’, ‘Enter credit card number’)
}
Я поместил эту таблицу в модуль, чтобы использовать ее в качестве основы будущих демонстрационных сценариев (работать с диалогами веселее, чем выводить текст в stdout). Затем напишем сценарий на языке Python, представленный в примере 8.9, который просто генерирует кнопки для всех этих элементов таблицы - использует ее ключи как метки кнопок, а значения как обработчики событий для кнопок.
Пример 8.9. PP4E\Gui\Tour\demoDlg.py
"создает панель с кнопками, которые вызывают диалоги”
from tkinter import * # импортировать базовый набор виджетов
from dialogTable import demos # обработчики событий для кнопок from quitter import Quitter # прикрепить к себе объект quit
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.pack()
Label(self, text=”Basic demos”).pack() for (key, value) in demos.items():
Button(self, text=key, command=value).pack(side=TOP, fill=BOTH)
Quitter(self).pack(side=TOP, fill=BOTH)
if __name__ == ‘__main__’: Demo().mainloop()
Если запустить этот пример как самостоятельный сценарий, он создаст окно, изображенное на рис. 8.11: это панель демонстрационных кнопок, при нажатии которых просто выполняется передача управления в соответствии со значениями в таблице из модуля dialogTable.
Рис. 8.11. Главное окно demoDlg
Обратите внимание, что этот сценарий управляется содержимым словаря из модуля dialogTable, поэтому мы можем изменять набор кнопок, изменяя только dialogTable (никакого выполняемого программного кода в demoDlg менять не нужно). Отметьте также, что кнопка Quit является в данном случае прикрепленным экземпляром класса Quitter из предыдущего раздела, причем она скомпонована с теми же параметрами, что и остальные кнопки, - по крайней мере эту часть программного кода уже не нужно будет писать снова.
Кроме всего прочего этот класс обеспечивает передачу любых именованных аргументов **options конструктору своего суперкласса Frame. Хотя в данном примере эта возможность и не используется, тем не менее вызывающие программы могут передавать параметры настройки во время создания экземпляра (Demo(o=v)), вместо того чтобы выполнять настройку позднее (d.config(o=v)). В этом нет особой необходимости, но такая реализация обеспечивает возможность использования класса Demo как обычного виджета фрейма (в чем, собственно, и заключается прием создания подклассов). Позднее мы увидим, как можно использовать эту особенность.
Мы уже видели некоторые диалоги, запускаемые другими кнопками этой демонстрационной панели, поэтому я коснусь здесь только новых.
Например, нажатие кнопки Query генерирует стандартный диалог, изображенный на рис. 8.12.
Этот диалог askquestion выглядит как askyesno, который мы видели раньше, но в действительности возвращает строку "yes” или "no” (askyesno и askokcancel возвращают True или False). Нажатие кнопки Input генерирует стандартный диалог askfloat, изображенный на рис. 8.13.
Рис. 8.12. Запрос demoDlg, диалог askquestion
Рис. 8.13. Ввод demoDlg, диалог askfloat
Прежде чем вернуть управление, этот диалог автоматически проверяет введенные данные на соответствие синтаксису записи чисел с плавающей точкой; он является представителем группы диалогов ввода одного значения (помимо askinteger и askstring, предлагающих ввести целое число и строку). Он возвращает введенные данные как объект числа с плавающей точкой (а не строку) при нажатии кнопки OK и клавиши Enter, либо объект Python None, если пользователь щелкнет на кнопке CanceL. Два родственных ему диалога возвращают объекты целого числа и строки.
При нажатии кнопки Open мы получаем стандартный диалог открытия файла, создаваемый вызовом функции askopenfilename и изображенный на рис. 8.14. Это внешний вид в Windows 7 - в Mac OS, Linux и в более старых версиях Windows этот диалог может выглядеть совершенно иначе.
Рис. 8.14. Открытие файла в demoDlg, диалог askopenfilename
Аналогичный диалог выбора имени сохраняемого файла создается вызовом функции asksaveasfilename (пример можно найти в разделе, посвященном виджету Text, в главе 9). Оба файловых диалога дают пользователю возможность перемещаться по файловой системе для выбора нужного имени файла, которое возвращается вместе с полным путем к файлу при нажатии кнопки Open; если была нажата кнопка Cancel, возвращается пустая строка. Оба диалога поддерживают дополнительные протоколы, не показанные в этом примере:
• Им можно передать именованный аргумент filetypes - группу шаблонов имен для выбора файлов, появляющихся в раскрывающемся списке в нижней части диалога.
• Им можно передать параметры initialdir (начальный каталог), ini-tialfile (для поля ввода File name), title (заголовок окна диалога), de-faultextension (расширение, добавляемое, когда у выбранного файла нет расширения) и parent (для отображения в виде встроенного дочернего элемента, а не всплывающего диалога).
• Можно заставить их запомнить последний выбранный каталог путем использования экспортированных объектов вместо этих вызовов функций - эту особенность мы будем использовать в последующих примерах.
В модуле filedialog, в библиотеке tkinter, имеется еще один часто используемый диалог, вызываемый функцией askdirectory, который может использоваться, чтобы дать пользователю возможность выбрать каталог. Он выводит структуру каталогов в виде дерева - пользователь может перемещаться по этому дереву и выбирать нужный ему каталог. Эта функция принимает именованные аргументы, включая initialdir и title. Для сохранения имени последнего выбранного каталога, который будет автоматически открыт при следующем вызове диалога, можно использовать соответствующий объект Directory.
Большинство из этих интерфейсов позднее будут использованы в книге, особенно для реализации диалогов выбора файлов в приложении PyEdit, в главе 11, но вы можете, забежав вперед, узнать дополнительные подробности прямо сейчас. Диалог выбора каталога будет показан в примере приложения PyPhoto, в главе 11, и в примере приложения PyMailGUI, в главе 14 - опять же, вы можете забежать вперед, чтобы посмотреть примеры программного кода и снимки с экрана.
Наконец, кнопка Color вызывает стандартную функцию askcolor, которая генерирует стандартный диалог выбора цвета, изображенный на рис. 8.15.
Рис. 8.15. Выбор цвета в demoDlg, диалог askcolor
При нажатии в нем кнопки OK возвращается структура данных, идентифицирующая выбранный цвет, которую можно использовать в любом контексте tkinter, где требуется указать цвет. В нее входят значения RGB и шестнадцатеричная строка цвета (например, ((160, 160, 160), #a0a0a0)). Подробнее об использовании этого набора будет рассказываться несколько позже. При нажатии кнопки Cancel диалог возвращает кортеж, состоящий из двух значений None.
Вывод результатов, возвращаемых диалогами, и передача данных обработчикам с помощью lambda-выражений
Демонстрационная панель запуска диалогов выводит стандартные диалоги и может быть использована для вывода других диалогов простым изменением импортируемого модуля dialogTable. Однако в существующем виде этот пример только показывает диалоги, и было бы неплохо посмотреть на возвращаемые ими значения, чтобы знать, как использовать их в сценариях. В примере 8.10 добавлен вывод результатов стандартных диалогов в стандартный поток вывода stdout.
Пример 8.10. PP4E\Gui\Tour\demoDlg-print.py
то же, что и предыдущий пример, но выводит значения, возвращаемые диалогами; lambda-выражение сохраняет данные из локальной области видимости для передачи их обработчику (обработчик события нажатия кнопки обычно не получает аргументов, а автоматические ссылки в объемлющую область видимости некорректно работают с переменными цикла) и действует подобно вложенной инструкции def, такой как: def func(key=key): self.printit(key)
from tkinter import * # импортировать базовый набор виджетов
from dialogTable import demos # обработчики событий от кнопок
from quitter import Quitter # прикрепить к себе объект quit
class Demo(Frame):
def __init__(self, parent=None):
Frame.__init__(self, parent)
self.pack()
Label(self, text=”Basic demos”).pack() for key in demos:
func = (lambda key=key: self.printit(key))
Button(self, text=key, command=func).pack(side=TOP, fill=BOTH) Quitter(self).pack(side=TOP, fill=BOTH)
def printit(self, name):
print(name, ‘returns =>’, demos[name]()) # извлечь, вызвать, вывести
if __name__ == ‘__main__’: Demo().mainloop()
Этот сценарий создает то же самое главное окно панели кнопок, но обратите внимание, что обработчик события теперь является анонимной функцией, созданной с помощью lambda-выражения, а не прямой ссылкой на вызов диалога в словаре demos, импортированном из модуля
dialogTable:
# задействовать поиск значения в объемлющей области видимости func = (lambda key=key: self.printit(key))
Мы уже говорили о такой возможности в предыдущей главе, но здесь мы впервые использовали lambda-выражение подобным образом, поэтому разберемся в том, что происходит. Так как обработчики событий нажатий кнопок вызываются без аргументов, то при необходимости передать обработчику дополнительные данные для него нужно создать оболочку в виде объекта, который запомнит эти дополнительные данные и передаст их фактическому обработчику. В данном случае при нажатии кнопки вызывается функция, создаваемая lambda-выражением, -промежуточная функция, сохраняющая информацию из объемлющей области видимости. Благодаря этому действительный обработчик, printit, получит дополнительный аргумент name и выполнит действия, связанные с нажатой кнопкой, несмотря на то, что этот аргумент не был передан самой библиотекой tkinter. Фактически lambda-выражение сохраняет и передает информацию о состоянии.
Заметьте, однако, что в теле функции, создаваемой этим lambda-выражением, используются ссылки на значения self и key, находящиеся в объемлющей области видимости. Во всех последних версиях Python ссылка на self действует автоматически, в соответствии с правилами поиска значений в объемлющих областях видимости, но значение key необходимо передать явно, в виде аргумента со значением по умолчанию, иначе все функции, сгенерированные lambda-выражением, получат одно и то же значение - которое получит переменная key в последней итерации цикла. Как мы узнали в главе 7, ссылки на переменные в объемлющей области видимости разрешаются в момент вызова вложенной функции, а ссылки на значения по умолчанию - в момент создания вложенной функции. Так как значение self не изменится после создания функции, мы можем довериться правилам поиска только этого имени, но не переменной цикла key.
В прежних версиях Python требовалось явно передавать любые значения из объемлющей области видимости в виде аргументов со значениями по умолчанию, используя любой из двух следующих приемов:
# использовать простые аргументы со значениями по умолчанию
func = (lambda self=self, name=key: self.printit(name))
# использовать связанный метод по умолчанию
func = (lambda handler=self.printit, name=key: handler(name))
В настоящее время для получения значения self можно использовать более простой прием автоматических ссылок в объемлющую область видимости, однако для передачи значения переменной key по-прежнему требуется использовать значение по умолчанию аргумента (а кроме того, передачу данных в виде значений по умолчанию можно встретить в давно написанных сценариях на языке Python).
Обратите внимание, что круглые скобки вокруг lambda-выражений здесь не являются обязательными - я добавляю их, потому что предпочитаю визуально отделять lambda-выражения от окружающего программного
кода (ваши предпочтения могут отличаться от моих). Отметьте также, что здесь lambda-выражение можно заменить вложенной инструкцией def. Однако в отличие от инструкции def lambda-выражение может появляться внутри вызова конструктора Button, потому что это выражение и ему не требуется присваивать имя. Следующие две формы совершенно равноценны:
for (key, value) in demos.items():
func = (lambda key=key: self.printit(key)) # может вкладываться в вызов
# Button()
for (key, value) in demos.items():
def func(key=key): self.printit(key) # а инструкция def - нет
Здесь можно также использовать вызываемый объект класса, который сохраняет состояние в виде атрибутов экземпляра (смотрите подсказку в учебном примере__call__главы 7). Но как правило, если нужно, что
бы результат lambda-выражения в последующих вызовах использовал переменные из объемлющей области, просто используйте их имена и позвольте интерпретатору самому сохранять значения для последующего использования или передайте их в качестве значений по умолчанию, чтобы обеспечить сохранение значений на этапе создания функции. Последний способ необходим, только если используемая переменная может изменить значение перед тем, как произойдет вызов обработчика.
Если запустить этот сценарий, он создаст то же окно (рис. 8.11) и дополнительно будет выводить значения, возвращаемые диалогами, в стандартный поток вывода. Ниже приводится вывод сценария после щелчков мышью на всех кнопках в главном окне и выбора в каждом диалоге обеих кнопок Cancel/No и OK/Yes:
C:\...\PP4E\Gui\Tour> python demoDlg-print.py
Color returns => (None, None)
Color returns => ((128.5, 1 28.5, 255.99609375), ‘ #8080ff’)
Query returns => no Query returns => yes Input returns => None Input returns => 3.14159 Open returns =>
Open returns => C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py Error returns => ok
Теперь, когда были показаны результаты вызова всех диалогов, я хочу продемонстрировать фактическое использование одного из них.
Предоставление возможности динамического выбора цвета
Стандартный диалог выбора цвета - это не украшение ради украшения. Сценарии могут передавать возвращаемую этим диалогом шестнадцатеричную строку цветов в уже знакомые нам параметры bg и fg настройки цветов виджетов. То есть параметры bg и fg принимают имя цвета (например, blue) и шестнадцатеричные строки со значениями насыщенности цветов RGB, возвращаемые функцией askcolor, которые начинаются с # (например, #8080ff из последней строки вывода в предыдущем разделе).
Это добавляет новое измерение в модификацию графических интерфейсов на базе tkinter: вместо того чтобы жестко определять значения цветов в создаваемых интерфейсах, можно создать кнопку, выводящую диалог выбора цвета, с помощью которой пользователи смогут осуществлять настройку цветов на лету. Нужно просто передать строку цвета методу config в обработчиках событий, как показано в примере 8.11.
Пример 8.11. PP4E\Gui\Tour\setcolor.py
from tkinter import *
from tkinter.colorchooser import askcolor
def setBgColor():
(triple, hexstr) = askcolor() if hexstr:
print(hexstr)
push.config(bg=hexstr)
root = Tk()
push = Button(root, text=’Set Background Color’, command=setBgColor) push.config(height=3, font=(‘times’, 20, ‘bold’)) push.pack(expand=YES, fill=BOTH) root.mainloop()
Этот сценарий создает окно, изображенное на рис. 8.16 (фон его кнопки зеленоватый, и вам придется поверить мне на слово). Нажатие кнопки выводит диалог выбора цвета, который мы видели выше. Цвет, выбранный в этом окне, становится цветом фона этой кнопки после нажатия кнопки OK в диалоге.
Рис. 8.16. Главное окно setcolor
Строки со значениями цвета также выводятся в поток stdout (окно консоли). Запустите этот сценарий на своем компьютере и поэкспериментируйте с возможными настройками цветов:
C:\...\PP4E\Gui\Tour> python setcolor.py
#0080c0
#408080
#77d5df
Другие стандартные диалоги
Мы уже видели большую часть стандартных диалогов, и мы будем пользоваться ими в примерах на протяжении оставшейся части книги. Если нужны дополнительные сведения о других имеющихся диалогах и параметрах, обращайтесь к другой документации по библиотеке tkinter или просмотрите исходные тексты модулей, используемых в начале модуля dialogTable, представленного в примере 8.8, - все они являются обычными файлами с программным кодом на языке Python, установленными на вашем компьютере в подкаталоге tkinter стандартной библиотеки Python (например, в каталоге C:\Python31\Lib, в Windows). И сохраните этот пример с демонстрационной панелью на будущее - мы снова воспользуемся им позднее, когда встретимся с другими виджетами, похожими на кнопки.
Модуль диалогов в старом стиле
В более старом программном коде на языке Python можно иногда увидеть диалоги, реализованные с использованием стандартного модуля dialog. Сейчас он несколько устарел и использует внешний вид, характерный для X Window, но на случай, если вам придется встретить такой программный код при сопровождении программ на языке Python, пример 8.12 может дать представление об этом интерфейсе.
Пример 8.12. PP4E\Gui\Tour\dlg-old.py
from tkinter import *
from tkinter.dialog import Dialog
class OldDialogDemo(Frame):
def __init__(self, master=None):
Frame.__init__(self, master)
Pack.config(self) # то же, что и self.pack()
Button(self, text=’Pop1’, command=self.dialog1).pack()
Button(self, text=’Pop2’, command=self.dialog2).pack()
def dialog1(self): ans = Dialog(self,
title = ‘Popup Fun!’,
text = ‘An example of a popup-dialog ‘
‘box, using older “Dialog.py”.’, bitmap = ‘questhead’,
default = 0, strings = (‘Yes’, ‘No’, ‘Cancel’)) if ans.num == 0: self.dialog2()
def dialog2(self):
Dialog(self, title = ‘HAL-9000’,
text = “I’m afraid I can’t let you do that, Dave...”,
bitmap = ‘hourglass’,
default = 0, strings = (‘spam’, ‘SPAM’))
if __name__ == ‘__main__’: OldDialogDemo().mainloop()
Если передать функции Dialog кортеж с метками для кнопок и текст сообщения, она вернет индекс нажатой кнопки (самая левая кнопка имеет индекс ноль). Окна Dialog являются модальными: доступ к остальным окнам приложения блокируется, пока Dialog ожидает ответа пользователя. При нажатии кнопки Pop2 в главном окне этого сценария выводится второй диалог, как показано на рис. 8.17.
Рис. 8.17. Диалог в старом стиле
Сценарий был запущен в Windows, и, как видите, этот диалог нисколько не похож на то, что можно было бы ожидать на этой платформе. При вызове на любой платформе этот диалог имеет внешний вид, принятый в X Window. Из-за внешнего вида диалогов, воспроизводимых модулем dialog и повышенной сложности его использования лучше использовать стандартные диалоги, продемонстрированные в предыдущем разделе.
Пользовательские диалоги
Все встреченные нами до сих пор диалоги имеют стандартный внешний вид и способы взаимодействия. Для многих задач этого достаточно, но иногда требуется нечто более специфическое. Например, формы, требующие заполнения нескольких полей ввода (например, имя, возраст и размер обуви), не поддерживаются непосредственно библиотекой стандартных диалогов. Можно было бы поочередно выводить диалоги для ввода каждого значения, но такой интерфейс нельзя назвать дружественным.
Пользовательские диалоги поддерживают произвольные интерфейсы, но работать с ними сложнее. Впрочем, многого для этого не требуется: создать окно, такое как Toplevel, с прикрепленными виджетами и добавить обработчик события, который соберет данные, введенные пользователем (если они есть), и закроет окно. Чтобы сделать такой диалог модальным, необходимо передать окну фокус ввода, сделать другие окна неактивными и ожидать события. Реализация такого диалога демонстрируется в примере 8.13.
Пример 8.13. PP4E\Gui\Tour\dlg-custom.py
import sys
from tkinter import * makemodal = (len(sys.argv) > 1)
def dialog():
win = Toplevel() # создать новое окно
Label(win, text=’Hard drive reformatted!’).pack() # добавить виджеты Button(win, text=’OK’, command=win.destroy).pack() # установить обработчик if makemodal:
win.focus_set() # принять фокус ввода,
win.grab_set() # запретить доступ к др. окнам, пока открыт диалог
win.wait_window() # ждать, пока win не будет уничтожен print(‘dialog exit’) # иначе - сразу вернуть управление
root = Tk()
Button(root, text=’popup’, command=dialog).pack() root.mainloop()
Этот сценарий создает модальное или немодальное окно, в зависимости от значения глобальной переменной makemodal. Если запустить его без аргументов командной строки, выбирается немодальный стиль, как показано на рис. 8.18.
Рис. 8.18. Немодальные пользовательские диалоги в действии
Окно справа сверху - это корневое окно. При нажатии в нем кнопки popup создается новое диалоговое окно. Поскольку в этом режиме диалоги являются немодальными, корневое окно сохраняет активность после вывода диалога. Немодальные диалоги не блокируют другие окна, поэтому кнопку в корневом окне можно нажать несколько раз и создать столько копий диалога, сколько поместится на экране. Любые из этих окон можно закрыть щелчком на их кнопках OK, при этом остальные окна останутся на экране.
Создание модальных пользовательских диалогов
Если запустить сценарий, передав ему аргумент в командной строке (например, python dlg-custom.py 1), окно диалога будет сделано модальным. Так как модальные диалоги сосредоточивают на себе все внимание интерфейса, главное окно становится недоступным, пока не будет закрыто окно диалога - пока диалог открыт, нельзя даже щелкнуть на корневом окне, чтобы активизировать его. Поэтому невозможно создать на экране больше одного всплывающего окна, как показано на рис. 8.19.
Рис. 8.19. Модальный пользовательский диалог в действии
Фактически функция dialog в этом сценарии не возвращает управление, пока диалог в левой части не будет закрыт нажатием кнопки OK. В результате модальные диалоги накладывают на модель программирования, в других случаях управляемую событиями, модель вызова функций - введенные пользователем данные можно обрабатывать сразу, а не в обработчике события, который вызывается в какой-то неопределенный момент времени в будущем.
Однако навязывание такой линейной логики управления в графическом интерфейсе требует некоторой дополнительной работы. Секрет блокирования других окон и ожидания ответа сводится к трем строкам программного кода, являющимся общим шаблоном для большинства пользовательских модальных диалогов:
win.focus_set()
Передает окну фокус ввода приложения, как если бы оно было активизировано щелчком мыши. У этого метода есть также синоним, focus, и часто фокус ввода устанавливается не на все окно, а на виджет в нем, позволяющий вводить данные (например, Entry).
win.grab_set()
Блокирует доступ ко всем другим окнам приложения, пока не будет закрыто данное окно. В это время пользователь не может взаимодействовать с другими окнами программы.
win.wait_window()
Приостанавливает вызвавшую программу, пока не будет уничтожен виджет win, но при этом главный цикл обработки событий (main-loop) остается активным. Это означает, что графический интерфейс в целом остается активным во время ожидания. Например, его окна перерисовываются при скрытии под другими окнами или открытии. Когда окно закрывается вызовом метода destroy, оно удаляется с экрана, блокировка приложения автоматически снимается и происходит возврат из данного метода.
Так как сценарий ждет события закрытия окна, он должен предоставить обработчик события, уничтожающий окно в ответ на взаимодействие с виджетами в диалоговом окне (единственном, которое активно). Диалог в этом примере является простым информационным диалогом, поэтому его кнопка OK вызывает метод destroy окна. В диалогах для ввода данных можно установить обработчик события нажатия клавиши Enter, который извлечет данные, введенные в элемент Entry, и после этого вызовет destroy (как будет показано далее в этой главе).
Другие способы реализации модальности
Модальные диалоги обычно реализуются путем создания нового всплывающего окна и ожидания в нем события destroy, как в этом примере. Но существуют и другие схемы. Например, можно создать диалоговые окна заранее, и по мере необходимости показывать или скрывать их с помощью методов deiconify и withdraw окна верхнего уровня (подробности смотрите в сценариях раздела главы 9). С учетом того, что в настоящее время скорость выполнения такова, что создание окон происходит практически мгновенно, такой способ встречается значительно реже, чем создание окон с нуля и уничтожение их при каждом взаимодействии.
Можно также реализовать состояние модальности путем ожидания изменения значения переменной tkinter, а не уничтожения окна. Подробности смотрите в последующем обсуждении переменных tkinter в данной главе (они являются объектами классов, а не обычными переменными Python) и метода wait_variable в конце главы 9. В этой схеме обработчик события долгоживущего диалогового окна может подать сигнал об изменении состояния ожидающей головной программе без необходимости уничтожения диалогового окна.
Наконец, если вызвать метод mainloop рекурсивно, возврат из вызова произойдет только после выполнения метода quit виджета. Метод quit прекращает выполнение функции mainloop и потому обычно завершает выполнение программы с графическим интерфейсом. Но если был произведен рекурсивный вызов mainloop, метод quit просто завершит его. Благодаря этому модальные диалоги можно реализовать без обраще-
ния к методу ожидания. Так, сценарий в примере 8.14 работает аналогично dlg-custom в модальном режиме.
Пример 8.14. PP4E\Gui\Tour\dlg-recursive.py
from tkinter import *
def dialog():
win = Toplevel() # создать новое окно
Label(win, text=’Hard drive reformatted!').pack() # добавить виджеты Button(win, text='OK', command=win.quit).pack() # установить обр-к quit win.protocol('WM_DELETE_WINDOW', win.quit) # завершить и при
# закрытии окна!
win.focus_set() # принять фокус ввода,
win.grab_set() # запретить доступ к др. окнам, пока открыт диалог win.mainloop() # и запустить вложенный цикл обр. событий для ожидания win.destroy()
print(‘dialog exit’)
root = Tk()
Button(root, text=’popup’, command=dialog).pack() root.mainloop()
Выбирая этот путь, нужно вместо метода destroy вызывать в обработчиках событий метод quit (destroy не завершает функцию mainloop) и обеспечить вызов quit кнопкой закрытия окна с помощью метода protocol (иначе не будет завершаться рекурсивный вызов mainloop, что приведет к генерации странных сообщений об ошибках при окончательном выходе из программы). Из-за этой дополнительной сложности более удобным может оказаться использование wait_window или wait_variable, а не рекурсивных вызовов mainloop.
Как строить диалоги в виде форм с метками и полями ввода, мы увидим далее в этой главе, познакомившись с элементом Entry, и еще раз - при изучении менеджера grid в главе 9. Другие примеры пользовательских диалогов можно найти в демонстрационных приложениях ShellGui (глава 10), PyMailGui (глава 14), PyCalc (глава 19) и немодальном form. py (глава 12). А сейчас мы перейдем к более глубокому изучению событий, что несомненно пригодится на следующих этапах нашего турне.
Привязка событий
В предыдущей главе мы познакомились с методом bind виджетов, который использовался для перехвата нажатий кнопок. Так как метод bind часто используется вместе с другими виджетами (например, для перехвата нажатия клавиши Enter в полях ввода), остановимся на нем здесь в начале нашего обзора. Пример 8.15 иллюстрирует другие протоколы событий для метода bind.
Пример 8.15. PP4E\Gui\Tour\bind.py
from tkinter import *
def showPosEvent(event):
print(‘Widget=%s X=%s Y=%s’ % (event.widget, event.x, event.y))
def showAllEvent(event): print(event) for attr in dir(event):
if not attr.startswith(‘__’):
print(attr, ‘=>’, getattr(event, attr))
def onKeyPress(event):
print(‘Got key press:’, event.char)
def onArrowKey(event):
print(‘Got up arrow key press’)
def onReturnKey(event):
print(‘Got return key press’)
def onLeftClick(event):
print(‘Got left mouse button click:’, end=’ ‘) showPosEvent(event)
def onRightClick(event):
print(‘Got right mouse button click:’, end=’ ‘) showPosEvent(event)
def onMiddleClick(event):
print(‘Got middle mouse button click:’, end=’ ‘)
showPosEvent(event)
showAllEvent(event)
def onLeftDrag(event):
print(‘Got left mouse button drag:’, end=’ ‘) showPosEvent(event)
def onDoubleLeftClick(event):
print(‘Got double left mouse click’, end=’ ‘)
showPosEvent(event)
tkroot.quit()
tkroot = Tk()
labelfont = (‘courier’, 20, ‘bold’) # семейство, размер, стиль
widget = Label(tkroot, text=’Hello bind world’)
widget.config(bg=’red’, font=labelfont) # красный фон, большой шрифт
widget.config(height=5, width=20) # начальн. размер: строк,символов
widget.pack(expand=YES, fill=BOTH)
widget.bind(‘
widget.bind(‘
widget.bind(‘
widget.bind(‘
widget.bind(‘
widget.bind(‘
widget.focus() # или привязать нажатие клавиши
# к tkroot
tkroot.title(‘Click Me’) tkroot.mainloop()
Этот файл состоит в основном из функций обработчиков событий, вызываемых при возникновении связанных событий. Как было показано в главе 7, обработчики данного типа получают в качестве аргумента объект события, содержащий сведения о сгенерированном событии. Технически этот аргумент является экземпляром класса Event из библиотеки tkinter, и содержащиеся в нем подробности представлены атрибутами. Большинство обработчиков просто выводят информацию о событиях, извлекая значения из их атрибутов.
Если запустить этот сценарий, он создаст окно, изображенное на рис. 8.20. Главное его назначение - служить областью для запуска событий щелчков мышью и нажатия клавиш.
Рис. 8.20. Окно сценария bind для щелчков мышью
Черно-белое издание, которое вы держите в руках, не позволяет оценить этот сценарий. При запуске вживую он использует приведенные выше настройки и выводит текст черным по красному большим шрифтом Courier. Вам придется поверить мне на слово (или запустить его самим).
Но главная задача этого примера - продемонстрировать действие других протоколов связывания событий. Мы уже встречали в главе 7 сценарий, который с помощью метода bind виджета и имен событий
Чтобы перехватывать нажатия одиночных клавиш на клавиатуре, можно зарегистрировать обработчик для события с идентификатором
Этот сценарий перехватывает также перемещение мыши при нажатой кнопке: зарегистрированный обработчик события
Этот сценарий перехватывает также щелчки правой и средней кнопками мыши (называемыми также кнопками 3 и 2). Воспроизвести щелчок средней кнопкой 2 с помощью двухкнопочной мыши можно, щелкнув одновременно обеими кнопками, - если этот прием не действует, проверьте настройки мыши в интерфейсе свойств (Панель управления (Control PaneL) в Windows)40.
Чтобы перехватывать нажатия более специфических клавиш, в данном сценарии зарегистрированы обработчики событий нажатия клавиш Return/Enter и «стрелки вверх». В противном случае эти события были бы отправлены универсальному обработчику события
Ниже показано, что попадает в поток вывода stdout после щелчка левой кнопкой, правой кнопкой, левой кнопкой и перетаскивания, нескольких нажатий клавиш, нажатия клавиш Enter и «стрелки вверх» и наконец, двойного щелчка левой кнопкой для завершения. При нажатии левой кнопки мыши и перемещении курсора по экрану возникает множество сообщений с информацией о событии перетаскивания - одно сообщение выводится для каждого движения при перетаскивании (и для каждого производится вызов обработчика на языке Python):
C:\...\PP4E\Gui\Tour> python bind.py
Got left mouse button click: Widget=.25763696 X=376 Y=53
Got right mouse button click: Widget=.25763696 X=36 Y=60
Got left mouse button click: Widget=.25763696 X=144 Y=43
Got left mouse button drag: Widget=.25763696 X=144 Y=45
Got left mouse button drag: Widget=.25763696 X=144 Y=47
Got left mouse button drag: Widget=.25763696 X=145 Y=50
Got left mouse button drag: Widget=.25763696 X=146 Y=51
Got left mouse button drag: Widget=.25763696 X=149 Y=53
Got key press: s
Got key press: p
Got key press: a
Got key press: m
Got key press: 1
Got key press: -
Got key press: 2
Got key press: .
Got return key press Got up arrow key press
Got left mouse button click: Widget=.25763696 X=300 Y=68 Got double left mouse click Widget=.25763696 X=300 Y=68
Для событий, связанных с мышью, обработчики выводят координаты X и Y указателя мыши, которые передаются в объекте события. Обычно координаты измеряются в пикселях от верхнего левого угла (0, 0), относительно того виджета, на котором произведен щелчок. Ниже показано, что выводится для щелчка левой кнопкой, средней кнопкой и двойного щелчка левой. Обратите внимание, что обработчик щелчка средней кнопкой выводит свой аргумент целиком - все атрибуты объекта Event (исключая внутренние атрибуты с именами, начинающимися
с двух символов подчеркивания «__», в число которых входит атрибут
__doc__и методы перегрузки операторов, унаследованные от суперкласса object, подразумеваемого в Python 3.X по умолчанию). Различные типы событий устанавливают различные атрибуты. Например, нажатие большинства клавиш записывает некоторое значение в атрибут char:
C:\...\PP4E\Gui\Tour> python bind.py
Got left mouse button click: Widget=.25632624 X=6 Y=6
Got middle mouse button click: Widget=.25632624 X=212 Y=95
char => ??
delta => 0
height => ??
keycode => ??
keysym => ?? keysym_num => ?? num => 2
send_event => False serial => 17 state => 0 time => 549707945 type => 4
widget => .25632624 width => ?? x => 212 x_root => 311 y => 95 y_root => 221
Got left mouse button click: Widget=.25632624 X=400 Y=183 Got double left mouse click Widget=.25632624 X=400 Y=183
Другие события, доступные с помощью метода bind
Помимо событий, которые были проиллюстрированы в данном примере, сценарий, использующий библиотеку tkinter, может зарегистрировать обработчики других видов связываемых событий. Например:
•
•
• Обработчики
•
•
•
•
•
•
Этот список не полон, а для записи названий событий есть свой довольно сложный синтаксис, например:
• Модификаторы - могут добавляться к идентификаторам событий, чтобы сделать их еще более специфическими. Например,
• Синонимы. - могут использоваться для имен некоторых частых событий. Например,
• Имеется возможность определять идентификаторы виртуальных событий, обозначающие последовательности из одного или нескольких событий, с помощью пары угловых скобок (например, <
С целью экономии места за исчерпывающими сведениями по этой теме мы отсылаем вас к другим источникам информации по Tk и tkinter. Кроме того, изменяя настройки в сценарии и запуская его заново, также можно выяснить некоторые особенности поведения событий - в конце концов, это Python.
Подробнее о событии
Прежде чем двинуться дальше, необходимо сказать несколько слов о событии
Важно знать, что в момент возбуждения события виджет находится в «полумертвом» состоянии (в терминологии библиотеки Tk) - он по-прежнему существует, но большинство операций над ним будут терпеть неудачу. По этой причине событие
Кроме того, вы должны знать, что вызов метода quit виджетов не возбуждает никаких событий
Сценарий может также выполнять заключительные операции в программном коде, следующем за вызовом функции mainloop, но к этому моменту графический интерфейс уже будет полностью уничтожен, и данный программный код не может быть привязан к какому-то конкретному виджету. Мы еще будем говорить об этом событии, когда будем изучать программу PyEdit в главе 11 - мы найдем это событие непригодным для проверки наличия изменений в тексте, определяющих необходимость его сохранения.
Виджеты Message и Entry
Виджеты Message и Entry позволяют отображать и вводить простой текст. Оба они, в сущности, являются функциональными подмножествами виджета Text, с которым мы познакомимся позднее, - Text может делать все то, что могут Message и Entry, при этом обратное утверждение неверно.
Message
Виджет Message служит всего лишь местом для отображения текста. Хотя с помощью стандартного диалога showinfo, с которым мы встречались ранее, выводить всплывающие сообщения, вероятно, удобнее, тем не менее виджет Message автоматически и гибко разбивает длинные строки и может встраиваться внутрь элементов-контейнеров, когда нужно вывести на экране какой-либо текст, доступный только для чтения. Кроме того, этот виджет обладает более чем десятком параметров настройки, позволяющих изменять его внешний вид. Пример 8.16 и рис. 8.21 иллюстрируют основы применения Message и демонстрируют, как этот виджет реагирует на растягивание по горизонтали с применением параметров fill и expand. Дополнительные сведения об изменении размеров виджетов вы найдете в главе 7, а сведения о других поддерживаемых параметрах ищите в справочниках по Tk или tkinter.
Пример 8.16. PP4E\Gui\tour\message.py
from tkinter import *
msg = Message(text=”Oh by the way, which one’s Pink?”) msg.config(bg=’pink’, font=(‘times’, 16, ‘italic’)) msg.pack(fill=X, expand=YES) mainloop()
Рис. 8.21. Виджет Message в действии
Entry
Виджет Entry служит простым полем ввода одной строки текста. Обычно он используется для реализации полей ввода в диалогах, имеющих вид форм, и всюду, где пользователь должен ввести значение в поле. Виджет Entry также поддерживает более сложные понятия, такие как прокрутка, привязка клавиш для редактирования и выделение текста, при этом он очень прост в использовании. Сценарий в примере 8.17 создает окно для ввода, изображенное на рис. 8.22.
Пример 8.17. PP4E\Gui\tour\entry1.py
from tkinter import * from quitter import Quitter
def fetch():
print(‘Input => “%s”’ % ent.get()) # извлечь текст
root = Tk() ent = Entry(root)
ent.insert(0, ‘Type words here’) # записать текст
ent.pack(side=TOP, fill=X) # растянуть по горизонтали
ent.focus() # избавить от необходимости
# выполнять щелчок мышью
ent.bind(‘
Quitter(root).pack(side=RIGHT)
root.mainloop()
Рис. 8.22. Сценарий entry1 в действии
Если запустить сценарий entry1, он заполнит поле ввода в этом интерфейсе текстом «Type words here» вызовом метода insert виджета. Поскольку щелчок на кнопке Fetch и нажатие клавиши Enter запускают в сценарии функцию обратного вызова fetch, оба эти события извлекут из поля ввода текущий текст с помощью метода get виджета и выведут его:
C:\...\PP4E\Gui\Tour> python entry1.py
Input => “Type words here”
Input => “Have a cigar”
Мы уже встречались выше с событием
Программирование виджетов Entry
Вообще говоря, значения, вводимые в виджеты Entry и отображаемые ими, могут быть записаны или получены с помощью связанных объектов «переменных» (описываемых далее в этой главе) или с помощью следующих методов виджета Entry:
ent.insert(0, ‘some text’) # запись значения
value = ent.get() # извлечение значения (строки)
Первый параметр метода insert определяет позицию в строке, начиная с которой должен быть введен текст. Здесь «0» означает ввод в начало строки, поскольку смещения начинают отсчитываться с нуля, а целое число 0 и строка ‘0’ означают одно и то же (аргументы методов в библиотеке tkinter всегда при необходимости преобразуются в строки). Если виджет Entry уже содержит текст, то обычно требуется удалить его содержимое перед записью нового значения, иначе новый текст будет просто добавлен к уже существующему:
ent.delete(0, END) # сперва удалить текст с начала до конца
ent.insert(0, ‘some text’) # затем записать значение
Имя END здесь является предопределенной константой tkinter, обозначающей конец содержимого виджета - она снова встретится нам в главе 9 при изучении полномасштабного и многострочного виджета Text (более мощного собрата Entry). Поскольку после удаления виджет не будет ничего содержать, предыдущая последовательность инструкций эквивалентна следующей:
ent.delete(‘0’, END) # удалить текст с начала до конца
ent.insert(END, ‘some text’) # добавить в конец пустой строки текста
В любом случае, если сначала не удалить текст, новый текст просто будет добавлен к нему. Если вам интересно увидеть, как это происходит, измените функцию fetch, как показано ниже, и при каждом щелчке кнопкой или нажатии клавиши в начало и в конец поля ввода будет добавляться «х»:
def fetch():
print(‘Input => “%s”’ % ent.get()) # получить текст
ent.insert(END, ‘x’) # для очистки: ent.delete(‘0’, END)
ent.insert(0, ‘x’) # новый текст просто добавляется
В последующих примерах мы встретимся также с параметром state= ’disabled’ виджета Entry, делающим его доступным только для чтения, а также параметром show=’*’, заставляющим его выводить каждый символ как * (полезно для организации ввода паролей). Поэкспериментируйте с этим сценарием, изменяя и запуская его. Виджет Entry поддерживает и другие параметры, которые мы здесь также пропустим; дополнительные сведения ищите в последующих примерах и других источниках.
Компоновка элементов ввода в формах
Как уже отмечалось, виджеты Entry часто применяются в качестве полей ввода при реализации форм. Мы часто будем создавать такие формы в этой книге. Простую иллюстрацию такого применения дает пример 8.18, в котором несколько меток, полей ввода и фреймов объединены в форму для ввода нескольких значений, изображенную на рис. 8.23.
Пример 8.18. PP4E\Gui\Tour\entry2.py
непосредственное использование виджетов Entry и размещение их по рядам с метками фиксированной ширины: такой способ компоновки, а также использование менеджера grid обеспечивают наилучшее представление для форм
from tkinter import * from quitter import Quitter fields = ‘Name’, ‘Job’, ‘Pay’
def fetch(entries):
for entry in entries:
print(‘Input => “%s”’ % entry.get()) # извлечь текст
def makeform(root, fields): entries = [] for field in fields:
row = Frame(root) # создать новый ряд
lab = Label(row, width=5, text=field) # добавить метку, поле ввода
ent = Entry(row)
row.pack(side=TOP, fill=X) # прикрепить к верхнему краю
lab.pack(side=LEFT)
ent.pack(side=RIGHT, expand=YES, fill=X) # растянуть по горизонтали entries.append(ent) return entries
if__name__== ‘__main__’:
root = Tk()
ents = makeform(root, fields) root.bind(‘
Button(root, text=’Fetch’,
command = (lambda: fetch(ents))).pack(side=LEFT) Quitter(root).pack(side=RIGHT) root.mainloop()
Рис. 8.23. Внешний вид форм entry2 (и entry3)
Полями ввода здесь служат простые виджеты Entry. Сценарий создает список виджетов, с помощью которого потом будут извлекаться их значения. При каждом нажатии кнопки Fetch текущие значения извлекаются из всех полей ввода и выводятся в стандартный поток вывода:
C:\...\PP4E\Gui\Tour> python entry2.py
Input => “Bob”
Input => “Technical Writer”
Input => “Jack”
Тот же результат дает нажатие клавиши Enter, когда окно обладает фокусом ввода, - на этот раз событие привязано к корневому окну в целом, а не к отдельному полю ввода.
Искусство создания структуры формы состоит в основном в организации иерархии виджетов. В данном сценарии каждый ряд метка/поле ввода конструируется как новый фрейм Frame, прикрепляемый к текущему краю TOP окна. Метки прикрепляются к левому краю ряда (LEFT), а поля - к правому (RIGHT). Поскольку каждый ряд представляет собой отдельный фрейм, его содержимое изолируется от других операций компоновки, производимых в этом окне. Кроме того, этот сценарий разрешает увеличение горизонтального размера при изменении размеров окна только для полей ввода, как показано на рис. 8.24.
Рис. 8.24. Возможность растягивания полей ввода в сценариях entry2 (и entry3) в действии
Снова о создании модальных окон
Далее мы увидим, как создавать аналогичные структуры форм с помощью менеджера компоновки grid, где вместо фреймов размещение виджетов выполняется по номерам рядов и столбцов. Но сейчас, реализовав структуру формы, посмотрим, как применять технологию создания модальных диалогов к более сложным формам.
Сценарий в примере 8.19, используя функции makeform и fetch из предыдущего примера, создает форму и выводит ее содержимое подобно тому, как это делалось раньше. Но теперь поля ввода прикрепляются к новому всплывающему окну Toplevel, создаваемому по требованию и содержащему кнопку OK, генерирующую событие уничтожения окна. Как мы уже знаем, метод wait_window влечет приостановку программы, пока окно не будет закрыто.
Пример 8.19. PP4E\Gui\Tour\entry2-modal.py
# создает модальный диалог с формой;
# данные должны извлекаться до уничтожения окна с полями ввода from tkinter import *
from entry2 import makeform, fetch, fields
def show(entries, popup):
fetch(entries) # извлечь данные перед уничтожением окна!
popup.destroy() # если инструкции поменять местами, сценарий
# будет возбуждать исключение
def ask():
popup = Toplevel() # отобразить форму в виде модального диалога
ents = makeform(popup, fields)
Button(popup, text=’OK’, command=(lambda: show(ents, popup))).pack()
popup.grab_set()
popup.focus_set()
popup.wait_window() # ждать закрытия окна
root = Tk()
Button(root, text=’Dialog’, command=ask).pack() root.mainloop()
Если нажать кнопку в главном окне, сценарий создаст окно диалога с формой, блокирующее остальное приложение, изображенное на рис. 8.25.
В реализации этого модального диалога таится малозаметная опасность: поскольку он извлекает данные, вводимые пользователем, из виджетов Entry, встроенных во всплывающее окно, эти данные необходимо получить прежде чем окно будет уничтожено в обработчике события нажатия кнопки OK. Оказывается, что вызов destroy действительно уничтожает все виджеты окна - попытка получить значение из уничтоженного виджета Entry не только не действует, но и порождает исключение с выводом трассировочной информации и сообщения об ошибке в окне консоли - попробуйте изменить порядок команд в функции show, и вы убедитесь в этом сами.
Рис. 8.25. Окна, создаваемые сценарием entry2-modal (и entry3-modal)
Чтобы избежать этой проблемы, нужно следить за тем, чтобы выборка значений осуществлялась перед уничтожением, или использовать переменные tkinter, являющиеся предметом обсуждения следующего раздела.
«Переменные» tkinter и альтернативные способы компоновки форм
Виджеты Entry (наряду с другими) поддерживают понятие ассоциированной переменной - изменение значения ассоциированной переменной изменяет текст, отображаемый виджетом Entry, а изменение текста в Entry изменяет значение переменной. Однако это не обычные переменные Python. Переменные, связанные с виджетами, являются экземплярами классов переменных в библиотеке tkinter. Эти классы носят названия StringVar, IntVar, DoubleVar и BooleanVar. Выбор того или иного класса зависит от контекста, в котором он должен использоваться. Например, можно связать с полем Entry экземпляр класса StringVar, как показано в примере 8.20.
Пример 8.20. PP4E\Gui\Tour\entry3.py
использует переменные StringVar
компоновка по колонкам: вертикальные координаты виджетов могут не совпадать (смотрите entry2)
from tkinter import * from quitter import Quitter fields = ‘Name’, ‘Job’, ‘Pay’
def fetch(variables):
for variable in variables:
print(‘Input => “%s”’ % variable.get()) # извлечь из переменных def makeform(root, fields):
form = Frame(root) # создать внешний фрейм
left = Frame(form) # создать две колонки
rite = Frame(form)
form.pack(fill=X)
left.pack(side=LEFT)
rite.pack(side=RIGHT, expand=YES, fill=X) # растягивать по горизонтали
variables = [] for field in fields:
lab = Label(left, width=5, text=field) # добавить в колонки
ent = Entry(rite)
lab.pack(side=TOP)
ent.pack(side=TOP, fill=X) # растягивать по горизонтали
var = StringVar()
ent.config(textvariable=var) # связать поле с переменной
var.set(‘enter here’) variables.append(var) return variables
if__name__== ‘__main__’:
root = Tk()
vars = makeform(root, fields)
Button(root, text=’Fetch’, command=(lambda: fetch(vars))).pack(side=LEFT)
Quitter(root).pack(side=RIGHT)
root.bind(‘
root.mainloop()
За исключением того обстоятельства, что поля ввода инициализируются строкой enter here’, этот сценарий создает окно, практически идентичное по внешнему виду и функциям тому, которое создает сценарий entry2 (рис. 8.23 и 8.24). Для наглядности виджеты в окне компонуются другим способом - как фрейм с двумя вложенными фреймами, образующими левую и правую колонки в области формы, - но конечный результат при отображении на экран оказывается тем же самым (на некоторых платформах, по крайней мере: смотрите примечание в конце этого раздела, где описывается, почему компоновка на основе рядов обычно бывает предпочтительнее).
Главное, на что здесь нужно обратить внимание, это использование переменных StringVar. Вместо списка виджетов Entry, из которого извлекаются введенные значения, эта версия хранит список объектов StringVar, которые ассоциируются с виджетами Entry следующим способом:
ent = Entry(rite) var = StringVar()
ent.config(textvariable=var) # связать поле с переменной
После того как переменные будут связаны, операции изменения и получения значения переменной
var.set(‘text here’) value = var.get()
действительно будут изменять и получать значение соответствующего поля ввода на экране.41 Метод get объекта переменной возвращает строку для StringVar, целое число для IntVar и число с плавающей точкой для
DoubleVar.
Конечно, как мы уже видели, можно легко изменять и извлекать текст непосредственно из полей Entry, без всяких дополнительных переменных. Зачем же утруждать себя обработкой объектов переменных? Во-первых, исчезает опасность попыток извлечения значений после уничтожения, о чем говорилось в предыдущем разделе. Поскольку объекты StringVar продолжают существовать после уничтожения виджетов Entry, к которым они привязаны, сохраняется возможность извлекать из них значения, когда модального диалога уже давно нет, как показано в примере 8.21.
Пример 8.21. PP4E\Gui\Tour\entry3-modal.py
# значения могут извлекаться из StringVar и после уничтожения виджета
from tkinter import *
from entry3 import makeform, fetch, fields def show(variables, popup):
popup.destroy() # здесь порядок не имеет значения
fetch(variables) # переменные сохраняются после уничтожения окна
def ask():
popup = Toplevel() # отображение формы в модальном диалоге
vars = makeform(popup, fields)
Button(popup, text=’OK’, command=(lambda: show(vars, popup))).pack()
popup.grab_set()
popup.focus_set()
popup.wait_window() # ждать уничтожения окна
root = Tk()
Button(root, text=’Dialog’, command=ask).pack() root.mainloop()
Эта версия такая же, как исходная (представленная в примере 8.19 и на рис. 8.25), но теперь функция show уничтожает всплывающее окно до извлечения введенных данных из переменных StringVar в списке, созданном функцией makeform. Иными словами, переменные оказываются более надежными в некоторых контекстах, потому что они не являются частью действительного дерева виджетов. Например, они также часто используются с флажками, группами переключателей и ползунками, обеспечивая доступ к текущим значениям и связывая вместе несколько виджетов. Так уж совпало, что им посвящен следующий раздел.
В этом разделе мы использовали два способа компоновки форм: во фреймах по рядам, с метками фиксированной ширины (entry2), и во фреймах по колонкам (entry3). В главе 9 мы познакомимся с третьим способом: компоновкой с помощью менеджера grid. Из них наилучший результат на всех платформах обеспечивают компоновка по сетке и по рядам с метками фиксированной ширины, как в сценарии entry2.
Компоновка по колонкам, использованная в сценарии ent ry3, может использоваться только на платформах, где высота каждой метки в точности соответствует высоте каждого поля ввода. Поскольку эти виджеты напрямую никак не связаны, их вертикальные координаты могут не совпадать на некоторых платформах. Когда я попытался протестировать в системе Linux некоторые формы, замечательно выглядевшие в Windows XP, метки и соответствующие им поля ввода оказались на разной высоте.
Даже такое простое окно, которое воспроизводит сценарий entry3, при ближайшем рассмотрении выглядит несколько кривовато. На некоторых платформах оно только кажется похожим на окно, воспроизводимое сценарием entry2, из-за небольшого количества полей ввода и небольших размеров по умолчанию. В Windows 7 на моем нетбуке несовпадение меток и полей ввода по вертикали становится заметным после добавления 3-4 дополнительных полей ввода в кортеж полей в сценарии ent ry3.
Если переносимость имеет для вас важное значение, компонуйте свои формы либо с помощью фреймов по рядам и с метками фиксированной/максимальной ширины, как в сценарии entry2, либо с выравниванием виджетов по сетке. Дополнительные примеры таких форм мы увидим в следующей главе. А в главе 12 мы напишем свой инструмент конструирования форм, скрывающий тонкости их компоновки от клиента (включая пример клиента в главе 13).
Флажки, переключатели и ползунки
Этот раздел знакомит с тремя типами виджетов - Checkbutton («флажок», виджет для выбора нескольких вариантов одновременно), Radiobutton («переключатель», виджет для выбора единственного варианта из нескольких) и Scale («шкала», иногда называемый «slider» - «ползунок»). Все они являются вариациями на одну тему и в какой-то мере связаны с простыми кнопками, поэтому мы будем изучать их здесь вместе. Чтобы тренироваться с этими элементами было интереснее, мы повторно используем модуль dialogTable, представленный в примере 8.8, где определяются обработчики событий выбора виджетов (обработчики, вызывающие диалоги). Попутно мы воспользуемся только что рассмотренными переменными tkinter для получения значений состояния этих виджетов.
Флажки
Виджеты Checkbutton и Radiobutton предусматривают возможность ассоциирования с переменными tkinter: щелчок на виджете изменяет значение переменной, а изменение значения переменной изменяет состояние виджета, к которому она привязана. В действительности переменные tkinter составляют функциональную основу этих графических элементов:
• Группа флажков Checkbutton реализует интерфейс с выбором нескольких вариантов путем присвоения каждому виджету (флажку) собственной переменной.
• Группа переключателей Radiobutton реализует модель выбора единственного из нескольких взаимоисключающих вариантов путем придания каждому виджету уникального значения и назначения одной и той же переменной tkinter.
У обоих типов виджетов есть параметры command и variable. Параметр command позволяет зарегистрировать обработчик, который вызывается, как только возникает событие щелчка на виджете, подобно обычным виджетам Button. Но передавая переменную tkinter в параметре variable, можно также в любой момент получать или изменять состояние виджета путем получения или изменения значения связанной с ним переменной.
Флажки tkinter несколько проще в обращении, поэтому с них и начнем. Пример 8.22 создает группу из пяти флажков, изображенную на рис. 8.26. Для большей пользы он также добавляет кнопку, с помощью которой выводится текущее состояние всех флажков, и прикрепляет экземпляр кнопки Quitter, которую мы создали в начале главы.
Рис. 8.26. Сценарий demoCheck в действии
Пример 8.22. PP4E\Gui\Tour\demoCheck.py
“создает группу флажков, которые вызывают демонстрационные диалоги”
from tkinter import * # импортировать базовый набор виджетов
from dialogTable import demos # импортировать готовые диалоги
from quitter import Quitter # прикрепить к “себе” объект Quitter
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.pack()
self.tools()
Label(self, text=”Check demos”).pack()
self.vars = [] for key in demos: var = IntVar()
Checkbutton(self,
text=key,
variable=var,
command=demos[key]).pack(side=LEFT)
self.vars.append(var)
def report(self):
for var in self.vars:
print(var.get(), end=’ ‘) # текущие значения флажков: 1 или 0
print()
def tools(self):
frm = Frame(self) frm.pack(side=RIGHT)
Button(frm, text=’State’, command=self.report).pack(fill=X) Quitter(frm).pack(fill=X)
if __name__ == ‘__main__’: Demo().mainloop()
С точки зрения программного кода, флажки похожи на обычные кнопки, они даже добавляются в контейнерный виджет. Однако функционально они несколько отличаются. Как можно видеть по рисунку (а лучше - запустив пример), флажок работает как переключатель: щелчок на нем изменяет его состояние из выключенного во включенное (из невыбранного в выбранное) или обратно - из включенного в выключенное. Когда флажок выбран, на нем выводится галочка, а связанная с ним переменная IntVar получает значение 1; когда он не выбран, галочка исчезает, а его переменная IntVar получает значение 0.
Чтобы смоделировать приложение, содержащее флажки, кнопка State в этом графическом интерфейсе запускает метод report в сценарии, который выводит текущие состояния всех пяти флажков в поток stdout. Ниже приводится вывод, полученный после нескольких щелчков:
C:\...\PP4E\Gui\Tour> python demoCheck.py
0 0 0 0 0
1 0 0 0 0
1 0 1 0 0
1 0 1 1 0
1 0 0 1 0
1 0 0 1 1
В действительности это значения пяти переменных tkinter, ассоциированных с флажками Checkbutton посредством параметров variable, и при опросе они совпадают со значениями виджетов. В этом сценарии с каждым из флажков Checkbutton на экране ассоциирована переменная IntVar, поскольку это двоичные индикаторы, способные принимать значение 0 или 1. Переменные StringVar тоже можно использовать, но при этом их методы будут возвращать строки 0’ или ‘1’, а не целые числа, а их начальным состоянием будет пустая строка (а не целое число 0).
Параметр command этого виджета позволяет зарегистрировать обработчик, который будет вызываться при каждом щелчке на виджете. Для иллюстрации в качестве обработчика для каждого из флажков в этом сценарии зарегистрирован вызов демонстрации стандартного диалога: щелчок изменяет состояние переключателя, а кроме того, выводит один из знакомых диалогов.
Интересно, что вызвать метод report можно также в интерактивном сеансе. При работе в таком режиме виджеты отображаются во всплывающем окне при вводе строк и полностью действуют даже без вызова функции mainloop:
C:\...\PP4E\Gui\Tour> python >>> from demoCheck import Demo >>> d = Demo()
>>> d.report()
0 0 0 0 0 >>> d.report()
1 0 0 0 0 >>> d.report()
1 0 0 1 1
Флажки и переменные
Когда я впервые изучал этот виджет, моей первой реакцией было: «Зачем вообще здесь нужны переменные tkinter, если можно зарегистрировать обработчики щелчков на виджетах?» На первый взгляд связанные переменные могут показаться излишними, но они упрощают некоторые действия с графическим интерфейсом. Не буду просить принять это на веру, а постараюсь объяснить, почему.
Имейте в виду, что обработчик для флажка, указанный в параметре command, будет выполняться при каждом щелчке - при переключении и в выбранное, и в невыбранное состояние. Поэтому если нужно совершить действие немедленно после щелчка на флажке, как правило, в обработчике события требуется узнать текущее значение флажка. Поскольку у флажка нет метода «get», с помощью которого можно было бы получить текущее его значение, обычно требуется запрашивать ассоциированную переменную, чтобы узнать, включен флажок или выключен.
Кроме того, в некоторых графических интерфейсах пользователям разрешается устанавливать флажки без вызова обработчиков, зарегистрированных с помощью параметра command, и получать значения где-либо позже в программе. В таком сценарии переменные служат для автоматического запоминания состояний флажков. Представителем этого последнего подхода является метод report в сценарии demoCheck.
Конечно, можно и вручную запоминать состояние каждого флажка в обработчиках событий. В примере 8.23 ведется свой список состояний флажков, который вручную обновляется в обработчиках событий, определяемых с помощью параметра command.
Пример 8.23. PP4E\Gwi\Towr\demo-eheek-manual.py
# флажки, сложный способ (без переменных)
from tkinter import *
states = [] # изменение объекта - не имени
def onPress(i): # сохраняет состояния
states[i] = not states[i] # изменяет False->True, True->False
root = Tk()
for i in range(10):
chk = Checkbutton(root, text=str(i), command=(lambda i=i: onPress(i)) ) chk.pack(side=LEFT) states.append(False) root.mainloop()
print(states) # при выходе вывести все состояния
Здесь lambda-выражение передает индекс нажатой кнопки в списке states. Иначе для каждой кнопки потребовалось бы создавать отдельный обработчик. Здесь мы снова вынуждены использовать аргумент со значением по умолчанию, чтобы передать переменную цикла lambda-выражению. В противном случае все 10 сгенерированных функций получили бы значение переменной цикла, присвоенное ей в последней итерации цикла (щелчок на любом флажке изменял бы состояние десятого элемента в списке - причины такого поведения описываются в главе 7). При запуске этот сценарий создает окно с 10 флажками, как показано на рис. 8.27.
Рис. 8.27. Окно флажков с изменением состояний, производимым вручную
Состояния флажков, поддерживаемые вручную, обновляются при каждом щелчке на флажках и выводятся при выходе из программы (формально, при возврате из вызова mainloop) - это список логических значений, которые можно было бы представить целыми числами 1 и 0, если бы потребовалось в точности имитировать оригинал:
C:\...\PP4E\Gui\Tour> python demo-check-manual.py
[False, False, True, False, True, False, False, False, True, False]
Такой способ действует, и его не столь уж трудно реализовать. Но связанные переменные tkinter заметно упрощают эту задачу, особенно если до какого-то момента в будущем нет необходимости проверять состояния флажков. Это проиллюстрировано в примере 8.24.
Пример 8.24. PP4E\Gui\Tour\demo-eheek-auto.py
# проверка состояния флажков, простой способ
from tkinter import * root = Tk() states = [] for i in range(10): var = IntVar()
chk = Checkbutton(root, text=str(i), variable=var)
chk.pack(side=LEFT)
states.append(var)
root.mainloop() # пусть следит библиотека tkinter
print([var.get() for var in states]) # вывести все состояния при выходе
# (можно также реализовать с помощью
# функции map и lambda-выражение)
Этот сценарий выводит такое же окно и действует точно так же, но здесь мы не передаем обработчики в параметре command, потому что библиотека tkinter автоматически отслеживает изменение состояний:
C:\...\PP4E\Gui\Tour> python demo-check-auto.py
[0, 0, 1, 1, 0, 0, 1, 0, 0, 1]
Смысл здесь в том, что необязательно связывать переменные с флажками, но если сделать это, то работать с графическим интерфейсом будет проще. Между прочим, генератор списков в самом конце этого сценария является эквивалентом следующим вызовам функции map со связанным методом или lambda-выражением в качестве аргумента:
print(list(map(IntVar.get, states))) print(list(map(lambda var: var.get(), states)))
Хотя генераторы списков получили большое распространение в настоящее время, тем не менее то, какая форма наиболее понятна вам, может заметно зависеть от вашего... размера обуви.
Переключатели
Переключатели (или радиокнопки) обычно используются группами: так же, как для механических кнопок выбора станций в старых радиоприемниках, щелчок на одном виджете Radiobutton из группы автоматически делает невыбранными все кнопки, кроме той, на которой был выполнен последний щелчок. Иными словами, одновременно может быть выбрано не более одного виджета. В tkinter связывание всех переключателей из группы с уникальными значениями с одной и той же переменной гарантирует, что в каждый данный момент времени может быть выбрано не более одного переключателя.
Подобно флажкам и обычным кнопкам переключатели поддерживают параметр command для регистрации функции обратного вызова, обрабатывающей щелчок. Подобно флажкам у переключателей также есть атрибут variable для связывания кнопок в группу и получения текущего выбора в произвольный момент времени.
Кроме того, у переключателей есть атрибут value, позволяющий сообщить библиотеке tkinter, какое значение должна иметь ассоциированная переменная, когда выбирается тот или иной переключатель в группе. Поскольку несколько переключателей ассоциируется с одной и той же переменной, каждому переключателю должно соответствовать свое значение (это не просто схема с переключением между 1 и 0). Основы использования переключателей демонстрируются в примере 8.25.
Пример 8.25. PP4E\Gui\Tour\demoRadio.py
“создает группу переключателей, которые вызывают демонстрационные диалоги”
from tkinter import * # импортировать базовый набор виджетов
from dialogTable import demos # обработчики событий
from quitter import Quitter # прикрепить к “себе” объект Quitter
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.pack()
Label(self, text=”Radio demos”).pack(side=TOP) self.var = StringVar() for key in demos:
Radiobutton(self, text=key,
command=self.onPress,
variable=self.var,
value=key).pack(anchor=NW)
self.var.set(key) # при запуске выбрать последний переключатель Button(self, text=’State’, command=self.report).pack(fill=X) Quitter(self).pack(fill=X)
def onPress(self):
pick = self.var.get() print(‘you pressed’, pick) print(‘result:’, demos[pick]())
def report(self):
print(self.var.get())
if __name__ == ‘__main__’: Demo().mainloop()
На рис. 8.28 изображено окно, которое создается при запуске этого сценария. Щелчок на любом из переключателей в этом окне вызывает обработчик command, запускает один из стандартных диалогов, с которыми мы познакомились выше, и автоматически делает невыбранным переключатель, на котором выполнялся щелчок перед этим. Как и флажки, переключатели здесь также компонуются; в данном сценарии они прикрепляются к верхнему краю, располагаясь по вертикали, а затем выравниваются, прикрепляясь якорями к северо-западному углу отведенного им пространства.
Рис. 8.28. Сценарий demoRadio в действии
Как и в примере с флажками, кнопка State служит для запуска метода report класса и вывода информации о текущем состоянии переключателей (выбранного переключателя). В отличие от примера с флажками, в этом сценарии выводятся также значения, возвращаемые диалогами, которые запускаются щелчками на переключателях. Ниже показано, как выглядит поток stdout после нескольких щелчков на переключателях - информация о состоянии выделена полужирным шрифтом:
C:\...\PP4E\Gui\Tour> python demoRadio.py
you pressed Input result: 3.14 Input
you pressed Open
result: C:/PP4thEd/Examples/PP4E/Gui/Tour/demoRadio.py Open
you pressed Query result: yes Query
Переключатели и переменные
Так зачем здесь нужны переменные? Первое, у переключателей нет метода «get», который позволил бы получить значение выбора. Еще более важно то, что в группах переключателей именно параметры value и variable обслуживают режим выбора единственного варианта. Вообще, работа переключателей обеспечивается тем, что вся группа ассоциируется с одной и той же переменной tkinter и при этом все переключатели имеют различные значения. Чтобы до конца разобраться в этом, нужны еще некоторые сведения о том, как взаимодействуют переключатели и переменные.
Как мы уже видели, при изменении состояния виджета изменяется ассоциируемая с ним переменная tkinter, и наоборот. Но также верно и то, что любое изменение переменной автоматически изменяет каждый виджет, с которым она связана. При работе с переключателями щелчок на одном из них устанавливает значение совместно используемой переменной, которая, в свою очередь, оказывает воздействие на другие переключатели, ассоциированные с этой переменной. При условии, что все переключатели имеют различные значения, это вызывает ожидаемый эффект. Когда в результате щелчка на переключателе значение совместно используемой переменной изменяется на значение выбранного переключателя, все остальные переключатели оказываются невыбранными, потому что значение переменной не совпадает с их значениями.
Это правило действует в обоих направлениях: когда пользователь выбирает переключатель - он неявно изменяет значение совместно используемой переменной; когда сценарий изменяет значение переменной, -он изменяет состояние переключателей. Например, когда сценарий в примере 8.25 на этапе инициализации присваивает совместно используемой переменной последнее значение последнего переключателя (вызовом self.var.set), он выбирает последний переключатель, а остальные автоматически становятся невыбранными. В результате изначально будет выбран только один переключатель. Если бы в переменную была записана строка, не являющаяся именем какого-либо демонстрационного диалога (например, ‘ ‘ ), все переключатели при запуске оказались бы невыбранными.
Это довольно тонкий волновой эффект, но его будет проще понять, представив картину с другой стороны: если в группе переключателей, связанных с одной и той же переменной, назначить нескольким переключателям одно и то же значение, то при щелчке на любом из них все они будут автоматически выбраны. Рассмотрим пример 8.26 и рис. 8.29. При запуске сценария не выбран ни один переключатель (так как совместно используемая переменная инициализирована значением, не соответствующим ни одному из значений переключателей), но поскольку переключатели 0, 3, 6 и 9 - имеют значение 0 (остаток от деления на 3), при выборе любого из них выбираются они все.
Рис. 8.29. Переключатели испортились?
Пример 8.26. PP4E\Gui\Tour\demo-radio-multi.py
# посмотрите, что произойдет, если несколько переключателей
# будут иметь одно и то же значение
from tkinter import * root = Tk() var = StringVar() for i in range(10):
rad = Radiobutton(root, text=str(i), variable=var, value=str(i % 3)) rad.pack(side=LEFT)
var.set(‘ ‘) # все переключатели сделать невыбранными
root.mainloop()
Если теперь щелкнуть на любом из переключателей 1, 4 или 7, будут выбраны все три, а предыдущие выбранные окажутся сброшенными (их значение не равно «1»). Обычно это не то, что требуется, - переключатели как правило используются для представления групп с возможностью выбора единственного варианта (возможность выбора сразу нескольких вариантов реализуется с помощью флажков). Если вы хотите, чтобы переключатели действовали, как им положено, следите за тем, чтобы всем переключателям была назначена одна и та же переменная, но разные значения. Например, в сценарии demoRadio имя демонстрационного диалога дает естественное уникальное значение для каждой кнопки.
Переключатели без переменных
Строго говоря, в этом примере мы могли бы обойтись и без переменных tkinter. В примере 8.27 также реализована модель с одним выбором, но без переменных, - путем выбора и сброса элементов в группе вручную, в обработчике события. При каждом событии щелчка на переключателе вызывается метод deselect для всех объектов в группе и метод select для того переключателя, на котором был выполнен щелчок.
Пример 8.27. PP4E\Gui\Tour\demo-radio-manual.py
переключатели, сложный способ (без переменных)
обратите внимание, что метод deselect переключателя просто устанавливает пустую строку в качестве его значения, поэтому нам по-прежнему требуется присвоить переключателям уникальные значения или использовать флажки;
from tkinter import * state = ‘’ buttons = []
def onPress(i): global state state = i
for btn in buttons:
btn.deselect()
buttons[i].select()
root = Tk()
for i in range(10):
rad = Radiobutton(root, text=str(i),
value=str(i), command=(lambda i=i: onPress(i)) )
rad.pack(side=LEFT)
buttons.append(rad)
onPress(0) # первоначально выбрать первый переключатель
root.mainloop()
print(state) # вывести информацию о состоянии перед выходом
Этот сценарий создает такое же окно с 10 переключателями, как на рис. 8.29, но реализует интерфейс с единственным выбором, причем текущее состояние хранится в глобальной переменной Python, значение которой выводится при завершении сценария. Все это библиотека tkinter может сделать вместо вас, если использовать связанную переменную tkinter и уникальные значения, как показано в примере 8.28.
Пример 8.28. PP4E\Gui\Tour\demo-radio-auto.py
# переключатели, простой способ
from tkinter import *
root = Tk() # IntVar также можно использовать
var = IntVar(0) # выбрать 0-й переключатель при запуске
for i in range(10):
rad = Radiobutton(root, text=str(i), value=i, variable=var) rad.pack(side=LEFT) root.mainloop()
print(var.get()) # вывести информацию о состоянии перед выходом
Этот сценарий действует точно так же, но вводить и отлаживать его значительно проще. Обратите внимание, что в этом сценарии переключатели связываются с переменной типа IntVar, целочисленным собратом StringVar, которая инициализируется нулевым значением (которое также является значением по умолчанию) - если значения переключателей уникальны, можно также пользоваться и целыми числами.
Берегите свои переменные!
Небольшое предостережение: в целом следует сохранять объект переменной tkinter, используемой для связи с переключателями в течение всего времени, пока переключатели отображаются на экране. Присвойте ссылку на объект глобальной переменной модуля, запомните в структуре данных с длительным временем существования или сохраните как атрибут долгоживущего объекта класса, как сделано в сценарии demoRadio. Просто сохраните ссылку на него где-нибудь. В этом случае у вас всегда будет возможность тем или иным способом получить информацию о состоянии, и вас вряд ли когда-нибудь коснется то, о чем я хочу рассказать.
В текущей версии tkinter классы переменных обладают деструктором
__del__, который автоматически сбрасывает созданную переменную Tk,
когда уничтожается объект Python (то есть утилизируется сборщиком мусора). В итоге все ваши переключатели могут оказаться невыбранными, если объект переменной будет утилизирован, по крайней мере, до того момента, когда очередной щелчок мышью установит новое значение переменной Tk. В примере 8.29 демонстрируется ситуация, в которой это может случиться.
Пример 8.29. PP4E\Gui\Tour\demo-radio-elear.py
# берегите переменные переключателей (о чем действительно легко можно забыть)
from tkinter import * root = Tk()
def radio1(): # локальные переменные являются временными
#global tmp # сделав их глобальными, вы решите проблему
tmp = IntVar() for i in range(10):
rad = Radiobutton(root, text=str(i), value=i, variable=tmp) rad.pack(side=LEFT)
tmp.set(5) # выбрать 6-й переключатель
radio1()
root.mainloop()
Кажется, что первоначально должен быть выбран переключатель «5», но этого не происходит. Локальная переменная tmp уничтожается при выходе из функции, переменная Tk сбрасывается и значение 5 теряется (все переключатели оказываются невыбранными). Тем не менее эти переключатели прекрасно работают, если попробовать выполнять на них щелчки мышью, поскольку при этом переменная Tk переустанавливается. Если раскомментировать инструкцию global, кнопка 5 будет появляться в выбранном состоянии, как и задумывалось.
Однако в версии Python 3.X это явление, похоже, приобрело дополнительные отрицательные черты: в этой версии переключатель «5» не только не выбирается изначально, но и перемещение указателя мыши над невыбранными переключателями порождает эффект незаказанного выбора многих их них, пока не будет выполнен щелчок мышью. (В версии 3.X также требуется инициализировать строковую переменную StringVar, совместно используемую переключателями, как мы делали это в предыдущих примерах; в противном случае переменная получит пустую строку, как значение по умолчанию, что переведет все переключатели в выбранное состояние!)
Конечно, это нетипичный пример - в таком виде невозможно узнать, какая кнопка нажата, потому что переменная не сохраняется (и параметр command не установлен). Довольно бессмысленно использовать группу переключателей, если позднее нельзя получить значение выбора. Фактически это настолько невразумительно, что я отсылаю вас к примеру demo-radio-elear2.py в пакете примеров для книги, в котором делается попытка другими способами заставить проявиться эту странность. Возможно, вам это не понадобится, но если вы столкнетесь с этим, не говорите, что я вас не предупреждал.
Ползунки
Ползунки («scales» или «sliders») используются для выбора значения из диапазона чисел. Перемещение ползунка с помощью перетаскивания или щелчка мышью изменяет значение виджета в диапазоне целых чисел и запускает обработчик, если он зарегистрирован.
Подобно флажкам и переключателям ползунки обладают параметром command для регистрации управляемого событиями обработчика, выполняемого немедленно при перемещении ползунка, а также параметром variable для связи с переменной tkinter, которая позволяет в любой момент времени получить или установить положение ползунка. Обрабатывать значение можно сразу же после его установки или позже.
Кроме того, у ползунков есть третий способ обработки - методы get и set, с помощью которых можно непосредственно обращаться к значению виджета, не связывая с ним переменную. Поскольку обработчики, регистрируемые с помощью параметра command, получают текущее значение ползунка в качестве аргумента, часто этого достаточно, чтобы не прибегать к связанным переменным или вызовам методов get/set.
Для иллюстрации основ применения этого элемента в примере 8.30 приводится сценарий, который создает два ползунка - горизонтальный и вертикальный, связанные между собой через ассоциированную переменную, что позволяет их синхронизировать.
Пример 8.30. PP4E\Gui\Tour\demoSeale.py
“создает два связанных ползунка для запуска демонстрационных диалогов”
from tkinter import * # импортировать базовый набор виджетов
from dialogTable import demos # обработчики событий
from quitter import Quitter # прикрепить к “себе” объект Quitter
class Demo(Frame):
def __init__(self, parent=None, **options):
Frame.__init__(self, parent, **options)
self.pack()
Label(self, text=”Scale demos”).pack() self.var = IntVar()
Scale(self, label=’Pick demo number’,
command=self.onMove, # перехватывать перемещения variable=self.var, # отражает положение from_=0, to=len(demos)-1).pack()
Scale(self, label=’Pick demo number’,
command=self.onMove, # перехватывать перемещения variable=self.var, # отражает положение from_=0, to=len(demos)-1, length=200, tickinterval=1, showvalue=YES, orient=’horizontal’).pack() Quitter(self).pack(side=RIGHT)
Button(self, text=”Run demo”, command=self.onRun).pack(side=LEFT) Button(self, text=”State”, command=self.report).pack(side=RIGHT)
def onMove(self, value):
print(‘in onMove’, value)
def onRun(self):
pos = self.var.get() print(‘You picked’, pos)
demo = list(demos.values())[pos] # отображение позиции на ключ
# (представление в версии 3.X)
print(demo()) # или
# demos[ list(demos.keys())[pos] ]()
def report(self):
print(self.var.get())
if__name__== ‘__main__’:
print(list(demos.keys()))
Demo().mainloop()
Кроме доступа к значениям и регистрации обработчиков у ползунков имеются параметры, соответствующие понятию диапазона выбираемых значений, большинство из которых продемонстрировано в этом примере:
• Параметр label позволяет определить текст, появляющийся рядом со шкалой, параметр length позволяет определить начальный размер в пикселях, а параметр orient - направление.
• Параметры from_ и to позволяют определить минимальное и максимальное значения шкалы (обратите внимание, что from в языке Python является зарезервированным словом, а from_ - нет).
• Параметр tickinterval позволяет определить количество единиц измерения между отметками, наносимыми рядом со шкалой через равные интервалы (значение 0 по умолчанию означает, что отметки не выводятся).
• Параметр resolution позволяет определить количество единиц, на которое изменяется значение ползунка при каждом перетаскивании или щелчке левой кнопки мыши (по умолчанию 1).
• Параметр showvalue позволяет определить, должно ли отображаться текущее значение рядом с ползунком (по умолчанию showvalue=YES, то есть отображается).
Обратите внимание, что ползунки тоже прикрепляются к своим контейнерам, как и прочие виджеты tkinter. Посмотрим, как эти параметры используются на практике. На рис. 8.30 изображено окно, создаваемое этим сценарием в Windows 7 (в Unix и Mac получается аналогичная картина).
Рис. 8.30. Сценарий demoSeale в действии
Для наглядности кнопка State выводит текущие значения ползунков, а Run demo - запускает те же стандартные диалоги, используя целочисленные значения ползунков в качестве индекса таблицы demos. Сценарий также регистрирует обработчик command, который вызывается при каждом перемещении ползунка по любой из шкал и выводит новые значения ползунков. Ниже приводятся сообщения, отправленные в поток stdout после нескольких перемещений, с информацией о запускаемых демонстрационных диалогах (курсив) и о значениях ползунка (полужирный):
C:\...\PP4E\Gui\Tour> python demoScale.py
[‘Color’, ‘Query’, ‘Input’, ‘Open’, ‘Error’] in onMove 0 in onMove 0 in onMove 1 1
in onMove 2 You picked 2
123.0
in onMove 3 3
You picked 3
C:/Users/mark/Stuff/Books/4E/PP4E/dev/Examples/PP4E/Launcher.py
Ползунки и переменные
Как можно догадаться, ползунки предлагают различные способы обработки своих значений: непосредственно в обработчике события перемещения или позже, путем получения текущего положения через переменные или вызовы методов. В действительности переменные tkinter вообще не нужны для программирования ползунков - достаточно зарегистрировать обработчик события перемещения или вызывать метод get ползунка, чтобы при необходимости получать значение шкалы, как показано в более простом примере 8.31.