问题
I am making a GTK application that will draw complex images that can take long time to finish. Because of that I can't do the drawing in the DrawingArea's 'draw' callback. I decided to use Python's multiprocessing module that allows true parallelism and does not have problems with GTK and thread-safety.
Python's multiprocessing module uses Pickle protocol to communicate between processes. GTK and Cairo objects do not implement the protocol. My solution was to convert the Cairo surface to bytes and send it over a Pipe to another process.
After some research I found that Cairo has ways to get and set the internal data of an ImageSurface, but Pycairo for Python 3 does not support it. (Pycairo for Python 2 seems to have the support, but I have some reasons to use Python 3.)
However there is another Python library cairocffi that supports it. So I decided to:
- Draw in a separate process using cairocffi.
- Convert the resulting surface to bytes.
- Send bytes through the Pipe and convert them back to cairocffi surface.
- Convert cairocffi surface to Pycairo surface and display it.
Is this a good way to do it? Does my code have any bugs? I don't really understand the conversion_magic.py
file.
main.py
#!python3.4
from multiprocessing import Process, Pipe
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib,Gtk
import cairocffi
import cairo # also known as pycairo
from conversion_magic import _UNSAFE_cairocffi_context_to_pycairo
from math import pi
import array
# Draw a circle on cairocffi context.
def circle(cr, radius):
cr.set_source_rgb(1,0,0)
cr.paint()
cr.set_source_rgb(0,1,0)
cr.arc(radius, radius, radius, 0, 2 * pi)
cr.fill()
# Drawing process
def example_target(conn):
while True:
# Wait for new task.
radius = conn.recv()
# Create a cairocffi surface, draw on it and send it as bytes.
surface = cairocffi.ImageSurface(cairo.FORMAT_ARGB32, 100,100)
cr = cairocffi.Context(surface)
circle(cr, radius)
surface.flush()
data = surface.get_data()
conn.send(bytes(data))
def main():
# Create 2 connections for two-way communication between processes.
parent,child = Pipe()
proc = Process(target=example_target, args=(child,), daemon=True)
proc.start()
# Tell process to draw circles with radius i.
for i in range(0,50):
parent.send(i)
win = Gtk.Window(default_height=300, default_width=300)
win.connect("delete-event", Gtk.main_quit)
drawing_area = Gtk.DrawingArea()
win.add(drawing_area)
def on_draw(widget,cr):
# Check if we have new images to draw.
if parent.poll():
# Convert recieved data into a cairocffi surface. The arguments of
# the ImageSurface must be the same as those in example_target.
data = parent.recv()
a = array.array('b', data)
cffi_surf = cairocffi.ImageSurface(cairo.FORMAT_ARGB32,
100,100, data = a)
# Convert cairocffi surface to pycairo surface.
cffi_cr = cairocffi.Context(cffi_surf)
pycairo_cr = _UNSAFE_cairocffi_context_to_pycairo(cffi_cr)
pycairo_surf = pycairo_cr.get_target()
# Draw pycairo surface to the surface from GTK. Using cairocffi
# surface would not work here, as GTK uses pycairo.
cr.set_source_surface(pycairo_surf)
cr.paint()
else:
pass
# TODO: Implement a buffer that holds the last image we got from
# parent.recv() and draw it. Not included in this example to make
# things easier.
return True
drawing_area.connect('draw', on_draw)
# Draw new image after each 100ms.
def cause_drawing():
drawing_area.queue_draw()
return True
GLib.timeout_add(100, cause_drawing)
win.show_all()
Gtk.main()
if __name__ == '__main__': main()
conversion_magic.py
# A magical conversion function, taken from cairocffi documentation.
# http://cairocffi.readthedocs.io/en/latest/cffi_api.html#converting-cairocffi-wrappers-to-pycairo
import ctypes
import cairo # pycairo
import cairocffi
pycairo = ctypes.PyDLL(cairo._cairo.__file__)
pycairo.PycairoContext_FromContext.restype = ctypes.c_void_p
pycairo.PycairoContext_FromContext.argtypes = 3 * [ctypes.c_void_p]
ctypes.pythonapi.PyList_Append.argtypes = 2 * [ctypes.c_void_p]
def _UNSAFE_cairocffi_context_to_pycairo(cairocffi_context):
# Sanity check. Continuing with another type would probably segfault.
if not isinstance(cairocffi_context, cairocffi.Context):
raise TypeError('Expected a cairocffi.Context, got %r'
% cairocffi_context)
# Create a reference for PycairoContext_FromContext to take ownership of.
cairocffi.cairo.cairo_reference(cairocffi_context._pointer)
# Casting the pointer to uintptr_t (the integer type as wide as a pointer)
# gets the context’s integer address.
# On CPython id(cairo.Context) gives the address to the Context type,
# as expected by PycairoContext_FromContext.
address = pycairo.PycairoContext_FromContext(
int(cairocffi.ffi.cast('uintptr_t', cairocffi_context._pointer)),
id(cairo.Context),
None)
assert address
# This trick uses Python’s C API
# to get a reference to a Python object from its address.
temp_list = []
assert ctypes.pythonapi.PyList_Append(id(temp_list), address) == 0
return temp_list[0]
Tested with GTK 3.18.9 and Python 3.4 on Windows.
来源:https://stackoverflow.com/questions/43400688/parallel-drawing-with-gtk-and-cairo-in-python-3