Как и 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=Выбрать ширину рамки u= Выбрать шаг перемещения о=Рисовать овалы 1=Рисовать линии d=Удалить объект 2=Опустить объект Ь=Выполнить заливку фона 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(‘<ButtonPress-1>’, self.onStart)
canvas.bind(‘<B1-Motion>’, self.onGrow)
canvas.bind(‘<Double-1>’, self.onClear)
canvas.bind(‘<ButtonPress-3>’, self.onMove)
canvas.bind(‘<Button-2>’, self.onSelect)
canvas.bind(‘<B2-Motion>’, self.onDrag) parent.bind(‘<KeyPress>’, self.onOptions)
self.createMethod = Canvas.create_oval
self.canvas = canvas
self.moving = []
self.images = []
self.object = None
self.where = None
self.scribbleMode = 0
parent.title(‘PyDraw — Moving Pictures 1.1’) parent.protocol(‘WM_DELETE_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(pickDelays[0])
for i in range(reptY):
canvas.move(object, 0, 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\MovingPics\movingpics_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\MovingPics\movingpics_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()
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011