3

我正在制作一个 GTK 应用程序,它将绘制可能需要很长时间才能完成的复杂图像。因此,我无法在 DrawingArea 的“绘制”回调中进行绘制。我决定使用 Python 的多处理模块,它允许真正的并行性,并且在 GTK 和线程安全方面没有问题。

Python 的多处理模块使用 Pickle 协议在进程之间进行通信。GTK 和 Cairo 对象不实现该协议。我的解决方案是将 Cairo 表面转换为字节,然后通过管道将其发送到另一个进程。

经过一番研究,我发现 Cairo 有办法获取和设置 ImageSurface 的内部数据,但 Pycairo for Python 3 不支持它。(Python 2 的 Pycairo 似乎有支持,但我有一些理由使用 Python 3。)

然而,还有另一个 Python 库cairocffi支持它。所以我决定:

  1. 使用 cairocffi 在单独的进程中绘制。
  2. 将生成的表面转换为字节。
  3. 通过管道发送字节并将它们转换回 cairocffi 表面。
  4. 将 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 进行了测试。

4

0 回答 0