GTK+ 2.0 教学-产生一个组合元件

介绍

你可能乐于创建这样一种类型的元件,它仅仅是其它 GTK 元件的一个组合。这个型态的元件没有产生就不能处理任何事但却提供了便利的方法以重复使用。在标准发布中的FileSelection和ColorSelection元件就是这种类型元件的范例。

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

 注意:井字游戏的完整程式码会在GTK+ 2.0 教学-范例程式说明。

选择一个父类别

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

标头档

每个GObject类别有一个标头档,该标头档用于宣告元件的物件、类别结构和公用函式。有两个特性是值得指出的。为避免重复定义,我们把整个标头档包在:

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

并且让 C++ 程式也能包含该标头档:

#include <glib.h>

G_BEGIN_DECLS
.
.
.
G_END_DECLS

在我们的标头档里,和函式、结构一起宣告的还有三个标准巨集。 TICTACTOE(obj), TICTACTOE_CLASS(class)IS_TICTACTOE(obj),这三个巨集分别是将指标转换为指向物件、类别的指标和检测一个物件是否是一个井字游戏元件。

_get_type() 函式

现在,我们继续做我们的元件。WIDGETNAME_get_type() 函式是每个元件的核心函式。当第一次呼叫时,该函式告之 GTK 这个元件类别并得到一个能唯一识别该元件类别的 ID。之后的呼叫,只传回这个ID。

GType
tictactoe_get_type (void)
{
  static GType ttt_type = 0;

  if (!ttt_type)
    {
      const GTypeInfo ttt_info =
      {
	sizeof (TictactoeClass),
	NULL, /* base_init */
	NULL, /* base_finalize */
	(GClassInitFunc) tictactoe_class_init,
	NULL, /* class_finalize */
	NULL, /* class_data */
	sizeof (Tictactoe),
	0,    /* n_preallocs */
	(GInstanceInitFunc) tictactoe_init,
      };

      ttt_type = g_type_register_static (GTK_TYPE_TABLE,
                                         "Tictactoe",
                                         &ttt_info,
                                         0);
    }

  return ttt_type;
}

GtkTypeInfo 结构定义如下:

struct _GTypeInfo
{
  /* interface types, classed types, instantiated types */
  guint16                class_size;

  GBaseInitFunc          base_init;
  GBaseFinalizeFunc      base_finalize;

  /* classed types, instantiated types */
  GClassInitFunc         class_init;
  GClassFinalizeFunc     class_finalize;
  gconstpointer          class_data;

  /* instantiated types */
  guint16                instance_size;
  guint16                n_preallocs;
  GInstanceInitFunc      instance_init;

  /* value handling */
  const GTypeValueTable *value_table;
};

这个结构的栏位不用加以说明就可以明了。我们在这里忽略 arg_set_funcarg_get_func 以及value_table等栏位,一旦 GTK 正确的填充了该结构,它就知道了如何去创建一个特殊型别的物件。

_class_init() 函式

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

enum {
  TICTACTOE_SIGNAL,
  LAST_SIGNAL
};

static guint tictactoe_signals[LAST_SIGNAL] = { 0 };

static void
tictactoe_class_init (TictactoeClass *klass)
{
  tictactoe_signals[TICTACTOE_SIGNAL] =
    g_signal_new ("tictactoe",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
                  G_STRUCT_OFFSET (TictactoeClass, tictactoe),
                  NULL, NULL,
                  g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0);
}

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

guint g_signal_new( const gchar         *signal_name,
                    GType                itype,
                    GSignalFlags         signal_flags,
                    guint                class_offset,
                    GSignalAccumulator  *accumulator,
                    gpointer             accu_data,
                    GSignalCMarshaller  *c_marshaller,
                    GType                return_type,
                    guint                n_params,
                    ...);

创建一个新信号。参数是:

  • name:信号的名称。
  • run_type:设定是在用户处理函式之前还是之后执行预设处理函式。该值通常会是 GTK_RUN_FIRSTGTK_RUN_LAST,虽然也有其它值。
  • object_type:发出该信号的物件的ID。(也可能是子物件的ID。)
  • function_offset:类别结构指标相对于预设处理函式的偏移量。
  • marshaller:一个用于呼叫信号处理函式的函式。对于除了发出信号的物件和用户资料外没有其它的参数的信号处理函式,我们可以使用事先提供的 marshaller 函式 gtk_signal_default_marshaller
  • return_val:传回值的型别。
  • nparams:信号处理函式的参数个数 (除了前面提到的”发出信号的物件”和”用户资料”这两个预设参数 )
  • ...:参数的资料型别。

当指定型别时,要用到下列的标准型别:

G_TYPE_INVALID
G_TYPE_NONE
G_TYPE_INTERFACE
G_TYPE_CHAR
G_TYPE_UCHAR
G_TYPE_BOOLEAN
G_TYPE_INT
G_TYPE_UINT
G_TYPE_LONG
G_TYPE_ULONG
G_TYPE_INT64
G_TYPE_UINT64
G_TYPE_ENUM
G_TYPE_FLAGS
G_TYPE_FLOAT
G_TYPE_DOUBLE
G_TYPE_STRING
G_TYPE_POINTER
G_TYPE_BOXED
G_TYPE_PARAM
G_TYPE_OBJECT

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

_init() 函式

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

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

  gtk_table_resize (GTK_TABLE (ttt), 3, 3);
  gtk_table_set_homogeneous (GTK_TABLE (ttt), TRUE);

  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 (ttt), ttt->buttons[i][j],
				   i, i+1, j, j+1);
	g_signal_connect (ttt->buttons[i][j], "toggled",
			  G_CALLBACK (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() 是一个公用函式,它重设元件中的所有按钮至upper的位置。注意 gtk_signal_handler_block_by_data() 函式用来防止按钮切换信号处理函式被不必要地触发。 tictactoe_toggle() 是当用户点击按钮时呼叫的信号处理函式。它判断双态按钮中是否出现了导致赢的组合,如果是,则发出 “tictactoe” 信号。

GtkWidget*
tictactoe_new (void)
{
  return GTK_WIDGET ( g_object_new (TICTACTOE_TYPE, NULL));
}

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

  for (i=0;i<3;i++)
    for (j=0;j<3;j++)
      {
	g_signal_handlers_block_matched (G_OBJECT (ttt->buttons[i][j]),
                                         G_SIGNAL_MATCH_DATA,
                                         0, 0, NULL, NULL, ttt);
	gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (ttt->buttons[i][j]),
				     FALSE);
	g_signal_handlers_unblock_matched (G_OBJECT (ttt->buttons[i][j]),
                                           G_SIGNAL_MATCH_DATA,
                                           0, 0, NULL, NULL, 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)
	{
	  g_signal_emit (G_OBJECT (ttt),
			 tictactoe_signals[TICTACTOE_SIGNAL], 0);
	  break;
	}
    }
}

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

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

/* Invoked when a row, column or diagonal is completed */
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");

  g_signal_connect (window, "destroy",
                    G_CALLBACK (exit), NULL);

  gtk_container_set_border_width (GTK_CONTAINER (window), 10);

  /* Create a new Tictactoe widget */
  ttt = tictactoe_new ();
  gtk_container_add (GTK_CONTAINER (window), ttt);
  gtk_widget_show (ttt);

  /* And attach to its "tictactoe" signal */
  g_signal_connect (ttt, "tictactoe",
                    G_CALLBACK (win), NULL);

  gtk_widget_show (window);

  gtk_main ();

  return 0;
}
<<< Previous 单元首页 Next >>>
GTK+ 2.0 教学-元件的解析 Up GTK+ 2.0 教学-从草稿中产生元件

Comments are closed.