介绍

你可能乐于创建这样一种类型的构件,它仅仅是其它 GTK 构件的一个组合。为组装用户界面元素以重复使用提供了便利的方法。在标准发布中的文件选择和颜色选择构件就是这种类型构件的示例。

我们将要在这一节创建一个井字游戏构件,一个 3X3 的开关按钮矩阵,当同一列、同一行或是对角线的所有三个按钮都被按下时触发一个信号。

创建一个复合构件 - 图1

选择一个父类

通常,一个复合构件的父类是一个容纳复合构件的所有元素的容器类。例如,文件选择构件的父类就是对话框类。因为我们的按钮排列在一个表中,看起来应该让表类作为我们的父类。但不幸的是,这样无法工作。构件的创建分为两个函数 - 一个用户调用的 WIDGETNAME_new() 函数,另一个是 WIDGETNAME_init() 函数作基本的初始化构件的工作,它不使用传递给 new() 函数的参数。子构件只调用父构件的 _init 函数。但是这个分工不能在表中正常工作,因为创建表时需要知道表的行数和列数。除非我们想重新实现 gtktable_new() 的大多数功能,我们最好避免从表派生构件。由于这个原因,代替表,我们从纵向盒派生构件,然后把表放入纵向盒。

头文件

每个构件类有一个头文件,该头文件用于声明构件的对象、类结构和公共函数。有两个特性是值得指出的。为避免重复定义,我们把整个头文件放入如下语句里:

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__
.
.
.
#endif /* __TICTACTOE_H__ */

并且让 C++ 程序也能包含该头文件:

#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */
.
.
.
#ifdef __cplusplus
}
#endif /* __cplusplus */

在我们的头文件里,和函数、结构一起声明的还有三个标准宏。 TICTACTOE(obj), TICTACTOE_CLASS(class) 和 IS_TICTACTOE(obj),这三个宏分别是将指针转换为指向对象、类的指针和检测一个对象是否是一个井字游戏构件。

下面是全部的头文件:

/* GTK - GIMP工具包
 * 版权 (C) 1995-1997 Peter Mattis, Spencer Kimball 和 Josh MacDonald 所有
 *
 * 本程序是自由软件。你可以在自由软件基金发布的 GNU GPL 的条款下重新分发
 * 或修改它。GPL 可以使用版本 2 或(由你选择)任何随后的版本。
 *
 * 本程序分发的目的是它可能对其他人有用,但不提供任何的担保,包括隐含的
 * 和适合特定用途的保证。请查阅GNU通用公共许可证获得详细的信息。
 *
 * 你应该已经随该软件一起收到一份GNU通用公共许可。如果还没有,请写信给
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#ifndef __TICTACTOE_H__
#define __TICTACTOE_H__


#include <gdk/gdk.h>
#include <gtk/gtkvbox.h>


#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

#define TICTACTOE(obj)          GTK_CHECK_CAST (obj, tictactoe_get_type (), Tictactoe)
#define TICTACTOE_CLASS(klass)  GTK_CHECK_CLASS_CAST (klass, tictactoe_get_type (), TictactoeClass)
#define IS_TICTACTOE(obj)       GTK_CHECK_TYPE (obj, tictactoe_get_type ())


typedef struct _Tictactoe       Tictactoe;
typedef struct _TictactoeClass  TictactoeClass;

struct _Tictactoe
{
  GtkVBox vbox;

  GtkWidget *buttons[3][3];
};

struct _TictactoeClass
{
  GtkVBoxClass parent_class;

  void (* tictactoe) (Tictactoe *ttt);
};

GtkType        tictactoe_get_type        (void);
GtkWidget*     tictactoe_new             (void);
void           tictactoe_clear           (Tictactoe *ttt);

#ifdef __cplusplus
}
#endif /* __cplusplus */

#endif /* __TICTACTOE_H__ */

gettype() 函数

现在,我们继续实现我们的构件。WIDGETNAME_get_type() 函数是每个构件的核心函数。当第一次调用时,该函数告之 GTK 这个构件类并得到一个能唯一识别该构件类的 ID。之后的调用,只返回这个ID。

GtkType
tictactoe_get_type ()
{
  static guint ttt_type = 0;

  if (!ttt_type)
    {
      GtkTypeInfo ttt_info =
      {
    "Tictactoe",
    sizeof (Tictactoe),
    sizeof (TictactoeClass),
    (GtkClassInitFunc) tictactoe_class_init,
    (GtkObjectInitFunc) tictactoe_init,
    (GtkArgSetFunc) NULL,
        (GtkArgGetFunc) NULL
      };

      ttt_type = gtk_type_unique (gtk_vbox_get_type (), &ttt_info);
    }

  return ttt_type;
}

GtkTypeInfo 结构定义如下:

struct _GtkTypeInfo
{
  gchar *type_name;
  guint object_size;
  guint class_size;
  GtkClassInitFunc class_init_func;
  GtkObjectInitFunc object_init_func;
  GtkArgSetFunc arg_set_func;
  GtkArgGetFunc arg_get_func;
};

这个结构的域很形象。我们在这里忽略 arg_set_func 和 arg_get_func 这两个域:它们很重要,但大部分还未实现,它们允许解释性语言方便地设置构件的属性。一旦 GTK 正确的填充了该结构,它就知道了如何去创建一个特殊构件类型的对象。

classinit() 函数

WIDGETNAME_class_init()函数初始化构件类结构的域,并为类设置任何信号。我们的井字游戏构件是这样的:

enum {
  TICTACTOE_SIGNAL,
  LAST_SIGNAL
};


static gint tictactoe_signals[LAST_SIGNAL] = { 0 };

static void
tictactoe_class_init (TictactoeClass *class)
{
  GtkObjectClass *object_class;

  object_class = (GtkObjectClass*) class;

  tictactoe_signals[TICTACTOE_SIGNAL] = gtk_signal_new ("tictactoe",
                     GTK_RUN_FIRST,
                     object_class->type,
                     GTK_SIGNAL_OFFSET (TictactoeClass, tictactoe),
                     gtk_signal_default_marshaller, GTK_TYPE_NONE, 0);


  gtk_object_class_add_signals (object_class, tictactoe_signals, LAST_SIGNAL);

  class->tictactoe = NULL;
}

我们的构件只有一个tictactoe信号,当连成一行、一列或一个对角线时,该信号被触发。并不是每个复合构件都需要信号,因此当你第一次阅读这节时,可以跳过看下一节,因为这一节对初学者有点难。

函数:

gint gtk_signal_new( const gchar         *name,
                     GtkSignalRunType     run_type,
                     GtkType              object_type,
                     gint                 function_offset,
                     GtkSignalMarshaller  marshaller,
                     GtkType              return_val,
                     guint                nparams,
                     ...);

创建一个新信号。参数是:
name:信号的名称。
run_type:设定是在用户处理函数之前还是之后运行默认处理函数。该值通常会是 GTK_RUN_FIRST 或 GTK_RUN_LAST,虽然也有其它值。
object_type:发出该信号的对象的ID。(也可能是子对象的ID。)
function_offset:类结构指针相对于默认处理函数的偏移量。
marshaller:一个用于调用信号处理函数的函数。对于除了发出信号的对象和用户数据外没有其它的参数的信号处理函数,我们可以使用事先提供的 marshaller 函数 gtk_signal_default_marshaller。
return_val:返回值的类型。
nparams:信号处理函数的参数个数 (除了前面提到的”发出信号的对象”和”用户数据”这两个默认参数 )
…:参数的数据类型。

当指定类型时,要用到 GtkType 枚举类型:

typedef enum
{
  GTK_TYPE_INVALID,
  GTK_TYPE_NONE,
  GTK_TYPE_CHAR,
  GTK_TYPE_BOOL,
  GTK_TYPE_INT,
  GTK_TYPE_UINT,
  GTK_TYPE_LONG,
  GTK_TYPE_ULONG,
  GTK_TYPE_FLOAT,
  GTK_TYPE_DOUBLE,
  GTK_TYPE_STRING,
  GTK_TYPE_ENUM,
  GTK_TYPE_FLAGS,
  GTK_TYPE_BOXED,
  GTK_TYPE_FOREIGN,
  GTK_TYPE_CALLBACK,
  GTK_TYPE_ARGS,

  GTK_TYPE_POINTER,

  /* 如果下面两个最终能被删除就好了。*/
  GTK_TYPE_SIGNAL,
  GTK_TYPE_C_CALLBACK,

  GTK_TYPE_OBJECT

} GtkFundamentalType;

gtk_signal_new() 返回一个能识别信号的唯一整数,我们把它存储在 tictactoe_signals 数组里,并用枚举值来做索引。(依照惯例,枚举成员是信号的名称,且是大写的,但在这里会与 TICTACTOE() 宏冲突,因此我们用 TICTACTOE_SIGNAL 代替。)

创建了信号之后,我们需要让 GTK 将我们的信号和 Tictactoe 类联系起来。调用 gtk_object_class_add_signals() 函数可以做这件事。然后我们把指向 “tictactoe” 信号默认处理函数的指针设为 NULL,表明没有默认的动作。

_init() 函数

每个构件类也需要一个初始化对象结构的函数。通常,该函数有个相当有限的任务,就是设置结构成员为缺省值。对于复合构件,这个函数还创建成分(component)构件。

static void
tictactoe_init (Tictactoe *ttt)
{
  GtkWidget *table;
  gint i,j;

  table = gtk_table_new (3, 3, TRUE);
  gtk_container_add (GTK_CONTAINER(ttt), table);
  gtk_widget_show (table);

  for (i=0;i<3; i++)
    for (j=0;j<3; j++)
      {
    ttt->buttons[i][j] = gtk_toggle_button_new ();
    gtk_table_attach_defaults (GTK_TABLE(table), ttt->buttons[i][j], 
                   i, i+1, j, j+1);
    gtk_signal_connect (GTK_OBJECT (ttt->buttons[i][j]), "toggled",
                GTK_SIGNAL_FUNC (tictactoe_toggle), ttt);
    gtk_widget_set_size_request (ttt->buttons[i][j], 20, 20);
    gtk_widget_show (ttt->buttons[i][j]);
      }
}

其余的…

另外还有一个函数每个构件(除了基本的构件类型,如 Bin 不能实例化)都需要 - 被用户调用以创建一个该类型的对象的函数。这个调用通常是 WIDGETNAME_new()。在有些构件里,该函数获得几个参数,依据这几个参数做一些设置,我们的井字游戏构件没有这样做。另外两个函数是针对井字游戏构件的。

tictactoe_clear() 是一个公共函数,它复位构件中的按钮。注意 gtk_signal_handler_block_by_data() 函数用来防止按钮切换信号处理函数被不必要地触发。

tictactoe_toggle() 是当用户点击按钮时调用的信号处理函数。它判断开关按钮中是否出现了导致赢的组合,如果是,则发出 “tictactoe” 信号。

GtkWidget*
tictactoe_new ()
{
  return GTK_WIDGET ( gtk_type_new (tictactoe_get_type ()));
}

void           
tictactoe_clear (Tictactoe *ttt)
{
  int i,j;

  for (i=0;i<3;i++)
    for (j=0;j<3;j++)
      {
    gtk_signal_handler_block_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
    gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]),
                     FALSE);
    gtk_signal_handler_unblock_by_data (GTK_OBJECT(ttt->buttons[i][j]), ttt);
      }
}

static void
tictactoe_toggle (GtkWidget *widget, Tictactoe *ttt)
{
  int i,k;

  static int rwins[8][3] = { { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                 { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                 { 0, 1, 2 }, { 0, 1, 2 } };
  static int cwins[8][3] = { { 0, 1, 2 }, { 0, 1, 2 }, { 0, 1, 2 },
                 { 0, 0, 0 }, { 1, 1, 1 }, { 2, 2, 2 },
                 { 0, 1, 2 }, { 2, 1, 0 } };

  int success, found;

  for (k=0; k<8; k++)
    {
      success = TRUE;
      found = FALSE;

      for (i=0;i<3;i++)
    {
      success = success && 
        GTK_TOGGLE_BUTTON(ttt->buttons[rwins[k][i]][cwins[k][i]])->active;
      found = found ||
        ttt->buttons[rwins[k][i]][cwins[k][i]] == widget;
    }

      if (success && found)
    {
      gtk_signal_emit (GTK_OBJECT (ttt), 
               tictactoe_signals[TICTACTOE_SIGNAL]);
      break;
    }
    }
}

最后,一个使用我们的井字游戏构件的程序示例:

#include <gtk/gtk.h>
#include "tictactoe.h"

/* 当赢了时调用 */
void
win (GtkWidget *widget, gpointer data)
{
  g_print ("Yay!\n");
  tictactoe_clear (TICTACTOE (widget));
}

int 
main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *ttt;

  gtk_init (&argc, &argv);

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

  gtk_window_set_title (GTK_WINDOW (window), "Aspect Frame");

  gtk_signal_connect (GTK_OBJECT (window), "destroy",
              GTK_SIGNAL_FUNC (gtk_exit), NULL);

  gtk_container_set_border_width (GTK_CONTAINER (window), 10);

  /* 创建一个新的井字游戏构件 */
  ttt = tictactoe_new ();
  gtk_container_add (GTK_CONTAINER (window), ttt);
  gtk_widget_show (ttt);

  /* 连接 "tictactoe" 信号的处理函数 */
  gtk_signal_connect (GTK_OBJECT (ttt), "tictactoe",
              GTK_SIGNAL_FUNC (win), NULL);

  gtk_widget_show (window);

  gtk_main ();

  return 0;
}