22-й час

Функции рисования библиотеки Tk

  В предыдущей главе мы продолжили изучение функций рисования библиотеки Tk, рассмотрев функции управления цветом и добавление простой анимации. В этой главе мы продолжим заниматься анимацией, а в завершении познакомимся с менеджерами геометрии объектов. Закончив изучение материала этой главы, Вы сможете создавать достаточно сложную анимацию в объекте Canvas; использовать три метода размещения объектов в окне: Place, Pack и Grid.

Ещё анимация

  В предыдущей главе я пообещал Вам создать игровое поле, по которому вместо бильярдных шаров будут летать изображения богов индейцев Майя. Давайте начнём с создания игрового поля. На рис. 22.1 показано, как я его себе представил.

Рис. 22.1. Игровое поле

  Выглядит достаточно скучновато, не правда ли? Особенно в монохромном отображении. Запустите на своём компьютере программу tkfield1.py, код которой показан в листинге 22.1, чтобы посмотреть, как это поле выглядит на самом деле.

  *Прим. В. Шипкова: поле выглядит иначе. Я не стал приводить его здесь, так как это несущественно (поле с вертикальным цветовым градиентом).

Листинг 22.1. Программа tkfieldl.py

import sys, string, random
from Tkinter import *
from Canvas import ImageItem, Rectangle, CanvasText
from colormap import *

class Glyphs(Frame):
 
def die(self,event=0):
    sys.exit(0)

  
def __init__(self, parent=None, nobj=0, wwidth=640,\

                wheight=480):
    self.pause=0
    self.xdelta=[]
    self.ydelta=[]
    self.xmin=16
    self.ymin=16
    self.windowwidth=wwidth
    self.windowheight=wheight
    self.xlim=self.windowwidth - self.xmin
    self.ylim=self.windowheight - self.ymin
    self.xpos=[]
    self.ypos=[]
    self.img=[]
    self.pictures=[]
    self.deltav=1
    self.parent=parent
    Frame.__init__(self, self.parent)
    Pack.config(self)

    self.nobj=nobj
    self.buildglyphs(nobj)

 
def buildglyphs(self,n=1):
    self.ncolors=128
    self.cmap=SetupColormap1(self.ncolors)

    self.draw = Canvas(self,
    width=self.windowwidth, height=self.windowheight)
    self.llabel=CanvasText(self.draw, \

      self.windowwidth-100, self.windowheight-32,\

      text="",font="Helvetica 20")
    self.parent.bind("<Escape>", self.die)
    sr=1+(self.windowheight/self.ncolors)
    x=0
    y=0
    w=self.windowwidth
   
for c in self.cmap:
     
if c=="#000000":
       
break
      item=Rectangle(self.draw, x, y, w, y+sr, fill=c,
      outline="",width=0)
      y=y+sr
    self.draw.pack()

if __name__ == "__main__":
  wwidth=-1
  wheight=-1
 
if len(sys.argv)>1:
    nobj=string.atoi(sys.argv[1])
   
if len(sys.argv)>3:
      wwidth=string.atoi(sys.argv[2])
      wheight=string.atoi(sys.argv[3])
 
else:
    nobj=-1
  root=Tk()
 
if wwidth==-1:
    wwidth=root.winfo_screenwidth()
    wheight=root.winfo_screenheight()
 
if nobj==1:
    suff=""
 
else:
    suff=" Brothers"
    root.title("Flying Glyph" + suff)
    glyphs=Glyphs(root, nobj, wwidth, wheight)

  glyphs.mainloop()

  Хотя данный код не делает ничего, кроме как выводит на экран радужное поле, в действительности он достаточно функционален. Просто его функциональность нацелена на то, чтобы быть основой для будущей большой программы. Поскольку ожидается, что конечная программа будет достаточно сложной, наш исходный небольшой код мы начинаем с определения класса Glyphs в строке 8. Обратите внимание, что класс Glyphs наследуется от класса графического объекта Frame (рамка), следовательно, и caм является классом графического объекта, предоставляющим доступ к методам класса Frame. Когда класс наследуется от другого класса, то для создания унаследованных переменных-членов в коде дочернего класса нужно явно вызвать метод __init__() родительского класса. Это и происходит в строке 28. В строке 29 вызывается метод pack(), который необходим для того, чтобы сделать графический объект видимым на экране. В нашем примере этот метод выполнен таким образом, чтобы при реализации класса его экземпляр тут же выводился на экран. В строке 74 создаётся объект класса Glyphs, а в строке 76 для этого объекта вызывается метод mainloop(). Как видите, метод mainloop() также наследуется пользовательскими графическими объектами от корневого окна, благодаря чему их вывод ничем не отличается от вывода базовых объектов. Это создаёт дополнительную гибкость программирования GUI, в чем мы убедимся, рассмотрев листинг 22.2.

Листинг 22.2. Программа tkm.py

from Tkinter import *
import sys

def die(event=0):
  sys.exit(0)

x=Button(None, text="Hello, World!", command=die)
x.pack()
x.mainloop()

  Обратите внимание, что в этом примере кнопка создаётся без предварительного создания корневого окна инструкцией root=Tk(), как это было раньше. На экран выводится обычная кнопка, правда, мы не можем вызвать для неё метод title() и некоторые другие методы, характерные для окон верхнего уровня. (Как Вы помните, кнопки, создаваемые нами до сих пор, представляли собой окна и владели всей функциональностью окон верхнего уровня.) Тем не менее данная кнопка обладает достаточной функциональностью, чтобы использовать её по прямому назначению в небольших окнах, вроде окон сообщений, или в окнах, поддерживающих ввод данных в текстовых полях.

  В строке 42 листинга 22.1 метод die() назначается нажатию клавиши <Esc>, благодаря чему мы можем обойтись без вывода на экран кнопки "Выход". В строках 67, 68 устанавливается размер дисплея, чтобы по умолчанию вывести окно программы во весь экран. При желании Вы можете открывать окно размером в пол-экрана. Для этого достаточно разделить возвращённые значения высоты и ширины экрана на 2. В строках 47-52 в объект холста выводится множество разноцветных прямоугольников, которые следуют друг за другом, образуя градиентный переход сверху вниз. Подобные градиенты мы уже создавали в прошлой главе. Чтобы настроить градиентные переходы цветов и тонов требуемым образом, следует воспользоваться различными функциями SetupColormapn() модуля colormap. Вспомните, что последним цветом в списке mар всегда является чёрный. Поэтому завершение цикла инструкцией break мы связываем с соответствием текущего значения черному цвету. Обратите также внимание на то, что объект текста, который создаётся в строках 40, 41 для вывода в объекте холста, в действительности будет невидимым на экране, поскольку ему пока что присвоено пустое строковое значение. Этим объектом мы воспользуемся в следующих вариантах программы, впрочем, как и всеми остальными членами класса Glyphs.

  Первое изменение нашей исходной программы будет состоять в добавлении графических изображений, причём не одного, а сразу двадцати. Кроме того, эти изображения будут выводиться на экран случайным образом. Полный код программы с добавленными графическими изображениями показан в листинге 22.3.

Листинг 22.3. Программа tkfield2.py

  *Прим. В. Шипкова: текст программы составляет более 120 строк. Соответственно, его здесь нет. Кому надо - скачать файл.

  Первое изменение, которое бросается в глаза, — это пара списков, добавленных в программу в строках 13-26. Это имена графических файлов, которые мы хотим использовать в нашем "Пантеоне летающих богов". Данные файлы следует загрузить с Web-страницы этой книги и скопировать в ту же папку, из которой запускается программа. Все файлы имеют формат GIF. Мой выбор этого формата был обусловлен следующими фактами.

  1. Файлы PNG, которые часто рекомендуются как альтернатива файлам GIF, поддерживают прозрачность изображений. В то же время встроенная прозрачность изображений PNG поддерживается далеко не всеми броузерами, используемыми для просмотра Web-страниц. Для поддержания прозрачности в изображениях GIF нужно использовать дополнительные утилиты, такие как Alchemy Mindwork's Gif Construction Set Pro, но в этом случае свойство прозрачности станет переносимым.

  2. Метод PhotoImage() работает с графическими изображениями только в форматах РРМ и GIF, но файлы РРМ не поддерживают прозрачность.

  Два списка графических файлов отличаются тем, что в списке smallglyphlist представлены изображения размером 32x32 пикселя, а в списке bigglyphlist — размером 64x64 пикселя. В строке 36 проверяется текущий размер окна программы, и если он меньше 800 пикселей, то выводятся маленькие изображения. В противном случае используются большие изображения. Таким образом, я пытаюсь достичь пропорциональности изображений богов размеру окна программы. Подход, при котором элементы графического интерфейса выбираются из альтернативных наборов и выводятся только после анализа текущего состояния программы, в современном программировании называется итеративным выводом, или пошаговой настройкой. Программирование итеративного вывода пользовательского интерфейса в других языках выглядит достаточно тяжеловесно, как бы мило и многообещающе не звучали названия специальных средств программирования, зарезервированных для выполнения этих задач. Для выполнения этой задачи предполагается создание множества спецификаций, маркирующих программные блоки. В Python, каким бы большим ни был проект, до создания спецификаций дело, как правило, не доходит. Самый большой проект в Python обычно завершается до возникновения необходимости в спецификации и задолго до Вашего обеда.

  Продолжим рассмотрение кода листинга 22.3. После создания объекта графического изображения в строке 77 мы добавляем его в список img, откуда затем в любой момент можем его возвратить. А нам они понадобятся позже, когда мы станем запускать изображения богов в полет по игровому полю. В строках 79-83 программа извлекает первый объект изображения из списка и вычисляет, как близко к краю окна оно может подлететь. По умолчанию координаты объектов на холсте вычисляются по центру соответствующего изображения. Но если мы хотим добиться видимости того, что лики древне-индейских богов подлетают к краю окна и отражаются от него, нужно чтобы центры изображений не подходили к краю ближе, чем на половину самого изображения.

  В строке 74 вычисляется значение переменной с именем deltav. (Это имя было взято из терминологии ракетостроения, правда, вместо слова delta используется греческая буква А. Этим термином обозначается ускорение ракеты при старте.) Наши изображения богов будут летать без ускорения, но с разной длиной единичного шага перехода. Маленькие изображения должны перемещаться мелкими шажками, а крупные — делать шаги побольше. Эти значения как раз и заносятся в переменную deltav. В строках 84-89 переменным сдвига по осям X и У присваивается положительное значение deltav, если выводимый объект чётный в списке, или отрицательное — если нечётный. В результате одни изображения будут перемещаться на экране слева направо, а другие — наоборот. В строках 91, 92 определяется, где на холсте будет выводиться текущий объект изображения. Метод random.randint() возвращает случайные целочисленные значения в указанном диапазоне. В нашей программе этот диапазон вычисляется по текущему размеру окна, с учетом размера используемых графических объектов. Так, если размер окна составляет 256x256 пикселей, случайные значения для координат х и у будут выводиться в диапазоне от 32 до 224. Полученные значения добавляются в списки координат по оси X (xpos) и координат по оси Y (ypos). И, наконец, в строках 94-96 с помощью метода ImageItem в холсте создаётся собственно сам объект графического изображения, который добавляется в ещё один список — pictures. После создания списка объектов графических изображений все они будут выведены в холсте вызовом метода mainloop() в строке 121. Чтобы запустить эту программу, введите в командной строке окна терминала команду python tkfield2.py 1256 256. В результате появится окно программы размером 256x256 пикселей с единственным изображением на нем, как показано на рис. 22.2.

Рис. 22.2. Первая пиктограмма уже на экране, но ещё не летает

  При следующем вызове изображение будет выведено в другом месте на холсте. Введите теперь в командной строке python tkfield2.py 20 256 256 и нажмите <Enter>. Результат выполнения программы показан на рис. 22.3.

Рис. 22.3. "Пантеон" заполнен, но боги ещё не летают

  Обратите внимание, что когда на экран выводится только одно изображение, то в заголовок окна программы выносится только слово "Пантеон", чтобы у пользователя не возник вопрос, а где же остальные боги. Но если на экран выводится несколько изображений, то в заголовке мы видим: "Пантеон летающих богов". Это ещё один пример итеративного вывода элементов интерфейса, который реализуется в строках программы 114-118. Если запустить программу без аргументов, то окно приложения откроется во весь экран и будут выведены крупные изображения всех богов.

  Теперь мы готовы к добавлению анимации. Представьте себе, что изображения богов, показанные на рис. 22.3, не просто показаны на экране, но беспорядочно движутся, натыкаясь и отскакивая от краев окна. Код программы с анимацией показан в листинге 22.4.

Листинг 22.4. Программа tkfield3.py

  *Прим. В. Шипкова: код этой программы больше чем предыдущей - 150 строк. Молча даю ссылку на файл.

  Что нового в этом коде? В строке 99 вызывается метод moveglyphs(), определение которого представлено в строках 101-127. В предыдущей главе Вы уже видели пример применения метода after(). Особенность его применения состоит в том, что когда функция обработки события вызывается методом after(), то в самой функции также должен быть вызов этого метода. Так происходит и у нас. В строке 127 метод after() вызывает функцию moveglyphs(), а в строке 103 в теле функции moveglyphs повторяется тот же вызов метода after(). Перед этим в строке 102 проверяется состояние переменной pause, с помощью которой можно остановить движение изображений. Как это происходит, мы рассмотрим чуть позже.

  Вас может удивить, почему для управления движением всех объектов изображений используется единственный вызов метода after(), вместо того, чтобы вызывать его для каждого индивидуального объекта. Проблема состоит в том, что хотя в программах для UNIX можно создавать сколько угодно таймеров, в Windows этот ресурс крайне ограничен. Хотя в Windows 95 само по себе такое ограничение было снято, в действительности мало сделано для того, чтобы эффективно поддерживать эту продекларированную возможность. Эффективность выполнения программы в Windows стремительно снижается после того, как число таймеров станет больше десяти. Но часто в программе для группы объектов удаётся обойтись всего одним таймером, что и продемонстрировано выше в программе.

  *Прим. В. Шипкова: на самом деле в документации по Visual Basic написано, что таймеров не может быть более 32. Даже если, речь шла об одном приложении - это хороший показатель того, как не продуман этот вопрос.

  Структура метода moveglyphs() достаточно проста и прямолинейна. Этот метод даже проще, чем аналогичная ему программа tkpool.py, рассмотренная в предыдущей главе. В данном случае мы даже обходимся без импортирования метода math. Дело в том, что в нашей небольшой программе с летающими шарами для вычисления углов использовались тригонометрические функции. Сейчас мы обходимся даже без этих простейших тригонометрических вычислений. Мне было интересно попытаться обойтись без них. Математические вычисления расходуют достаточно много компьютерных ресурсов. Я мог бы поспорить, что в тех игральных аппаратах, которые я видел в 1972 году, наверняка не использовалось никаких тригонометрических вычислений, так как снабжены они были в лучшем случае процессором 6502 CPU и 1 или 2 Кбайт оперативной памяти. Нам, конечно, нет нужды сейчас ограничивать себя такими ресурсами, тем не менее, мне показалось крайне интересным максимально оптимизировать нашу программу.

  *Прим. В. Шипкова: 6502 - очевидно речь идёт об одном из первых процессоров фирмы Motorola.

  В предыдущей главе я сообщил, что при ударении движущегося объекта о край окна он отражается по принципу: "угол отражения равен углу падения". Не трудно представить, что если бы наши изображения богов вели себя подобным образом, без изменения направления движения, то очень скоро вошли бы в бесконечно повторяющийся цикл. Понаблюдав за выполнением нашей текущей программы, Вы убедитесь, что такого не происходит. Итак, нет тригонометрических вычислений, нет строгого соблюдения принципа равенства угла падения углу отражения. Что же здесь происходит?

  Давайте проанализируем цикл for функции moveglyphs(). Цикл выполняется столько раз, сколько у нас было создано объектов графических изображений. Список объектов изображений формируется в несколько этапов. Сначала в строке 77 создаётся список графических файлов imv, а потом, в строке 94, с помощью метода ImageItem создаются собственно объекты изображений и помещаются в список pictures. При добавлении объектов в список одновременно в строке 96 переменной-члену tags каждого объекта присваивается строковое значение, по которому затем можно будет обратиться к этому объекту в списке. Мы идем самым простым путём: присваиваем переменной tags имя соответствующего графического файла. Все, что мы делаем затем в цикле функции moveglyphs(), — это перемещаем каждый объект с помощью метода холста move() (строка 107) в соответствии с его текущими значениями xdelta и ydelta. При этом для обращения к объектам используются присвоенные им имена.

  После очередного смещения объекта мы вычисляем, куда он сместится на следующей итерации (после следующего вызова метода after()). Для этого в строке 109 используются текущие координаты объекта и значение его смещения xdelta, а результат заносится во временную переменную tmp. Если полученное значение остаётся в пределах, заданных переменными xmin и xlim, то новая координата по оси X просто передаётся переменной xpos (строка 116). Если же очередная позиция объекта выходит за очерченные рамки, то следует предпринять какие-то действия, предупреждающие выход объекта за пределы окна программы, для чего выполняются строки 111-115. В строке 111 вычисляется угол отражения, точно так же, как мы делали это в предыдущей главе. Затем мы суммируем значение xdelta со значением, возвращенным функцией random.choice(), и отнимаем 2. (Как Вы, наверное, уже догадались, модуль random содержит функции генератора случайных чисел.) Метод choice() случайным образом выбирает один из элементов списка, переданного ему с аргументом (в нашем случае — (0,1)), и возвращает его. Таким образом, в нашей программе этот метод может возвратить одно их двух значений: 0 или 1. После того как от этого значения отнимается 2, получаем -1 или -2, что и прибавляем к текущему значению xdelta. Таким образом, после каждого удара объекта о стенку мы немного изменяем значение xdelta, что равносильно изменению стороны b нашего мнимого треугольника, который мы рассматривали в предыдущей главе на рис. 21.6. Чтобы напомнить Вам, что это за треугольник, давайте внимательно рассмотрим движение объектов по экрану. Обратите внимание, что они никогда не движутся точно по вертикали или по горизонтали, а всегда под углом. Можно сказать, что они движутся по гипотенузе правильного треугольника, катетами которого являются xdelta (сторона b) и ydelta (сторона а). Наши значения приращений xdelta и ydelta — это всегда небольшие целые числа, поэтому рассматриваемый нами треугольник также достаточно маленький. Поскольку значения xdelta и ydelta изменяются только во время ударений объекта о край окна, в пределах окна объекты движутся прямолинейно. Но отражения от стенок теперь уже не соответствуют принципу "угол отражения равен углу паления". Иногда Вы увидите, что объект отражается под более острым углом, иногда под более тупым и движется медленнее. Первый случай происходит, когда, например, xdelta принимает значение 1, a ydelta — 9, тогда как второй случай может соответствовать значениям 2 для xdelta и 1 для ydelta.

  В строках 119-124 повторяется та же операция, только для переменной ydelta, или, если хотите, для стороны а мнимого треугольника. Теперь Вы, наверное, уже поняли, как нам удаётся обходиться без тригонометрических функций и почему объекты не движутся по замкнутому циклу.

  *Прим. В. Шипкова: приведённый пример, имхо, является классикой того случая, когда можно обойтись без синусов и косинусов. :)

  Добавим теперь ряд последних изменений в код программы, как показано в листинге 22.5.

Листинг 22.5. Программа tkfield4.py

  *Прим. В. Шипкова: код этой программы больше чем предыдущей - 170 строк. Молча даю ссылку на файл.

  Новыми в этом коде являются два метода — snap() и lifter() (строки 12-26). В строках 79, 80 эти методы назначены для обработки событий щелчков кнопками мыши. Метод snap() назначен событию "<Button-l>" и служит для остановки движения всех объектов. Этот метод устанавливает значение переменной pause, которая проверяется в функции moveglyphs() перед выполнением новой итерации. Если значение pause равно 1, то функция вызывает метод after(), после чего выполняется инструкция return. Поэтому, несмотря на предельную простоту метода snap(), его влияние на выполнение программы огромно.

  Второй новый метод lifter() выполняет две задачи. Он назначен для обработки события "<Button-3>", что соответствует щелчку правой кнопкой мыши. После щелчка мышью в пределах окна программы строка 19 проверяет, находится ли в данный момент под указателем мыши какой-либо объект изображения. Если это так, в переменной х окажется больше одного элемента (условие, проверяемое в строке 20), причём первый элемент набора будет содержать имя файла текущего объекта, которое мы назначили переменной-члену tags при создании объекта изображения с помощью метода ImageItem. В этом случае данная строка текста выводится на холст методом CanvasText в строке 76. Поскольку поймать движущийся объект и щелкнуть на нем правой кнопкой мыши может оказаться сложно, щелкните предварительно левой кнопкой мыши, чтобы методом snap() "заморозить" снующие объекты, после чего щелкните на понравившемся объекте правой кнопкой мыши. Результат показан на рис. 22.4.

Рис. 22.4. Возвращение имени файла объекта изображения

  Нашу программу можно дорабатывать и дальше, снабжая её все новыми и новыми возможностями. Некоторые задачи, над которыми Вы можете поработать самостоятельно, предложены в разделе "Примеры и задания" в конце главы.

  В следующих разделах мы рассмотрим использование специальных методов размещения объектов в окне.

Менеджеры геометрии объектов

  Библиотека Tkinter содержит три метода, с помощью которых можно управлять размещением объектов внутри других объектов-контейнеров, изменять размеры встроенных объектов и позиционировать их относительно друг друга. Я не собираюсь тратить на эту тему много времени, во-первых, потому, что с одним из методов Вы уже познакомились при изучении графических объектов библиотеки Tkinter, а во-вторых, поскольку их использование предельно просто и не требует создания сложных конструкций.

  Ниже перечислены три метода управления размещением, называемых ещё менеджерами геометрии объектов, в порядке их усложнения:

  Поскольку Python является объектно-ориентированным языком, в котором все графические объекты являются классами, унаследованными от общего базового класса, данные методы наследуются всеми классами графических объектов. В других языках программирования такие методы создаются отдельно с использованием средств программирования графического интерфейса и требуют передачи им в параметрах указателей на графические объекты, над которыми следует выполнять операции. В Python эти методы можно вызвать в любой момент для любого графического объекта как обычный метод place(), pack() или grid().

Метод place()

  Несмотря на то что метод place() имеет простейший интерфейс, его использование может вызвать некоторые затруднения. С его помощью выполняют следующие задачи:

  Как правило, в программах не устанавливаются жёсткие координаты объектов в окне, иначе у пользователя не будет никакой возможности изменить их во время выполнения программы. Для изменения координат объектов используется метод place, который вызывается непосредственно для изменяемого объекта. Например:

х = Button(root)

text="Кнопка"

x.place(x=10,y=10)

  В результате выполнения этого кода кнопка будет помещена на расстоянии 10 пикселей от верхнего и левого краев объекта-контейнера. Не менее полезна возможность управления положением объекта в относительных координатах. В листинге 22.6 показано определение класса, в котором метод place() используется для того, чтобы разместить вложенные графические объекты по периметру окна-контейнера.

Листинг 22.6. Относительное размещение объектов в окне

class Placer:

  def __init__(self, w):

    global death

    opts=[(N,0.5,0), (NE,1,0), (E,l,0.5), {SE,1,1), \

      (5,0.5,1), (SW,0,1), (W,0,0.5), (NW,0,0),\

      (CENTER,0.5,0.5)]

 

    for an, xx, yy in (opts):

      item=Button(w,  text=an)

      item.place (relx=xx, rely=yy, anchor=an)

      if an == CENTER:

        item["command"] = die

        item["image"] = death

      else:

        item["command"] = lambda x=an

    prn(x)

  По нумерации строк Вы видите, что в листинге 22.6 представлена часть большой программы tkgeom.py. На рис. 22.5 показано окно, создаваемое классом Placer.

  *Прим. В. Шипкова: уже традиционно для графики рисунок пришлось заменить на более актуальный.

Рис. 22.5. Относительное размещение графических объектов внутри окна с помощью метода place

  При относительном размещении объектов в окне длина сторон окна принимается равной 1 (не сантиметру и не метру, а абстрактной единице). Координаты объекта в окне в этом случае определяются не в пикселях, а в десятых долях от длины стороны окна. Список opts содержит элементы, представленные наборами из трёх аргументов, необходимых методу plасе(). Этот метод вызывается в строке 76 в теле цикла for. При первой итерации цикла в теле цикла происходит распаковка первого элемента списка opts и значения набора присваиваются соответственно переменным an, xx и уу. Эти переменные передаются методу place в качестве аргументов. При этом используется следующий синтаксис (имена переменных заменены соответствующими значениями первого набора списка opts):

item.place(relx=0.5,rely=0,anchor=N)

  Эта запись означает: отцентровать объект по верхнему (северному — n) краю окна. Как Вы видите на рис. 22.5, кнопка n действительно находится посередине верхнего края окна. В строке 77 проверяется значение параметра anchor текущего объекта, и если это значение CENTER, то событию щелчка на кнопке назначается выполнение функции die(), а вместо текста на кнопке выводится рисунок из файла, объект которого присваивается параметру image. В результате в центре окна вместо тоскливой кнопки "Выход" мы получили яркий значок, на котором изображен бог смерти индейцев Майя. Возможно, интерфейс получился не очень дружественным, так как назначение кнопки не столь очевидно, как если бы на ней просто было бы написано "Выход", но моя цель в данном случае состояла в том, чтобы показать Вам способ подмены текста в объекте кнопки и ярлыка на пиктограмму, хранящуюся в графическом файле. Причем если объекту уже присвоена строка текста, то она автоматически будет замещена изображением сразу же после инициализации свойства image. Но только предварительно Вам нужно создать объект изображения с помощью метода PhotoImage(). (В листинге 22.6 не показана строка с методом PhotoImage() по той причине, что эта операция в программе tkgeom.py выполнялась в начале кода.) Проанализируйте другие элементы списка opts и постарайтесь представить, где метод place() разместит в окне соответствующие кнопки и как они будут выглядеть. Обратите внимание, что если изменить размеры окна, относительное размещение кнопок в окне сохранится прежним.

Метод pack()

  Как Вы видели во всех предыдущих примерах, метод pack() вызывается каждый раз перед выводом графического объекта на экран. Если метод place() удобно использовать для размещения объектов в окнах в абсолютных или относительных координатах, то метод pack() особенно полезен в тех случаях, когда нужно создать ряды и колонки объектов, или, другими словами, создавать "упаковки" объектов (отсюда и название pack), опредёленным образом ориентированные в корневом окне. Код класса Packer все той же программы tkgeom.py показан в листинге 22.7.

Листинг 22.7. Класс Packer

class Packer:

  def __init__(self,w):

    global death

    n=0

    self.a=Frame(w)

    self.a.pack(side=TOP,expand=l,fill=BOTH)

    for i in range(3):

      item=Button(self.a, text='n')

      item.pack{side=LEFT, expand=l, fill=BOTH)

      item["command"] = lambda x=n : prn(x)

      n=n+1

    self.b=Frame(w)

    self.b.pack(side=TOP, expand=l, fill=BOTH)

    for i in range(3):

      item=Button(self.b, text='n')

      item, pack (side=LEFT, expand=l, fill=BOTH)

      if i==1:

        item["command"]=die

        item["image"] = death

      else:

        item["command"]=lambda x=n : prn(x)

      n=n+1

    self.с=Frame(w)

    self.с.pack(side=TOP, expand=l, fill=BOTH)

    for i in range(3):

      item=Button(self.c,text='n')

      item.pack(side=LEFT, expand=l, fill=BOTH)

      item["command"]=lambda x=n : prn(x)

      n=n+1

  В программе создаются 9 кнопок, как и в классе Place, но в данном случае создаются три рамки (self.a, self.b и self.с), в каждую из которых добавляются по три кнопки. Центральной кнопке второй рамки назначаются выполнение функции die() и объект изображения. В итоге мы получаем стек из трёх рамок, каждая из которых представляет собой стек из трёх кнопок. Окно, полученное в результате реализации класса Place, показано на рис. 22.6.

  *Прим. В. Шипкова: рисунок заменён.

Рис. 22.6. Ряды объектов, созданные с помощью метода pack()

  Кнопки 0, 1 и 2 принадлежат рамке self.а, кнопка 3, кнопка с пиктограммой и кнопка 5 принадлежат рамке self.b, а кнопки 6, 7 и 8 — рамке self.с. Размеры и форму окна можно произвольно изменять с помощью мыши. Посмотрите, как себя при этом будут вести вложенные кнопки. Они меняют свой размер в тех же пропорциях, по-прежнему заполняя всю поверхность корневого окна. Это происходит потому, что для всех объектов рамок и объектов кнопок были установлены следующие свойства: expand=l и fill=BOTH. Попробуйте изменить значение свойства fill для всех объектов и посмотрите, как изменится поведение окна. Например, попробуйте присвоить этому свойству значения None, X и Y.

Метод grid()

  Это, пожалуй, самый интересный и полезный из всех методов управления размещением объектов. Этот метод использует функции рисования таблицы из надстройки troff. Если эти названия Вам ничего не говорят, не волнуйтесь. Пока что детали работы метода Вам не нужны, сконцентрируйтесь на принципах его использования. Метод grid () разбивает окно на конструкцию из строк и столбцов. Часто даже не нужно вычислять самостоятельно, сколько строк и столбцов нужно. Можно написать код, который будет автоматически вычислять значения аргументов метода grid(). Чтобы разобраться в принципах использования этого метода, рассмотрим листинг 22.8, в котором показано определение класса Gridder в программе tkgeom.py.

Листинг 22.8. Класс Gridder

class Gridder:

  def __init__ (self ,w):

    global death

    names=[ "A","B","C", "D" ,"E" ,"F",

     "G","H","I",]

    n=0

    for i in range{3):

      for j in range(3):

        item=Button(w,text=names[n] )

    if names [n]=="E":

      item["command"]=die

      item["image"]=death

    else:

      item["command"]=lambda x=names[n] : prn(x)

      item.grid(row=i , column= j , sticky=E+W+N+S )

    n = n+1

 

  Мы вновь создаём 9 кнопок с помощью двух вложенных циклов for. Обратите внимание на вызов метода grid() в строке 28, в результате которого каждой кнопке задаётся соответствующая ячейка в таблице. В этой же строке происходит инициализация свойства sticky набором константных значений, указывающих, что форма объектов может произвольно изменяться в пределах ячейки во всех направлениях (константы E+W+N+S означают на восток, запад, север и юг). На рис. 22.7 показано окно, созданное классом Gridder.

Рис. 22.7. Таблица кнопок, созданная с помощью метода grid()

  Вам, наверное, показалось, что результат выполнения метода grid() не сильно отличается от результата выполнения метода pack() (см. рис. 22.6). Но в действительности отличия существенные. Мы убедимся в этом, когда создадим сложную конструкцию объектов в корневом окне. Например, попробуем создать ещё одну строку таблицы с помощью метода grid() и вместо одного объекта добавим в эту строку пакет объектов, созданный с помощью метода pack(). Соответствующий код показан в листинге 22.9.

Листинг 22.9. Совместное использование методов pack и grid

class Gridder:

  def __init__(self, w):

    global death

    names=["A","B","C", "D","E","F", "G","H","I",]

    n=0

    for i in range(3):

      for j in range(3):

        item=Button(w,text=names[n])

    if names[n]=="E":

      item["command"]=die

      item["image")=death

    else:

     item["command"]=lambda x=names[n] : prn(x)

     item.grid(row=i, column=j, sticky=E+W+N+S)

    n=n+1

    f=Frame(w)

    b1=Button(f, text="Левая кнопка")

    b1["command"]=lambda x="left" : prn(x)

    b2=Button(f,text="Пpaвaя кнопка")

    b2["command"] = lambda x="right" : prn(x)

    b1.pack(side=LEFT)

    b2.pack(side=RIGHT)

    f.grid(row=3,column=0,columnspan=3)

Результат выполнения этой программы показан на рис. 22.8.

Рис. 22.8. Совместное использование методов grid() и pack()

  Было бы сложно также выровнять наборы из трёх и двух кнопок только с помощью одного метода pack(). При использовании данного подхода будьте внимательны во время определения объектов-контейнеров. Так, кнопки от А до I и рамка f принадлежат коренному окну w, тогда как две нижние кнопки принадлежат рамке f. Если в параметрах объектов кнопок b1 или b2 вместо объекта-контейнера f указать w, то между окном и рамкой возникнет неразрешимый конфликт. Один метод управления размещением объектов можно использовать внутри другого, но их нельзя использовать в программе таким образом, чтобы они ссылались на один и тот же объект-контейнер.

  В следующей главе при создании программы mandelbrot.py мы вновь воспользуемся конструкцией из метода pack(), вложенного в метод grid().

Резюме

  В этой главе нам удалось создать программу с достаточно сложной анимацией, и Вы познакомились с тремя менеджерами геометрии объектов: pack(), place() и grid(). В следующей главе мы займёмся разработкой достаточно сложной программы, выполняющей вычисления наборов Мандельброта и построением на их основе сложных графических изображений.

Практикум

Вопросы и ответы

  Является ли Alchemy Workshop единственной компанией, создающей утилиты для поддержания прозрачности в изображениях формата GIF?

  Нет. Вопросами поддержания прозрачности в графических изображениях и анимацией занимается также компания CompuPic Photodex. Причем её утилиты поддерживают работу с файлами разных форматов, а не только GIF. Правда, среди этих утилит нет таких, которые распространялись бы бесплатно. В Internet можно найти много подобных утилит, но вопрос их легальности остаётся открытым, поэтому я не хотел бы давать Вам каких-либо рекомендаций, кроме двух вышеупомянутых компаний.

  Код листинга 22.9 довольно сложен.  Нельзя ли упростить его каким-либо способом и обойтись без метода pack?

  Если первая строка с кнопками А, В и С была разделена на 3 колонки, уже невозможно сообщить методу grid() о том, что последнюю строку следует разделить на 2 колонки. Если Вы предпочитаете выполнить эту задачу только с помощью метода grid(), то в нём следует установить деление строки на 6 колонок, после чего для кнопок первых трёх строк объединять по 2 ячейки с помощью метода columnspan, а в последней строке с помощью этого же метода объединить по 3 ячейки вместе. Но в результате код получится ещё сложнее, чем показанный в листинге 22.9.

Контрольные вопросы

  1. Чем были заменены тригонометрические функции в программе tkfield4.py?

    а) Генератором случайных чисел.

    б) Предустановленными координатами х и у.

    в) Логарифмической таблицей.

    г) Альтернативными математическими вычислениями.

  2. Почему в программе tkfield4.py мы не создавали отдельные таймеры для всех перемещающихся объектов графических изображений?

    а) Нам бы пришлось 20 раз переписать один и тот же код.

    б) Мы так и поступили, просто в программе это выглядит как один таймер.

    в) В Windows возможность одновременного использования программе нескольких таймеров сильно ограничена.

    г) Мы сделали так для наглядности кода, хотя в реальной программе следовало бы снабдить таймером каждый объект.

  3. Каким образом в программе tkfield4.py останавливается движение пиктограмм по экрану?

    a) Удаляется вызов таймера.

    б) Переменным xdelta и ydelta присваивается одно и то же значение.

    в) Устанавливается флаг, который сообщает функции обработки ситуации, что необходимо завершить работу без выполнения каких-либо действий.

    г) Устанавливается флаг, который сообщает функции обработки ситуации о переустановке себя как функции обработки ситуации и завершении работы до выполнения функций перемещения объектов.

Ответы

  1. а. Умелое использование генератора случайных целых чисел из модуля random позволило нам обойтись без вычислений тригонометрических функций.

  2. в. Несмотря на то что начиная с Windows 95 официально было объявлено о снятии ограничений на число таймеров, используемых одной программой, в действительности одновременное использование более десяти таймеров существенно снижает производительность компьютера. Поэтому при программировании для Windows число таймеров в программе следует сократить до минимума.

  3. г. Щелчок левой кнопкой мыши где-либо в пределах окна программы устанавливает флаг, который распознается функцией обработки события. В результате эта функция восстанавливает себя как функцию обработки события и прекращает свою работу. Новый щелчок мышью снимает флаг.

Примеры и задания

  Чтобы узнать больше о пантеоне богов Майя и познакомиться с их изображениями, посетите узел по адресу http://vww.pauahtun.org/days.html. Здесь Вы найдёте изображения духов 20-ти дней священного календаря Майя.

  Домашняя страница Alchemy Mindworks находится по адресу http://www.mindworkshop.com/alchemy/alchemy.html. Лицензионный договор на использование утилит этой компании для преобразования графических файлов GIF можно найти по адресу http://www.mindworkshop.com/alchemy/lzw.html. С помощью программы Gif Construction Set Pro также можно создавать анимацию на основе файлов GIF. Домашняя страница компании CompuPic Photodex находится по адресу http://www.photodex.com.

  Попробуйте сделать так, чтобы окно программы tkfield4.py можно было произвольно изменять во время выполнения. Подсказка: не забудьте, что помимо объектов графических изображений следует учесть все объекты прямоугольников, из которых создаётся игровое поле.

  Попробуйте вместо изображений богов Майя создать и добавить свои собственные графические файлы.

Назад  Оглавление  Дальше