GObject 教程

GObject 教程

如何定义和实现一个新的 GObject

本文重点介绍 GObject 子类型的实现,例如创建自定义类层次结构,或子类化 GTK 小部件。

在本章中,使用一个文件查看器程序的运行示例,该程序有一个 ViewerFile 类来表示正在查看的单个文件,以及各种派生类来处理不同类型的文件,如音频文件,具有特殊功能。该示例应用程序还支持编辑文件(例如,调整正在查看的照片),使用一个 ViewerEditable 接口。

模板头文件代码

在编写您的 GObject 代码之前,首先要编写类型头文件,其中包含所需的类型、函数和宏定义。这些元素都只是遵循了几乎所有 GObject 用户都遵循的约定,并且这些约定在多年的 GObject 基础代码开发经验中得到完善。如果您正在编写库,您一定要严格遵守这些约定;您的库的用户会假设您已经这样做了。即使不是在编写库,这也会帮助其他想要参与您的项目的人。

为您的头文件和源代码选择一个命名约定并坚持使用它

  • 使用连字符将前缀与类型名分开:viewer-file.hviewer-file.c(这是大多数 GNOME 库和应用程序使用的约定)
  • 使用下划线将前缀与类型名分开:viewer_file.hviewer_file.c
  • 不要在前缀与类型名之间分开:viewerfile.hviewerfile.c(这是 GTK 使用的约定)

有些人更喜欢前两种解决方案:对于视力不佳的人来说,这使得阅读文件名更容易。

任何暴露 GType 的头文件的基本约定在类型系统介绍的部分中描述为 “约定”

如果您想在“viewer”命名空间中声明名为“file”的类型,则将类型实例命名为 ViewerFile,并将它的类命名为 ViewerFileClass(名称是区分大小写的)。声明类型的推荐方法取决于该类型是最终类型还是可派生类型。

最终类型不能进一步子类化,应该是新类型的默认选择——将最终类型改为可派生类型总是与现有代码的用法兼容的变化,但反过来通常会导致问题。最终类型使用 G_DECLARE_FINAL_TYPE 宏声明,并在源代码中声明实例数据结构(而非头文件)。

/*
 * Copyright/Licensing information.
 */

/* inclusion guard */
#pragma once

#include <glib-object.h>

/*
 * Potentially, include other headers on which this header depends.
 */

G_BEGIN_DECLS

/*
 * Type declaration.
 */
#define VIEWER_TYPE_FILE viewer_file_get_type()
G_DECLARE_FINAL_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)

/*
 * Method definitions.
 */
ViewerFile *viewer_file_new (void);

G_END_DECLS

可派生类型可以进一步子类化,并且它们的类和实例结构构成了必须不变以维护 API 稳定的公共 API 的一部分。它们使用 G_DECLARE_DERIVABLE_TYPE 宏声明。

/*
 * Copyright/Licensing information.
 */

/* inclusion guard */
#pragma once

#include <glib-object.h>

/*
 * Potentially, include other headers on which this header depends.
 */

G_BEGIN_DECLS

/*
 * Type declaration.
 */
#define VIEWER_TYPE_FILE viewer_file_get_type()
G_DECLARE_DERIVABLE_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)

struct _ViewerFileClass
{
  GObjectClass parent_class;

  /* Class virtual function fields. */
  void (* open) (ViewerFile  *file,
                 GError     **error);

  /* Padding to allow adding up to 12 new virtual functions without
   * breaking ABI. */
  gpointer padding[12];
};

/*
 * Method definitions.
 */
ViewerFile *viewer_file_new (void);

G_END_DECLS

头文件包含的约定是在顶级添加所需的最少数量的 #include 指令,以便编译该头文件。这样,客户端代码可以简单地 #include "viewer-file.h",而不需要了解 viewer-file.h 的先决条件。

模板代码

在您的代码中,第一步是 #include 所需的头文件。

/*
 * Copyright/Licensing information
 */

#include "viewer-file.h"

/* Private structure definition. */
typedef struct {
  char *filename;

  /* other private fields */
} ViewerFilePrivate;

/*
 * forward definitions
 */

如果类是使用 G_DECLARE_FINAL_TYPE 声明为最终类型的,则其实例结构应在 C 文件中定义。

struct _ViewerFile
{
  GObject parent_instance;

  /* Other members, including private data. */
};

使用类型名称、函数前缀和父GType调用宏G_DEFINE_TYPE(如果你的类需要私有数据,则还需使用G_DEFINE_TYPE_WITH_PRIVATE——最终类型不需要私有数据)来减少所需的样板代码。此宏将:

  • 实现viewer_file_get_type函数
  • 定义一个可在整个.c文件中访问的父类指针
  • 为类型添加私有实例数据(如果使用G_DEFINE_TYPE_WITH_PRIVATE

如果类已经使用G_DECLARE_FINAL_TYPE声明为最终类型,则私有数据应放置在实例结构体ViewerFile中,应使用G_DEFINE_TYPE而不是G_DEFINE_TYPE_WITH_PRIVATE。最终类的实例结构体不对公众公开,并不嵌入任何派生类的实例结构体(因为该类是最终的),因此其大小可能发生变化而不会导致与使用该类的代码不兼容。相反,派生类私有数据必须包含在私有结构体中,并且必须使用G_DEFINE_TYPE_WITH_PRIVATE

G_DEFINE_TYPE (ViewerFile, viewer_file, G_TYPE_OBJECT)

或者

G_DEFINE_TYPE_WITH_PRIVATE (ViewerFile, viewer_file, G_TYPE_OBJECT)

还可以使用宏G_DEFINE_TYPE_WITH_CODE来控制get_type函数的实现——例如,添加对G_IMPLEMENT_INTERFACE宏的调用以实现接口。

对象构造

当尝试构造GObject时,由于可以以多种不同方式挂钩到对象的构造过程中,人们往往感到困惑:很难确定正确的、推荐的方式。

对象实例化的文档展示了在对象实例化期间调用的用户提供的函数以及它们调用的顺序。寻找类似简单的C++构造函数函数的用户应使用instance_init方法。它将在所有父母的instance_init函数被调用之后被调用。它不能接受任意的构造参数(如C++中一样),但如果你需要任意参数来完成初始化,则可以使用构造属性。

构造属性只有在所有instance_init函数运行之后才会设置。在所有构造属性设置完成之前,不会将对象引用返回到g_object_new()的客户端。

重要的是要注意,对象构造永远不可能失败。如果你需要可失败的GObject构造,你可以使用GInitableGAsyncInitable接口,它们由GIO库提供。

你应该首先编写以下代码

G_DEFINE_TYPE_WITH_PRIVATE (ViewerFile, viewer_file, G_TYPE_OBJECT)

static void
viewer_file_class_init (ViewerFileClass *klass)
{
}

static void
viewer_file_init (ViewerFile *self)
{
  ViewerFilePrivate *priv = viewer_file_get_instance_private (self);

  /* initialize all public and private members to reasonable default values.
   * They are all automatically initialized to 0 to begin with. */
}

如果你需要特殊的构造属性(设置了G_PARAM_CONSTRUCT_ONLY),则在class_init()函数中安装属性,覆盖GObject类的set_property()get_property()方法,并按“对象属性”部分中所述实现它们。

属性标识符必须从1开始,因为0被GObject保留用于内部使用。

enum
{
  PROP_FILENAME = 1,
  PROP_ZOOM_LEVEL,
  N_PROPERTIES
};

static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, };

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = viewer_file_set_property;
  object_class->get_property = viewer_file_get_property;

  obj_properties[PROP_FILENAME] =
    g_param_spec_string ("filename",
                         "Filename",
                         "Name of the file to load and display from.",
                         NULL  /* default value */,
                         G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE);

  obj_properties[PROP_ZOOM_LEVEL] =
    g_param_spec_uint ("zoom-level",
                       "Zoom level",
                       "Zoom level to view the file at.",
                       0  /* minimum value */,
                       10 /* maximum value */,
                       2  /* default value */,
                       G_PARAM_READWRITE);

  g_object_class_install_properties (object_class,
                                     N_PROPERTIES,
                                     obj_properties);
}

如果你需要这样做,请确保你可以构建和运行类似于上面显示的代码。同时,确保你的构造属性可以在构造期间设置而不会产生副作用。

有些人有时需要在将构造函数传递的属性设置之后才能完成类型的实例的初始化。这可以通过使用“对象实例化”部分中描述的constructor()类方法或更简单地说,使用constructed()类方法来实现。请注意,只有当标记为G_PARAM_CONSTRUCT_ONLYG_PARAM_CONSTRUCT的属性消耗完毕后,才会调用虚拟函数constructed(),但在将通常的属性传递给g_object_new()之前。

对象销毁

再次,确定要挂钩到对象销毁过程的机制通常很困难:当执行最后一次g_object_unref()函数调用时,将会发生很多事情,如文档“对象内存管理”部分中所述。

您的对象销毁过程分为两个阶段:释放(dispose)和终态(finalize)。这种分割是必要的,以便处理由 GObject 所使用的引用计数机制的特性所导致的潜在循环,以及处理在销毁序列期间信号发射时实例的临时复活。

struct _ViewerFilePrivate
{
  gchar *filename;
  guint zoom_level;

  GInputStream *input_stream;
};

G_DEFINE_TYPE_WITH_PRIVATE (ViewerFile, viewer_file, G_TYPE_OBJECT)

static void
viewer_file_dispose (GObject *gobject)
{
  ViewerFilePrivate *priv = viewer_file_get_instance_private (VIEWER_FILE (gobject));

  /* In dispose(), you are supposed to free all types referenced from this
   * object which might themselves hold a reference to self. Generally,
   * the most simple solution is to unref all members on which you own a
   * reference.
   */

  /* dispose() might be called multiple times, so we must guard against
   * calling g_object_unref() on an invalid GObject by setting the member
   * NULL; g_clear_object() does this for us.
   */
  g_clear_object (&priv->input_stream);

  /* Always chain up to the parent class; there is no need to check if
   * the parent class implements the dispose() virtual function: it is
   * always guaranteed to do so
   */
  G_OBJECT_CLASS (viewer_file_parent_class)->dispose (gobject);
}

static void
viewer_file_finalize (GObject *gobject)
{
  ViewerFilePrivate *priv = viewer_file_get_instance_private (VIEWER_FILE (gobject));

  g_free (priv->filename);

  /* Always chain up to the parent class; as with dispose(), finalize()
   * is guaranteed to exist on the parent's class virtual function table
   */
  G_OBJECT_CLASS (viewer_file_parent_class)->finalize (gobject);
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->dispose = viewer_file_dispose;
  object_class->finalize = viewer_file_finalize;
}

static void
viewer_file_init (ViewerFile *self);
{
  ViewerFilePrivate *priv = viewer_file_get_instance_private (self);

  priv->input_stream = g_object_new (VIEWER_TYPE_INPUT_STREAM, NULL);
  priv->filename = /* would be set as a property */;
}

对象方法可能在释放之后和终态之前被调用。GObject 不认为这是一个程序错误:您必须优雅地检测到这种情况,既不要崩溃也不要警告用户,通过使已释放的实例恢复到惰性状态。

对象方法

与 C++ 一样,有许多不同的方式来定义对象方法并扩展它们:以下列表和章节借鉴了 C++ 词汇。(读者应了解基本的 C++ 概念。那些最近没有编写过 C++ 代码的人可以参考C++ 教程来复习。)

  • 非虚拟公共方法
  • 虚公共方法
  • 虚私有方法
  • 非虚拟私有方法

非虚拟方法

这些是最简单的,提供了一个简单的针对对象的方法。在头文件中提供函数原型,并在源文件中实现该原型。

/* declaration in the header. */
void viewer_file_open (ViewerFile  *self,
                       GError     **error);
/* implementation in the source file */
void
viewer_file_open (ViewerFile  *self,
                  GError     **error)
{
  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  /* do stuff here. */
}

虚公共方法

这是创建可重写的 GObjects 方法的首选方法

  • 在公共头文件中定义常见的类结构和它的虚函数
  • 在头文件中定义常见的类,并在源文件中实现它
  • 在源文件中实现虚拟函数的基版,并在对象的 class_init 函数中初始化虚拟函数指针到这个实现;或将它留为 NULL 以实现一个必须由子类重写的“纯虚拟”方法
  • 在需要重写每个派生类中实现虚函数

请注意,如果类是可导出的,则可以定义虚函数,即用 G_DECLARE_DERIVABLE_TYPE 声明,以便可以定义类结构。

/* declaration in viewer-file.h. */
#define VIEWER_TYPE_FILE viewer_file_get_type ()
G_DECLARE_DERIVABLE_TYPE (ViewerFile, viewer_file, VIEWER, FILE, GObject)

struct _ViewerFileClass
{
  GObjectClass parent_class;

  /* stuff */
  void (*open) (ViewerFile  *self,
                GError     **error);

  /* Padding to allow adding up to 12 new virtual functions without
   * breaking ABI. */
  gpointer padding[12];
};

void viewer_file_open (ViewerFile  *self,
                       GError     **error);
/* implementation in viewer-file.c */
void
viewer_file_open (ViewerFile  *self,
                  GError     **error)
{
  ViewerFileClass *klass;

  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  klass = VIEWER_FILE_GET_CLASS (self);
  g_return_if_fail (klass->open != NULL);

  klass->open (self, error);
}

上面的代码仅仅是将对 open 调用重定向到相关的虚拟函数。

您可以在对象的 class_init 函数中为此类方法提供一个默认实现:初始化 klass->open 字段到一个实际的实现指针。默认情况下,非继承的类方法被初始化为 NULL,因此被视为“纯虚拟”。

static void
viewer_file_real_close (ViewerFile  *self,
                        GError     **error)
{
  /* Default implementation for the virtual method. */
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  /* this is not necessary, except for demonstration purposes.
   *
   * pure virtual method: mandates implementation in children.
   */
  klass->open = NULL;

  /* merely virtual method. */
  klass->close = viewer_file_real_close;
}

void
viewer_file_open (ViewerFile  *self,
                  GError     **error)
{
  ViewerFileClass *klass;

  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  klass = VIEWER_FILE_GET_CLASS (self);

  /* if the method is purely virtual, then it is a good idea to
   * check that it has been overridden before calling it, and,
   * depending on the intent of the class, either ignore it silently
   * or warn the user.
   */
  g_return_if_fail (klass->open != NULL);
  klass->open (self, error);
}

void
viewer_file_close (ViewerFile  *self,
                   GError     **error)
{
  ViewerFileClass *klass;

  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  klass = VIEWER_FILE_GET_CLASS (self);
  if (klass->close != NULL)
    klass->close (self, error);
}

虚私有方法

这些非常类似于虚公共方法。它们只是没有可以直接调用的公共函数。头文件中只包含虚函数的声明

/* declaration in viewer-file.h. */
struct _ViewerFileClass
{
  GObjectClass parent;

  /* Public virtual method as before. */
  void     (*open)           (ViewerFile  *self,
                              GError     **error);

  /* Private helper function to work out whether the file can be loaded via
   * memory mapped I/O, or whether it has to be read as a stream. */
  gboolean (*can_memory_map) (ViewerFile *self);

  /* Padding to allow adding up to 12 new virtual functions without
   * breaking ABI. */
  gpointer padding[12];
};

void viewer_file_open (ViewerFile *self, GError **error);

这些虚函数通常用来将部分工作委托给子类

/* this accessor function is static: it is not exported outside of this file. */
static gboolean
viewer_file_can_memory_map (ViewerFile *self)
{
  return VIEWER_FILE_GET_CLASS (self)->can_memory_map (self);
}

void
viewer_file_open (ViewerFile  *self,
                  GError     **error)
{
  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  /*
   * Try to load the file using memory mapped I/O, if the implementation of the
   * class determines that is possible using its private virtual method.
   */
  if (viewer_file_can_memory_map (self))
    {
      /* Load the file using memory mapped I/O. */
    }
  else
    {
      /* Fall back to trying to load the file using streaming I/O… */
    }
}

同样,您可以为此私有虚拟函数提供一个默认实现

static gboolean
viewer_file_real_can_memory_map (ViewerFile *self)
{
  /* As an example, always return false. Or, potentially return true if the
   * file is local. */
  return FALSE;
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  /* non-pure virtual method; does not have to be implemented in children. */
  klass->can_memory_map = viewer_file_real_can_memory_map;
}

派生类可以通过以下代码来覆盖该方法

static void
viewer_audio_file_class_init (ViewerAudioFileClass *klass)
{
  ViewerFileClass *file_class = VIEWER_FILE_CLASS (klass);

  /* implement pure virtual function. */
  file_class->can_memory_map = viewer_audio_file_can_memory_map;
}

链式调用

链式调用通常由以下条件组合松散定义

  • 父类 A 定义了一个名为 foo 的公共虚方法并提供了一个默认实现
  • 子类 B 重新实现了方法 foo
  • B 实现 foo 调用(“链式调用”)其父类 A 中 foo 的实现

这个习语的用途很多

  • 您需要扩展类的行为而不修改其代码。您创建一个子类来继承它的实现,重新实现一个公共虚方法来修改行为,并进行链式调用以确保不会真正修改先前的行为,只是扩展它
  • 您需要实现责任链模式:继承树中的每个对象都要将与父类的链接链起来(通常在方法开始或结束时),以确保每个处理器依次运行

为了显式地将链接向上到父类中虚拟方法的实现,首先需要获取原有父类结构的句柄。然后,可以使用此指针直接访问原始虚拟函数指针并调用它

上面句子中使用的“原始”形容词并非无害。为了完全理解其含义,请回忆类结构是如何初始化的:对于每种对象类型,首先通过复制其父类型的类结构(简单的memcpy)来创建与该对象关联的类结构,然后在该结果类结构上调用class_init回调。由于class_init回调负责用类的用户重新实现的方法覆盖类结构,因此无法使用存储在派生实例中的修改后的父类结构副本。需要一个父类结构实例的类结构副本。

要实现链式链接,可以使用由G_DEFINE_TYPE家族宏创建和初始化的parent_class指针,例如

static void
b_method_to_call (B *obj, int some_param)
{
  /* do stuff before chain up */

  /* call the method_to_call() virtual function on the
   * parent of BClass, AClass.
   *
   * remember the explicit cast to AClass*
   */
  A_CLASS (b_parent_class)->method_to_call (obj, some_param);

  /* do stuff after chain up */
}

如何定义和实现接口

定义接口

在名为“不可实例化的类类型:接口”的部分中解释了GObject接口背后的理论;本节涵盖了如何定义和实现接口。

第一步是确保头文件正确。此接口定义了三个方法

/*
 * Copyright/Licensing information.
 */

#pragma once

#include <glib-object.h>

G_BEGIN_DECLS

#define VIEWER_TYPE_EDITABLE viewer_editable_get_type()
G_DECLARE_INTERFACE (ViewerEditable, viewer_editable, VIEWER, EDITABLE, GObject)

struct _ViewerEditableInterface
{
  GTypeInterface parent_iface;

  void (*save) (ViewerEditable  *self,
                GError         **error);
  void (*undo) (ViewerEditable  *self,
                guint            n_steps);
  void (*redo) (ViewerEditable  *self,
                guint            n_steps);
};

void viewer_editable_save (ViewerEditable  *self,
                           GError         **error);
void viewer_editable_undo (ViewerEditable  *self,
                           guint            n_steps);
void viewer_editable_redo (ViewerEditable  *self,
                           guint            n_steps);

G_END_DECLS

此代码与从GObject派生的正常GType的代码相同,除了几个细节

  • _GET_CLASS函数被调用为_GET_IFACE(并由G_DECLARE_INTERFACE定义)
  • 实例类型ViewerEditable未全部定义:它仅用作抽象类型,代表实现此接口的任何对象的实例
  • ViewerEditableInterface的父类型是GTypeInterface,而不是GObjectClass

ViewerEditable类型的实现是微不足道的

  • G_DEFINE_INTERFACE创建一个viewer_editable_get_type函数,该函数在类型系统中注册了类型。第三个参数用于定义一个前提接口(我们稍后再讨论)。如果接口没有前提,则为此参数传递0
  • 期望viewer_editable_default_init注册接口的信号(如果有的话)(我们稍后将会看到如何使用它们)
  • 接口方法viewer_editable_saveviewer_editable_undoviewer_editable_redo取消引用接口结构以访问其关联的接口函数并调用它
G_DEFINE_INTERFACE (ViewerEditable, viewer_editable, G_TYPE_OBJECT)

static void
viewer_editable_default_init (ViewerEditableInterface *iface)
{
    /* add properties and signals to the interface here */
}

void
viewer_editable_save (ViewerEditable  *self,
                      GError         **error)
{
  ViewerEditableInterface *iface;

  g_return_if_fail (VIEWER_IS_EDITABLE (self));
  g_return_if_fail (error == NULL || *error == NULL);

  iface = VIEWER_EDITABLE_GET_IFACE (self);
  g_return_if_fail (iface->save != NULL);
  iface->save (self, error);
}

void
viewer_editable_undo (ViewerEditable *self,
                      guint           n_steps)
{
  ViewerEditableInterface *iface;

  g_return_if_fail (VIEWER_IS_EDITABLE (self));

  iface = VIEWER_EDITABLE_GET_IFACE (self);
  g_return_if_fail (iface->undo != NULL);
  iface->undo (self, n_steps);
}

void
viewer_editable_redo (ViewerEditable *self,
                      guint           n_steps)
{
  ViewerEditableInterface *iface;

  g_return_if_fail (VIEWER_IS_EDITABLE (self));

  iface = VIEWER_EDITABLE_GET_IFACE (self);
  g_return_if_fail (iface->redo != NULL);
  iface->redo (self, n_steps);
}

实现接口

一旦定义了接口,实现它就相对简单。

第一步是像往常一样定义一个正常的最终GObject类。

第二步是使用G_DEFINE_TYPE_WITH_CODEG_IMPLEMENT_INTERFACE而不是G_DEFINE_TYPE来定义实现ViewerFile

static void viewer_file_editable_interface_init (ViewerEditableInterface *iface);

G_DEFINE_TYPE_WITH_CODE (ViewerFile, viewer_file, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_file_editable_interface_init))

这个定义与之前看到的类似函数非常相似。这里特有的代码接口是使用了 G_IMPLEMENT_INTERFACE

类可以通过在 G_DEFINE_TYPE_WITH_CODE 中多次调用 G_IMPLEMENT_INTERFACE 来实现多个接口。

viewer_file_editable_interface_init 是接口初始化函数:它内部必须为接口的每一个虚方法指定其实际实现

static void
viewer_file_editable_save (ViewerFile  *self,
                           GError     **error)
{
  g_print ("File implementation of editable interface save method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_undo (ViewerFile *self,
                           guint       n_steps)
{
  g_print ("File implementation of editable interface undo method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_redo (ViewerFile *self,
                           guint       n_steps)
{
  g_print ("File implementation of editable interface redo method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_interface_init (ViewerEditableInterface *iface)
{
  iface->save = viewer_file_editable_save;
  iface->undo = viewer_file_editable_undo;
  iface->redo = viewer_file_editable_redo;
}

static void
viewer_file_init (ViewerFile *self)
{
  /* Instance variable initialisation code. */
}

如果对象不是最终类型,例如使用 G_DECLARE_DERIVABLE_TYPE 声明,那么应该添加 G_ADD_PRIVATE 宏。私有结构应该如同普通可派生对象一样声明。

G_DEFINE_TYPE_WITH_CODE (ViewerFile, viewer_file, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (ViewerFile)
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_file_editable_interface_init))

接口定义前提条件

为了指定接口在实现时需要其他接口的存在,GObject 引入了前提条件的概念:可以将一系列前提类型与一个接口关联。例如,如果对象 A 希望实现接口 I1,而接口 I1 要求接口 I2 的存在,则 A 必须实现 I1 和 I2。

上述机制在实践中与 Java 的接口 I1 继承接口 I2 非常相似。下面的例子显示了 GObject 的同等概念:

/* Make the ViewerEditableLossy interface require ViewerEditable interface. */
G_DEFINE_INTERFACE (ViewerEditableLossy, viewer_editable_lossy, VIEWER_TYPE_EDITABLE)

在上述 G_DEFINE_INTERFACE 调用中,第三个参数定义了前提类型。这是接口或类的 GType。在这个例子中,ViewerEditable 接口是 ViewerEditableLossy 的前提。下面的代码显示了如何实现这两个接口及其实现注册:

static void
viewer_file_editable_lossy_compress (ViewerEditableLossy *editable)
{
  ViewerFile *self = VIEWER_FILE (editable);

  g_print ("File implementation of lossy editable interface compress method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_lossy_interface_init (ViewerEditableLossyInterface *iface)
{
  iface->compress = viewer_file_editable_lossy_compress;
}

static void
viewer_file_editable_save (ViewerEditable  *editable,
                           GError         **error)
{
  ViewerFile *self = VIEWER_FILE (editable);

  g_print ("File implementation of editable interface save method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_undo (ViewerEditable *editable,
                           guint           n_steps)
{
  ViewerFile *self = VIEWER_FILE (editable);

  g_print ("File implementation of editable interface undo method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_redo (ViewerEditable *editable,
                           guint           n_steps)
{
  ViewerFile *self = VIEWER_FILE (editable);

  g_print ("File implementation of editable interface redo method: %s.\n",
           self->filename);
}

static void
viewer_file_editable_interface_init (ViewerEditableInterface *iface)
{
  iface->save = viewer_file_editable_save;
  iface->undo = viewer_file_editable_undo;
  iface->redo = viewer_file_editable_redo;
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  /* Nothing here. */
}

static void
viewer_file_init (ViewerFile *self)
{
  /* Instance variable initialisation code. */
}

G_DEFINE_TYPE_WITH_CODE (ViewerFile, viewer_file, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_file_editable_interface_init)
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE_LOSSY,
                                                viewer_file_editable_lossy_interface_init))

需要注意的一个重要的事实是,接口实现添加到主对象中的顺序并非随机:由 G_IMPLEMENT_INTERFACE 调用的 g_type_add_interface_static() 必须首先在无前提条件的接口上调用,然后再在其他接口上调用。

接口属性

GObject 接口也可以有属性。接口属性的声明类似于在章节《对象属性》( “Object properties”)中所述的普通 GObject 类型属性的声明,不同之处在于使用 g_object_interface_install_property() 代替了 g_object_class_install_property() 来声明属性。

要在上面的 ViewerEditable 接口示例代码中加入一个名为 'autosave-frequency'、类型为 gdouble 的属性,我们只需要在 viewer_editable_default_init() 中添加一个调用,如下所示

static void
viewer_editable_default_init (ViewerEditableInterface *iface)
{
  g_object_interface_install_property (iface,
    g_param_spec_double ("autosave-frequency",
                         "Autosave frequency",
                         "Frequency (in per-seconds) to autosave backups of the editable content at. "
                         "Or zero to disable autosaves.",
                         0.0,  /* minimum */
                         G_MAXDOUBLE,  /* maximum */
                         0.0,  /* default */
                         G_PARAM_READWRITE));
}

值得注意的是,声明的属性没有分配一个整数 ID。原因是属性的唯一整数 ID 只在 get_propertyset_property 虚拟方法内部使用。由于接口声明但不实现属性,所以没有必要为它们分配整数 ID。

实现以通常的方式声明和定义其属性(如章节《对象属性》所述),除了一个小改动:它可以使用 g_object_class_override_property() 代替 g_object_class_install_property() 来声明它所实现的接口的属性。以下代码片段显示了在 ViewerFile 声明和实现中所需进行的修改:

struct _ViewerFile
{
  GObject parent_instance;

  double autosave_frequency;
};

enum
{
  PROP_AUTOSAVE_FREQUENCY = 1,
  N_PROPERTIES
};

static void
viewer_file_set_property (GObject      *object,
                          guint         prop_id,
                          const GValue *value,
                          GParamSpec   *pspec)
{
  ViewerFile *file = VIEWER_FILE (object);

  switch (prop_id)
    {
    case PROP_AUTOSAVE_FREQUENCY:
      file->autosave_frequency = g_value_get_double (value);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
viewer_file_get_property (GObject    *object,
                          guint       prop_id,
                          GValue     *value,
                          GParamSpec *pspec)
{
  ViewerFile *file = VIEWER_FILE (object);

  switch (prop_id)
    {
    case PROP_AUTOSAVE_FREQUENCY:
      g_value_set_double (value, file->autosave_frequency);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
viewer_file_class_init (ViewerFileClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = viewer_file_set_property;
  object_class->get_property = viewer_file_get_property;

  g_object_class_override_property (object_class, PROP_AUTOSAVE_FREQUENCY, "autosave-frequency");
}

覆盖接口方法

如果基类已经实现了接口,而派生类需要实现同一个接口但需要覆盖某些方法,你必须重新实现接口并只设置需要覆盖的接口方法。

在本例中,ViewerAudioFile类是从ViewerFile类派生的。两者都实现了ViewerEditable接口。ViewerAudioFile类只实现了ViewerEditable接口中的一个方法,并使用基类的其他方法实现。

static void
viewer_audio_file_editable_save (ViewerEditable  *editable,
                                 GError         **error)
{
  ViewerAudioFile *self = VIEWER_AUDIO_FILE (editable);

  g_print ("Audio file implementation of editable interface save method.\n");
}

static void
viewer_audio_file_editable_interface_init (ViewerEditableInterface *iface)
{
  /* Override the implementation of save(). */
  iface->save = viewer_audio_file_editable_save;

  /*
   * Leave iface->undo and ->redo alone, they are already set to the
   * base class implementation.
   */
}

G_DEFINE_TYPE_WITH_CODE (ViewerAudioFile, viewer_audio_file, VIEWER_TYPE_FILE,
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_audio_file_editable_interface_init))

static void
viewer_audio_file_class_init (ViewerAudioFileClass *klass)
{
  /* Nothing here. */
}

static void
viewer_audio_file_init (ViewerAudioFile *self)
{
  /* Nothing here. */
}

要访问基类接口的实现,请在接口的default_init函数中使用g_type_interface_peek_parent()

如果一个接口方法在其派生类中被覆盖,则要从派生类调用基类接口方法实现,请将g_type_interface_peek_parent()返回的指针存储在全局变量中。

在此示例中,ViewerAudioFile类覆盖了保存接口方法。在其覆盖方法中,它调用了相同接口的基类实现方法。

static ViewerEditableInterface *viewer_editable_parent_interface = NULL;

static void
viewer_audio_file_editable_save (ViewerEditable  *editable,
                                 GError         **error)
{
  ViewerAudioFile *self = VIEWER_AUDIO_FILE (editable);

  g_print ("Audio file implementation of editable interface save method.\n");

  /* Now call the base implementation */
  viewer_editable_parent_interface->save (editable, error);
}

static void
viewer_audio_file_editable_interface_init (ViewerEditableInterface *iface)
{
  viewer_editable_parent_interface = g_type_interface_peek_parent (iface);

  iface->save = viewer_audio_file_editable_save;
}

G_DEFINE_TYPE_WITH_CODE (ViewerAudioFile, viewer_audio_file, VIEWER_TYPE_FILE,
                         G_IMPLEMENT_INTERFACE (VIEWER_TYPE_EDITABLE,
                                                viewer_audio_file_editable_interface_init))

static void
viewer_audio_file_class_init (ViewerAudioFileClass *klass)
{
  /* Nothing here. */
}

static void
viewer_audio_file_init (ViewerAudioFile *self)
{
  /* Nothing here. */
}

如何创建和使用信号

GType中的信号系统相当复杂且灵活:用户可以在运行时将任意数量的回调(用支持的语言实现)连接到任意信号,并在信号发射过程中的任何状态下停止发射任何信号。这种灵活性使得GSignal的应用远不止于向多个客户端发射信号。

信号的基本使用

信号最基本的使用是实现事件通知。例如,给定一个具有写入方法的ViewerFile对象,每当使用该方法更改文件时,可以发出一个信号。下面的代码显示了用户如何将回调连接到“changed”信号。

file = g_object_new (VIEWER_FILE_TYPE, NULL);

g_signal_connect (file, "changed", (GCallback) changed_event, NULL);

viewer_file_write (file, buffer, strlen (buffer));

class_init函数中注册了信号

file_signals[CHANGED] = 
  g_signal_newv ("changed",
                 G_TYPE_FROM_CLASS (object_class),
                 G_SIGNAL_RUN_LAST | G_SIGNAL_NO_RECURSE | G_SIGNAL_NO_HOOKS,
                 NULL /* closure */,
                 NULL /* accumulator */,
                 NULL /* accumulator data */,
                 NULL /* C marshaller */,
                 G_TYPE_NONE /* return_type */,
                 0     /* n_params */,
                 NULL  /* param_types */);

并在viewer_file_write中发射该信号

void
viewer_file_write (ViewerFile   *self,
                   const guint8 *buffer,
                   gsize         size)
{
  g_return_if_fail (VIEWER_IS_FILE (self));
  g_return_if_fail (buffer != NULL || size == 0);

  /* First write data. */

  /* Then, notify user of data written. */
  g_signal_emit (self, file_signals[CHANGED], 0 /* details */);
}

如上所示,如果不需要传达任何详细信息,可以安全地将详细信息参数设置为零。关于它的用途,请参阅“细节参数”部分。

C信号编组的应该始终是NULL,在这种情况下,GLib将选择给定闭包类型的最佳编组器。这可能是一个针对闭包类型特定的内部编组器,或者g_cclosure_marshal_generic(),它实现了参数数组到C回调调用通用转换。GLib曾要求用户编写或生成特定类型的编组器并传递,但已被自动选择编组器所取代。

请注意,g_cclosure_marshal_generic()比非通用编组器慢,因此应避免在性能关键代码中使用它。然而,性能关键代码通常很少使用信号,因为信号是同步的,其发射阻塞直到所有监听器被调用,这可能带来边界的成本。