# об этом событии смотрите в книге и в модуле destroyer.py;
# -----------------------------------------------------------------------------
########################################
# когда текстовый редактор владеет окном ########################################
class TextEditorMain(TextEditor, GuiMakerWindowMenu):
главное окно редактора PyEdit, которое вызывает метод quit() при выполнении операции Quit графического интерфейса для завершения приложения и конструирует меню в окне; родителем может быть окно Tk, по умолчанию, окно Tk, создаваемое явно, или объект Toplevel: родитель должен быть окном и, вероятно, окном Tk, чтобы избежать закрытия без предупреждения вместе с родителем; при выполнении операции Quit графического интерфейса все главные окна PyEdit проверяют остальные окна
def __init__(self, parent=None, loadFirst=’’, winTitle=’’, loadEncode=’’):
# создать собственное окно self.popup = Toplevel(parent)
GuiMaker.__init__(self, self.popup) # использует главное меню окна
TextEditor.__init__(self, loadFirst, loadEncode) # фрейм в новом окне
assert self.master == self.popup self.popup.title(‘PyEdit ‘ + Version + winTitle) self.popup.iconname(‘PyEdit’)
self.popup.protocol(‘WM_DELETE_WINDOW’, self.onQuit) TextEditor.editwindows.append(self)
def onQuit(self):
close = not self.text_edit_modified() if not close:
close = askyesno(‘PyEdit’,
‘Text changed: quit and discard changes?’)
if close:
self.popup.destroy() # закрыть только это окно
TextEditor.editwindows.remove(self) # (и все дочерние окна)
def onClone(self):
TextEditor.onClone(self, makewindow=False) # я создаю собственное окно
###########################################
# когда редактор встраивается в другое окно ###########################################
class TextEditorComponent(TextEditor, GuiMakerFrameMenu):
прикрепляемый фрейм компонента PyEdit с полными меню/панелью инструментов, который вызывает destroy() при выполнении операции Quit графического интерфейса и стирает только себя; при выполнении операции Quit проверяется наличие несохраненных изменений только в этом редакторе; не перехватывает щелчок на кнопке X в заголовке окна: не имеет собственного окна; не добавляет себя в список отслеживаемых окон: является частью более крупного приложения;
def __init__(self, parent=None, loadFirst=’’, loadEncode=’’):
# использовать меню на основе фрейма
GuiMaker.__init__(self, parent) # все меню, кнопки в GuiMaker должны TextEditor.__init__(self, loadFirst, loadEncode) # создаваться первыми
def onQuit(self):
close = not self.text_edit_modified() if not close:
close = askyesno(‘PyEdit’,
‘Text changed: quit and discard changes?’)
if close:
self.destroy() # стереть свой фрейм, но не завершать вмещающее # приложение
class TextEditorComponentMinimal(TextEditor, GuiMakerFrameMenu):
прикрепляемый фрейм компонента PyEdit без операции Quit и без меню File; на запуске удаляет кнопку Quit из панели инструментов и удаляет меню File или запрещает все его пункты (грубовато, зато эффективно); структуры меню и панели инструментов являются данными экземпляра: изменение их не затрагивает другие экземпляры;
Операция Quit графического интерфейса никогда не запускается, потому что она удаляется из доступных операций;
def __init__(self, parent=None, loadFirst=’’,
deleteFile=True, loadEncode=’’): self.deleteFile = deleteFile
GuiMaker.__init__(self, parent) # фрейм GuiMaker прикрепляет себя сам
TextEditor.__init__(self, loadFirst, loadEncode) # TextEditor
# добавляется
# в середину
def start(self):
TextEditor.start(self) # вызов метода start GuiMaker
for i in range(len(self.toolBar)): # удалить quit из панели инстр. if self.toolBar[i][0] == ‘Quit’: # удалить пункты меню file del self.toolBar[i] # или просто запретить их
break
if self.deleteFile:
for i in range(len(self.menuBar)): if self.menuBar[i][0] == ‘File’: del self.menuBar[i] break
else:
for (name, key, items) in self.menuBar: if name == ‘File’:
items.append([1,2,3,4,6])
##############################################################################
# запуск как самостоятельной программы
##############################################################################
def testPopup():
# проверку запуска как компонента смотрите в PyView и PyMail root = Tk()
TextEditorMainPopup(root)
TextEditorMainPopup(root)
Button(root, text=’More’, command=TextEditorMainPopup).pack(fill=X) Button(root, text=’Quit’, command=root.quit).pack(fill=X) root.mainloop()
def main(): # из командной строки или щелчком
try: # либо как ассоциированная программа в Windows
fname = sys.argv[1] # аргумент = необязательное имя файла except IndexError: # создается в корневом окне Tk по умолчанию
fname = None
TextEditorMain(loadFirst=fname).pack(expand=YES, fill=BOTH)# pack -mainloop() # необязательно
if __name__ == ‘__main__’: # когда запускается как сценарий
#testPopup()
main() # используйте .pyw, чтобы запустить без окна DOS
PyPhoto: программа просмотра и изменения размеров изображений
В главе 9 мы написали простую программу просмотра миниатюр изображений, реализующую прокрутку коллекции миниатюр в холсте. Эта программа в свою очередь была основана на приемах и программном коде для работы с изображениями, разработанных в конце главы 8. В обоих случаях я обещал, что мы в конечном счете встретимся с более полнофункциональным воплощением рассматривавшихся идей.
В этом разделе мы наконец завершим обсуждение миниатюр изображений знакомством с PyPhoto - улучшенной программой просмотра и изменения размеров изображений. Основу программы PyPhoto составляют простые операции: для заданного каталога с изображениями PyPhoto отображает их миниатюры в холсте с прокруткой. При выборе миниатюры отображается соответствующее ей полноразмерное изображение во всплывающем окне.
В отличие от предыдущих программ просмотра изображений, PyPhoto предусматривает возможность прокрутки изображения (вместо обрезания), если оно оказывается больше физического экрана. Кроме того, программа PyPhoto вводит понятие изменения размеров изображения -она поддерживает события от мыши и клавиатуры, которые изменяют размер изображения по одной из осей и увеличивают или уменьшают масштаб изображения. После того как изображение открыто, логика изменения размеров позволяет растягивать и сжимать изображение до произвольных размеров, что особенно удобно при просмотре цифровых фотографий, которые могут быть слишком большими, чтобы их можно было просмотреть целиком.
Кроме того, PyPhoto позволяет сохранять изображения в файлах (возможно, после изменения размеров) и дает возможность выбирать и открывать каталоги с изображениями в самом графическом интерфейсе, а не только с помощью аргументов командной строки.
Все вместе особенности PyPhoto образуют программу обработки изображений, хотя и с небольшим, по современным меркам, количеством инструментов. Я предлагаю вам самим попробовать добавить в нее новые возможности - после овладения навыками работы с прикладным интерфейсом библиотеки Python Imaging Library (PIL) объектноориентированная природа PyPhoto делает добавление новых инструментов удивительно простым делом.
Запуск PyPhoto
Чтобы запустить PyPhoto, необходимо получить и установить пакет расширения PIL, описанный в главе 8. Программа PyPhoto использует многие функциональные возможности PIL; эта библиотека поддерживает дополнительные форматы изображений, помимо тех, что поддерживаются стандартной библиотекой tkinter (например, изображений JPEG), и используется для выполнения операций над изображениями, таких как изменение размеров, создание миниатюр и сохранение в файлах. Расширение PIL распространяется с открытыми исходными текстами, как и Python, но в настоящее время оно не является частью стандартной библиотеки Python. Ищите PIL в Интернете (в настоящее время самый точный адрес: http://www.pythonware.com). Проверьте также каталог Extensions в дереве примеров, где находится самоустанавливающийся пакет PIL.
Самый лучший способ получить представление о программе PyPhoto -запустить ее у себя на компьютере и посмотреть, как она выполняет прокрутку изображений и изменение их размеров. Ниже будет представлено несколько снимков с экрана, дающих общее представление о взаимодействии с программой. Запустить PyPhoto можно щелчком на ее ярлыке или из командной строки. При непосредственном запуске программа открывает подкаталог images в исходном каталоге, который содержит несколько фотографий. При запуске из командной строки программе можно передать имя начального каталога в виде аргумента. На рис. 11.7 изображено главное окно с миниатюрами, которое выводится при непосредственном запуске программы.
Прежде чем появится это окно, PyPhoto загружает или создает миниатюры, используя инструменты, реализованные в главе 8. Если каталог с изображениями открывается впервые, запуск программы может занять несколько секунд, но все последующие запуски будут протекать быстро - PyPhoto кэширует миниатюры в локальном подкаталоге, чтобы можно было пропустить этап создания миниатюр, когда этот же каталог будет открыт в следующий раз.
Технически, существует три разных варианта поведения PyPhoto при запуске: она отображает содержимое определенного каталога, указанного в командной строке; отображает содержимое каталога images по умолчанию при запуске без аргументов командной строки и когда каталог images находится в каталоге запуска программы; или отображает единственную кнопку, после щелчка на которой предоставляется возможность выбрать и открыть каталог для просмотра, когда начальный каталог не указан или отсутствует (смотрите логику работы раздела __main__).
Рис. 11.7. Главное окно PyPhoto, каталог по умолчанию
Программа PyPhoto позволяет также открывать дополнительные папки в новых окнах с миниатюрами, для чего достаточно нажать клавишу D в окне с миниатюрами или в окне с изображением. Например, на рис. 11.8 показан диалог выбора новой папки с изображениями в Windows 7, а на рис. 11.9 показан результат того, что я открыл каталог, куда были скопированы фотографии с карты памяти моей цифровой фотокамеры, - это второе окно PyPhoto с миниатюрами на экране. Окно, изображенное на рис. 11.8, также открывается из окна с единственной кнопкой, если при запуске программе не указать начальный каталог или если этот каталог недоступен.
После выбора миниатюры на экране появляется новое окно, где в холсте отображается соответствующее изображение. Если изображение оказывается слишком большим для экрана, его можно будет прокрутить с помощью полос прокрутки в окне. На рис. 11.10 показано изображение, которое было выведено после щелчка на миниатюре, а на рис. 11.11 - диалог Save As, запущенный нажатием клавиши S в окне с изображением. В этом диалоге Save As необходимо ввести требуемое
Рис. 11.9. Окно PyPhoto с миниатюрами, другой каталог
Рис. 11.10. Окно PyPhoto для просмотра изображения
Рис. 11.11. Диалог Save As программы PyPhoto (клавиша S; включает расширение)
расширение имени файла (например, .jpg), потому что расширение PIL использует его, чтобы определить, в каком формате сохранять изображение в файле. В целом, программа PyPhoto позволяет открывать любое количество окон с миниатюрами и с полноразмерными изображениями, и каждое изображение может сохраняться независимо от других.
Помимо уже показанных снимков с экранов, довольно сложно изобразить особенности взаимодействий с программой в такой статической среде, как книга, - более полное представление вы сможете получить, опробовав программу у себя на компьютере.
Например, щелчки левой и правой кнопками мыши будут изменять высоту и ширину изображения, соответственно, а нажатие клавиш I и O будет изменять масштаб, увеличивая и уменьшая изображение с шагом 10 процентов. Обе схемы изменения размеров позволяют сжать изображение, которое не умещается на экране целиком, а также растягивать маленькие фотографии. Они также сохраняют исходное отношение сторон фотографий, пропорционально изменяя высоту или ширину, чего не делает изменение размера только по одной из осей (может растягиваться по ширине или высоте).
После изменения размеров изображения можно сохранять в файлах с их текущими размерами. Кроме того, программа PyPhoto достаточно интеллектуальна, чтобы открывать в Windows окна в полный размер, если изображение не помещается в открытое окно.
Исходный программный код PyPhoto
Поскольку PyPhoto просто расширяет и повторно использует приемы и программный код, с которыми мы встречались ранее в книге, здесь мы опустим детальное обсуждение исходных текстов. За исходными сведениями обращайтесь к обсуждению приемов обработки изображений и применения PIL в главе 8 и к описанию виджета холста в главе 9.
В двух словах отмечу, что PyPhoto использует холсты в двух случаях: для отображения коллекций миниатюр и для вывода открываемых изображений. Для вывода миниатюр используется тот же прием компоновки, что и раньше, в примере 9.15. Для вывода полноразмерных изображений также используется холст, прокручиваемая (полная) область которого соответствует размеру изображения, а видимая область вычисляется как минимум из размера физического экрана и размера самого изображения. Физический размер экрана можно определить вызовом метода maxsize() окна Toplevel. Благодаря этому полноразмерное изображение можно прокручивать, что очень удобно при просмотре изображений, размеры которых слишком велики, чтобы уместиться на экране (что весьма характерно для фотографий, снятых новейшими цифровыми фотокамерами).
Кроме того, PyPhoto выполняет привязку событий от клавиатуры и мыши для реализации операций изменения размеров и масштаби-
Рис. 11.8. Диалог открытия каталога в программе PyPhoto (клавиша D)
PyPhoto: программа просмотра и изменения размеров изображений
923
рования. Благодаря PIL эти операции реализуются очень просто - мы сохраняем оригинальное изображение в объекте изображения PIL, вызываем его метод resize, передавая новые размеры, и перерисовываем изображение на холсте. Программа PyPhoto также использует диалоги открытия и сохранения файла, чтобы запомнить последний посещенный каталог.
Расширение PIL поддерживает дополнительные операции, которыми мы могли бы расширить набор обрабатываемых событий, но для просмотра изображений вполне достаточно изменения размеров. В настоящее время PyPhoto не использует потоки выполнения, чтобы с их помощью избежать блокирования во время выполнения продолжительных операций (например, операция первого открытия большого каталога). Такие расширения я оставляю для самостоятельного упражнения.
Программа PyPhoto реализована в виде единого файла, представленного в примере 11.5, хотя она получает бесплатно некоторую дополнительную функциональность от повторного использования функции, генерирующей миниатюры, из модуля viewer_thumbs, который мы написали в конце главы 8, в примере 8.45. Чтобы не заставлять вас листать страницы взад и вперед, ниже приводится фрагмент программного кода импортируемой функции создания миниатюр, используемой здесь:
# импортировано из главы 8...
def makeThumbs(imgdir, size=(100, 100), subdir=’thumbs’):
# возвращает список кортежей
# (имя_файла_изображения, объект_миниатюры_изображения);
thumbdir = os.path.join(imgdir, subdir)
if not os.path.exists(thumbdir): os.mkdir(thumbdir)
thumbs = []
for imgfile in os.listdir(imgdir):
thumbpath = os.path.join(thumbdir, imgfile) if os.path.exists(thumbpath):
thumbobj = Image.open(thumbpath) # использовать созданные ранее thumbs.append((imgfile, thumbobj)) else:
print(‘making’, thumbpath)
imgpath = os.path.join(imgdir, imgfile)
try:
imgobj = Image.open(imgpath) # создать миниатюру
imgobj.thumbnail(size, Image.ANTIALIAS) # фильтр, дающий
# лучшее качество при
# уменьшении размеров
imgobj.save(thumbpath) # тип определяется
thumbs.append((imgfile, imgobj)) # расширением
except:
print("Skipping: ", imgpath)
return thumbs
Программный код, реализующий окно выбора миниатюр, также очень схож с представленным в главе 9 примером с прокручиваемой коллекцией миниатюр, но он не импортируется этим файлом, а просто повторяется в нем, чтобы обеспечить будущее его развитие (и его статус в главе 9 - функционального подмножества - здесь понижен до уровня прототипа).
При изучении этого файла особое внимание обратите на организацию программного кода в виде набора функций и методов многократного пользования, которая позволяет избежать избыточности, - если нам, например, когда-нибудь придется изменить реализацию операции изменения размеров, нам достаточно будет изменить один метод, а не два. Кроме того, обратите внимание на класс ScrolledCanvas - компонент многократного пользования, который обеспечивает автоматическое связывание полос прокрутки и холстов.
Пример 11.5. PP4E\Gui\PIL\pyphoto1.py
############################################################################
PyPhoto 1.1: программа просмотра миниатюр изображений с возможностью изменения размеров и сохранения.
Позволяет открывать несколько окон для просмотра миниатюр из разных каталогов - в качестве начального каталога с изображениями принимается аргумент командной строки, каталог по умолчанию "images” или выбранный щелчком на кнопке в главном окне; последующие каталоги могут открываться нажатием клавиши “D” в окне с миниатюрами или в окне просмотра полноразмерного изображения.
Программа также позволяет прокручивать изображения, если они слишком большие и не умещаются на экране;
все еще необходимо: (1) реализовать переупорядочение миниатюр при изменении размеров окна, исходя из текущего размера окна; (2) [ВЫПОЛНЕНО] возможность изменения размеров изображения в соответствии с текущими размерами окна?
(3) отключать прокрутку, если размер изображения меньше максимального размера окна: использовать Label, если шир_изобр <= шир_окна и выс_изобр <= выс_окна?
Новое в версии 1.1: работает под управлением Python 3.1 и с последней версией PIL;
Новое в версии 1.0: реализован пункт (2) выше: щелчок мышью изменяет размер изображения в соответствии с одним из размеров экрана, и предусмотрена возможность увеличения и уменьшения масштаба изображения с шагом 10% нажатием клавиши; требуется поискать более универсальные решения; ВНИМАНИЕ: похоже, что после многократного изменения размеров теряется качество изображения (вероятно, это ограничение PIL)
Следующий алгоритм масштабирования, заимствованный из реализации создания миниатюр средствами PIL, напоминает алгоритм масштабирования по высоте экрана, используемый в программе, но только для сжатия: x, y = imgwide, imghigh
if x > scrwide: y = max(y * scrwide // x, 1); x = scrwide
if y > scrhigh: x = max(x * scrhigh // y, 1); y = scrhigh ############################################################################ import sys, math, os from tkinter import *
from tkinter.filedialog import SaveAs, Directory
from PIL import Image # PIL Image: также имеется в tkinter
from PIL.ImageTk import PhotoImage # версия виджета PhotoImage из PIL from viewer_thumbs import makeThumbs # разработан ранее в книге
# запомнить последний открытый каталог
saveDialog = SaveAs(title=’Save As (filename gives image type)’) openDialog = Directory(title=’Select Image Directory To Open’)
trace = print # or lambda *x: None appname = ‘PyPhoto 1.1: ‘
class ScrolledCanvas(Canvas):
холст в контейнере, который автоматически создает вертикальную и горизонтальную полосы прокрутки
def __init__(self, container):
Canvas.__init__(self, container)
self.config(borderwidth=0)
vbar = Scrollbar(container)
hbar = Scrollbar(container, orient=’horizontal’)
vbar.pack(side=RIGHT, fill=Y) # холст прикрепляется после
hbar.pack(side=BOTTOM, fill=X) # полос, чтобы обрезался первым
self.pack(side=TOP, fill=BOTH, expand=YES)
vbar.config(command=self.yview) # вызвать при перемещении полосы
hbar.config(command=self.xview) # прокрутки
self.config(yscrollcommand=vbar.set) # вызвать при прокрутке холста
self.config(xscrollcommand=hbar.set)
class ViewOne(Toplevel):
при создании открывает единственное изображение во всплывающем окне; реализовано в виде класса, потому что объект PhotoImage должен сохраняться, иначе изображение будет стерто при утилизации; обеспечивает прокрутку больших изображений; щелчок мыши изменяет размер изображения в соответствии с высотой или шириной окна: растягивает или сжимает; нажатие клавиш I и O увеличивает и уменьшает размеры изображения; оба алгоритма изменения размеров предусматривают сохранение оригинального отношения сторон; программный код организован так, чтобы избежать избыточности, насколько это возможно;
def __init__(self, imgdir, imgfile, forcesize=()):
Toplevel.__init__(self)
helptxt = ‘(click L/R or press I/O to resize, S to save, D to open)’
self.title(appname + imgfile + ‘ ‘ + helptxt)
imgpath = os.path.join(imgdir, imgfile)
imgpil = Image.open(imgpath)
self.canvas = ScrolledCanvas(self)
self.drawImage(imgpil, forcesize)
self.canvas.bind(‘
def drawImage(self, imgpil, forcesize=()):
imgtk = PhotoImage(image=imgpil) # file != imgpath
scrwide, scrhigh = forcesize or self.maxsize() # размеры x,y экрана imgwide = imgtk.width() # размеры в пикселях
imghigh = imgtk.height() # то же,
# что и imgpil.size
fullsize = (0, 0, imgwide, imghigh) # прокручиваемая
viewwide = min(imgwide, scrwide) # видимая
viewhigh = min(imghigh, scrhigh)
canvas = self.canvas
canvas.delete(‘all’) # удалить предыд. изобр.
canvas.config(height=viewhigh, width=viewwide) # видимые размеры окна canvas.config(scrollregion=fullsize) # размер прокр. области
canvas.create_image(0, 0, image=imgtk, anchor=NW)
if imgwide <= scrwide and imghigh <= scrhigh: # слишком велико?
self.state(‘normal’) # нет: размер окна по изобр.
elif sys.platform[:3] == ‘win’: # в Windows на весь экран
self.state(‘zoomed’) # в других исп. geometry()
self.saveimage = imgpil
self.savephoto = imgtk # сохранить ссылку на меня
trace((scrwide, scrhigh), imgpil.size)
def sizeToDisplaySide(self, scaler):
# изменить размер, чтобы полностью заполнить одну сторону экрана imgpil = self.saveimage
scrwide, scrhigh = self.maxsize() # размеры x,y экрана
imgwide, imghigh = imgpil.size # размеры изображения в пикселях
newwide, newhigh = scaler(scrwide, scrhigh, imgwide, imghigh) if (newwide * newhigh < imgwide * imghigh):
filter = Image.ANTIALIAS # сжатие: со сглаживанием
else: # растягивание: бикубическая
filter = Image.BICUBIC # аппроксимация
imgnew = imgpil.resize((newwide, newhigh), filter) self.drawImage(imgnew)
def onSizeToDisplayHeight(self, event):
def scaleHigh(scrwide, scrhigh, imgwide, imghigh): newhigh = scrhigh
newwide = int(scrhigh * (imgwide / imghigh)) # истинное деление return (newwide, newhigh) # пропорциональные
self.sizeToDisplaySide(scaleHigh)
def onSizeToDisplayWidth(self, event):
def scaleWide(scrwide, scrhigh, imgwide, imghigh): newwide = scrwide
newhigh = int(scrwide * (imghigh / imgwide)) # истинное деление return (newwide, newhigh) self.sizeToDisplaySide(scaleWide)
def zoom(self, factor):
# уменьшить или увеличить масштаб с шагом imgpil = self.saveimage
wide, high = imgpil.size
if factor < 1.0: # сглаживание дает лучшее качество
filter = Image.ANTIALIAS # при сжатии, также можно else: # использовать NEAREST, BILINEAR
filter = Image.BICUBIC
new = imgpil.resize((int(wide * factor), int(high * factor)), filter) self.drawImage(new)
def onZoomIn(self, event, incr=.10): self.zoom(1.0 + incr)
def onZoomOut(self, event, decr=.10): self.zoom(1.0 - decr)
def onSaveImage(self, event):
# сохранить изображение в текущем виде в файл filename = saveDialog.show()
if filename:
self.saveimage.save(filename)
def onDirectoryOpen(event):
открывает новый каталог с изображениями в новом окне
может вызываться в обоих окнах, с изображением и с миниатюрами
dirname = openDialog.show() if dirname:
viewThumbs(dirname, kind=Toplevel)
def viewThumbs(imgdir, kind=Toplevel, numcols=None, height=400, width=500):
создает окно и кнопки с миниатюрами;
использует кнопки фиксированного размера, прокручиваемый холст; устанавливает прокручиваемый (полный) размер и размещает миниатюры в холсте по абсолютным координатам x,y;
больше не предполагает, что все миниатюры имеют одинаковые размеры: за основу берет максимальные размеры (x,y) среди всех миниатюр, некоторые могут быть меньше;
win = kind()
helptxt = ‘(press D to open other)’
win.title(appname + imgdir + ‘ ‘ + helptxt)
quit = Button(win, text=’Quit’, command=win.quit, bg=’beige’)
quit.pack(side=BOTTOM, fill=X)
canvas = ScrolledCanvas(win)
canvas.config(height=height, width=width) # видимый размер окна, может
# изменяться пользователем
thumbs = makeThumbs(imgdir) # [(imgfile, imgobj)]
numthumbs = len(thumbs) if not numcols:
numcols = int(math.ceil(math.sqrt(numthumbs))) # фиксир. или N x N numrows = int(math.ceil(numthumbs / numcols)) # истинное деление
# максимальная шир|выс: thumb=(name, obj), thumb.size=(width, height) linksize = max(max(thumb[1].size) for thumb in thumbs) trace(linksize)
fullsize = (0, 0, # X,Y верхн. левого угла
(linksize*numcols),(linksize*numrows)) # X,Y прав. нижнего угла canvas.config(scrollregion=fullsize) # размер прокруч. области
rowpos = 0 savephotos = [] while thumbs:
thumbsrow, thumbs = thumbs[:numcols], thumbs[numcols:] colpos = 0
for (imgfile, imgobj) in thumbsrow: photo = PhotoImage(imgobj) link = Button(canvas, image=photo) def handler(savefile=imgfile):
ViewOne(imgdir, savefile)
link.config(command=handler, width=linksize, height=linksize) link.pack(side=LEFT, expand=YES) canvas.create_window(colpos, rowpos, anchor=NW,
window=link, width=linksize, height=linksize) colpos += linksize savephotos.append(photo) rowpos += linksize
win.bind(‘
if__name__== ‘__main__’:
открываемый каталог = по умолчанию или из аргумента командной строки, иначе вывести простое окно с кнопкой для выбора каталога
imgdir = ‘images’
if len(sys.argv) > 1: imgdir = sys.argv[1] if os.path.exists(imgdir):
mainwin = viewThumbs(imgdir, kind=Tk) else:
mainwin = Tk()
mainwin.title(appname + ‘Open’) handler = lambda: onDirectoryOpen(None)
Button(mainwin, text=’Open Image Directory’, command=handler).pack() mainwin.mainloop()
PyView: слайд-шоу для изображений и примечаний
Одна картинка стоит тысячи слов, и их понадобится значительно меньше, чтобы вывести картинку с помощью Python. В следующей программе, PyView, представлена простая и переносимая реализация алгоритма слайд-шоу на языке Python и в библиотеке tkinter. Эта программа не обладает возможностями обработки изображений, такими как изменение их размеров, но она реализует другие инструменты, такие как файлы с примечаниями для изображений, и может выполняться при отсутствии PIL.
Запуск PyView
В PyView соединились многие из тем, изучавшихся в главе 9: последовательная смена изображений реализована с применением метода after, объекты изображений выводятся на холсте, автоматически изменяющем размер, и так далее. В главном окне программы на холсте выводится фотография; пользователь может открыть и просматривать ее непосредственно или запустить режим поочередного показа слайдов, в котором фотографии, случайным образом выбранные из каталога, выводятся через равные промежутки времени, задаваемые с помощью виджета ползунка.
По умолчанию показ слайдов в PyView производится для каталога с изображениями, входящего в состав примеров для книги (хотя кнопка Open позволяет загружать изображения из любых каталогов). Чтобы посмотреть другую коллекцию фотографий, передайте имя каталога в качестве первого аргумента командной строки или измените имя каталога по умолчанию в самом сценарии. Я не могу показать, как действует программа в режиме показа слайдов, но главное окно привести можно.
На рис. 11.12 изображено главное окно PyView, созданное сценарием slideShowPlus.py из примера 11.6, как оно выглядит в Windows 7.
На рисунке в книге этого не видно, но в действительности на метке вверху окна черным по красному выведен путь к отображаемому файлу. Сейчас переместите ползунок до конца к отметке «0», чтобы определить отсутствие задержки между сменой фотографий, и щелкните на кнопке Start, чтобы начать очень быстрый показ слайдов. Если ваш компьютер обладает хотя бы таким же быстродействием, как мой, то фотографии будут мелькать слишком быстро, чтобы их можно было применить где-либо, кроме как в рекламе, действующей на подсознание. Демонстрируемые фотографии загружаются при начальном запуске, чтобы сохранить ссылки на них (напомню, что объекты с изображениями нужно удерживать). Но скорость, с которой могут отображаться большие GIF-файлы на языке Python, впечатляет, а то и просто восхищает.
Во время показа слайдов кнопка Start изменяется на Stop (изменяется ее текстовый атрибут с помощью метода config виджета). На рис. 11.13 изображено окно после щелчка на кнопке Stop в некоторый момент.
Рис. 11.12. PyView без примечаний
Рис. 11.13. PyView после остановки показа слайдов
Кроме того, у каждой фотографии может быть свой файл «примечаний», который автоматически открывается вместе с изображением. С помощью этой функции можно записывать основные данные о фотографии. Нажмите кнопку Note, чтобы открыть дополнительный набор виджетов, с помощью которых можно просматривать и изменять файл примечаний, связанный с фотографией, просматриваемой в данный момент. Этот дополнительный набор виджетов должен показаться вам знакомым - это текстовый редактор PyEdit, представленный ранее в этой главе, прикрепленный к PyView в качестве средства просмотра и редактирования примечаний к фотографиям. На рис. 11.14 показано окно программы PyView вместе с прикрепленным к нему компонентом PyEdit для редактирования примечаний.
Встраивание PyEdit в PyView
В результате получается очень большое окно, которое обычно лучше просматривать развернутым на весь экран. Однако главное, на что нужно обратить внимание, - это правый нижний угол экрана над ползунком - там находится прикрепленный объект PyEdit, выполняющий тот же самый код, который был приведен выше. Так как редактор PyEdit
Рис. 11.14. PyView с примечаниями
реализован в виде класса, подобным образом его можно повторно использовать в любом графическом интерфейсе, где требуется обеспечить возможность редактирования текста.
При встраивании таким способом PyEdit оказывается вложенным фреймом, прикрепляемым к фрейму в интерфейсе программы слайд-шоу. При этом PyEdit создает меню на основе фрейма (он не владеет окном в целом), текстовое содержимое сохраняется и выбирается непосредственно вмещающей программой, а некоторые возможности автономного режима отсутствуют (например, отсутствуют меню FiLe и кнопка Quit). При этом вы получаете все остальные функции PyEdit, включая вырезание и копирование, поиск и поиск с заменой, поиск во внешних файлах, настройку цвета и шрифта, поддержку отмены и возврата операций редактирования и так далее. Доступна даже операция Clone, которая создает новое окно редактирования, хотя при этом меню создается на основе фрейма без операции Quit и без меню File, а при выходе не проверяется наличие несохраненных изменений, - все эти функции при желании можно связать с новым классом компонента PyEdit верхнего уровня.
Кроме того, если передать PyView третий аргумент командной строки, после имени каталога с изображениями, он будет интерпретироваться как индекс в списке классов PyEdit в соответствии с режимами верхнего уровня. Значению 0 аргумента соответствует режим главного окна, в этом случае редактор примечаний помещается под изображением, а его меню - в верхнюю часть окна (его фрейм при компоновке получает оставшееся место в окне, а не во фрейме PyView). При значении 1 редактор выводится в отдельном, независимом окне Toplevel (деактивируется при выключении показа примечаний). При значениях 2 и 3 PyEdit используется как встраиваемый компонент, прикрепляемый к фрейму PyView, с меню на основе фрейма (при значении 2 редактор включает все имеющиеся у него пункты меню, которые могут не подходить для данного случая его применения, а значение 3 обеспечивает ограниченный набор пунктов меню).
На рис. 11.15 изображен случай использования значения 0, когда PyEdit запускается в режиме главного окна. Здесь в окне в действительности создаются два независимых фрейма - фрейм PyView в верхней части и фрейм текстового редактора в нижней части. Недостаток этого режима перед режимом вложенного компонента или отдельного окна состоит в том, что PyEdit берет управление окном программы на
Рис. 11.15. PyView с PyEdit в другом режиме
себя (включая его заголовок и обработку события щелчка на кнопке закрытия), а его расположение в нижней части окна означает, что редактор может оказаться скрытым при просмотре изображений большого размера по высоте. Поэкспериментируйте с этой возможностью у себя, чтобы почувствовать особенности использования других разновидностей PyEdit, используя командную строку такого вида:
C:\...\PP4E\Gui\SlideShow> slideShowPlus.py ../gifs 0
Средство просмотра примечаний появляется только после щелчка на кнопке Note и удаляется после повторного щелчка на ней. Чтобы показать или скрыть фрейм просмотра примечаний, PyView пользуется методами pack и pack_forget виджетов, с которыми мы познакомились в конце главы 9. Окно автоматически расширяется, чтобы разместить средство просмотра примечаний, когда оно прикрепляется и отображается. Очень важно, что при переводе в видимое состояние редактор повторно прикрепляется с параметрами expand=YES и fill=BOTH, иначе в некоторых режимах он не будет растягиваться - фрейм PyEdit компонует себя в GuiMaker именно с этим параметрами, когда создается впервые, но метод pack_forget, похоже... действительно забывает51.
Файл примечаний можно также открыть во всплывающем окне PyEdit, но по умолчанию PyView встраивает редактор, чтобы сохранить прямую зрительную ассоциацию между изображением и примечанием и избежать проблем, которые могут возникнуть при независимом закрытии окна редактора. В данной реализации классы PyEdit приходится обертывать классом WrapEditor, чтобы перехватить операцию уничтожения фрейма PyEdit, когда он выполняется в отдельном всплывающем окне или в режиме полнофункционального компонента, - после уничтожения редактор будет недоступен, и его невозможно будет вновь прикрепить к графическому интерфейсу. Это не представляет проблемы при использовании редактора в режиме главного окна (операция Quit завершает программу) или в режиме минимального компонента (когда редактор не имеет операции Quit). Мы еще встретимся с приемом встраивания PyEdit внутрь другого графического интерфейса, когда будем рассматривать PyMailGUI в главе 14.
Предупреждение: в таком виде PyView поддерживает те же форматы представления графических изображений, что и объект PhotoImage библиотеки tkinter, поэтому по умолчанию он ищет файлы в формате GIF. Улучшить положение можно, установив расширение PIL для просмотра JPEG (и многих других форматов). Поскольку сегодня PIL является необязательным расширением, он не включен в данную версию PyView. Подробнее о расширении PIL и о графических форматах рассказывается в конце главы 8.
Исходный программный код PyView
Поскольку программа PyView разрабатывалась поэтапно, вам придется изучить объединение двух файлов и классов, чтобы понять, как она в действительности работает. В одном файле реализован класс, предоставляющий основные функции показа слайдов, а в другом реализован класс, расширяющий исходный и добавляющий новые функции поверх базового поведения. Начнем с класса расширения: пример 11.6 добавляет ряд функций в импортируемый базовый класс показа слайдов - редактирование примечаний, ползунок, определяющий задержку, метку для отображения имени файла и так далее. Это тот файл, который фактически запускает PyView.
Пример 11.6. PP4E\Gui\SlideShow\slideShowPlus.py
#############################################################################
PyView 1.2: программа показа слайдов с прилагаемыми к ним примечаниями.
Подкласс класса SlideShow, который добавляет отображение содержимого файлов с примечаниями в прикрепляемом объекте PyEdit, ползунок для установки интервала задержки между сменами изображений и метку с именем текущего отображаемого файла изображения;
Версия 1.2 работает под управлением Python 3.x и дополнительно использует улучшенный алгоритм повторного прикрепления компонента PyEdit, чтобы обеспечить его растягиваемость, перехватывает операцию закрытия примечания в подклассе, чтобы избежать появления исключения при закрытии PyEdit, использующегося в режиме всплывающего окна или полнофункционального компонента, и вызывает метод update() перед вставкой текста во вновь прикрепленный редактор примечаний, чтобы обеспечить правильное позиционирование в первой строке (смотрите описание этой проблемы в книге).
#############################################################################
import os
from tkinter import *
from PP4E.Gui.TextEditor.textEditor import *
from slideShow import SlideShow
#from slideShow_threads import SlideShow
Size = (300, 550) # 1.2: начальные размеры, (высота, ширина)
class SlideShowPlus(SlideShow):
def __init__(self, parent, picdir, editclass, msecs=2000, size=Size):
self.msecs = msecs self.editclass = editclass
SlideShow.__init__(self, parent, picdir, msecs, size)
def makeWidgets(self):
self.name = Label(self, text='None', bg=’red’, relief=RIDGE) self.name.pack(fill=X)
SlideShow.makeWidgets(self)
Button(self, text=’Note’, command=self.onNote).pack(fill=X) Button(self, text=’Help’, command=self.onHelp).pack(fill=X) s = Scale(label=’Speed: msec delay’, command=self.onScale, from_=0, to=3000, resolution=50, showvalue=YES, length=400, tickinterval=250, orient=’horizontal’) s.pack(side=BOTTOM, fill=X) s.set(self.msecs)
# 1.2: знать о закрытии редактора необходимо, если он используется
# в режиме всплывающего окна или полнофункционального компонента self.editorGone = False
class WrapEditor(self.editclass):# расширяет PyEdit для перехвата Quit def onQuit(editor): # editor - экземпляр PyEdit
self.editorGone = True # self - вмещающий экземпляр self.editorUp = False # класса слайд-шоу self.editclass.onQuit(editor) # предотвратить рекурсию
# прикрепить фрейм редактора к окну или к фрейму слайд-шоу
if issubclass(WrapEditor, TextEditorMain): # создать объект редактора self.editor = WrapEditor(self.master) # указать корень для меню else: # встраиваемый компонент
self.editor = WrapEditor(self) # или компонент всплывающего окна self.editor.pack_forget() # скрыть редактор при запуске
self.editorUp = self.image = None
def onStart(self):
SlideShow.onStart(self)
self.config(cursor=’watch’)
def onStop(self):
SlideShow.onStop(self)
self.config(cursor=’hand2’)
def onOpen(self):
SlideShow.onOpen(self) if self.image:
self.name.config(text=os.path.split(self.image[0])[1])
self.config(cursor=’crosshair’)
self.switchNote()
def quit(self): self.saveNote()
SlideShow.quit(self)
def drawNext(self):
SlideShow.drawNext(self) if self.image:
self.name.config(text=os.path.split(self.image[0])[1])
self.loadNote()
def onScale(self, value): self.msecs = int(value)
def onNote(self):
if self.editorGone: # 1.2: был уничтожен
return # не воссоздавать: видимо, он был нежелателен
if self.editorUp:
#self.saveNote() # если редактор уже открыт
self.editor.pack_forget() # сохранить текст?, скрыть редактор
self.editorUp = False else:
# 1.2: повторно прикрепить с параметрами, управляющими
# растягиванием, иначе виджет редактора не будет
# растягиваться
# 1.2: вызвать update после прикрепления и перед вставкой текста,
# иначе текстовый курсор будет изначально помещен во 2 строку
self.editor.pack(side=TOP, expand=YES, fill=BOTH) self.editorUp = True # или показать/прикрепить редактор self.update() # смотрите Pyedit: та же проблема с loadFirst
self.loadNote() # и загрузить текст примечания
def switchNote(self): if self.editorUp:
self.saveNote() # сохранить примечание к текущему изображению
self.loadNote() # загрузить примечание для нового изображения
def saveNote(self): if self.editorUp:
currfile = self.editor.getFileName() # или self.editor.onSave() currtext = self.editor.getAllText() # текст может отсутствовать if currfile and currtext: try:
open(currfile, ‘w’).write(currtext) except:
pass # неудача является нормальным явлением при # выполнении за пределами текущего каталога
def loadNote(self):
if self.image and self.editorUp:
root, ext = os.path.splitext(self.image[0]) notefile = root + ‘.note’ self.editor.setFileName(notefile) try:
self.editor.setAllText(open(notefile).read())
except:
self.editor.clearAllText() # примечание может отсутствовать
def onHelp(self):
showinfo(‘About PyView’,
‘PyView version 1.2\nMay, 2010\n(1.1 July, 1999)\n’
‘An image slide show\nProgramming Python 4E’)
if __name__ == ‘__main__’: import sys picdir = ‘../gifs’ if len(sys.argv) >= 2: picdir = sys.argv[1]
editstyle = TextEditorComponentMinimal if len(sys.argv) == 3: try:
editstyle = [TextEditorMain,
TextEditorMainPopup,
TextEditorComponent,
TextEditorComponentMinimal][int(sys.argv[2])]
except: pass root = Tk()
root.title(‘PyView 1.2 - plus text notes’)
Label(root, text=”Slide show subclass”).pack()
SlideShowPlus(parent=root, picdir=picdir, editclass=editstyle) root.mainloop()
Базовая функциональность, расширяемая классом SlideShowPlus, приводится в примере 11.7. Этот пример представляет первоначальную реализацию показа слайдов - он открывает файлы изображений, отображает их и организует показ слайдов в цикле. Его можно запустить как самостоятельный сценарий, но при этом вы не получите дополнительных функций, таких как примечания и ползунки, добавляемые подклассом SlideShowPlus.
Пример 11.7. PP4E\Gui\SlideShow\slideShow.py
######################################################################
SlideShow: простая реализация показа слайдов на Python/tkinter; базовый набор функций, реализованных здесь, можно расширять в подклассах; ######################################################################
from tkinter import *
from glob import glob
from tkinter.messagebox import askyesno
from tkinter.filedialog import askopenfilename
import random
Size = (450, 450) # начальная высота и ширина холста
imageTypes = [(‘Gif files’, ‘.gif’), # для диалога открытия файла (‘Ppm files’, ‘.ppm’), # плюс jpg с исправлениями Tk,
(‘Pgm files’, ‘.pgm’), # плюс растровые с помощью BitmapImage (‘All files’, ‘*’)]
class SlideShow(Frame):
def __init__(self, parent=None, picdir=’.’, msecs=3000, size=Size,**args)
Frame.__init__(self, parent, **args) self.size = size self.makeWidgets() self.pack(expand=YES, fill=BOTH) self.opens = picdir files = []
for label, ext in imageTypes[:-1]:
files = files + glob(‘%s/*%s’ % (picdir, ext)) self.images = [(x, PhotoImage(file=x)) for x in files] self.msecs = msecs self.beep = True self.drawn = None
def makeWidgets(self):
height, width = self.size
self.canvas = Canvas(self, bg=’white’, height=height, width=width) self.canvas.pack(side=LEFT, fill=BOTH, expand=YES) self.onoff = Button(self, text=’Start’, command=self.onStart) self.onoff.pack(fill=X)
Button(self, text=’Open’, command=self.onOpen).pack(fill=X) Button(self, text=’Beep’, command=self.onBeep).pack(fill=X) Button(self, text=’Quit’, command=self.onQuit).pack(fill=X)
def onStart(self): self.loop = True
self.onoff.config(text=’Stop’, command=self.onStop) self.canvas.config(height=self.size[0], width=self.size[1]) self.onTimer()
def onStop(self): self.loop = False
self.onoff.config(text=’Start’, command=self.onStart)
def onOpen(self): self.onStop()
name = askopenfilename(initialdir=self.opens, filetypes=imageTypes) if name:
if self.drawn: self.canvas.delete(self.drawn) img = PhotoImage(file=name)
self.canvas.config(height=img.height(), width=img.width()) self.drawn = self.canvas.create_image(2, 2, image=img, anchor=NW) self.image = name, img
def onQuit(self): self.onStop() self.update()
if askyesno(‘PyView’, ‘Really quit now?’): self.quit()
def onBeep(self):
self.beep = not self.beep # toggle, or use ^ 1
def onTimer(self): if self.loop:
self.drawNext()
self.after(self.msecs, self.onTimer) def drawNext(self):
if self.drawn: self.canvas.delete(self.drawn) name, img = random.choice(self.images)
self.drawn = self.canvas.create_image(2, 2, image=img, anchor=NW) self.image = name, img if self.beep: self.bell() self.canvas.update()
if __name__ == ‘__main__’: import sys
if len(sys.argv) == 2: picdir = sys.argv[1] else:
picdir = ‘../gifs’ root = Tk()
root.title(‘PyView 1.2’) root.iconname(‘PyView’)
Label(root, text=”Python Slide Show Viewer”).pack()
SlideShow(root, picdir=picdir, bd=3, relief=SUNKEN) root.mainloop()
Чтобы вы могли получить более полное представление о том, что реализует этот базовый класс, на рис. 11.16 показано, как выглядит графический интерфейс, создаваемый этим примером, если запустить его в виде самостоятельного сценария. Здесь изображены два экземпляра, создаваемые сценарием slideShow_frames, который можно найти в дереве примеров и основная реализация которого приводится ниже:
root = Tk()
Label(root, text=”Two embedded slide shows: Frames”).pack()
SlideShow(parent=root, picdir=picdir, bd=3, relief=SUNKEN).pack(side=LEFT) SlideShow(parent=root, picdir=picdir, bd=3, relief=SUNKEN).pack(side=RIGHT) root.mainloop()
Простой сценарий slideShow_frames прикрепляет два экземпляра SlideShow к одному окну. Это возможно благодаря тому, что информация о состоянии сохраняется не в глобальных переменных, а в переменных экземпляра класса. Сценарий slideShow_toplevels (также можно найти в дереве примеров) прикрепляет два экземпляра SlideShow к двум всплывающим окнам верхнего уровня. В обоих случаях показ слайдов происходит независимо, но управляется событиями after, генерируемыми одним и тем же циклом событий в одном процессе.
Рис. 11.16. Два прикрепленных объекта SlideShow
PyDraw: рисование и перемещение графики
В главе 9 мы познакомились с простыми приемами воспроизведения анимации с помощью инструментов из библиотеки tkinter (смотрите версии canvasDraw в обзоре tkinter). Представленная здесь программа PyDraw, основываясь на тех же идеях, реализует на языке Python более богатые функциональные возможности. В ней появились новые режимы рисования мышью, возможность заливки объектов и фона, встраивание фотографий и многое другое. Кроме того, в ней реализованы приемы перемещения объектов и анимации - нарисованные объекты можно перемещать по холсту, щелкая на них и перетаскивая мышью, и любой нарисованный объект можно плавно переместить через экран в место, указанное щелчком мыши.
Запуск PyDraw
PyDraw, по сути, представляет собой холст tkinter с многочисленными привязками событий от клавиатуры и мыши, которые дают возможность пользователю осуществлять стандартные операции рисования. Эту программу нельзя назвать графическим редактором профессионального уровня, но поразвлечься с ней можно. На самом деле - даже нужно, поскольку книга не позволяет передать такие вещи, как движущийся объект. Запустите PyDraw из какой-нибудь панели запуска программ (или непосредственно файл movingpics.py из примера 11.8). Нажмите клавишу ? и посмотрите подсказку по всем имеющимся командам (или прочтите строку helpstr в листинге).
На рис. 11.17 изображено окно PyDraw после того как на холсте было нарисовано несколько объектов. Чтобы переместить какой-либо из объектов, щелкните на нем средней кнопкой мыши и перетащите указателем мыши, либо щелкните средней кнопкой на объекте, а затем правой кнопкой в том месте, куда требуется его переместить. В последнем случае PyDraw воспроизводит анимационный эффект, постепенно перемещая объект в указанное место. Попробуйте сделать это с картинкой, находящейся вверху, и вы увидите, как она плавно перемещается по экрану.
Рис. 11.17. Окно программы PyDraw с нарисованными объектами, готовыми к перемещению
Для вставки фотографий нажмите клавишу p, для рисования фигур используйте левую кнопку мыши. (Пользователям Windows: щелчок средней кнопкой обычно равносилен нажатию двух кнопок одновременно или повороту колесика, но для этого может потребоваться выполнить настройки в Панели Управления.) Помимо событий от мыши можно пользоваться еще 17 командами клавиш для редактирования рисунков, о которых я не буду рассказывать здесь. Требуется некоторое время, чтобы освоиться со всеми командами клавиатуры и мыши, после чего вы тоже сможете создавать бессмысленные электронные рисованные объекты, такие как приведены на рис. 11.18.
Рис. 11.18. Окно программы PyDraw после экспериментов с ней
Исходный программный код PyDraw
Как и PyEdit, программа PyDraw размещается в одном файле. За главным модулем, представленным в примере 11.8, приводятся два расширения, изменяющие реализацию перемещения.
Пример 11.8. PP4E\Gui\MovingPics\movingpics.py
"""
##############################################################################
PyDraw 1.1: простая программа рисования на холсте и перемещения объектов с воспроизведением анимационного эффекта.
В реализации перемещения объектов используются циклы time.sleep, поэтому в каждый момент времени может перемещаться только один объект; перемещение выполняется плавно и быстро, однако далее приведены подклассы, реализующие другие режимы перемещения на основе метода widget.after и потоков выполнения. Версия 1.1 была дополнена возможностью выполнения под управлением Python 3.X (версия 2.X не поддерживается)
##############################################################################
"""
helpstr = """--PyDraw версия 1.1--
Операции, выполняемые мышью:
Левая = Начальная точка рисования
Левая+Перемещение = Рисовать новый объект Двойной щелчок левой = Удалить все объекты Правая = Переместить текущий объект
Средняя = Выбрать ближайший объект
Средняя+Перемещение = Перетащить текущий объект Keyboard commands:
w=Выбрать ширину рамки c=Выбрать цвет
u= Выбрать шаг перемещения s=Выбрать задержку при перемещении
o=Рисовать овалы т=Рисовать прямоугольники
ПРисовать линии a=Рисовать дуги
d=Удалить объект 1=Поднять объект
2=Опустить объект f=Выполнить заливку объекта
b=Выполнить заливку фона p=Добавить фотографию
z=Сохранить в формате Postscript x=Выбрать режим рисования ?=Справка другие=стереть текст
"""
import time, sys from tkinter import * from tkinter.filedialog import * from tkinter.messagebox import *
PicDir = ‘../gifs’
if sys.platform[:3] == ‘win’:
HelpFont = (‘courier’, 9, ‘normal’) else:
HelpFont = (‘courier’, 12, ‘normal’)
pickDelays = [0.01, 0.025, 0.05, 0.10, 0.25, 0.0, 0.001, 0.005] pickUnits = [1, 2, 4, 6, 8, 10, 12] pickWidths = [1, 2, 5, 10, 20]
pickFills = [None,’white’,’blue’,’red’,’black’,’yellow’,’green’,’purple’] pickPens = [‘elastic’, ‘scribble’, ‘trails’]
class MovingPics:
def __init__(self, parent=None):
canvas = Canvas(parent, width=500, height=500, bg= ‘white’) canvas.pack(expand=YES, fill=BOTH) canvas.bind(‘
self.where = None self.scribbleMode = 0
parent.title(‘PyDraw - Moving Pictures 1.1’) parent.protocol(‘WM_DEbETE_WINDOW’, self.onQuit) self.realquit = parent.quit self.textInfo = self.canvas.create_text(
5, 5, anchor=NW, font=HelpFont, text=’Press ? for help’)
def onStart(self, event): self.where = event self.object = None
def onGrow(self, event): canvas = event.widget
if self.object and pickPens[0] == ‘elastic’: canvas.delete(self.object) self.object = self.createMethod(canvas,
self.where.x, self.where.y, # начало event.x, event.y, # конец
fill=pickFills[0], width=pickWidths[0]) if pickPens[0] == ‘scribble’:
self.where = event # нач. координаты для следующей итерации def onClear(self, event):
if self.moving: return # если идет перемещение event.widget.delete(‘all’) # использовать тег all self.images = []
self.textInfo = self.canvas.create_text(
5, 5, anchor=NW, font=HelpFont, text=’Press ? for help’)
def plotMoves(self, event):
diffX = event.x - self.where.x # план анимированного перемещения diffY = event.y - self.where.y # по горизонтали, затем по вертикали reptX = abs(diffX) // pickUnits[0] # приращение на шаге, число шагов reptY = abs(diffY) // pickUnits[0] # от предыдущего до текущего щелчка incrX = pickUnits[0] * ((diffX > 0) or -1) # 3.x требуется деление // incrY = pickUnits[0] * ((diffY > 0) or -1) # с усечением return incrX, reptX, incrY, reptY
def onMove(self, event):
traceEvent(‘onMove’, event, 0) # переместить объект в точку щелчка
object = self.object # игнорировать некоторые
if object and object not in self.moving: # операции при движении msecs = int(pickDelays[0] * 1000)
parms = ‘Delay=%d msec, Units=%d’ % (msecs, pickUnits[0])
self.setTextInfo(parms)
self.moving.append(object)
canvas = event.widget
incrX, reptX, incrY, reptY = self.plotMoves(event) for i in range(reptX):
canvas.move(object, incrX, 0) canvas.update() time.sleep(pickC'0lays[Q]) for i in range(reptY):
canvas.move(object, Q, incrY)
canvas.update() # update выполнит другие операции
time.sleep(pickDelays[0]) # приостановить до следующего шага self.moving.remove(object) if self.object == object: self.where = event
def onSelect(self, event): self.where = event
self.object = self.canvas.find_closest(event.x, event.y)[0] # кортеж
def onDrag(self, event):
diffX = event.x - self.where.x # OK, если объект перемещается
diffY = event.y - self.where.y # переместить в новом направлении
self.canvas.move(self.object, diffX, diffY) self.where = event
def onOptions(self, event): keymap = {
‘w’: lambda self: self.changeOption(pickWidths, ‘Pen Width’),
‘c’: lambda self: self.changeOption(pickFills, ‘Color’),
‘u’: lambda self: self.changeOption(pickUnits, ‘Move Unit’),
‘s’: lambda self: self.changeOption(pickDelays, ‘Move Delay’),
‘x’: lambda self: self.changeOption(pickPens, ‘Pen Mode’),
‘o’: lambda self: self.changeDraw(Canvas.create_oval, ‘Oval’), ‘r’: lambda self: self.changeDraw(Canvas.create_rectangle, ‘Rect’), ‘l’: lambda self: self.changeDraw(Canvas.create_line, ‘Line’),
‘a’: lambda self: self.changeDraw(Canvas.create_arc, ‘Arc’),
‘d’: MovingPics.deleteObject,
‘1’: MovingPics.raiseObject,
‘2’: MovingPics.lowerObject, # если только 1 схема вызова ‘f’: MovingPics.fillObject, # использовать несвязанные методы ‘b’: MovingPics.fillBackground, # иначе передавать self в lambda ‘p’: MovingPics.addPhotoItem,
‘z’: MovingPics.savePostscript,
‘?’: MovingPics.help} try:
keymap[event.char](self) except KeyError:
self.setTextInfo(‘Press ? for help’)
def changeDraw(self, method, name):
self.createMethod = method # несвязанный метод объекта Canvas self.setTextInfo(‘Draw Object=’ + name)
def changeOption(self, list, name): list.append(list[0]) del list[0]
self.setTextInfo(‘%s=%s’ % (name, list[0])) def deleteObject(self):
if self.object != self.textInfo: # ok если объект перемещается
self.canvas.delete(self.object) # стереть, но движение продолжится self.object = None
def raiseObject(self):
if self.object: # ok если объект перемещается
self.canvas.tkraise(self.object) # поднять в процессе перемещения
def lowerObject(self): if self.object:
self.canvas.lower(self.object)
def fillObject(self): if self.object:
type = self.canvas.type(self.object) if type == ‘image’: pass
elif type == ‘text’:
self.canvas.itemconfig(self.object, fill=pickFills[0]) else:
self.canvas.itemconfig(self.object,
fill=pickFills[0], width=pickWidths[0])
def fillBackground(self):
self.canvas.config(bg=pickFills[0])
def addPhotoItem(self):
if not self.where: return
filetypes=[(‘Gif files’, ‘.gif’), (‘All files’, ‘*’)]
file = askopenfilename(initialdir=PicDir, filetypes=filetypes)
if file:
image = PhotoImage(file=file) # загрузить изображение
self.images.append(image) # сохранить ссылку
self.object = self.canvas.create_image( # на холст,
self.where.x, self.where.y, # в точку
image=image, anchor=NW) # посл. щелчка
def savePostscript(self):
file = asksaveasfilename() if file:
self.canvas.postscript(file=file) # сохранить холст в файл
def help(self):
self.setTextInfo(helpstr)
#showinfo(‘PyDraw’, helpstr)
def setTextInfo(self, text):
self.canvas.dchars(self.textInfo, 0, END) self.canvas.insert(self.textInfo, 0, text) self.canvas.tkraise(self.textInfo)
def onQuit(self): if self.moving:
self.setTextInfo(“Can’t quit while move in progress”) else:
self.realquit() # стандартная операция закрытия окна: сообщит # об ошибке, если выполняется перемещение
def traceEvent(label, event, fullTrace=True): print(label) if fullTrace:
for atrr in dir(event): if attr[:2] ! = ‘__’:
print(attr, ‘=>’, getattr(event, attr))
if __name__ == ‘__main__’:
from sys import argv # когда выполняется как сценарий,
if len(argv) == 2: PicDir = argv[1] # ‘..’ не действует при запуске из
# другого каталога
root = Tk() # создать и запустить объект
MovingPics(root) # MovingPics
root.mainloop()
Так как одновременно перемещаться может только один объект, запуск процедуры перемещения объекта в тот момент, когда другой уже находится в движении, приводит к приостановке перемещения первого объекта, пока не будет закончено перемещение нового. Так же как в примерах canvasDraw из главы 9, можно добавить поддержку одновременного перемещения более чем одного объекта с помощью событий планируемых обратных вызовов after или потоков выполнения.
В примере 11.9 приводится подкласс MovingPics, в котором проведены изменения, необходимые для обеспечения параллельного перемещения с помощью событий after. Он позволяет одновременно и независимо друг от друга перемещать любое количество объектов на холсте, включая картинки. Запустите этот файл непосредственно, и вы увидите разницу - я мог бы попытаться сделать снимок с экрана в момент, когда одновременно перемещаются несколько объектов, но из этого вряд ли бы что-то вышло.
Пример 11.9. PP4E\Gui\MovingPies\movingpies_after.py
PyDraw-after: простая программа рисования на холсте и перемещения объектов с воспроизведением анимационного эффекта.
Для реализации перемещения объектов используются циклы на основе метода widget. after, благодаря чему оказалось возможным организовать одновременное перемещение нескольких объектов без применения потоков выполнения; движение осуществляется параллельно, но медленнее, чем в версии с использованием time.sleep; смотрите также пример canvasDraw в обзоре: он конструирует и передает сразу весь список incX/incY: здесь могло бы быть allmoves = ([(incrX, 0)] * reptX) + ([(0, incrY)] * reptY)
from movingpics import *
class MovingPicsAfter(MovingPics):
def doMoves(self, delay, objectId, incrX, reptX, incrY, reptY): if reptX:
self.canvas.move(objectId, incrX, 0) reptX -= 1 else:
self.canvas.move(objectId, 0, incrY) reptY -= 1
if not (reptX or reptY):
self.moving.remove(objectId)
else:
self.canvas.after(delay,
self.doMoves, delay, objectId, incrX, reptX, incrY, reptY)
def onMove(self, event):
traceEvent(‘onMove’, event, 0)
object = self.object # переместить текущий объект в точку щелчка
if object:
msecs = int(pickDelays[0] * 1000)
parms = ‘Delay=%d msec, Units=%d’ % (msecs, pickUnits[0])
self.setTextInfo(parms)
self.moving.append(object)
incrX, reptX, incrY, reptY = self.plotMoves(event) self.doMoves(msecs, object, incrX, reptX, incrY, reptY) self.where = event
if __name__ == ‘__main__’:
from sys import argv # когда выполняется как сценарий
if len(argv) == 2:
import movingpics # глобальная перем. не из этого модуля
movingpics.PicDir = argv[1] # а from* не связывает имена root = Tk()
MovingPicsAfter(root) root.mainloop()
Чтобы оценить работу этого примера, распахните окно сценария на весь экран и создайте несколько объектов на его холсте, нажимая клавишу p после предварительного щелчка, чтобы вставить картинки, нарисуйте несколько фигур и так далее. Теперь, когда уже выполняется одно или несколько перемещений, можно запустить перемещение еще одного объекта, щелкнув на нем средней кнопкой и затем правой кнопкой в том месте, куда требуется его переместить. Перемещение начинается немедленно, даже если на холсте присутствуют другие движущиеся объекты. Запланированные события after всех объектов помещаются в одну и ту же очередь цикла событий и передаются библиотекой tkinter после срабатывания таймера настолько быстро, насколько возможно.
Если запустить этот модуль подкласса непосредственно, то можно заметить, что перемещение не такое плавное и быстрое, как первоначально (в зависимости от быстродействия вашего компьютера и наличия дополнительных программных уровней под Python), зато одновременно может выполняться несколько перемещений.
В примере 11.10 демонстрируется, как обеспечить параллельное перемещение нескольких объектов с помощью потоков. Этот прием действует, но, как отмечалось в главах 9 и 10, обновление графического интерфейса в дочерних потоках выполнения является, вообще говоря, опасным делом. На моей машине перемещение в этом сценарии с потоками происходит не так плавно, как в первоначальной версии, что отражает накладные расходы, связанные с переключением интерпретатора (и ЦП) между несколькими потоками, но, опять же, во многом это зависит от быстродействия компьютера.
Пример 11.10. PP4E\Gui\MovingPies\movingpies_threads.py
PyDraw-threads: использует потоки для перемещения объектов; прекрасно работает в Windows, если не вызывать метод canvas.update() в потоках (иначе сценарий будет завершаться с фатальными ошибками, некоторые объекты будут начинать движение сразу после того как будут нарисованы, и так далее); имеется как минимум несколько методов холста, которые могут вызываться из потоков выполнения; движение осуществляется менее плавно, чем с применением time.sleep, и данная реализация более опасна в целом: внутри потоков лучше ограничиться изменением глобальных переменных и никак не касаться графического интерфейса;
import _thread as thread, time, sys, random from tkinter import Tk, mainloop
from movingpics import MovingPics, pickUnits, pickDelays
class MovingPicsThreaded(MovingPics):
def __init__(self, parent=None):
MovingPics.__init__(self, parent) self.mutex = thread.allocate_lock() import sys
#sys.setcheckinterval(0) # переключение контекста после каждой
# операции виртуальной машины: не поможет
def onMove(self, event): object = self.object if object and object not in self.moving: msecs = int(pickDelays[0] * 1000)
parms = ‘Delay=%d msec, Units=%d’ % (msecs, pickUnits[0]) self.setTextInfo(parms)
#self.mutex.acquire()
self.moving.append(object)
#self.mutex.release()
thread.start_new_thread(self.doMove, (object, event))
def doMove(self, object, event): canvas = event.widget
incrX, reptX, incrY, reptY = self.plotMoves(event) for i in range(reptX):
canvas.move(object, incrX, 0)
# canvas.update()
time.sleep(pickDelays[0]) # может измениться for i in range(reptY):
canvas.move(object, 0, incrY)
# canvas.update() # update выполняет другие операции
time.sleep(pickDelays[0]) # приостановиться до следующего шага
#self.mutex.acquire()
self.moving.remove(object)
if self.object == object: self.where = event
#self.mutex.release()
if __name__ == ‘__main__’: root = Tk()
MovingPicsThreaded(root)
mainloop()
PyClock: виджет аналоговых/цифровых часов
Изучая новый интерфейс компьютера, я всегда вначале отыскиваю часы. Я столько времени неотрывно нахожусь за компьютером, что у меня совершенно не получается следить за временем, если оно не отображается прямо передо мной на экране (и даже тогда это проблематично). Следующая программа, PyClock, реализует такой виджет часов на языке Python. Своим внешним видом она не очень отличается от тех часов, которые вы привыкли видеть в системе X Window. Но так как она написана на языке Python, ее легко перенастраивать и переносить между Windows, X Window и Mac, как и все программы из этой главы. В дополнение к развитым технологиям конструирования графических интерфейсов, этот пример демонстрирует использование модулей Python math и time.
Краткий урок геометрии
Прежде чем продемонстрировать вам PyClock, немного предыстории и признаний. Ну-ка, ответьте: как поставить точки на окружности? Эта задача, а также форматы времени и возникающие события оказываются основными при создании графических элементов часов. Чтобы нарисовать циферблат аналоговых часов на холсте, необходимо уметь рисовать круг - сам циферблат состоит из точек окружности, а секундная, минутная и часовая стрелки представляют собой линии, проведенные из центра в точки на окружности. Цифровые часы нарисовать проще, но смотреть на них неинтересно.
Теперь признание: начав писать PyClock, я не знал ответа на первый вопрос предыдущего абзаца. Я совершенно забыл формулу нахождения координат точек окружности (как и большинство профессиональных программистов, к которым я с этим обращался). Бывает. Такие знания, не будучи востребованными в течение нескольких десятилетий, могут быть утилизированы сборщиком мусора. В конце концов мне удалось смахнуть пыль с нескольких нейронов, длины которых оказалось достаточно, чтобы запрограммировать действия, необходимые для построения, но блеснуть умом мне не удалось.52
Если вы в таком же положении, то я покажу вам один способ простой записи формул построения точек на языке Python, хотя для подробных занятий геометрией места здесь нет. Прежде чем взяться за более сложную задачу реализации часов, я написал сценарий plotterGui, представленный в примере 11.11, чтобы сосредоточиться только на логике построения круга.
Логика построения круга реализуется в функции point - она находит координаты (X,Y) точки окружности по относительному номеру точки, общему количеству точек, помещаемых на окружности, и радиусу окружности (расстоянию между центром окружности и ее точками). Сначала вычисляется угол между нужной точкой и верхней точкой окружности путем деления 360 на количество рисуемых точек и умножения на номер точки. Напомню, что полный круг составляет 360 градусов (например, если на окружности рисуется 4 точки, то каждая отстоит от предыдущей на 90 градусов, или на 360/4). Стандартный модуль Python math предоставляет все необходимые константы и функции - pi, sine и cosine. В действительности математика тут не такая уж непонятная, если вы потратите некоторое время, чтобы ее рассмотреть (возможно, еще взяв старый учебник геометрии). Существуют альтернативные способы реализации нужных математических расчетов, но я не буду углубляться здесь в детали (ищите подсказки в пакете с примерами).
Даже если вы не хотите разбираться с математикой, просмотрите функцию circle в примере 11.11. По указанным координатам (X,Y) точки окружности, возвращаемым функцией point, она чертит линию из центра окружности в точку и маленький прямоугольник вокруг самой точки, что несколько напоминает стрелки и отметки аналоговых часов. Чтобы удалять нарисованные объекты перед каждым построением, используются теги холста.
Пример 11.11. PP4E\Gui\Cloek\plotterGui.py
# рисует окружности на холсте
import math, sys from tkinter import *
def point(tick, range, radius): angle = tick * (360.0 / range) radiansPerDegree = math.pi / 180
pointX = int( round( radius * math.sin(angle * radiansPerDegree) )) pointY = int( round( radius * math.cos(angle * radiansPerDegree) )) return (pointX, pointY)
def circle(points, radius, centerX, centerY, slow=0): canvas.delete(‘lines’) canvas.delete(‘points’) for i in range(points):
x, y = point(i+1, points, radius-4) scaledX, scaledY = (x + centerX), (centerY - y) canvas.create_line(centerX, centerY, scaledX, scaledY, tag=’lines’) canvas.create_rectangle(scaledX-2, scaledY-2, scaledX+2, scaledY+2, fill=’red’, tag=’points’)
if slow: canvas.update()
def plotter(): # в 3.x // - деление с усечением
circle(scaleVar.get(), (Width // 2), originX, originY, checkVar.get())
def makewidgets():
global canvas, scaleVar, checkVar
canvas = Canvas(width=Width, height=Width)
canvas.pack(side=TOP)
scaleVar = IntVar()
checkVar = IntVar()
scale = Scale(label=’Points on circle’, variable=scaleVar, from_=1, to=360)
scale.pack(side=LEFT)
Checkbutton(text=’Slow mode’, variable=checkVar).pack(side=LEFT) Button(text=’Plot’, command=plotter).pack(side=LEFT, padx=50)
if __name__ == ‘__main__’:
Width = 500 # ширина, высота по умолчанию
if len(sys.argv) == 2: Width = int(sys.argv[1]) # ширина в команд. строке?
originX = originY = Width // 2 # то же, что и радиус
makewidgets() # в корневом окне Tk по умолчанию
mainloop() # в 3.x требуется // - деление с усечением
По умолчанию ширина круга составит 500 пикселей, если не определить иначе в командной строке. Получив число точек на окружности, этот сценарий размечает окружность по часовой стрелке при каждом нажатии кнопки PLot, вычерчивая прямые из центра к маленьким прямоугольникам на окружности. Переместите ползунок, чтобы задать другое число точек, и щелкните на флажке, чтобы рисование происходило достаточно медленно и можно было заметить очередность вычерчивания линий и точек (при этом сценарий вызывает update для обновления экрана после вычерчивания каждой линии). На рис. 11.19 приводится результат нанесения 120 точек при установке в командной строке ширины круга равной 400; если задать на окружности 60 или 12 точек, сходство с часовым циферблатом станет более заметным.
Рис. 11.19. Сценарий plotterGui в действии
Дополнительную помощь могут оказать ориентированные на текстовый, а не на графический вывод версии этого сценария, имеющиеся в дереве примеров, которые выводят координаты точек окружности в поток stdout, а не отображают эти точки в графическом интерфейсе. Смотрите сценарииplotterText.py в каталоге часов. Ниже показано, что он выводит для случаев 4 и 12 точек на окружности шириной 400 точек. Формат вывода прост:
номер_точки: угол = (координатаX, координатаY)
Предполагается, что центр круга имеет координаты (0,0):
1 : 90.0 = (200, 0)
2 : 180.0 = (0, -200)
3 : 270.0 = (-200, 0)
4 : 360.0 = (0, 200)
1 : 30.0 = (100, 173)
2 : 60.0 = (173, 100)
3 : 90.0 = (200, 0)
4 : 120.0 = (173, -100)
5 : 150.0 = (100, -173)
6 : 180.0 = (0, -200)
7 : 210.0 = (-100, -173)
8 : 240.0 = (-173, -100)
9 : 270.0 = (-200, 0)
10 : 300.0 = (-173, 100)
11 : 330.0 = (-100, 173)
12 : 360.0 = (0, 200)
Инструменты Python для обработки чисел
Если вы сильны в математических расчетах настолько, чтобы разобраться в этом кратком уроке геометрии, вам, возможно, покажется интересным расширение NumPy для Python, предназначенное для поддержки численного программирования. В нем вы найдете такие объекты, как векторы, а также реализацию сложных математических операций, что превращает Python в инструмент решения научных задач, который эффективно реализует матричные операции и который сравним с MatLab. Расширение NumPy с успехом используется многими организациями, включая Ливерморскую и Лос-Аламосскую национальные лаборатории, - во многих случаях применение расширения NumPy позволяет писать на Python новые программы взамен устаревших программ на языке FORTRAN.
Расширение NumPy необходимо получать и устанавливать отдельно - смотрите ссылки на веб-сайте Python. В Интернете можно также найти родственные инструменты числовой обработки (например, SciPy), а также инструменты визуализации и трехмерной анимации (например, PyOpenGL, Blender, Maya, vtk и VPython). К моменту написания этих строк расширение NumPy (подобно многим числовым инструментам, опирающимся на его использование) официально доступно только для Python 2.X, однако версия, поддерживающая обе версии, 2.X и 3.X, уже находится в разработке53. Помимо модуля math, в языке Python имеется встроенная поддержка комплексных чисел для инженерных расчетов, в версии 2.4 появился тип десятичных чисел с фиксированной точностью, а в версии 2.6 и 3.0 была добавлена поддержка рациональных дробей. Подробности ищите в руководстве по стандартной библиотеке и в книгах, описывающих основы языка Python, таких как «Изучаем Python».
Чтобы понять, как эти точки отображаются на холст, нужно учесть, что ширина и высота окружности одинаковы и равны величине радиуса, умноженной на 2. Поскольку координаты холста tkinter (X,Y) начинаются с (0,0) в левом верхнем углу, центр окружности смещается в точку с координатами (ширина/2, ширина/2) - это будет точка, из которой вычерчиваются прямые. Например, в круге 400 на 400 центр холста будет в точке (200,200)54. Прямая в точку с углом 90 градусов (точка на правой стороне окружности) соединяет точку (200,200) и точку (400,200) - результат добавления к координатам центра координат точки (200,0), полученной для данного радиуса и угла. Линия вниз, к точке под углом 180 градусов, соединяет точки (200,200) и (200,400) после учета55 координат вычисленной точки (0,-200).
Этот алгоритм построения точек, применяемый в plotterGui, а также несколько констант масштабирования лежат в основе отображения циферблата аналоговых часов в PyClock. Если вам все же кажется, что это слишком сложно, предлагаю сначала сосредоточиться на реализации отображения цифровых часов. Аналоговые геометрические построения в действительности лишь являются расширением механизма отсчета времени, использующегося в обоих режимах отображения. В действительности в основе самих часов находится общий объект Frame, одинаковым образом посылающий встроенным объектам цифровых и аналоговых часов события изменения времени и размеров. Аналоговые часы - это прикрепленный виджет Canvas, умеющий рисовать окружности, а цифровые часы - просто прикрепленный фрейм Frame с метками, отображающими время.
Запуск PyClock
За исключением части, касающейся построения окружностей, программный код PyClock выглядит достаточно просто. Он рисует циферблат для отображения текущего времени и с помощью методов after вызывает себя 10 раз в секунду, проверяя, не перевалило ли системное время на следующую секунду. Если да, то перерисовываются секундная, минутная и часовая стрелки, чтобы показать новое время (либо изменяется текст меток цифровых часов). На языке создания графических интерфейсов это означает, что аналоговое изображение выводится на холсте, перерисовывается при изменении размеров окна и изменяется по запросу на цифровой формат.
В PyClock используется также стандартный модуль Python time, с помощью которого сценарий получает и преобразует системную информацию о времени в представление, необходимое для часов. В двух словах, метод onTimer получает системное время вызовом функции time.time, встроенного средства, возвращающего число с плавающей точкой, выражающее количество секунд, прошедших с начала эпохи, - точки начала отсчета времени на вашем компьютере. Затем с помощью функции time.localtime это время преобразуется в кортеж, содержащий значения часов, минут и секунд. Дополнительные подробности можно найти в самом сценарии и руководстве по библиотеке Python.
Проверка системного времени 10 раз в секунду может показаться излишней, но она гарантирует перемещение секундной стрелки вовремя и без рывков и скачков (события after синхронизируются не очень точно). На компьютерах, которыми я пользуюсь, это не влечет существенного потребления мощности ЦП. В Linux и в Windows PyClock незначительно расходует ресурсы процессора - в основном при обновлении экрана в аналоговом режиме, но не в событиях after.56
Чтобы минимизировать обновления экрана, PyClock перерисовывает только стрелки часов при переходе к следующей секунде - риски на циферблате перерисовываются только при начальном запуске и изменении размеров окна. На рис. 11.20 показан начальный циферблат PyClock в формате по умолчанию, который выводится при непосредственном запуске файла eloek.py.
Рис. 11.20. Аналоговые часы PyCloek по умолчанию
Линии, представляющие стрелки часов, имеют стрелку на одном конце, что определяется параметрами arrow и arrowshape объекта линии. Параметр arrow может принимать значение first, last, none или both; параметр arrowshape определяется как кортеж чисел, задающих длину стрелки на конце линии, общую длину линии и ее толщину.
Как и PyView, PyClock динамически удаляет и перерисовывает части изображения по требованию (то есть в ответ на связанные события) с помощью методов pack_forget и pack. Щелчок левой кнопкой мыши на часах изменяет формат вывода на цифровой путем удаления виджета аналоговых часов и вывода цифрового интерфейса. В результате получается более простой интерфейс, изображенный на рис. 11.21.
Рис. 11.21. Цифровые часы PyCloek
Такая цифровая форма может пригодиться, если вы хотите сберечь драгоценное место на экране и уменьшить использование ЦП (расходы на обновление изображения этих часов очень малы). Следующий щелчок левой кнопкой на часах снова переводит их в аналоговый режим отображения. При запуске сценария конструируются оба отображения -аналоговое и цифровое, но в каждый отдельный момент прикреплено только одно из них.
Щелчок правой кнопкой мыши на часах в любом режиме отображения вызывает появление или исчезновение прикрепленной метки, показывающей текущую дату в простом текстовом формате. На рис. 11.22 показан аналоговый интерфейс PyClock с меткой даты и размещенной в центре фотографией (в таком виде часы запускаются из панели запуска PyLauncher).
Рис. 11.22. Улучшенный графический интерфейс PyCloek с изображением
Изображение в центре на рис. 11.22 добавлено путем передачи объекта с соответствующими настройками конструктору объекта PyClock. Почти все особенности этого изображения могут быть настроены через атрибуты объектов PyClock - цвет стрелок, цвет меток, центральное изображение и начальный размер.
Так как сценарий PyClock в аналоговом режиме сам отображает фигуры на холсте, ему необходимо также самостоятельно обрабатывать события изменения размеров окна: когда окно уменьшается или увеличивается, нужно перерисовывать циферблат часов в соответствии с новыми размерами окна. Чтобы реагировать на изменение размеров окна, сценарий регистрирует событие
В третьем издании этой книги в часы был добавлен таймер обратного отсчета: нажатие клавиши s или m выводит простой диалог ввода числа секунд или минут, соответственно, через которое должен сработать таймер. По истечении отсчета таймера выводится всплывающее окно, как показано на рис. 11.23, заполняющее весь экран в Windows. Я иногда использую этот таймер на курсах, которые я веду, - для напоминания мне и моим студентам, когда подходит время двигаться дальше (эффект получается особенно потрясающий, когда изображение экрана компьютера проецируется во всю стену!).
Рис. 11.23. Истек таймер PyCloek
Наконец, подобно PyEdit, часы PyClock можно запускать автономно или прикреплять и встраивать их в другие графические интерфейсы, где требуется вывести текущее время. При автономном запуске повторно используется модуль windows из предыдущей главы (пример 10.16) -чтобы установить значок и заголовок окна, а также добавить вывод диалога подтверждения перед выходом. Для упрощения запуска часов, выполненных в заданном стиле, существует вспомогательный модуль clockStyles, предоставляющий ряд объектов с настройками, которые можно импортировать, расширять в подклассах и передавать конструктору часов. На рис. 11.24 показано несколько часов разных размеров и стилей, подготовленных заранее, ведущих синхронный отсчет времени.
Запустите сценарий eloekstyles.py (или щелкните на кнопке PyClock в программе PyDemos, которая делает то же самое), чтобы воссоздать эту сцену с часами на своем компьютере. Во всех этих часах 10 раз в секунду проверяется изменение системного времени с использованием со-
Рис. 11.24. Несколько готовых стилей часов: eloekstyles.py
бытий after. При выполнении в виде окон верхнего уровня в одном и том же процессе все они получают событие от таймера из одного и того же цикла событий. При запуске в качестве независимых программ в каждой из них имеется собственный цикл событий. В том и другом случае их секундные стрелки дружно перемещаются раз в секунду.
Исходный программный код PyClock
Вся реализация PyClock находится в одном файле, за исключением предварительно подготовленных объектов с настройками стилей. Если посмотреть в конец примера 11.12, можно заметить, что объект часов можно создать, либо передав конструктору объект с настройками, либо определив параметры настройки в аргументах командной строки, как показано ниже (в этом случае сценарий просто сам создаст объект с настройками):
C:\...\PP4E\Gui\Clock> clock.py -bg gold -sh brown -size 300
Вообще говоря, для запуска часов этот файл можно выполнить непосредственно, с аргументами или без; импортировать его и создать объекты, используя объекты с настройками, чтобы часы выглядели более индивидуально; или импортировать и прикрепить его объекты к другим графическим интерфейсам. Например, PyGadgets из главы 10 запускает этот файл с параметрами командной строки, управляющими внешним видом часов.
Пример 11.12. PP4E\Gui\Cloek\eloek.py
##############################################################################
PyClock 2.1: часы с графическим интерфейсом на Python/tkinter.
В обоих режимах отображения, аналоговом и цифровом, могут выводить метку с датой, графические изображения на циферблате, изменять размеры и так далее. Могут запускаться автономно или встраиваться (прикрепляться) в другие графические интерфейсы, где требуется вывести текущее время.
Новое в версии 2.0: клавиши s/m устанавливают таймер, отсчитывающий секунды/ минуты перед выводом всплывающего сообщения; значок окна.
Новое в версии 2.1: добавлена возможность выполнения под управлением Python 3.X (2.X больше не поддерживается)
##############################################################################
from tkinter import *
from tkinter.simpledialog import askinteger import math, time, sys
##############################################################################
# Классы параметров настройки
##############################################################################
class ClockConfig:
# умолчания - переопределите в экземпляре или в подклассе
size = 200 # ширина=высота
bg, fg = ‘beige’, ‘brown’ # цвет циферблата, рисок
hh, mh, sh, cog = ‘black’, ‘navy’, ‘blue’, ‘red’ # стрелок, центра picture = None # файл картинки
class PhotoClockConfig(ClockConfig):
# пример комплекта настроек size = 320
picture = ‘../gifs/ora-pp.gif’
bg, hh, mh = ‘white’, ‘blue’, ‘orange’
##############################################################################
# Объект цифрового интерфейса
##############################################################################
class DigitalDisplay(Frame):
def __init__(self, parent, cfg):
Frame.__init__(self, parent) self.hour = Label(self) self.mins = Label(self) self.secs = Label(self) self.ampm = Label(self)
for label in self.hour, self.mins, self.secs, self.ampm: label.config(bd=4, relief=SUNKEN, bg=cfg.bg, fg=cfg.fg) label.pack(side=LEFT) # TBD: при изменении размеров можно было бы # изменять размер шрифта def onUpdate(self, hour, mins, secs, ampm, cfg):
mins = str(mins).zfill(2) # или ‘%02d’ % x
self.hour.config(text=str(hour), width=4) self.mins.config(text=str(mins), width=4) self.secs.config(text=str(secs), width=4) self.ampm.config(text=str(ampm), width=4)
def onResize(self, newWidth, newHeight, cfg):
pass # здесь ничего перерисовывать не требуется
##############################################################################
# Объект аналогового интерфейса
##############################################################################
class AnalogDisplay(Canvas):
def __init__(self, parent, cfg):
Canvas.__init__(self, parent,
width=cfg.size, height=cfg.size, bg=cfg.bg) self.drawClockface(cfg)
self.hourHand = self.minsHand = self.secsHand = self.cog = None
def drawClockface(self, cfg): # при запуске и изменении размеров if cfg.picture: # рисует овалы, картинку
try:
self.image = PhotoImage(file=cfg.picture) # фон except:
self.image = BitmapImage(file=cfg.picture) # сохранить ссылку imgx = (cfg.size - self.image.width()) // 2 # центрировать
imgy = (cfg.size - self.image.height()) // 2 # 3.x деление //
self.create_image(imgx+1, imgy+1, anchor=NW, image=self.image) originX = originY = radius = cfg.size // 2 # 3.x деление //
for i in range(60):
x, y = self.point(i, 60, radius-6, originX, originY) self.create_rectangle(x-1, y-1, x+1, y+1, fill=cfg.fg) # минуты for i in range(12):
x, y = self.point(i, 12, radius-6, originX, originY) self.create_rectangle(x-3, y-3, x+3, y+3, fill=cfg.fg) # часы self.ampm = self.create_text(3, 3, anchor=NW, fill=cfg.fg)
def point(self, tick, units, radius, originX, originY): angle = tick * (360.0 / units) radiansPerDegree = math.pi / 180
pointX = int( round( radius * math.sin(angle * radiansPerDegree) )) pointY = int( round( radius * math.cos(angle * radiansPerDegree) )) return (pointX + originX+1), (originY+1 - pointY)
def onUpdate(self, hour, mins, secs, ampm, cfg): # вызывается из
if self.cog: # обработчика событий
self.delete(self.cog) # таймера, перерисовывает
self.delete(self.hourHand) # стрелки, центр
self.delete(self.minsHand) self.delete(self.secsHand)
originX = originY = radius = cfg.size // 2 # 3.x деление //
hour = hour + (mins / 60.0)
hx, hy = self.point(hour, 12, (radius * .80), originX, originY)
mx, my = self.point(mins, 60, (radius * .90), originX, originY)
sx, sy = self.point(secs, 60, (radius * .95), originX, originY)
self.hourHand = self.create_line(originX, originY, hx, hy, width=(cfg.size * .04),
arrow=’last’, arrowshape=(25,25,15), fill=cfg.hh) self.minsHand = self.create_line(originX, originY, mx, my, width=(cfg.size * .03),
arrow=’last’, arrowshape=(20,20,10), fill=cfg.mh) self.secsHand = self.create_line(originX, originY, sx, sy, width=1,
arrow=’last’, arrowshape=(5,10,5), fill=cfg.sh) cogsz = cfg.size * .01
self.cog = self.create_oval(originX-cogsz, originY+cogsz,
originX+cogsz, originY-cogsz, fill=cfg.cog) self.dchars(self.ampm, 0, END) self.insert(self.ampm, END, ampm)
def onResize(self, newWidth, newHeight, cfg): newSize = min(newWidth, newHeight)
#print(‘analog onResize’, cfg.size+4, newSize) if newSize != cfg.size+4: cfg.size = newSize-4 self.delete(‘all’)
self.drawClockface(cfg) # onUpdate called next
##############################################################################
# Составной объект часов
##############################################################################
ChecksPerSec = 10 # частота проверки системного времени
class Clock(Frame):
def __init__(self, config=ClockConfig, parent=None):
Frame.__init__(self, parent) self.cfg = config
self.makeWidgets(parent) # дочерние виджеты компонуются методом pack, self.labelOn = 0 # но клиенты могут использовать pack или grid
self.display = self.digitalDisplay self.lastSec = self.lastMin = -1 self.countdownSeconds = 0
self.onSwitchMode(None)
self.onTimer()
def makeWidgets(self, parent):
self.digitalDisplay = DigitalDisplay(self, self.cfg) self.analogDisplay = AnalogDisplay(self, self.cfg) self.dateLabel = Label(self, bd=3, bg=’red’, fg=’blue’) parent.bind(‘
def onSwitchMode(self, event): self.display.pack_forget() if self.display == self.analogDisplay: self.display = self.digitalDisplay else:
self.display = self.analogDisplay self.display.pack(side=TOP, expand=YES, fill=BOTH)
def onToggleLabel(self, event): self.labelOn += 1 if self.labelOn % 2:
self.dateLabel.pack(side=BOTTOM, fill=X) else:
self.dateLabel.pack_forget()
self.update()
def onResize(self, event):
if event.widget == self.display:
self.display.onResize(event.width, event.height, self.cfg)
def onTimer(self):
secsSinceEpoch = time.time() timeTuple = time.localtime(secsSinceEpoch) hour, min, sec = timeTuple[3:6] if sec != self.lastSec: self.lastSec = sec
ampm = ((hour >= 12) and ‘PM’) or ‘AM’ # 0...23
hour = (hour % 12) or 12 # 12..11
self.display.onUpdate(hour, min, sec, ampm, self.cfg) self.dateLabel.config(text=time.ctime(secsSinceEpoch)) self.countdownSeconds -= 1 if self.countdownSeconds == 0:
self.onCountdownExpire() # таймер обратного отсчета
self.after(1000 // ChecksPerSec, self.onTimer) # вызывать N раз в сек.
# 3.x // целочисленное
# деление с усечением
def onCountdownSec(self, event):
secs = askinteger(‘Countdown’, ‘Seconds?’) if secs: self.countdownSeconds = secs
def onCountdownMin(self, event):
secs = askinteger(‘Countdown’, ‘Minutes’) if secs: self.countdownSeconds = secs * 60
def onCountdownExpire(self):
# ВНИМАНИЕ: только один активный таймер,
# текущее состояние таймера не отображается win = Toplevel()
msg = Button(win, text=’Timer Expired!’, command=win.destroy) msg.config(font=(‘courier’, 80, ‘normal’), fg=’white’, bg=’navy’) msg.config(padx=10, pady=10) msg.pack(expand=YES, fill=BOTH)
win.lift() # поднять над другими окнами
if sys.platform[:3] == ‘win’: # в Windows - на полный экран win.state(‘zoomed’)
##############################################################################
# Автономные часы
##############################################################################
appname = ‘PyClock 2.1’
# использовать новые окна Tk, Toplevel со своими значками и так далее from PP4E.Gui.Tools.windows import PopupWindow, MainWindow
class ClockPopup(PopupWindow):
def __init__(self, config=ClockConfig, name=’’):
PopupWindow.__init__(self, appname, name) clock = Clock(config, self) clock.pack(expand=YES, fill=BOTH)
class ClockMain(MainWindow):
def __init__(self, config=ClockConfig, name=’’):
MainWindow.__init__(self, appname, name) clock = Clock(config, self) clock.pack(expand=YES, fill=BOTH)
# для обратной совместимости: рамки окна устанавливаются вручную,
# передается родитель class ClockWindow(Clock):
def __init__(self, config=ClockConfig, parent=None, name=’’):
Clock.__init__(self, config, parent) self.pack(expand=YES, fill=BOTH) title = appname
if name: title = appname + ‘ - ‘ + name
self.master.title(title) # владелец=parent или окно по умолчанию self.master.protocol('WM_DELETE_WINDOW', self.quit)
##############################################################################
# Запуск программы
##############################################################################
if __name__ == ‘__main__’:
def getOptions(config, argv):
for attr in dir(ClockConfig): # заполнить объект с настройками
try: # из арг. ком. строки “-attr val”
ix = argv.index(‘-’ + attr) # пропустит внутр. __x__
except:
continue else:
if ix in range(1, len(argv)-1):
if type(getattr(ClockConfig, attr)) == int: setattr(config, attr, int(argv[ix+1])) else:
setattr(config, attr, argv[ix+1])
#config = PhotoClockConfig() config = ClockConfig() if len(sys.argv) >= 2:
getOptions(config, sys.argv) # clock.py -size n -bg ‘blue’...
#myclock = ClockWindow(config, Tk()) # при автономном выполнении
#myclock = ClockPopup(ClockConfig(), ‘popup’) # родителем является корневое myclock = ClockMain(config) # окно Tk
myclock.mainloop()
И наконец, в примере 11.13 приводится модуль, выполняемый сценарием PyDemos, - в нем определяется несколько стилей часов и производится запуск одновременно семи экземпляров часов, прикрепляемых к новым окнам верхнего уровня для создания демонстрационного эффекта (хотя на практике обычно достаточно иметь на экране одни часы, даже мне!).
Пример 11.13. PP4E\Gui\Cloek\eloekStyles.py
# предопределенные стили часов
from clock import *
from tkinter import mainloop
gifdir = ‘../gifs/’ if __name__ == ‘__main__’: from sys import argv if len(argv) > 1:
gifdir = argv[1] + ‘/’
class PPClockBig(PhotoClockConfig):
picture, bg, fg = gifdir + ‘ora-pp.gif’, ‘navy’, ‘green’
class PPClockSmall(ClockConfig): size = 175
picture = gifdir + ‘ora-pp.gif’
bg, fg, hh, mh = ‘white’, ‘red’, ‘blue’, ‘orange’
class GilliganClock(ClockConfig): size = 550
picture = gifdir + ‘gilligan.gif’
bg, fg, hh, mh = ‘black’, ‘white’, ‘green’, ‘yellow’
class LP4EClock(GilliganClock): size = 700
picture = gifdir + ‘ora-lp4e.gif’ bg = ‘navy’
class LP4EClockSmall(LP4EClock): size, fg = 350, ‘orange’
class Pyref4EClock(ClockConfig):
size, picture = 400, gifdir + ‘ora-pyref4e.gif’ bg, fg, hh = ‘black’, ‘gold’, ‘brown’
class GreyClock(ClockConfig):
bg, fg, hh, mh, sh = ‘grey’, ‘black’, ‘black’, ‘black’, ‘white’ class PinkClock(ClockConfig):
bg, fg, hh, mh, sh = ‘pink’, ‘yellow’, ‘purple’, ‘orange’, ‘yellow’
class PythonPoweredClock(ClockConfig):
bg, size, picture = ‘white’, 175, gifdir + ‘pythonPowered.gif’
if __name__ == ‘__main__’: root = Tk() for configClass in [
ClockConfig,
PPClockBig,
#PPClockSmall,
LP4EClockSmall,
#GilliganClock,
Pyref4EClock,
GreyClock,
PinkClock,
PythonPoweredClock
]:
ClockPopup(configClass, configClass.__name__)
Button(root, text=’Quit Clocks’, command=root.quit).pack() root.mainloop()
При запуске этот сценарий создает множество часов различного вида, как показано на рис. 11.24. Объекты конфигурации поддерживают большое число параметров. Судя по семи парам часов, отображаемых на экране, пришло время перейти к последнему примеру.
PyToe: виджет игры в крестики-нолики
И наконец, в завершение главы немного развлечемся. В нашем последнем примере, PyToe, на языке Python реализована программа игры в крестики-нолики с привлечением искусственного интеллекта. Большинству читателей, вероятно, знакома эта простая игра, поэтому я не стану останавливаться на ее описании. В двух словах: игроки поочередно ставят свои метки в клетках игрового поля, пытаясь занять целиком строку, колонку или диагональ. Победителем является тот, кому удалось сделать это первым.
В PyToe позиции на игровом поле помечаются щелчком мыши, а одним из игроков является программа на языке Python. Само игровое поле реализовано в виде простого графического интерфейса на основе tkinter. По умолчанию PyToe создает игровое поле размером 3 на 3 (стандартный вариант игры), но можно настроиться на игру произвольного размера N на N.
Когда приходит очередь компьютера сделать ход, с помощью алгоритмов искусственного интеллекта (ИИ) оцениваются возможные ходы и ведется поиск в дереве этих ходов и возможных ответов на них. Это довольно простая задача для игровых программ, а эвристики, применяемые для выбора ходов, несовершенны. Все же PyToe обычно достаточно сообразителен, чтобы победить на несколько ходов раньше, чем пользователь.
Запуск PyToe
Графический интерфейс PyToe реализован в виде фрейма с прикрепленными к нему метками и привязкой обработчиков событий щелчков мыши к этим меткам для перехвата ходов пользователя. Текст метки устанавливается равным метке игрока после каждого хода компьютера или пользователя. Здесь также повторно был использован класс GuiMaker, который мы создали ранее в предыдущей главе (пример 10.3), для создания простой полосы меню в верхней части окна (но без панели инструментов внизу, так как PyToe оставляет ее дескриптор пустым). По умолчанию пользователь ставит крестики («X»), а PyToe - нолики («O»). На рис. 11.25 показано игровое поле сценария PyToe, запущенного с помощью PyGadgets, и диалог с информацией о результатах игры; игра отображена на стадии, когда у сценария есть два хода, ведущие к победе.
Рис. 11.25. PyToe обдумывает путь к победе
На рис. 11.26 изображен всплывающий диалог со справочной информацией о параметрах командной строки PyToe. Есть возможность определить цвет и размер для меток игрового поля, игрока, делающего первый ход, метку пользователя («X» или «О»), размер игрового поля (переопределяющий размер 3 на 3 по умолчанию) и стратегию выбора хода для компьютера (например, «Minimax» выполняет поиск выигрышей и поражений в дереве ходов, а «Expert1» и «Expert2» используют статические эвристические функции оценки).
Используемая в PyToe технология ИИ интенсивно использует ЦП, и в зависимости от игровой ситуации компьютер тратит на определение следующего хода разное время, но скорость ответа компьютера зависит в основном от скорости компьютера. Задержка, связанная с выбором хода на игровом поле 3 на 3, составляет доли секунды для любой стратегии выбора хода «-mode».
На рис. 11.27 изображен альтернативный вариант настройки PyToe (сценарий PyToe был запущен непосредственно из командной строки без аргументов) в момент, когда программа только что выиграла у меня. Хотя по сценам игры, отобранным для этой книги, этого не скажешь, но при установке некоторых режимов выбора хода мне все же удается иногда выигрывать. На игровом поле большего размера и на более
Рис. 11.26. Диалог со справочной информацией о параметрах командной строки PyToe
сложных уровнях алгоритм выбора хода, реализованный в PyToe, становится еще более эффективным.
Исходный программный код PyToe (внешний)
PyToe является крупной системой, для знакомства с которой предполагается наличие некоторой подготовки в области ИИ, но в отношении графического интерфейса, в сущности, не демонстрирует ничего ново-
Рис. 11.27. Альтернативный вариант настройки
го. Кроме того, она была написана для выполнения под управлением Python 2.X более десяти лет тому назад, и хотя она и была перенесена на Python 3.X для этого издания, некоторые ее части было бы лучше реализовать заново. Отчасти по этой причине, но в основном из-за того, что я уже исчерпал объем страниц, отведенных на эту главу, я не привожу здесь исходный программный код, а отсылаю вас к пакету с примерами. За деталями реализации PyToe обращайтесь к следующим двум файлам из пакета примеров:
PP4E\Ai\TieTaeToe\tietaetoe.py
Сценарий оболочки верхнего уровня PP4E\Ai\TieTaeToe\tietaetoe_lists.py Основная реализация
Если вы решитесь заглянуть в эти сценарии, могу посоветовать обратить внимание на структуру данных, используемую для представления состояния игрового поля, которая составляет наибольшую сложность. Если вы разберетесь, каким образом моделируется игровое поле, то остальная часть реализации станет вполне понятна.
Например, в варианте, основанном на списках, для представления состояния игрового поля используется список списков, а также простой словарь из виджетов полей ввода для графического интерфейса, индексируемый координатами игрового поля. Очистка игрового поля после игры заключается в простой очистке исходных структур данных, как показано в следующем фрагменте программного кода из указанных выше примеров:
def clearBoard(self):
for row, col in self.label.keys(): self.board[row][col] = Empty self.label[(row, col)].config(text=’ ‘)
Аналогично выбор хода, по крайней мере, в случайном режиме, заключается в том, чтобы найти пустую ячейку в массиве, представляющем игровое поле, и записать метку компьютера в нее и передать в графический интерфейс (атрибут degree хранит размер игрового поля):
def machineMove(self):
row, col = self.pickMove() self.board[row][col] = self.machineMark self.label[(row, col)].config(text=self.machineMark)
def pickMove(self): empties = [] for row in self.degree: for col in self.degree:
if self.board[row][col] == Empty: empties.append((row, col)) return random.choice(empties)
Наконец, проверка состояния конца игры сводится к просмотру строк, колонок и диагоналей по следующей схеме:
def checkDraw(self, board=None): board = board or self.board for row in board: if Empty in row: return 0
return 1 # не пусто: ничья или победа
def checkWin(self, mark, board=None): board = board or self.board for row in board:
if row.count(mark) == self.degree: # проверка горизонтали return 1
for col in range(self.degree):
for row in board: # проверка вертикали
if row[col] != mark: break else:
return 1
for row in range(self.degree): # проверка первой диагонали
col = row # row == col
if board[row][col] != mark: break else:
return 1
for row in range(self.degree): # проверка второй диагонали
col = (self.degree-1) - row # row+col = degree-1
if board[row][col] != mark: break else:
return 1
def checkFinish(self):
if self.checkWin(self.userMark): outcome = "You’ve won!” elif self.checkWin(self.machineMark): outcome = ‘I win again :-)’ elif self.checkDraw():
outcome = ‘Looks like a draw’
Другой программный код, связанный с выбором хода, в основном просто проводит другие виды анализа структуры данных игрового поля или генерирует новые состояния для поиска в дереве ходов и контрхо-дов.
В том же каталоге находятся родственные файлы, реализующие альтернативные схемы поиска и оценки ходов, различные представления игрового поля и так далее. За дополнительными сведениями об оценке ходов в игре и о поиске в целом обращайтесь к учебникам по ИИ. Это интересный материал, но слишком сложный, чтобы его можно было достаточным образом осветить в данной книге.
Что дальше
На этом завершается часть данной книги, посвященная графическим интерфейсам, но рассказ о графических интерфейсах на этом не заканчивается. Если вам необходимы дополнительные знания о графических интерфейсах, посмотрите примеры использования tkinter, которые будут встречаться дальше в книге и описаны в начале этой главы. PyMailGUI, PyCalc, а также внешние примеры PyForm и PyTree - все они представляют собой дополнительные примеры реализации графических интерфейсов. В следующей части книги мы также узнаем, как создавать интерфейсы пользователя, выполняемые в веб-броузерах, -совершенно другая идея, но еще один вариант конструирования простых интерфейсов.
Имейте в виду, что даже если ни один из рассмотренных в этой книге примеров графического интерфейса не похож на тот, который вам нужно запрограммировать, тем не менее вам уже были представлены все необходимые конструктивные элементы. Создание более крупного графического интерфейса для вашего приложения в действительности заключается в иерархическом расположении составляющих его виджетов, представленных в этой части книги.
Например, сложный интерфейс может быть реализован в виде совокупности радиокнопок, списков, ползунков, текстовых полей, меню и других виджетов, располагаемых во фреймах или в сетках для получения требуемого внешнего вида. Сложный графический интерфейс может быть дополнен всплывающими окнами верхнего уровня, а также независимо выполняемыми программами с графическим интерфейсом, связь с которыми поддерживается через механизмы взаимодействий между процессами (IPC), такими как каналы, сигналы и сокеты.
Кроме того, крупные компоненты графического интерфейса могут быть реализованы в виде классов Python, прикрепляемых или расширяемых всюду, где требуется аналогичный инструмент интерфейса, - важным примером таких компонентов может служить редактор PyEdit и его использование в PyView и PyMailGUI. Подойдите к делу творчески, и набор виджетов tkinter и Python обеспечит вам практически неограниченное число структур.
Помимо данной книги, посмотрите также документацию по библиотеке tkinter, представленную в главе 7, и книги, перечисленные на сайте Python http://www.python.org и в Интернете в целом. Наконец, если мне удалось вас увлечь библиотекой tkinter, еще раз хочу порекомендовать загрузить пакеты, о которых было сказано в главе 7, особенно Pmw, PIL, Tix и ttk (в настоящее время Tix и ttk входят в состав стандартной библиотеки), и поэкспериментировать с ними. Такие инструменты наращивают мощь арсенала tkinter, позволяя создавать более сложные графические интерфейсы за счет небольшого объема программного кода.
Алфавитный указатель
Символы
* (звездочка), групповой символ, 245 /, прямой слеш, 148 \\ обратный слеш, 148 |, оператор создания канала, 183
A
after, метод
возможности, 752 недостатки, 757
планирование вызовов функций,
747, 756
append, метод списков, 45, 52 Array, объект (multiprocessing, пакет), 350
ASCII, кодировка, 224 askyesno, функция, 567
B
BDFL (Benevolent Dictator for Life), 82 bell, метод, 753
BigGui, клиентская демонстрационная программа, 781 bind, метод
возможности, 528 связывание событий, 529 BitmapImage, класс виджетов, 549, 633 BooleanVar, класс, 599 Button, класс виджетов, 511, 549 command, параметр, 603 bytearray, тип объектов, 141 bytes, строковый тип объектов, 141, 696, 701
C
Canvas, класс виджетов, 550, 634, 683 config, метод, 713 create_polygon, метод, 712 create_, метод, 713 delete, метод, 723 find_closest, метод, 714, 726
itemconfig, метод, 713 move, метод, 714 tag_bind, метод, 726 tkraise, метод, 713 update, метод, 756 xscrollcommand, параметр, 681 xview, метод, 680 yscrollcommand, параметр, 681 yview, метод, 680 базовые операции, 710 возможности, 709 идентификаторы объектов, 713 и миниатюры изображений, 718 перемещение объектов, 724 программирование, 711 прокрутка холстов, 715 система координат, 711 события, 722 создание объектов, 712 создание произвольной графики, 100 теги объектов, 714 cat, команда, 161 cd, команда, 171 cgi, модуль, 104
escape, функция, 111 CGI-сценарии
urllib, модуль, 109 веб-серверы, 106 основы, 103
предложения по усовершенствованию, 122
строки запроса, 109 форматирование текста ответа, 110 Checkbutton, класс виджетов, 549, 602 command, параметр, 603 и переменные, 605 chmod, команда, 57, 105 comparedirs, функция, 424 Connection, объект (multiprocessing, пакет), 349
csh, язык командной оболочки, 204 Cygwin, система
ветвление процессов, 262, 268
стстемные инструменты определение, 129
D
Dabo, построитель графических интерфейсов, 487 db.keys, метод, 53 dialog, модуль, 580 dialogTable, модуль, 572, 576 dirdiff, модуль, 425 dir, команда
пример использования, 157 шаблоны имен файлов, 244 dir, функция, 135 __doc__, атрибут, 135
форматирование вывода, 135 doctest, фреймворк, 416 DoubleVar, класс, 599
E
EBCDIC, кодировка символов, 222 EditVisitor, класс, 454 Entry, класс виджетов, 549, 593 xscrollcommand, параметр, 681 xview, метод, 680 yscrollcommand, параметр, 681 yview, метод, 680
и ассоциированные переменные, 599 компоновка в формах ввода, 595 программирование, 594
F
FileVisitor, класс, 449 find, команда, 436 find, модуль, 437 flash, метод, 753 Flex, фреймворк, 488 FLI, формат файлов, 763 fnmatch, модуль, 440 fork, функция, 261 Frame, класс виджетов, 549
добавление нескольких виджетов, 531
прикрепление виджетов к фреймам, 533
разработка графических интерфейсов, 89
G
getopt, модуль, 173 glob, модуль, 133, 245 glob, функция, 246, 254
обработка имен файлов в Юникоде, 254
возможности, 64 кнопки с изображениями, 638 поиск в деревьях каталогов, 436 сканирование каталогов, 378 grep, команда, 436
grid, менеджер компоновки, 597, 653, 726
изменение размеров в сетках, 736 объединение колонок и рядов, 737 преимущества, 727 реализация растягивания виджетов, 734
создание таблиц, 738 сочетание с pack, 731 сравнение с pack, 729 формы ввода, 727 GuiMaker, инструмент
BigGui, клиентская демонстрационная программа, 781 описание, 773
поддерживаемые классы, 779 программный код самотестирования, 779
протоколы подклассов, 778 GuiMixin, 676 GuiMixin, инструмент, 767
вспомогательные подмешиваемые классы, 769
GuiStreams, инструмент, 797
перенаправление для сценариев архивирования, 802
GUI (графический интерфейс пользователя), 479
возможности создания, 483 добавление кнопок, 511 добавление нескольких виджетов, 530
добавление обработчиков, 511 собственных, 514 запуск программ, 481, 501 менеджеры компоновки, 500 настройка виджетов с помощью классов, 537
повторно используемые компоненты GUI, 540
приемы программирования, 497 программа «Hello World», 497, 508 создание виджетов, 499
H
help, функция, 136
I
IDLE, графический интерфейс
проблема начального позиционирования в текстовом редакторе, 879 функциональные возможности, 495 ImageTk, модуль, 718 __import__, функция, 621 Input, класс, 194 input, функция, 180 interact, функция, 182 IntVar, класс, 599 io.BytesIO, класс, 196 io.StringIO, класс, 196 IronPython, 487
J
JavaFX, платформа, 488 Jython, 487
K
kill, команда оболочки, 343
L
Label, виджет
pack, метод, 508 Label, класс виджетов, 549 LabelFrame, класс виджетов, 765 lambda-выражения, как обработчики событий, 515
launchmodes, модуль, 369, 626 Listbox, класс виджетов, 550, 676 curselection, метод, 680 insert, метод, 678 runCommand, метод, 679 xscrollcommand, параметр, 681 xview, метод, 680 yscrollcommand, параметр, 681 yview, метод, 680 программирование, 678 ls, команда
шаблоны имен файлов, 244
M
mainloop, функция (tkinter), 498 Menu, класс виджетов, 549 add_cascade, метод, 660 описание, 660
Menubutton, класс виджетов, 550, 665 Message, класс виджетов, 549, 592 messagebox, модуль, 567 MFC (Microsoft Foundation Classes), библиотека, 489 mimetypes, модуль, 470
проигрывание медиафайлов, 464 mmap, модуль, 318 MPEG, формат файлов, 763 multiprocessing, модуль, 134, 318 multiprocessing, пакет, 343
дополнительные инструменты, 359 запуск независимых программ, 357 и глобальная блокировка интерпретатора (GIL), 305 ограничения, 360 правила использования, 348 процессы и блокировки, 346 реализация, 348
N
__name__, переменная, 143 NumPy, расширение, 955
O
open, функция, 207, 210
поддерживаемые режимы, 218 политика буферизации, 219 Optionmenu, класс виджетов, 668 optparse, модуль, 173 ORM (Object Relational Mapper объектно-реляционное отображение) другие разновидности баз данных, 82 os, модуль, 134, 150
abspath, функция, 156 chdir, функция, 152, 168 chmod, функция, 237 chown, функция, 237 close, функция, 326 dup2, функция, 326 dup, функция
перенаправление, 203 environ, словарь, 167
доступ к переменным оболочки, 175
изменение значений переменных оболочки, 177 environ, функция, 163 execle, функция, 266 execlpe, функция, 266 execlp, функция, 163, 265, 266 execl, функция, 266 execve, функция, 266 execvpe, функция, 266 execvp, функция, 266, 326 execv, функция, 266 _exit, функция, 307 fdopen, функция, 236, 321
fork, функция, 163, 261, 326 перенаправление, 203 getcwd, функция, 152, 167 getenv, функция, 179 getpid, функция, 152, 263 kill, функция, 343 linesep, константа, 153 listdir, функция, 254
вывод имен файлов с символами Юникода, 387
обработка имен файлов в Юникоде, 254
обход одного каталога, 247 сканирование деревьев каталогов, 252
соединение файлов, 397 lseek, функция, 233 mkdir, функция, 164 mkfifo, функция, 164, 332 open, функция, 163, 233, 234 pathsep, константа, 153 pipe, функция, 163, 326
и дескрипторы файлов, 319 перенаправление, 203 popen, функция,156, 158
выполнение команд оболочки для получения списка файлов, 243 и стандартные потоки ввода-вывода, 167 код завершения, 309 обмен данными с командами оболочки, 158
перенаправление потоков ввода-вывода, 198, 199 putenv, функция, 179 read, функция, 233 remove, функция, 164, 237 rename, функция, 237 sep, константа, 153 spawnve, функция, 362 spawnv, функция, 163, 178, 362 startfile, функция, 366, 368 stat, функция, 164, 239 system, функция, 156, 158, 309 unlink, функция, 238 walk, функция, 164, 254 и функция find, 438 обработка имен файлов в Юникоде, 254
сканирование деревьев каталогов, 249, 379
write, функция, 233 выполнение команд оболочки из сценариев, 156
завершение программ, 307 инструменты администрирования, 152
инструменты для работы с файлами, 233
константы переносимости, 153 os.path, модуль, 134 exists, функция, 153 getsize, функция, 153 isdir, функция, 153 isfile, функция, 153 join, функция, 154 split, функция, 154 инструменты, 152, 153 Output, класс, 194
P
Pack, класс, 508 pack, менеджер компоновки сочетание с grid, 731 сравнение с grid, 729 PanedWindow, класс виджетов, 765 Pexpect, пакет, 134, 202 PhotoImage, класс виджетов, 549, 633, 670
pickle, модуль
возможности, 61
сохранение каждой записи в отдельном файле, 64
PIL (Python Imaging Library), 641
отображение других типов графических изображений, 643 создание миниатюр изображений, 647
PIL, расширение, 483
функциональные возможности, 494 Pipe, объект (multiprocessing, пакет), 349
Pmw, библиотека, 483, 491
функциональные возможности, 493 popen, функция, 156, 158
выполнение команд оболочки для получения списка файлов, 243 и стандартные потоки ввода-вывода, 167
код завершения, 309 обмен данными с командами оболочки, 158
перенаправление потоков ввода-вывода, 198, 199 print, функция, 136
и стандартные потоки ввода-вывода, 180
перенаправление, 197
pprint, модуль
вывод содержимого баз данных, 53 Process, класс (multiprocessing, пакет), 346
pty, модуль, 330 py2exe, инструмент, 484 PyClock, программа, 951 запуск, 957
исходный программный код, 961 описание, 951 точки на окружности, 951 PyDemos, панель запуска, 846, 860 PyDoc, система, 136 PyDraw, программа рисования, 941 запуск, 941
исходный программный код, 943 описание, 941
PyEdit, текстовый редактор встраивание в PyView, 931 диалоги, 865, 872 другие примеры и рисунки, 871 запуск, 863
запуск программного кода, 866 изменения в версии 2.0, 872 диалог выбора шрифта, 872 модуль с настройками, 872, 873 неограниченное количество отмен и возвратов операций редактирования, 872, 873 перечень, 872
изменения в версии 2.1, 874
изменение модального режима диалогов, 874 новый диалог Grep, 875 перечень, 874
проверка при завершении, 875 исходный программный код обзор, 888
файл с настройками пользователя, 889
файл с основной реализацией, 892 файлы запуска, 891 меню и панель инструментов, 864 несколько окон, 867 новое в версии 2.1
исправление проблемы начального позиционирования, 878 поддержка Юникода, 880 проверка при завершении, 886 улучшения в операции запуска программного кода, 879 описание, 862 поддержка Юникода, 706 пример, реализация, 682
PyGadgets, панель запуска, 852, 860 PyGame, пакет, 763 PyGTK, пакет, 486 PyInstaller, инструмент, 484 pyjamas, фреймворк, 488 PyMailGUI, программа
помещение обработчиков событий в очередь, 817 PyObjC,библиотека, 489 PyPhoto, просмотр изображений, 917 запуск, 918
исходный программный код, 922 обзор, 917 PyQt, пакет, 486 pySerial, интерфейс, 134 PythonCard, построитель графических интерфейсов, 487
PYTHONPATH, переменная окружения определение, 176 синтаксические ошибки, 147 Python, язык программирования происхождение имени, 69 сторонние расширения, 134 PyToe, виджет игры, 969 запуск, 969
исходный программный код, 971 описание, 969 PyTree, программа, 718 PyView, программа просмотра изображений, 929 запуск, 929
исходный программный код, 935 описание, 929 PyWin32, пакет обзор, 489
Q
queue, модуль, 134, 293
аргументы или глобальные переменные, 295
завершение программ с дочерними потоками выполнения, 295 запуск сценариев, 297 и потоки выполнения, 272 Queue, объект (multiprocessing, пакет), 350
R
Radiobutton, класс виджетов, 549, 602 command, параметр, 607 и ассоциированные переменные, 602 и переменные, 609 описание, 607
random, модуль, 101, 638 repeater, метод, 753 ReplaceVisitor, класс, 456
S
Scale, класс виджетов, 549 command, параметр, 614 from_, параметр, 615 get/set методы, 614 label, параметр, 615 length, параметр, 615 orient, параметр, 615 resolution, параметр, 615 showvalue, параметр, 616 tickinterval, параметр, 615 to, параметр, 615 и переменные, 617 описание, 614 scanner, функция, 239 Scrollbar, класс виджетов, 550, 676 возможности, 676 компоновка полос прокрутки, 681 программирование, 680 ScrolledCanvas, класс, 715 ScrolledList, класс компонента, 677 ScrolledText, класс компонента, 684, 690, 694
search_all, сценарий, 448 searcher, функция, 446 SearchVisitor, класс, 449, 471 select, модуль, 134 ShellGui, инструмент, 785 диалоги ввода, 792 добавление графических интерфейсов к инструментам командной строки, 789
классы наборов утилит, 788 обобщенный графический интерфейс инструментов оболочки, 785 сценарии командной строки, 789 shelve, модуль close, метод, 66 open, метод, 66 веб-интерфейс, 111 возможности, 66 интерфейс командной строки, 83 формат словаря словарей, 54 shutil, модуль, 134
дополнительная информация, 238 signal, модуль, 134, 340 alarm, функция, 342 pause, функция, 341 signal, функция, 341
Silverligh, фреймворк, 488 SimpleEditor, класс возможности, 691 наследование, 694 ограничения, 695 поддержка буфера обмена, 693 socket, модуль, 133, 335 socketserver, модуль, 107 sorted, функция, 187 Spinbox, класс виджетов, 765 SqlAlchemy, система, 82 SQLObject, система, 82 start, команда, 162, 366 string, модуль константы, 139 StringVar, класс, 599 struct, модуль
pack, функция, 228 unpack, функция, 229 анализ двоичных данных, 228 str, тип объектов, 141 и виджет Text, 696 особенности использования, 702 subprocess, модуль, 134, 156, 159, 178 Popen, объект, 165 код завершения, 309, 311 перенаправление потоков ввода-вывода, 198, 200 SumGrid, класс, 743 sys, модуль
argv, параметр, 167, 171 exc_info, функция, 149 exit, функция, 306 getdefaultencoding, функция, 222 getrefcount, функция, 149 modules, словарь, 148 platform, строка, 146 setcheckinterval, функция, 302 завершение программ, 306 завершение программы, 511 и аргументы командной строки, 171 и стандартные потоки ввода-вывода, 180
источники документации по модулям, 135
и текущий рабочий каталог, 169 платформы и версии, 146 путь поиска модулей, 146, 169, 381 сведения об исключениях, 149 таблица загруженных модулей, 148 sys.stderr, поток вывода ошибок буферизация, 327 особенности, 167 перехват потока, 197 sys.stdin
и взаимодействие с пользователем, 188
особенности, 167
перенаправление в объекты Python, 192
символ конца файла, 182 sys.stdout
особенности, 167
перенаправление в объекты Python, 192
перенаправление в функции print, 197
T
Tcl, apsr программирования, 551 tempfile, модуль, 134 Text, класс виджетов, 550 get, метод, 688 mark_set, метод, 688 tag_add, метод, 688 tag_bind, метод, 707 tag_config, метод, 707 tag_delete, метод, 689 tag_remove, метод, 689 более сложные операции с текстом, 707
возможности, 683 и Юникод, 695, 701 метки, 688
операции редактирования текста, 689
поддержка индексирования, 687 программирование, 685 теги, 688
Thread, класс, 288 _thread, модуль, 134, 274
allocate_lock, функция, 281 start_new_thread, функция, 275 альтернативные приемы, 285 запуск нескольких потоков выполнения, 277
ожидание завершения порожденных потоков выполнения, 282 основы использования, 275 синхронизация доступа, 280 способы реализации потоков выполнения, 276
threading, модуль, 134, 287
завершение потоков выполнения в графических интерфейсах, 816 синхронизация доступа, 290 способы реализации потоков выполнения, 289 time, модуль, 134
sleep, функция, 263, 756, 757, 760 timeit, модуль, 134 Tix, расширение, 491
функциональные возможности, 493 tkinter, библиотека, 483
createfilehandler, функция, 749 документация, 975 tkinter, модуль after, метод, 300
альтернативные приемы использования, 502
документация, 492 менеджеры компоновки, 500 настройка заголовка окна, 506 обзор, 490
основы использования, 498 особенности, 87 поддержка расширений, 492 рограммная структура, 496 Tk, библиотека, 551 Tk, класс виджетов, 549, 560, 660 destroy, метод, 561, 563 iconbitmap, метод, 564 iconify, метод, 564 maxsize, метод, 564 menu, параметр, 565 protocol, метод, 563 quit, метод, 563 title, метод, 564 withdraw, метод, 564 Toplevel, класс виджетов, 549, 559, 660 deiconify, метод, 754 destroy, метод, 563 iconbitmap, метод, 564 iconify, метод, 564, 755 lift, метод, 755 maxsize, метод, 564 menu, параметр, 565 protocol, метод, 563 quit, метод, 563 state, метод, 755 title, метод, 564 tkraise, метод, 755 withdraw, метод, 564, 754 автоматизация создания окон, 805 и пользовательские диалоги, 581 независимые окна, 624 traceback, модуль, 149 try/finally, инструкция, 212 ttk, библиотека, 483, 491
функциональные возможности, 494
U
unittest, фреймворк, 416 Unix, платформа
и выполняемые сценарии, 174 перенаправление потоков ввода-вывода, 183 urllib, модуль, 109
поддержка CGI-сценариями, 109
V
Value, объект (multiprocessing, пакет), 350
W
webbrowser, модуль, 464, 468 Windows
и потоки ввода-вывода стандартные, 181 перенаправление, 183 пути к каталогам, 148 with, инструкция, 213 wxPython, система, 485
Z
ZODB, система, особенности, 82
А
анализ
аргументов командной строки, 172 двоичных данных, 228 анимация
другие эффекты, 762 и графика, 763 и потоки выполнения, 762 простые приемы, 755 циклы time.sleep, 756, 757, 760 анонимные каналы
буферизация потоков вывода, 327 взаимоблокировки, 327 двунаправленный обмен данными, 324
дескрипторы файлов, 319
обертывание объектами, 321 и потоки выполнения, 323 определение, 317, 318 основы использования, 319 аргументы
и глобальные переменные, 295, 518 и потоки выполнения, 285 командной строки анализ, 172 доступ, 167
ассоциированные переменные, 599, 602
Б
базы данных
вывод содержимого с помощью модуля pprint, 53
дополнительная информация, 81 безопасность и веб-интерфейсы, 115 буферизация потока вывода Pexpect, пакет, 202 pty, модуль, 330 взаимоблокировки, 327 и завершение программ, 310 буфер обмена, использование, 693
В
веб-интерфейсы
CGI-сценарии, 102 shelve, модуль, 111 urllib, модуль, 109 дополнительные инструменты, 102 запуск веб-сервера, 106 предложения по усовершенствованию, 122
строки запроса, 109 форматирование текста ответа, 110 веб-страницы
создание для переадресации, 403 сценарий генератора страниц, 405 файлы шаблонов, 404 ветвление процессов, 259, 260
os.exec, функция, формы вызова, 265 fork/exec, комбинация функций, 264 получение кода завершения, 312 порождение дочерней программы, 266
взаимодействия между процессами, 316 multiprocessing, модуль, 318, 349 socket, модуль, 133 анонимные каналы, 317, 319 двунаправленный обмен данными, 324
именованные каналы, 317, 331 обзор, 316 сигналы, 317, 340 сокеты, 317 виджеты, 592
after_cancel, метод, 748 after_idle, метод, 748 after, метод, 747, 756 anchor, параметр, 536 bd, параметр, 557 config, метод, 507, 555 cursor, параметр, 557 focus, метод, 750 grab, метод, 750 grid_forget, метод, 754 menu, параметр, 565 pack_forget, метод, 754
padx, параметр, 557
pady, параметр, 557 state, параметр, 557 update_idletasks, метод, 749 update, метод, 749 wait_variable, метод, 750 wait_visibility, метод, 750 wait_window, метод, 750 добавление без сохранения, 508 добавление нескольких виджетов,
530
дополнительные виджеты, 764 и диалоги, 566
изменение размеров, 504, 531 использование якорей, 536 компоновка элементов ввода в формах, 595 настройка
внешнего вида, 554 меток, 555 параметров, 506 с помощью классов, 537 обрезание, 531 окна верхнего уровня, 558 перенаправление потоков ввода-вывода, 797
порядок компоновки, 533 привязка событий, 585 прикрепление к фреймам, 532, 619 растягивание, 512 скрытие и перерисовка, 754 создание, 499 стандартизация
внешнего вида, 538 поведения, 538 вложенные структуры словари, 51 списки, 52
вспомогательные подмешиваемые классы, 769
входные файлы, 881 вывод
в файлы, 210
имен файлов с символами Юникода, 387
результатов диалогов, 576 выполнение программ
автоматизированный запуск, 473 обмен данными, 629 с графическим интерфейсом, 626
Г
Гвидо ван Россум (Guido van Rossum), 482
глобальная блокировка интерпретатора (Global Interpreter Lock, GIL), 302 API потоков выполнения на языке C, 304
multiprocessing, пакет, 305 атомарные операции, 304 интервал переключения потоков выполнения, 303 и потоки выполнения, 272 глобальная замена в деревьях каталогов, 456
глобальные переменные
multiprocessing, пакет, 352 и аргументы, 518 против аргументов, 295 графические интерфейсы
динамическая перезагрузка обработчиков, 803