Интерфейсы окон верхнего уровня были представлены в главе 8. Данный раздел продолжает обсуждение с того места, где это представление завершилось, и демонстрирует обертывание этих интерфейсов классами, автоматизирующими большую часть операций по созданию окон верхнего уровня, — установку заголовков, поиск и отображение ярлыков окон, выполнение действий по закрытию окна исходя из его назначения, перехват события щелчка мышью на кнопке закрытия окна в его заголовке и так далее.
В примере 10.16 приводятся определения классов-оберток для наиболее часто используемых типов окон — главного окна приложения, временного всплывающего окна и окна встроенного компонента графического интерфейса. Эти типы окон несколько отличаются друг от друга реализациями операции закрытия, но наследуют множество общих черт: ярлыки, заголовки и кнопки закрытия. Используя непосредственно, подмешивая или наследуя класс окна требуемого типа, вы бесплатно получаете в свое распоряжение все логику настройки.
Пример 10.16. PP4E\Gui\Tools\windows.py
############################################################################## Классы, инкапсулирующие интерфейсы верхнего уровня.
Позволяют создавать главные, всплывающие или присоединяемые окна; эти классы могут наследоваться непосредственно, смешиваться с другими классами или вызываться непосредственно, без создания подклассов; должны подмешиваться после (то есть правее) более конкретных прикладных классов: иначе подклассы будут получать методы (destroy, okayToQuit) из этих, а не из прикладных классов, и лишатся возможности переопределить их.
##############################################################################
import os, glob
from tkinter import Tk, Toplevel, Frame, YES, BOTH, RIDGE from tkinter.messagebox import showinfo, askyesno
class _window:
подмешиваемый класс, используется классами главных и всплывающих окон
foundicon = None # совместно используется всеми экземплярами
iconpatt = ‘*.ico’ # может быть сброшен
iconmine = ‘py.ico’
def configBorders(self, app, kind, iconfile):
if not iconfile: # ярлык не был передан?
iconfile = self.findIcon() # поиск в тек. каталоге и в каталоге
title = app # модуля
if kind: title += ‘ — ‘ + kind self.title(title) # на рамке окна
self.iconname(app) # при свертывании
if iconfile:
try:
self.iconbitmap(iconfile) # изображение ярлыка окна
except: # проблема с интерпретатором или
pass # платформой
self.protocol(‘WM_DELETE_WINDOW’, self.quit) # не закрывать без
# подтверждения
def findIcon(self): if _window.foundicon: # ярлык уже найден?
return _window.foundicon iconfile = None # сначала искать в тек. каталоге
iconshere = glob.glob(self.iconpatt) # допускается только один if iconshere: # удалить ярлык с красными
iconfile = iconshere[0] # буквами Tk
else: # поиск в каталоге модуля
mymod = __import__(__name__) # импортировать, получить каталог
path = __name__.split(‘.’) # возможно, путь пакета
for mod in path[1:]: # по всему пути до конца
mymod = getattr(mymod, mod) # только самый первый
mydir = os.path.dirname(mymod.__file__)
myicon = os.path.join(mydir, self.iconmine) # исп. myicon, а не tk if os.path.exists(myicon): iconfile = myicon
_window.foundicon = iconfile # не выполнять поиск вторично
return iconfile
class MainWindow(Tk, _window):
главное окно верхнего уровня
def __init__(self, app, kind=’’, iconfile=None):
Tk.__init__(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self):
if self.okayToQuit(): # потоки запущены?
if askyesno(self.__app, ‘Verify Quit Program?’): self.destroy() # завершить приложение
else:
showinfo(self.__app, ‘Quit not allowed’) # или в okayToQuit?
def destroy(self): # просто завершить
Tk.quit(self) # переопределить, если необходимо
def okayToQuit(self): # переопределить, если используются
return True # потоки выполнения
class PopupWindow(Toplevel, _window):
вторичное всплывающее окно
def __init__(self, app, kind=’’, iconfile=None):
Toplevel.__init__(self)
self.__app = app
self.configBorders(app, kind, iconfile)
def quit(self): # переопределить, если потребуется изменить
if askyesno(self.__app, ‘Verify Quit Window?’): # или вызвать destroy self.destroy() # чтобы закрыть окно
def destroy(self): # просто закрыть окно
Toplevel.destroy(self) # переопределить, если необходимо
class QuietPopupWindow(PopupWindow):
def quit(self):
self.destroy() # закрывать без предупреждения
class ComponentWindow(Frame):
при присоединении к другим интерфейсам
def __init__(self, parent): # если не фрейм
Frame.__init__(self, parent) # предоставить контейнер self.pack(expand=YES, fill=BOTH)
self.config(relief=RIDGE, border=2) # перенастроить при необходимости
def quit(self):
showinfo(‘Quit’, ‘Not supported in attachment mode’)
# destroy из фрейма: просто удалить фрейм # переопределить, если
# необходимо
Почему бы просто не определять ярлык приложения и заголовок окна непосредственно вызовом методов? С одной стороны, особенности такого рода нелегко запомнить (в результате вам придется большую часть времени тратить на копирование и вставку кода). С другой стороны, эти классы добавляют высокоуровневые функциональные возможности, реализацию которых иначе пришлось бы добавлять снова и снова. Кроме всего прочего, эти классы обеспечивают автоматический запрос подтверждения завершения и поиск ярлыка. Например, классы окон всего один раз пытаются отыскать файл ярлыка в текущем рабочем каталоге и в каталоге, где находится данный модуль.
Используя классы, инкапсулирующие — то есть скрывающие — такие подробности, мы получаем всю мощь инструмента, без необходимости задумываться об их реализации в будущем. Кроме того, с помощью этих классов мы можем придавать нашим приложениям стандартизованный внешний вид и поведение. А если в будущем их потребуется изменить, нам достаточно будет изменить программный код только в одном месте, а не во всех реализованных нами окнах.
Для тестирования этого модуля в примере 10.17 приводится сценарий, использующий эти классы в различных режимах — в качестве подмешиваемых классов, в качестве суперклассов и непосредственно — из обычного процедурного программного кода.
Пример 10.17. PP4E\Gui\Tools\windows-test.py
# модуль windows должен импортироваться, иначе атрибут __name__ будет иметь
# значение __main__ в функции findIcon
from tkinter import Button, mainloop
from windows import MainWindow, PopupWindow, ComponentWindow
def _selftest():
# использовать, как подмешиваемый класс class content:
“используется так же, как Tk, Toplevel и Frame” def __init__(self):
Button(self, text=’Larch’, command=self.quit).pack()
Button(self, text=’Sing ‘, command=self.destroy).pack()
class contentmix(MainWindow, content): def __init__(self):
MainWindow.__init__(self, ‘mixin’, ‘Main’) content.__init__(self) contentmix()
class contentmix(PopupWindow, content): def __init__(self):
PopupWindow.__init__(self, ‘mixin’, ‘Popup’) content.__init__(self) prev = contentmix()
class contentmix(ComponentWindow, content):
def __init__(self): # вложенный фрейм
ComponentWindow.__init__(self, prev) # в предыдущем окне content.__init__(self) # кнопка Sing стирает фрейм
contentmix()
# использовать в подклассах class contentsub(PopupWindow): def __init__(self):
PopupWindow.__init__(self, ‘popup’, ‘subclass’)
Button(self, text=’Pine’, command=self.quit).pack()
Button(self, text=’Sing’, command=self.destroy).pack() contentsub()
# использование в процедурном программном коде win = PopupWindow(‘popup’, ‘attachment’) Button(win, text=’Redwood’, command=win.quit).pack() Button(win, text=’Sing ‘, command=win.destroy).pack() mainloop()
if __name__ == ‘__main__’: _selftest()
Если запустить этот тест, он создаст четыре окна, как показано на рис. 10.10. Все окна автоматически получат ярлык с голубыми буквами «PY» и будут перехватывать и запрашивать подтверждение при попытке закрыть их щелчком на кнопке X в правом верхнем углу, благодаря логике поиска и настройки, унаследованной из классов окон в модуле. Некоторые кнопки в окнах, воспроизводимых тестовым сценарием, закрывают только вмещающее их окно, некоторые — все приложение, некоторые стирают только присоединенное окно, а некоторые выводят диалог с просьбой подтвердить закрытие окна. Запустите этот сценарий у себя на компьютере, чтобы увидеть, как действуют различные кнопки, и получить возможность сопоставить их поведение с реализацией в сценарии — действия, выполняемые при закрытии окна, зависят от его типа.
Рис. 10.10. Интерфейс сценария windows—test
Мы будем использовать эти классы-обертки в следующей главе, в примере PyClock, и еще раз — в главе 14, где они будут использоваться, чтобы уменьшить сложность программы PyMailGUI. Отчасти преимущество применения приемов ООП в языке Python состоит в том, что благодаря им мы можем позднее не вспоминать детали реализации.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011