七、Gtk4-Defining a final class

文章讲述了如何将一个简单的文件查看器转换为一个具有文本保存功能的编辑器,通过创建GtkTextView的子类TfeTextView来存储GFile指针,并介绍了如何处理窗口关闭前的保存操作。此外,还涉及到了GObject系统的使用和子类化的概念。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 定义一个最终类

1.1 一个非常简单的编辑器

在上一节中,我们创建了一个非常简单的文件查看器。现在我们继续重写它,并将其转换为非常简单的编辑器。它的源文件是tfe目录下的tfe1.c(文本文件编辑器1)。

GtkTextView是一个多行编辑器。因此,我们不需要从头开始编写编辑器。我们只需要在文件查看器中添加两件事:

  • 指向GFile实例的指针。
  • 文本保存功能。

有两种方法可以存储指针。

  • 使用全局变量
  • 创建一个GtkTextView的子类,它的每个实例都保存一个指向GFile实例的指针。

使用全局变量很容易实现。定义一个指向GFile的指针数组。例如,

GFile * f [20];

变量f[i]对应于与第i个GtkNotebookPage关联的文件。

然而,有两个问题。第一个是数组的大小。如果用户提供了太多的参数(在上面的例子中超过20个),则不可能存储所有指向GFile实例的指针。二是程序维护困难。到目前为止,我们有一个小程序。但是,你开发的程序越多,它的规模就越大。一般来说,在大型程序中维护全局变量是非常困难的。在检查全局变量时,需要检查使用该变量的所有代码。

从维护的角度来看,创建子类是个好主意。我们更喜欢它而不是一个全局变量。

请注意,我们考虑的是“子类”,而不是“子构件”。子类和子部件完全不同。类是GObject系统的一个术语。如果你不熟悉GObject,请参阅:

  • GObject API reference
  • GObject tutorial for beginners

子类继承了父类的一切,而且还扩展了它的性能。我们将TfeTextView定义为GtkTextView的一个子类。它拥有GtkTextView拥有的一切,并添加了一个指向GFile的指针。

在这里插入图片描述

1.2 如何定义一个GtkTextView的子类

你需要了解GObject系统约定。首先,看看下面的程序。

#define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)

struct _TfeTextView
{
  GtkTextView parent;
  GFile *file;
};

G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);

static void
tfe_text_view_init (TfeTextView *tv) {
}

static void
tfe_text_view_class_init (TfeTextViewClass *class) {
}

void
tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
  tv -> file = f;
}

GFile *
tfe_text_view_get_file (TfeTextView *tv) {
  return tv -> file;
}

GtkWidget *
tfe_text_view_new (void) {
  return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
}
  • TfeTextView分为两部分。Tfe和TextView。Tfe称为前缀(prefix)或命名空间(namespace)。TextView被称为object。
    有三种不同的标识符模式。TfeTextView(驼峰式大小写),tfe_text_view(经常用于函数)和TFE_TEXT_VIEW(用于将对象转换为TfeTextView)。
  • 首先,定义TFE_TYPE_TEXT_VIEW宏为tfe_text_view_get_type()。名称总是(prefix)TYPE(object),并且字母都是大写。替换文本总是(prefix)_(object)_get_type(),字母都是小写。该定义放在G_DECLARE_FINAL_TYPE宏之前。
  • G_DECLARE_FINAL_TYPE宏的参数是驼峰式的子类名、小写带下划线、前缀(大写)、对象(大写带下划线)和父类名(驼峰式)。在该宏的扩展中声明了下列两个C语言结构。
    • typedef struct _TfeTextView TfeTextView
    • typedef struct {GtkTextViewClass parent_class;} TfeTextViewClass;
      这些声明告诉我们TfeTextView和TfeTextViewClass是C结构体。“TfeTextView”有两个含义,类名和C结构名。C语言结构体TfeTextView被称为object。类似地,TfeTextViewClass被称为class。
  • 声明结构体_TfeTextView。下划线是必要的。第一个成员是父对象(C结构)。注意,这不是一个指针,而是对象本身。第二个成员和后面的成员是子对象的成员。TfeTextView结构有一个指向GFile实例的指针作为成员。
  • G_DEFINE_TYPE宏。参数是子对象名称(驼峰大小写),带有下划线的小写和父对象类型(prefix)TYPE(module)。这个宏主要用于向类型系统注册新类。类型系统是GObject的基础系统。每个类都有自己的类型。GObject、GtkWidget和TfeTextView的类型分别是G_TYPE_OBJECT、GTK_TYPE_WIDGET和TFE_TYPE_TEXT_VIEW。这样的类型(例如,TFE_TYPE_TEXT_VIEW)是一个宏,它被扩展为一个函数(tfe_text_view_get_type())。它返回一个在所有GObject系统类中唯一的整数。
  • 实例init函数(tfe_text_view_init)在创建实例时被调用。它与其他面向对象语言中的构造函数相同。
    类init函数(tfe_text_view_class_init)在创建类时被调用。
  • 两个函数tfe_text_view_set_file和tfe_text_view_get_file是公共函数。公共函数是开放的,你可以在任何地方调用它们。它们与其他面向对象语言中的public方法相同。tv是一个指向TfeTextView对象(C结构)的指针。它有一个成员file,它是被tv->file指向。
  • TfeTextView实例创建函数是tfe_text_view_new。它的名字是(prefix)_(object)_new。它使用g_object_new函数创建实例。参数是(prefix)TYPE(object)、初始化属性的列表和NULL。NULL是属性列表的结束标记。这里没有初始化任何属性。返回值转换为GtkWidget。

这个程序展示了如何定义子类的大纲。

1.3 Close-request信号

假设您正在使用这个编辑器。首先,带着参数运行编辑器。参数是文件名。编辑器读取文件并显示一个包含文件文本的窗口。然后编辑文本。完成编辑后,退出编辑器。编辑器会在窗口关闭前更新文件。

GtkWindow在关闭之前发出"close-request"信号。我们将在before_close之前连接信号和处理程序。(处理程序是一个连接到信号的C函数。)before_close函数会在发出"close-request"信号时被调用。

g_signal_connect (win, "close-request"G_CALLBACK (before_close)NULL);

参数win是一个GtkApplicationWindow,其中定义了"close-request"信号,before_close是处理程序。G_CALLBACK转换对处理程序是必要的。before_close的程序如下所示。

 1 static gboolean
 2 before_close (GtkWindow *win, GtkWidget *nb) {
 3   GtkWidget *scr;
 4   GtkWidget *tv;
 5   GFile *file;
 6   char *pathname;
 7   GtkTextBuffer *tb;
 8   GtkTextIter start_iter;
 9   GtkTextIter end_iter;
10   char *contents;
11   unsigned int n;
12   unsigned int i;
13 
14   n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
15   for (i = 0; i < n; ++i) {
16     scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
17     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
18     file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
19     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
20     gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
21     contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
22     if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
23       if ((pathname = g_file_get_path (file)) != NULL) {
24         g_printerr ("Can't save %s.", pathname);
25         g_free (pathname);
26       } else
27         g_printerr ("Pathname not exist.\n");
28     }
29     g_free (contents);
30     g_object_unref (file);
31   }
32   return FALSE;
33 }

项目左边的数字是源代码中的行号。

  • 14:将nb的总页数赋给n。
  • 15-31:针对每个页面的索引进行for循环。
  • 16-18: scr、tv和file被赋值给指向GtkScrolledWindow、TfeTextView和GFile的指针。当app_open处理程序被调用时,TfeTextView的GFile会被赋值(set)。稍后展示。
    19-21: tb被分配给TfeTextView的GtkTextBuffer。使用迭代器访问缓冲区。迭代器指向缓冲区中的某个位置。函数gtk_text_buffer_get_bounds将缓冲区的开始和结束分别分配给start_iter和end_iter。然后函数gtk_text_buffer_get_text返回start_iter和end_iter之间的文本,即缓冲区中的整个文本。
  • 22 ~ 28:文本保存到文件中。如果失败,则显示错误消息。
  • 29:释放存储内容的内存。
  • 30: GFile此时已经没用。g_object_unref减少GFile的引用计数。引用计数将在后面一节中解释。在这个程序中,引用计数将为0,GFile实例将销毁自己。(TfeTextView类没有定了处理file内存的函数,所以要自己unref)

1.4 Source code of tfe1.c

/* filename: tfe1.c */
  1 #include <gtk/gtk.h>
  2 
  3 /* Define TfeTextView Widget which is the child class of GtkTextView */
  4 
  5 #define TFE_TYPE_TEXT_VIEW tfe_text_view_get_type ()
  6 G_DECLARE_FINAL_TYPE (TfeTextView, tfe_text_view, TFE, TEXT_VIEW, GtkTextView)
  7 
  8 struct _TfeTextView
  9 {
 10   GtkTextView parent;
 11   GFile *file;
 12 };
 13 
 14 G_DEFINE_TYPE (TfeTextView, tfe_text_view, GTK_TYPE_TEXT_VIEW);
 15 
 16 static void
 17 tfe_text_view_init (TfeTextView *tv) {
 18   tv->file = NULL;
 19 }
 20 
 21 static void
 22 tfe_text_view_class_init (TfeTextViewClass *class) {
 23 }
 24 
 25 void
 26 tfe_text_view_set_file (TfeTextView *tv, GFile *f) {
 27   tv->file = f;
 28 }
 29 
 30 GFile *
 31 tfe_text_view_get_file (TfeTextView *tv) {
 32   return tv -> file;
 33 }
 34 
 35 GtkWidget *
 36 tfe_text_view_new (void) {
 37   return GTK_WIDGET (g_object_new (TFE_TYPE_TEXT_VIEW, NULL));
 38 }
 39 
 40 /* ---------- end of the definition of TfeTextView ---------- */
 41 
 42 static gboolean
 43 before_close (GtkWindow *win, GtkWidget *nb) {
 44   GtkWidget *scr;
 45   GtkWidget *tv;
 46   GFile *file;
 47   char *pathname;
 48   GtkTextBuffer *tb;
 49   GtkTextIter start_iter;
 50   GtkTextIter end_iter;
 51   char *contents;
 52   unsigned int n;
 53   unsigned int i;
 54 
 55   n = gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb));
 56   for (i = 0; i < n; ++i) {
 57     scr = gtk_notebook_get_nth_page (GTK_NOTEBOOK (nb), i);
 58     tv = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (scr));
 59     file = tfe_text_view_get_file (TFE_TEXT_VIEW (tv));
 60     tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
 61     gtk_text_buffer_get_bounds (tb, &start_iter, &end_iter);
 62     contents = gtk_text_buffer_get_text (tb, &start_iter, &end_iter, FALSE);
 63     if (! g_file_replace_contents (file, contents, strlen (contents), NULL, TRUE, G_FILE_CREATE_NONE, NULL, NULL, NULL)) {
 64       if ((pathname = g_file_get_path (file)) != NULL) {
 65         g_printerr ("Can't save %s.", pathname);
 66         g_free (pathname);
 67       } else
 68         g_printerr ("Pathname not exist.\n");
 69     }
 70     g_free (contents);
 71     g_object_unref (file);
 72   }
 73   return FALSE;
 74 }
 75 
 76 static void
 77 app_activate (GApplication *app) {
 78   g_print ("You need to give filenames as arguments.\n");
 79 }
 80 
 81 static void
 82 app_open (GApplication *app, GFile ** files, gint n_files, gchar *hint) {
 83   GtkWidget *win;
 84   GtkWidget *nb;
 85   GtkWidget *lab;
 86   GtkNotebookPage *nbp;
 87   GtkWidget *scr;
 88   GtkWidget *tv;
 89   GtkTextBuffer *tb;
 90   char *contents;
 91   gsize length;
 92   char *filename;
 93   int i;
 94 
 95   win = gtk_application_window_new (GTK_APPLICATION (app));
 96   gtk_window_set_title (GTK_WINDOW (win), "file editor");
 97   gtk_window_set_default_size (GTK_WINDOW (win), 600, 400);
 98 
 99   nb = gtk_notebook_new ();
100   gtk_window_set_child (GTK_WINDOW (win), nb);
101 
102   for (i = 0; i < n_files; i++) {
103     if (g_file_load_contents (files[i], NULL, &contents, &length, NULL, NULL)) {
104       scr = gtk_scrolled_window_new ();
105       tv = tfe_text_view_new ();
106       tb = gtk_text_view_get_buffer (GTK_TEXT_VIEW (tv));
107       gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (tv), GTK_WRAP_WORD_CHAR);
108       gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scr), tv);
109 
110       tfe_text_view_set_file (TFE_TEXT_VIEW (tv),  g_file_dup (files[i]));
111       gtk_text_buffer_set_text (tb, contents, length);
112       g_free (contents);
113       filename = g_file_get_basename (files[i]);
114       lab = gtk_label_new (filename);
115       gtk_notebook_append_page (GTK_NOTEBOOK (nb), scr, lab);
116       nbp = gtk_notebook_get_page (GTK_NOTEBOOK (nb), scr);
117       g_object_set (nbp, "tab-expand", TRUE, NULL);
118       g_free (filename);
119     } else if ((filename = g_file_get_path (files[i])) != NULL) {
120         g_print ("No such file: %s.\n", filename);
121         g_free (filename);
122     } else
123         g_print ("No valid file is given\n");
124   }
125   if (gtk_notebook_get_n_pages (GTK_NOTEBOOK (nb)) > 0) {
126     g_signal_connect (win, "close-request", G_CALLBACK (before_close), nb);
127     gtk_window_present (GTK_WINDOW (win));
128   } else
129     gtk_window_destroy (GTK_WINDOW (win));
130 }
131 
132 int
133 main (int argc, char **argv) {
134   GtkApplication *app;
135   int stat;
136 
137   app = gtk_application_new ("com.github.ToshioCP.tfe1", G_APPLICATION_HANDLES_OPEN);
138   g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
139   g_signal_connect (app, "open", G_CALLBACK (app_open), NULL);
140   stat =g_application_run (G_APPLICATION (app), argc, argv);
141   g_object_unref (app);
142   return stat;
143 }
  • 110: TfeTextView的GFile指针设置为files[i],这是一个用命令行参数创建的GFile。但是GFile稍后会被系统销毁。所以它需要在赋值之前被复制。g_file_dup复制GFile文件。
  • 126:“close-reques”信号连接到before_close处理程序。第四个参数被称为“用户数据”,它将是信号处理程序的第二个参数。因此,nb被赋给before_close作为第二个参数。

现在是时候编译和运行了。

$ cd src/tfe
$ comp tfe1
$ ./a.out taketori.txt`.

修改内容并关闭窗口。确保文件已被修改。

现在我们有了一个非常简单的编辑器。这并不明智。我们需要更多的功能,如打开,保存,保存,更改字体等。我们将在下一节和之后添加它们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值