文本文件编辑器(tfe)的新版本将在本节和以下四节中编写。它是tfe5。与之前的版本相比,有很多变化。它们位于两个目录中,src/tfe5和src/tfetextview。
1 封装
我们将C源文件分为两部分。但就封装而言,这还不够。
- tfe.c包含除Tfe TextView之外的所有内容。它至少应该分为两部分,tfeapplication.c和tfenotebook.c。
- 头文件也需要组织。
然而,首先,我想关注TfeTextView对象。它是GtkTextView的一个子对象,其中有一个新的成员文件。重要的是管理file指向的Gfile对象。
- 当你创建(或初始化)TfeTextView,对于GFile什么是必须的?
- 当你销毁TfeTextView,对于GFile什么是必须的?
- TfeTextView是否应该自己读/写文件?
- 它如何与其他的对象交流?
在考虑类、实例和信号之前,你至少需要知道它们。我将在本节和下一节中解释它们。之后我会解释:
- 组织功能(Organizing functions)
- 如何使用GtkFileChooserDialog
2 GObject and its children
GObject及其子对象都是对象,它们既有class C结构,也有object C结构。首先,考虑实例。一个实例是具有对象结构的内存。下面是TfeTextView的结构。
/* This typedef statement is automatically generated by the macro G_DECLARE_FINAL_TYPE */
typedef struct _TfeTextView TfeTextView;
struct _TfeTextView {
GtkTextView parent;
GFile *file;
};
该结构的成员如下:
- 父类的类型是GtkTextView,这是C结构体。它在gtktextview.h中声明。GtkTextView是TfeTextView的父类。
- file是一个GFile类型的指针。如果没有文件对应于TfeTextView实例,它可以是NULL。
您可以在GTK和GLib的源文件中找到父类对象结构的声明。下面的代码是从源文件中提取的(不完全相同)。
typedef struct _GObject GObject;
typedef struct _GObject GInitiallyUnowned;
struct _GObject
{
GTypeInstance g_type_instance;
volatile guint ref_count;
GData *qdata;
};
typedef struct _GtkWidget GtkWidget;
struct _GtkWidget
{
GInitiallyUnowned parent_instance;
GtkWidgetPrivate *priv;
};
typedef struct _GtkTextView GtkTextView;
struct _GtkTextView
{
GtkWidget parent_instance;
GtkTextViewPrivate *priv;
};
在每个结构中,其父元素声明在成员元素的顶部。所以,所有的父类都包含在子对象中。TfeTextView的结构如下图所示。
派生类(父类)都有它们自己的私有数据区,这些私有数据区不包含在上述结构中。例如,GtkWidget的私有数据为GtkWidgetPrivate (C结构体)。
注意声明(declarations)不是定义(definitions)。因此,在声明C结构体时不会分配内存。在调用tfe_text_view_new函数时,会从堆区域为它们分配内存。同时,对于TfeTetView父类的私有区域也会被分配。它们从TfeTextView隐藏,不能直接访问它们。创建的内存称为实例(instance)。创建TfeTextView实例时,会给它三个数据区域。
- 实例(C结构体)。
- GtkWidgetPrivate结构体。
- GtkTextViewPrivate结构体。
TfeTextView函数只能访问其实例。GtkWidgetPrivate和GtkTextViewPrivate被父类的函数使用。请看下面的例子。
GtkWidget *tv = tfe_text_view_new ();
GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
父函数gtk_text_view_get_buffer访问GtkTextViewPrivate数据(由tv拥有)。私有区域中有一个指向GtkBuffer的指针,该函数返回该指针。(实际的行为有点复杂。)
TfeTextView实例像这样继承了父类函数。
每次调用tfe_text_view_new函数时,都会创建一个TfeTextView实例。因此,可以存在多个TfeTextView实例。
3 初始化TfeTextView实例
函数tfe_text_view_new创建了一个新的TfeTextView实例。
1 GtkWidget *
2 tfe_text_view_new (void) {
3 return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, "wrap-mode", GTK_WRAP_WORD_CHAR, NULL));
4 }
调用这个函数时,会创建并初始化一个TfeTextView实例。初始化过程如下。
- 当创建实例时,也创建了GtkWidgetPrivate和GtkTextViewPrivate结构体。
- 在TfeTextView实例中初始化GObject (GInitiallyUnowned)部分。
- 在TfeTextView实例和GtkWidgetPrivate结构中初始化GtkWidget部分(第一个priv)。
- 初始化GtkTextView部分(第二个priv)在TfeTextView实例和GtkTextViewPrivate结构。
- 在TfeTextView实例中初始化TfeTextView部分(文件)。
第二到第四步由g_object_init、gtk_widget_init和gtk_text_view_init完成。它们由系统自动调用,你不需要关心它们。第四步是由tfetextview.c中的函数tfe_text_view_init完成的。
1 static void
2 tfe_text_view_init (TfeTextView *tv) {
3 tv->file = NULL;
4 }
这个函数只是将tv->file初始化为NULL。
4 方法和类
在Gtk中,所有从GObject派生的对象都有类和实例(抽象对象除外)。实例是C结构体的内存,在前两个小节中描述过。每个对象可以有多个实例。这些实例具有相同的结构。实例只有数据。因此,它没有定义对象的行为。我们至少需要两样东西。一个是函数,另一个是类方法。
你已经见过很多函数了。例如,
- TfeTextView *tfe_text_view_new (void);是一个创建TfeTextView实例的函数。
- GtkTextBuffer *gtk_text_view_get_buffer (GtkTextView *textview)是一个从GtkTextView获取GtkTextBuffer的函数。
函数是公有的,这意味着它们可以被其他对象使用。它们类似于面向对象语言中的公共方法。
类(C结构体)主要由指向函数指针组成。这些函数被称为类方法,由对象本身或其后代对象使用。例如,GObject类在GLib源文件的GObject .h中声明。
1 typedef struct _GObjectClass GObjectClass;
2 typedef struct _GObjectClass GInitiallyUnownedClass;
3
4 struct _GObjectClass
5 {
6 GTypeClass g_type_class;
7
8 /*< private >*/
9 GSList *construct_properties;
10
11 /*< public >*/
12 /* seldom overridden */
13 GObject* (*constructor) (GType type,
14 guint n_construct_properties,
15 GObjectConstructParam *construct_properties);
16 /* overridable methods */
17 void (*set_property) (GObject *object,
18 guint property_id,
19 const GValue *value,
20 GParamSpec *pspec);
21 void (*get_property) (GObject *object,
22 guint property_id,
23 GValue *value,
24 GParamSpec *pspec);
25 void (*dispose) (GObject *object);
26 void (*finalize) (GObject *object);
27 /* seldom overridden */
28 void (*dispatch_properties_changed) (GObject *object,
29 guint n_pspecs,
30 GParamSpec **pspecs);
31 /* signals */
32 void (*notify) (GObject *object,
33 GParamSpec *pspec);
34
35 /* called when done constructing */
36 void (*constructed) (GObject *object);
37
38 /*< private >*/
39 gsize flags;
40
41 gsize n_construct_properties;
42
43 gpointer pspecs;
44 gsize n_pspecs;
45
46 /* padding */
47 gpointer pdummy[3];
48 };
49
第23行有一个指向函数dispose的指针。
void (*dispose) (GObject *object);
这个声明有点复杂。标识符dispose之前的星号表示指针。因此,指针分配指向一个只有一个参数的函数,它指向一个GObject结构,并且没有返回值。同样,第24行说finalize是一个指向函数的指针,它有一个参数,指向GObject结构,并且没有返回值。
void (*finalize) (GObject *object);
看看_GObjectClass的声明,你会发现大多数成员都是指向函数的指针。
- 13:当实例创建的时候,通过constructor调用被指向的函数。它完成实例的初始化。
- 25:当实例析构时调用dispose指向的函数。销毁过程分为两个阶段。第一个被称为处理(disposing)。在这个阶段,实例释放对其他实例的所有引用。第二阶段是finalizing。
- 26:由finalize指向的函数完成析构过程。
- 其他指针指向在实例生命周期期间调用的函数。
这些函数称为类方法(class method)。这些方法对其后代开放。但不向非后代对象开放。
5 TfeTextView类
TfeTextView类是一个结构体,它包含了它所有父类。因此,类与实例具有相似的层次结构。
GObjectClass (GInitiallyUnownedClass) -- GtkWidgetClass -- GtkTextViewClass -- TfeTextViewClass
下面的代码从源代码中提取(不完全相同)。
1 struct _GtkWidgetClass
2 {
3 GInitiallyUnownedClass parent_class;
4
5 /*< public >*/
6
7 /* basics */
8 void (* show) (GtkWidget *widget);
9 void (* hide) (GtkWidget *widget);
10 void (* map) (GtkWidget *widget);
11 void (* unmap) (GtkWidget *widget);
12 void (* realize) (GtkWidget *widget);
13 void (* unrealize) (GtkWidget *widget);
14 void (* root) (GtkWidget *widget);
15 void (* unroot) (GtkWidget *widget);
16 void (* size_allocate) (GtkWidget *widget,
17 int width,
18 int height,
19 int baseline);
20 void (* state_flags_changed) (GtkWidget *widget,
21 GtkStateFlags previous_state_flags);
22 void (* direction_changed) (GtkWidget *widget,
23 GtkTextDirection previous_direction);
24
25 /* size requests */
26 GtkSizeRequestMode (* get_request_mode) (GtkWidget *widget);
27 void (* measure) (GtkWidget *widget,
28 GtkOrientation orientation,
29 int for_size,
30 int *minimum,
31 int *natural,
32 int *minimum_baseline,
33 int *natural_baseline);
34
35 /* Mnemonics */
36 gboolean (* mnemonic_activate) (GtkWidget *widget,
37 gboolean group_cycling);
38
39 /* explicit focus */
40 gboolean (* grab_focus) (GtkWidget *widget);
41 gboolean (* focus) (GtkWidget *widget,
42 GtkDirectionType direction);
43 void (* set_focus_child) (GtkWidget *widget,
44 GtkWidget *child);
45
46 /* keyboard navigation */
47 void (* move_focus) (GtkWidget *widget,
48 GtkDirectionType direction);
49 gboolean (* keynav_failed) (GtkWidget *widget,
50 GtkDirectionType direction);
51
52 gboolean (* query_tooltip) (GtkWidget *widget,
53 int x,
54 int y,
55 gboolean keyboard_tooltip,
56 GtkTooltip *tooltip);
57
58 void (* compute_expand) (GtkWidget *widget,
59 gboolean *hexpand_p,
60 gboolean *vexpand_p);
61
62 void (* css_changed) (GtkWidget *widget,
63 GtkCssStyleChange *change);
64
65 void (* system_setting_changed) (GtkWidget *widget,
66 GtkSystemSetting settings);
67
68 void (* snapshot) (GtkWidget *widget,
69 GtkSnapshot *snapshot);
70
71 gboolean (* contains) (GtkWidget *widget,
72 double x,
73 double y);
74
75 /*< private >*/
76
77 GtkWidgetClassPrivate *priv;
78
79 gpointer padding[8];
80 };
81
82 struct _GtkTextViewClass
83 {
84 GtkWidgetClass parent_class;
85
86 /*< public >*/
87
88 void (* move_cursor) (GtkTextView *text_view,
89 GtkMovementStep step,
90 int count,
91 gboolean extend_selection);
92 void (* set_anchor) (GtkTextView *text_view);
93 void (* insert_at_cursor) (GtkTextView *text_view,
94 const char *str);
95 void (* delete_from_cursor) (GtkTextView *text_view,
96 GtkDeleteType type,
97 int count);
98 void (* backspace) (GtkTextView *text_view);
99 void (* cut_clipboard) (GtkTextView *text_view);
100 void (* copy_clipboard) (GtkTextView *text_view);
101 void (* paste_clipboard) (GtkTextView *text_view);
102 void (* toggle_overwrite) (GtkTextView *text_view);
103 GtkTextBuffer * (* create_buffer) (GtkTextView *text_view);
104 void (* snapshot_layer) (GtkTextView *text_view,
105 GtkTextViewLayer layer,
106 GtkSnapshot *snapshot);
107 gboolean (* extend_selection) (GtkTextView *text_view,
108 GtkTextExtendSelection granularity,
109 const GtkTextIter *location,
110 GtkTextIter *start,
111 GtkTextIter *end);
112 void (* insert_emoji) (GtkTextView *text_view);
113
114 /*< private >*/
115
116 gpointer padding[8];
117 };
118
119 /* The following definition is generated by the macro G_DECLARE_FINAL_TYPE */
120 typedef struct {
121 GtkTextView parent_class;
122 } TfeTextViewClass;
123
- 120-122:这3行代码由宏G_DECLARE_FINAL_TYPE生成。因此,它们不是写在tfe_text_view.h或tfe_text_view.c中。
- 3,84, 121:每个类在其结构的第一个成员处都有父类。它与实例结构相同。
- 父类中的类成员对后代类开放。因此,它们可以在tfe_text_view_class_init函数中更改。例如,GObjectClass中的finalize指针将在稍后的tfe_text_view_class_init中被覆盖。Override是一个面向对象的编程术语。Override是在子类中重写父类的方法。)
- 有些类方法经常会被重写。set_property、get_property、dispose、finalize和construct就是这样的方法。
6 TfeTextView销毁
从GObject派生的每个对象都有一个引用计数。如果一个对象A引用了一个对象B,那么A会在A中保留一个指向B的指针,同时使用g_object_ref (B)函数将B的引用计数增加1。如果A不再需要B,那么A会丢弃指向B的指针(通常是通过将指针赋值NULL来实现,表示不再使用),并使用g_object_unref (B)函数将B的引用计数减少1。
如果两个对象A和B都指向C,那么C的引用计数是2。如果A不再需要C,则A丢弃指向C的指针,并将C中的引用计数减1。现在C的引用计数是1。同样,如果B不再需要C,则B丢弃指向C的指针,并将C中的引用计数减少1。此时,没有对象引用C, C的引用计数为0。这意味着C不再有用。然后,C会销毁自身,最终分配给C的内存会被释放。
上面的想法是基于一个假设,即由nothing引用的对象的引用计数为零。当引用计数降到0时,对象开始销毁过程。销毁过程分为两个阶段:disposing和finalizing。在disposing过程中,对象调用其类中的dispose指向的函数,以释放对其他实例的所有引用。之后,它会调用类中的finalize指向的函数来完成析构过程。例如,处理程序或处理方法。
在销毁过程中,TfeTextView需要解引用通过tv->file指向的GFile。必须编写处理程序tfe_text_view_dispose。
1 static void
2 tfe_text_view_dispose (GObject *gobject) {
3 TfeTextView *tv = TFE_TEXT_VIEW (gobject);
4
5 if (G_IS_FILE (tv->file))
6 g_clear_object (&tv->file);
7
8 G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject);
9 }
- 5、6:如果tv->file指向GFile,则减少其引用计数。g_clear_object减少引用计数并将NULL赋值给tv->file。在处置处理程序中,我们通常使用g_clear_object而不是g_object_unref。
- 8:调用父类的dispose处理程序。(稍后会解释。)
在处理过程中,对象使用其类中的指针来调用处理程序。因此,在初始化TfeTextView类时,需要在类中注册tfe_text_view_dispose。函数tfe_text_view_class_init是类初始化函数,它在G_DEFINE_TYPE宏扩展中声明。
static void
tfe_text_view_class_init (TfeTextViewClass *class) {
GObjectClass *object_class = G_OBJECT_CLASS (class);
object_class->dispose = tfe_text_view_dispose;
}
每个父类在创建TfeTextViewClass之前已经创建。因此,有四个类,每个类都有一个指向每个释放处理程序的指针。请看下图。有四个类——GObjectClass (GInitiallyUnownedClass), GtkWidgetClass, GtkTextViewClass和TfeTextViewClass。每个类都有自己的处理程序——dh1、dh2、dh3和tfe_text_view_dispose。
现在,看看上面的tfe_text_view_dispose程序。它首先释放tv->file指向的GFile对象的引用。然后在第8行中调用其父类的dispose处理程序。
G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject);
变量tfe_text_view_parent_class由G_DEFINE_TYPE宏创建,是一个指向父对象类的指针。gobject变量是一个指向TfeTextView实例的指针,该实例被强制转换为gobject实例。因此,G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose指向上图中的处理程序dh3。语句G_OBJECT_CLASS (tfe_text_view_parent_class)->dispose (gobject)与dh3 (gobject)相同,这意味着它在TfeTextView实例中,会释放GtkTextViewPrivate中其他实例的所有引用。之后,dh3调用dh2, dh2调用dh1。最后释放所有的引用。