Clutter Actor 的位置与尺寸约束
Clutter 不支持多线程

DIY 一个用于显示 PDF 页面的 Clutter Actor

Garfileo posted @ 2011年7月31日 16:03 in Clutter 笔记 with tags clutter poppler , 3270 阅读

最近打算基于 Clutter 做一个 PDF 文档的演示工具,首先要解决在 Clutter 场景中渲染 PDF 页面的问题。可以通过 Poppler 库将 PDF 页面转换为 Cairo 图形,进而利用 Clutter 的 CairoTexture Actor 进行 Cairo 图形渲染解决这一问题 [1]。本文将上述过程中的一些细节(例如尺寸与位置的控制)封装为一个便于使用的 Clutter Actor。

ClutterActor 子类的声明

下面的代码声明了 CkdPage 类,继承自 ClutterActor 类。相关知识可参考 GObject 子类化文档 [2, 3]。

#ifndef CKD_PAGE_H
#define CKD_PAGE_H

#include <glib.h>
#include <clutter/clutter.h>

#define CKD_TYPE_PAGE (ckd_page_get_type ())
#define CKD_PAGE(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), CKD_TYPE_PAGE, CkdPage))
#define CKD_IS_PAGE(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), CKD_TYPE_PAGE))
#define CKD_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), CKD_TYPE_PAGE, CkdPageClass))
#define CKD_IS_PAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), CKD_TYPE_PAGE))
#define CKD_PAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), CKD_TYPE_PAGE, CkdPageClass))

typedef struct _CkdPage CkdPage;
typedef struct _CkdPageClass CkdPageClass;

struct _CkdPage {
        ClutterActor parent_instance;
};

struct _CkdPageClass {
        ClutterActorClass parent_class;
};

GType ckd_page_get_type (void);

/* Public functons */
ClutterActor *ckd_page_new (PopplerPage *page);

#endif

属性

CkdPage 类有 3 个属性:

typedef struct _CkdPagePriv {
        PopplerPage *pdf_page;
        ClutterActor *cairo_texture;
        gfloat scale;
} CkdPagePriv;

enum {
        PROP_0,
        PROP_PDF_PAGE
};

其中 pdf_page 是 PDF 页面实例,cairo_texture 为 Clutter 的 Cairo 纹理实例,scale 是 cairo_texture 相对 pdf_page 的缩放比例。

上述的三个属性中,只有 pdf_page 可被外部访问,为了实现这一点,需要以下代码:

static void
ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec)
{
        CkdPage *self = CKD_PAGE (obj);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);
        gdouble w, h;

        switch (prop_id) {
        case PROP_PDF_PAGE:
                priv->pdf_page = g_value_get_pointer (value);
                if (priv->cairo_texture) {
                        clutter_actor_destroy (priv->cairo_texture);
                }
                poppler_page_get_size (priv->pdf_page, &w, &h);
                priv->cairo_texture = clutter_cairo_texture_new (w, h);
                clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self));
                clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE
                                                       (priv->cairo_texture), TRUE);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
                break;
        }
}

static void
ckd_page_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec)
{
        CkdPage *self = CKD_PAGE (obj);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        switch (prop_id) {
        case PROP_PDF_PAGE:
                g_value_set_pointer (value, priv->pdf_page);
                break;
        default:
                G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
                break;
        }
}

static void
ckd_page_class_init (CkdPageClass *klass)
{
        g_type_class_add_private (klass, sizeof (CkdPagePriv));

        GObjectClass *base_class = G_OBJECT_CLASS (klass);

        base_class->set_property = ckd_page_set_property;
        base_class->get_property = ckd_page_get_property;

        GParamSpec *pspec;

        pspec =
            g_param_spec_pointer ("pdf-page", "PDF Page",
                                  "A PDF Page which to be convert to CairoTexture",
                                  G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT);
        g_object_class_install_property (base_class, PROP_PDF_PAGE, pspec);
}

其中的要点是每次为 CkdPage 实例设置 pdf_page 属性时,都要释放已有的 cairo_texture 并重新分配。还要将 cairo_texture 的尺寸设置为自动与其所包含的 Cairo 图形(Cairo Surface) 的尺寸同步,如果 cairo_texture 与 Cairo 图形的尺寸不一致,会导致 Cairo 图形显示不完全或过小。另外也不要忘了将 CkdPage 实例设为 cairo_texture 的父 Actor。

如果看不明白上述代码,那说明你需要了解有关 GObject 子类属性方面的知识,可参考 [4]。

ClutterActor 子类的内存管理

在谈论 ClutterActor 子类的内存管理之前,我们需要先搞明白 ClutterActor 类是如何管理内存的。但是,在搞明白 ClutterActor 类的内存管理之前,你需要明白 GObject 的内存管理方法,可能你需要阅读 [5]。

通常,要释放一个 ClutterActor 占用的资源,可使用 clutter_actor_destroy()。虽然使用 g_object_unref() 有时也可以成功,但是几率非常之小。这是因为在 Clutter 中,各个 Actor 之间均存在某种联系,构成一个有向无环图,因此存在许多的引用计数环(Reference Count Cycle),若想释放某个 Actor 的资源,那么必须要打破与它相关的引用计数环。clutter_actor_destroy() 可以借助 g_object_run_dispose() 解决这一问题。如果一个 Actor 示例的父 Actor 是容器,那么 g_object_run_dispose() 会调用 ClutterActor 的 dispose() 方法,后者又会调用 clutter_container_remove_actor() 释放子 Actor 的资源。对于非容器 Actor,ClutterActor 的 dispose() 方法中会调用 clutter_actor_unparent() 解除它与父 Actor 之间的联系,并消减其引用计数。在 clutter_actor_destroy() 快要结束时,Actor 的引用计数会变为 0,于是其资源便会被释放。

上述过程可以简述为:

clutter_actor_destroy (self)
{
    g_object_ref (self);

    clutter_actor_dispose (self)
    {
        if (self 的父 Actor 是容器)
            clutter_container_remove_actor() 将 slef 从其父 Actor 中移除;
        else
            clutter_actor_unparent() 将 self 从其父 Actor 中脱离,并消减 self 的引用计数,此时 self 的引用计数为 1;
    }
    g_object_unref (self) 释放 self 自身,即调用 clutter_actor_finalize();
}

但是,上述过程中还有一个有趣的环节,那就是在 clutter_actor_dispose() 的最后会发射一个 "destroy" 信号,该信号默认的处理器是 ClutterActor 类的 destroy() 虚函数。也就是说如果我们在一个 ClutterActor 的子类中提供了 destroy() 函数的实现,那么该函数会在 clutter_actor_dispose() 的执行中被调用。

在 [6] 给出的示例程序中指出,对于自定义的复合 Actor,应当提供 destroy() 函数。那么在这个 destroy() 函数中应当处理哪些事情?自然是释放复合 Actor 所包含的那些 Actor。对于 CkdPage 这个 Actor 而言,要释放的 Actor 是 cairo_texture。因此 CkdPage 的 destroy() 函数的实现如下:

static void
ckd_page_destroy (ClutterActor *self)
{
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        if (priv->cairo_texture) {
                clutter_actor_destroy (priv->cairo_texture);
                priv->cairo_texture = NULL;
        }

        if (CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->destroy)
                CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->destroy (self);
}

由于 ckd_page_destroy() 是 ClutterActor 的虚函数的具体实现,那么需要在 ckd_page_class_init() 中将其作为 destroy 的实例,如下:

static void
ckd_page_class_init (CkdPageClass *klass)
{
        /* 省略了前面已出现的部分代码 */

        ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
        actor_class->destroy = ckd_page_destroy;
}

将自定义的 Actor 嵌入场景

上面的内容虽然是与 Clutter 自定义 Actor 相关,但实质上它们是 GObject 层面的知识。真正与 Clutter 相关的是我们自定义的 Actor 如何嵌入到 Clutter 场景并显示。

为了便于实现自定义的 Actor 嵌入场景,Clutter 为 ClutterActor 类提供了三个虚函数,即 get_preferred_width(), get_preferred_height() 以及 allocate(),每个 ClutterActor 的子类通常需要实现它们。但是如果你想显式的控制自定义 Actor 的尺寸与位置,像 ClutterRecangle、ClutterTexture 这样的 Actor,就没必要实现 get_preferred_width() 和 get_preferred_height(),或者自定义的 Actor 既非容器也非复合对象,那么就没必要实现 allocate()。

显然,CkdPage 是一个复合对象,因为它包含了一个 Cairo 纹理 Actor,同时我们也期望它能够自适应父 Actor 的变化,例如拖曳窗口时,Cairo 纹理 Actor 能自适应的放大或缩小。所以,CkdPage 必须实现上述的三个虚函数。

首先来看 get_preferred_width() 与 get_preferred_height() 的实现:

static void
ckd_page_get_size (PopplerPage *page, gfloat *width, 
                   gfloat *height, gfloat *aspect)
{
        gdouble w, h, a;

        poppler_page_get_size (page, &w, &h);
        if (h < G_MINFLOAT)
                h = G_MINFLOAT;
        a = w / h;

        if (width)
                *width = w;
        if (height)
                *height = h;
        if (aspect)
                *aspect = a;
}

static void
ckd_page_get_parent_size (ClutterActor *actor, gfloat *width, 
                          gfloat *height, gfloat *aspect)
{
        gfloat w, h, a;
        ClutterActor *parent = clutter_actor_get_parent (actor);
        ClutterActorBox box;

        clutter_actor_get_size (parent, &w, &h);

        if (h < G_MINFLOAT)
                h = G_MINFLOAT;
        a = w / h;

        if (width)
                *width = w;
        if (height)
                *height = h;
        if (aspect)
                *aspect = a;
}

static void
ckd_page_get_preferred_width (ClutterActor *actor, gfloat for_height, 
                              gfloat *min_width, gfloat *natural_width)
{
        CkdPage *self = CKD_PAGE (actor);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        gfloat w, h, a;
        ckd_page_get_parent_size (actor, &w, &h, &a);

        gfloat pw, pa;
        ckd_page_get_size (priv->pdf_page, &pw, NULL, &pa);

        if (a > pa)
                w = h * pa;

        *min_width = 0;
        *natural_width = w;

        priv->scale = w / pw;
}

static void
ckd_page_get_preferred_height (ClutterActor *actor, gfloat for_width, 
                               gfloat *min_height, gfloat *natural_height)
{
        CkdPage *self = CKD_PAGE (actor);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        gfloat w, h, a;
        ckd_page_get_parent_size (actor, &w, &h, &a);

        gfloat ph, pa;
        ckd_page_get_size (priv->pdf_page, NULL, &ph, &pa);

        if (a <= pa)
                h = w / pa;

        *min_height = 0;
        *natural_height = h;

        priv->scale = h / ph;
}

static void
ckd_page_class_init (CkdPageClass *klass)
{
        /* ==> 此处省略了前面已出现的部分代码 */

        ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
        
        /* ==> 此处省略了前面已出现的部分代码 */

        actor_class->get_preferred_height = ckd_page_get_preferred_height;
        actor_class->get_preferred_width = ckd_page_get_preferred_width;
}

由于 ckd_page_get_preferred_width() 与 ckd_page_get_preferred_height() 在实现上比较相似,因此仅以 ckd_page_get_preferred_width() 为例讲述 CkdPage 在 get_preferred_width() 实现中所体现的逻辑。

在 ckd_page_get_preferred_width(),首先调用自定义的 ckd_page_get_parent_size() 函数(基于 clutter_actor_get_size() 实现)获取 CkdPage Actor 实例的父 Actor 实例的尺寸,并调用自定义的 ckd_page_get_size() 函数,获取 PDF 页面(CkdPage 的 pdf_page 属性)尺寸,然后根据父 Actor 实例的尺寸以及 PDF 页面宽高比确定 CkdPage 实例的宽度,并记录该宽度相对 PDF 页面尺寸的缩放因子(CkdPage 的 scale 属性)。

上面虽然回答了 get_preferred_width / height() 的实现,但是还没有指出这两个函数究竟有什么用。其实很简单,当你将 CkdPage 实例添加到某个容器实例中的时候,这个容器实例便会调用 CkdPage 的这两个函数为 CkdPage 实例打造一个包围盒,并根据这个包围盒来在 Clutter 场景中为 CkdPage 实例分配一定的空间。

如果说 get_preferred_width / height() 是面向上级 Actor 的,那么 allocate() 则是面向下级的。例如 CkdPage 实例包含着一个 Cairo 纹理实例(ClutterCairoTexture),在 CkdPage 实例的上层 Actor 为 CkdPage 实例安排好了空间,那么 CkdPage 实例便应当在这个空间内为 Cairo 纹理实例分配一定的空间,而且分配方法也不固定,需要结合实际需要。例如可以在 CkdPage 的 allocate() 中通过调用 Cairo 纹理 Actor 的 get_preferred_width / height() 为其分配空间;也可以直接将 CkdPage 实例所得空间赋予 Cairo 纹理实例,下面的 ckd_page_allocate() 便是这样做的。

static void
ckd_page_allocate (ClutterActor *actor, const ClutterActorBox *box, ClutterAllocationFlags flags)
{
        CkdPage *self = CKD_PAGE (actor);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        CLUTTER_ACTOR_CLASS (ckd_page_parent_class)->allocate (actor, box, flags);

        gfloat w = clutter_actor_box_get_width (box);
        gfloat h = clutter_actor_box_get_height (box);
        ClutterActorBox page_box = { 0, 0, w, h };

        clutter_actor_allocate (priv->cairo_texture, &page_box, flags);
}

static void
ckd_page_class_init (CkdPageClass *klass)
{
        /* ==> 此处省略了前面已出现的部分代码 */

        ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
        
        /* ==> 此处省略了前面已出现的部分代码 */

        actor_class->allocate = ckd_page_allocate;
}

渲染

对于自定义的 Actor,若在场景中显示它,那么还需要实现 paint() 虚函数,除非这个 Actor 可被父类的 paint() 函数渲染。例如,你定义的 Actor 是 ClutterRectangle 的子类,而你在这个子类中所添加的功能与渲染无关,那么就没必要再覆盖 ClutterRectangle 所提供的 paint() 函数了。

CkdPage 是个复合 Actor,其父类是 ClutterActor,没有提供 paint() 虚函数的实现,因此我们必须自行实现。如下:

static void
ckd_page_paint (ClutterActor *actor)
{
        CkdPage *self = CKD_PAGE (actor);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        clutter_actor_paint (priv->cairo_texture);
}

static void
ckd_page_class_init (CkdPageClass *klass)
{
        /* ==> 此处省略了前面已出现的部分代码 */

        ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
        
        /* ==> 此处省略了前面已出现的部分代码 */

        actor_class->paint = ckd_page_paint;
}

这个 paint() 的实现是非常简单的,只是调用了 ClutterCairoTexture 所实现的 paint() 函数。

真正的复杂的是在前面设置 CkdPage 的 pdf_page 属性之处,即:

static void
ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec)
{
        /* ==> 此处省略前面出现的部分代码 */
        switch (prop_id) {
        case PROP_PDF_PAGE:
                priv->pdf_page = g_value_get_pointer (value);
                if (priv->cairo_texture) {
                        clutter_actor_destroy (priv->cairo_texture);
                }
                poppler_page_get_size (priv->pdf_page, &w, &h);
                priv->cairo_texture = clutter_cairo_texture_new (w, h);
                clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self));
                clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE
                                                       (priv->cairo_texture), TRUE);
                break;
        /* ==> 此处省略前面出现的部分代码 */
}

上述代码表示每次为 CkdPage 实例设置 pdf_page 属性时,都要创建一个 ClutterCairoTexture 实例(即 CkdPage 实例的 cairo_texture 属性)。从 Clutter 1.7.6 版本开始,ClutterCairoTexture 实例的渲染改为通过 "draw" 信号实现。因此在上面代码中,每次创建 ClutterCairoTexture 实例之后,需要将它与 "draw" 信号连接,并实现相应的信号处理函数,具体如下:

static gboolean
ckd_page_render (ClutterCairoTexture *actor, cairo_t *cr, gpointer user_data)
{
        CkdPage *self = CKD_PAGE (user_data);
        CkdPagePriv *priv = CKD_PAGE_GET_PRIVATE (self);

        clutter_cairo_texture_clear (actor);

        if (priv->pdf_page) {
                cairo_scale (cr, priv->scale, priv->scale);
                poppler_page_render (priv->pdf_page, cr);
        }
        return TRUE;
}

static void
ckd_page_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec)
{
        /* ==> 此处省略前面出现的部分代码 */
        switch (prop_id) {
        case PROP_PDF_PAGE:
                priv->pdf_page = g_value_get_pointer (value);
                if (priv->cairo_texture) {
                        clutter_actor_destroy (priv->cairo_texture);
                }
                poppler_page_get_size (priv->pdf_page, &w, &h);
                priv->cairo_texture = clutter_cairo_texture_new (w, h);
                clutter_actor_set_parent (priv->cairo_texture, CLUTTER_ACTOR (self));
                clutter_cairo_texture_set_auto_resize (CLUTTER_CAIRO_TEXTURE
                                                       (priv->cairo_texture), TRUE);
                
                /* ==> 将 ClutterCairoTexture 实例与 "draw" 信号连接 */
                g_signal_connect (priv->cairo_texture, "draw", G_CALLBACK (ckd_page_render), self);
                break;
        /* ==> 此处省略前面出现的部分代码 */
}

值得注意的是,在 "draw" 信号处理器,即 ckd_page_render() 中,需要使用 ckd_page_get_preferred_width / height() 函数中记录的 CkdPage 实例的 scale 属性对 Cairo 图形进行缩放,以充满 CkdPage 在场景中所占据的空间。

Demo

上文中所实现的 CkdPage Actor 的全部源码见 ckd-page.tar.gz

下面是一个 demo 程序。

#include <poppler.h>
#include "ckd-page.h"

int
main(int argc, char *argv[])
{
        if (clutter_init (&argc, &argv) != CLUTTER_INIT_SUCCESS)
                return 1;

        /* 使用 Poppler 库函数获得 PDF 页面,需要自己设定 PDF 的 uri 路径 */
        gchar *file = "file:///path/to/your/pdf/file";
        PopplerDocument *doc = poppler_document_new_from_file (file, NULL, NULL);
        PopplerPage *page = poppler_document_get_page (doc, 0);
        
        /* 创建场景 */
        ClutterColor stage_color = { 0x21, 0x43, 0x5e, 0xff };
        ClutterActor *stage = clutter_stage_get_default ();
        clutter_stage_set_title (CLUTTER_STAGE (stage), "CkdPage Demo!");
        clutter_actor_set_size (stage, 400, 400);
        clutter_stage_set_color (CLUTTER_STAGE (stage), &stage_color);
        clutter_stage_set_user_resizable (CLUTTER_STAGE (stage), TRUE);        

        /* 创建 CkdPage 实例 */
        ClutterActor *ckd_page = ckd_page_new (page);
        clutter_container_add_actor (CLUTTER_CONTAINER(stage), ckd_page);

        /* 使用约束机制将 CkdPage 实例居于场景中央 */
        ClutterConstraint *x_center, *y_center;
        x_center = clutter_align_constraint_new (stage, CLUTTER_ALIGN_X_AXIS, 0.5);
        y_center = clutter_align_constraint_new (stage, CLUTTER_ALIGN_Y_AXIS, 0.5);
        clutter_actor_add_constraint (ckd_page, x_center);
        clutter_actor_add_constraint (ckd_page, y_center);

        g_signal_connect (stage, "destroy", G_CALLBACK (clutter_main_quit), NULL);
        
        clutter_actor_show_all (stage);
  
        clutter_main ();

        /* 释放资源,虽然本例中没必要,只是表达了这样一种行为 :) */
        clutter_actor_destroy (ckd_page);
        g_object_unref (G_OBJECT (page));
        g_object_unref (G_OBJECT (doc));
          
        return 0;
}

参考资料

[1] 在 Clutter 场景中显示 PDF 页面

[2] 使用 GObject 库模拟类的数据封装形式

[3] GObject 常用的宏

[4] GObject 子类私有属性的外部访问

[5] GObject 子类实例的析构过程

[6] The Clutter CookBood:自定义一个简单的 Actor

转载时,希望不要链接文中图片,另外请保留本文原始出处:http://garfileo.is-programmer.com


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter