我正在制作一个 GTK 应用程序,它将绘制可能需要很长时间才能完成的复杂图像。因此,我无法在 DrawingArea 的“绘制”回调中进行绘制。我决定使用 Python 的多处理模块,它允许真正的并行性,并且在 GTK 和线程安全方面没有问题。
Python 的多处理模块使用 Pickle 协议在进程之间进行通信。GTK 和 Cairo 对象不实现该协议。我的解决方案是将 Cairo 表面转换为字节,然后通过管道将其发送到另一个进程。
经过一番研究,我发现 Cairo 有办法获取和设置 ImageSurface 的内部数据,但 Pycairo for Python 3 不支持它。(Python 2 的 Pycairo 似乎有支持,但我有一些理由使用 Python 3。)
然而,还有另一个 Python 库cairocffi支持它。所以我决定:
- 使用 cairocffi 在单独的进程中绘制。
- 将生成的表面转换为字节。
- 通过管道发送字节并将它们转换回 cairocffi 表面。
- 将 cairocffi 曲面转换为 Pycairo 曲面并显示。
这是一个好方法吗?我的代码有错误吗?我真的不明白这个conversion_magic.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()
转换魔法.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]
在 Windows 上使用 GTK 3.18.9 和 Python 3.4 进行了测试。