Я уже писал об использовании PyGTK и Cairo для создания собственого векторного графического редактора. К сожалению, при большом количестве объектов отрисовка не выглядит плавной, что крайне неприятно. Рассмотрим, как исправить эту проблему.
Буферизация изображения перед отрисовкой
Одна из причин мельканий заключается в том, что мы каждый раз перерисовываем все объекты на графическом контексте. Вместо этого можно рисовать сначала в буфер ImageSurface, а затем отрисовывать его на графическом контексте виджета:
surface = cairo.ImageSurface(cairo.FORMAT_RGB24, self.rect.width, self.rect.height)
scr = cairo.Context(surface)
# Рисуем на scr...
cr.set_source(cairo.SurfacePattern(surface))
cr.paint()
Кроме того можно использовать два буфера: один со всеми элементами кроме редактируемого
Хотя это и решило проблему с "мельканиями", более явной стала другая - изменение размера прямоугольника не всегда поспевает за движениями мыши пользователя. Связано это с тем, что обработчик события motion-event (движение мышью) каждый раз генерирует событие expose, и пока не будет перерисован контекст. Выход из этой ситуации - использовать отдельный поток для обработки motion_event.
Добавляем потоки
Перед вызовом gtk.main() нужно вызвать threads_init, а сам вызов обернуть в соответствующие функции:
def main(self):
self.win.show_all()
gtk.threads_init()
try:
gtk.threads_enter()
gtk.main()
gtk.threads_leave()
finally:
self.drawing_area.stop_threads()
При использовании потоков не следует совместно использовать объекты между потоками (например описания событий, передаваемые в обработчик). Кроме того, pygtk должен быть собран с опцией --enable-threads
Все обращения к gtk из внешних потоков также должны быть обернуты в thread_enter и threads_leave. Теперь создадим поток, а для передачи сообщений ему будем использовать синхронную очередь Queue:
def __init__(self):
...
# Создаем и запускаем поток
self.motion_thr = threading.Thread(target = self.motion_thread)
self.motion_queue = Queue()
self.motion_thr_running = True
self.motion_thr.start()
def stop_threads(self):
# Посылаем в очередь пустые объекты, чтобы
# выйти из блокирующего get() и завершаем поток
self.motion_thr_running = False
self.motion_queue.put((None, None, None))
def on_motion(self, widget, event):
if self.cur_element is None:
return
is_shift = event.state & gtk.gdk.SHIFT_MASK
self.motion_queue.put((event.x, event.y, is_shift))
def motion_thread(self):
proc_events = False
proc_time = time.time()
while self.motion_thr_running:
if proc_events and (time.time() - proc_time) > 0.01:
# Если есть хотя бы одно событие (proc_events) и они
# накапливались в течение 10 мс, обработать последнее и
# перерисовать область
self.async_motion(x, y, is_shift)
# Перерисовываем область
gtk.threads_enter()
area = self.cur_element.get_area() \
if self.cur_element is not None \
else None
self.redraw(area)
gtk.threads_leave()
proc_events = False
proc_time = time.time()
try:
if self.cur_element is None:
# Опрашиваем очередь, чтобы получить новое событие
# Если в течение 5 мс не появится новое, разблокируемся
x, y, is_shift = self.motion_queue.get(False, 0.005)
else:
# Блокируемся пока не получим первое событие
x, y, is_shift = self.motion_queue.get()
except Empty:
continue
proc_events = True
def async_motion(self, x, y, is_shift):
# Собственно обработчик события
...
Теперь вместо того, чтобы сразу инициировать событие expose, поток motion_thread будет ждать не менее чем 10 мс:
VIDEO
Скачать обновленный пример можно тут: pygtk-cairo-0.2.zip (13,9 Кб)
Ссылки
Делаем свой виджет с помощью PyGTK и Cairo
Использование модуля threading в Python
PyGTK FAQ : I am using a separate thread to run my code, but the application (or the UI) hangs.