我翻到一本 GTK+ 编程书的第160页时,说实话,第一眼觉得这例子也太“素”了:一个窗口、一块能输入文字的白色区域、几个菜单项。但如果只是把它当成“照着敲一遍就完事”的入门练习,那就太浪费了。书里这个简单的字处理程序,其实把 GTK+ 的窗口布局、文本缓冲区、信号回调、文件对话框这些核心机制全串了起来。源代码篇幅不长,但每一段都值得逐行抠开看,读懂之后,你后面写任何图形界面工具都会顺手很多。这篇文章就和你一起把这个例子从控件到代码逻辑整个拆一遍。
1. 这个例子到底在做什么
1.1 “字处理程序”的最小闭环
先明确一个概念:这里的“字处理程序”不是 Word 那种排版、插图、样式齐全的编辑器,而是一个最基础的文本编辑器。它要解决的需求很简单:创建一个窗口,里面有一块可以编辑文字的空白区域,然后让用户通过菜单完成打开文件、保存文件、剪切、复制、粘贴、新建、退出这几项基本操作。
可别小看这个“最小闭环”。文本编辑器的骨架不外乎就是这些:一个编辑区、一条菜单栏、一个文件选择对话框、一组文本缓冲区操作。无论你以后是做记事本、写代码编辑器、做日志分析工具,都得跟这套机制打交道。很多初学者看完教程觉得自己都会了,但一动手就发现窗口怎么都弹不出来、菜单点了没反应、打开文件全是乱码。这些问题的根源,基本都藏在这个例子里。
书里把这个例子放在第160页也有讲究。前面的章节大概率已经讲完了 GTK+ 的基础控件、信号与回调、容器布局。到这一页,正好需要把那些零零碎碎的知识组装成一个小项目。它不追求华丽,而是在有限篇幅里,把“窗口 + 容器 + 控件 + 信号 + 文件操作”这条完整链路展示出来。对这个例子做源代码阅读和分析,收益最大的点不在控件 API 本身,而在于理解“GTK+ 程序是如何被组织起来的”。
1.2 例子里反复出现的几个核心概念
从一个读者的角度,我建议你拿到源代码后,先别急着敲,先用几分钟把文件从头到尾扫一遍,找出它的骨架:开头通常是一堆#include,然后是全局变量和函数声明,接着是若干个静态回调函数,最后是main()。这个结构几乎在所有 GTK+ 程序里都一样。
例子里反复出现的几个核心概念值得先建立印象:
- 控件(Widget):GTK+ 里所有可见元素都是控件,窗口、按钮、菜单、文本框都是。你可以把控件理解成“乐高积木块”,程序做的就是拼装它们。
- 容器(Container):负责摆放控件。字处理程序一般会用垂直盒容器把菜单栏和滚动窗口纵向排列。
- 信号(Signal):当用户点击菜单或关闭窗口时,GTK+ 会发出信号,程序通过
g_signal_connect把对应的处理函数挂上去。这是整个程序“活起来”的关键。 - 文本缓冲区(GtkTextBuffer):文本视图里的内容其实不直接存在控件里,而是存在一个关联的缓冲区对象中。文本操作都要通过缓冲区来执行。
有了这几个概念打底,接着看源代码就不会迷路。
2. 源码结构与环境准备
2.1 开发环境搭建:先把这个例子跑起来
不管多高深的技术,第一步永远是“让它跑起来”。GTK+ 的开发环境搭建其实比很多人想象中简单。在 Debian/Ubuntu 系发行版上,安装编译所需的库和头文件:
sudo apt install libgtk-3-dev build-essential pkg-config在 Fedora/RHEL 系上则是对应的:
sudo dnf install gtk3-devel gcc make pkgconfig装好之后,简单验证一下环境是否正常:
pkg-config --cflags --libs gtk+-3.0如果这条命令能输出一堆类似-I/usr/include/gtk-3.0 -lgtk-3 -lgdk-3 ...的编译参数,说明开发环境没问题。这里不得不提一个新手最爱踩的坑:直接gcc file.c -o file去编译 GTK+ 程序,结果报一堆“未定义引用”。因为编译时需要告诉编译器去哪儿找头文件,链接时需要告诉链接器去哪儿找动态库。pkg-config就是干这个的,正确的编译命令是:
gcc editor.c -o editor $(pkg-config --cflags --libs gtk+-3.0)这个命令看着简单,但背后有好多层含义:--cflags提供编译期的头文件路径,--libs提供链接期的动态库路径和依赖选项。手动硬写这些路径在桌面环境下通常也能跑,但一旦换到别的机器或发行版,路径可能完全不同。所以请养成用pkg-config的习惯,这也是为什么书上例子末尾都会带上它的原因。
2.2 源文件里都有哪些“模块”
把第160页的源代码按函数划分,大概会有这么几个部分:
| 函数或部分 | 职责 | 关键点 |
|---|---|---|
#include区 | 引入 GTK 头文件 | <gtk/gtk.h>包含所有 GTK 控件 |
| 全局变量 | 保存窗口、文本视图、缓冲区指针 | 方便在多个回调函数间共享 |
create_editor | 创建窗口、容器、文本视图 | 布局的入口 |
open_file/save_file回调 | 文件对话框与读写 | 信号回调的典型写法 |
new_file/quit_app回调 | 清空缓冲区 / 退出程序 | 简单但容易忽略细节 |
cut/clipboard等回调 | 剪贴板操作 | 围绕缓冲区操作 |
main函数 | 初始化 GTK、创建控件、进入主循环 | gtk_main()阻塞运行 |
这种划分方式对任何例子都通用。我习惯在读源码时先给每个函数起一个“人类能听懂的名字”,比如open_file就是“用户点了打开按钮之后要执行的事情”。这样一来,代码的执行逻辑就变成了故事:用户在菜单里点“打开”,触发信号,程序弹出一个文件选择框,拿到文件路径后读取内容,把文本放进缓冲区,界面立刻刷新。
这里有个需要重点体会的设计思想:GTK+ 是事件驱动的,而不是线性执行的。main()里做完初始化之后,gtk_main()就进入了一个循环,一直在“收事件、派发信号、回调函数”。你写的各种回调函数不是按顺序被调用的,而是等用户在界面上做出动作后才被触发。理解了这一点,后续阅读代码的节奏就对了。
3. 核心控件与布局逻辑
3.1 窗口和立即可见的“隐藏惯例”
按惯例,先创建顶层窗口:
GtkWidget *window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "简单字处理程序"); gtk_window_set_default_size(GTK_WINDOW(window), 600, 400);这里有个细节,很多教材讲得比较模糊:创建出来的窗口默认是不可见的,必须调用gtk_widget_show_all(window)才会把整个窗口连同里面的子控件一起展示出来。在 GTK+ 3 里,也可以逐个控件调用gtk_widget_show(),但用gtk_widget_show_all()更直观,它会把容器里的所有子控件递归式地显示出来。
接着要给窗口挂上“关闭即退出”的信号:
g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL);destroy信号在用户点击窗口关闭按钮时发射。把gtk_main_quit作为回调,意思是“收到关闭事件就退出主循环”。这一步如果漏了,程序可能关掉窗口后还在后台挂着一个进程,任务栏图标不消失,非常别扭。
3.2 文本视图与滚动的“黄金搭档”
字处理程序最核心的区域就是文本编辑区。GTK+ 提供GtkTextView控件来做多行文本显示与编辑。但它有个特点:自身不处理滚动条。如果文本超过可视区域,默认不会自动出现滚动条。所以例子里几乎肯定会把GtkTextView放进一个GtkScrolledWindow里。
这段代码的典型写法是:
GtkWidget *scrolled = gtk_scrolled_window_new(NULL, NULL); gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC); GtkWidget *text_view = gtk_text_view_new(); gtk_container_add(GTK_CONTAINER(scrolled), text_view);gtk_scrolled_window_new的两个参数其实是水平和垂直滚动条的GtkAdjustment。传NULL的意思就是让滚动窗口自己创建和管理。而set_policy里的GTK_POLICY_AUTOMATIC表示“内容超过可视区时自动显示滚动条”,这个设置非常常用,能避免出现空荡荡的滚动条占位。
接下来是很多人容易忽略的关键一脚:要把GtkTextView加入容器,必须用gtk_container_add,而不是往窗口里直接pack_start。因为GtkScrolledWindow自己就是一个容器,它只会管理一个子控件。理清这个父子关系,你后面调整布局时才不会把控件塞错层级。
获取文本缓冲区的方式也值得一提:
GtkTextBuffer *buffer = gtk_text_view_get_buffer(GTK_TEXT_VIEW(text_view));为什么要绕一层去拿缓冲区?因为 GTK+ 的哲学是“数据与表现分离”。文本的内容和格式信息存在GtkTextBuffer里,GtkTextView只负责把缓冲区的内容画到屏幕上。你在代码里改缓冲区,界面会随之刷新;反之,用户在界面上打字,缓冲区也会同步更新。这个例子里后面的打开、保存、剪切、复制全部操作的是buffer而不是text_view。
3.3 布局:用盒容器把菜单和内容组装起来
一个典型的字处理程序界面在垂直方向上有两块:顶部是菜单栏,下面是滚动文本区。GTK+ 里最常用的布局容器就是GtkBox。垂直排列用GTK_ORIENTATION_VERTICAL,水平排列用GTK_ORIENTATION_HORIZONTAL。
示例代码片段:
GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_box_pack_start(GTK_BOX(vbox), menubar, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), scrolled, TRUE, TRUE, 0); gtk_container_add(GTK_CONTAINER(window), vbox);gtk_box_pack_start的参数分别是:子控件、是否扩展、是否填满、边距。这里菜单栏的“扩展”值设为FALSE,意思是让它保持自身的高度,不要被拉伸;滚动文本区则设置为TRUE, TRUE,让它占据剩余的所有空间。只要把这两个参数调反,你立刻就能看到界面布局完全变了样——这就是动手测试能带来的直观收获。
菜单栏本身也是一个容器,通常是GtkMenuBar,里面放GtkMenuItem,再往下是子菜单。在代码上手写菜单嵌套很容易漏掉层级,但只要记得“菜单栏 -> 菜单项 -> 子菜单 -> 菜单项”这套树形结构,逻辑就不容易乱。书里这个例子的菜单项大概率使用了gtk_menu_item_new_with_label后,再通过gtk_menu_shell_append链接到子菜单。
4. 关键字处理逻辑:源代码逐段分析
4.1 打开文件:对话框与回调配合
现在进入例子最有价值的部分。先看“打开文件”的回调函数,它一般长这样:
static void open_file(GtkWidget *widget, gpointer data) { GtkWidget *dialog; GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_OPEN; gint res; dialog = gtk_file_chooser_dialog_new("打开文件", GTK_WINDOW(window), action, "_取消", GTK_RESPONSE_CANCEL, "_打开", GTK_RESPONSE_ACCEPT, NULL); res = gtk_dialog_run(GTK_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); gchar *content = NULL; gsize length = 0; GError *error = NULL; if (g_file_get_contents(filename, &content, &length, &error)) { gtk_text_buffer_set_text(buffer, content, length); g_free(content); } else { g_printerr("读取文件出错: %s\n", error->message); g_error_free(error); } g_free(filename); } gtk_widget_destroy(dialog); }这里有三个值得展开讲的地方。
第一个是gtk_dialog_run,它是一种“模态运行”机制,会在函数内部开启一个嵌套事件循环,阻塞住当前回调,直到用户在对话框里做出选择。对于简单程序来说,这种写法非常省事,你不需要为对话框单独写任何回调,代码执行顺序看起来就像同步调用一样。但要注意:如果主程序里还有大量其他事件需要快速响应,模态对话框会在弹出期间阻塞那些事件的处理。对入门例子来说,这不是问题,反而让逻辑更清晰。
第二个是g_file_get_contents,这是 GLib 提供的一个一次性读取整个文件内容的工具函数。它自动处理缓冲区分配,还会通过返回的length给出字节数。对于文本文件来说,这个函数足够用,它把“文件操作”问题变成“拿到字节,放进缓冲区”的问题。如果读取失败,比如权限不足,error指针会被填充,你需要检查并释放它。很多初学者忽略error的检查,结果程序静默地什么都不做,像是按钮坏了,其实只是路径不对或者文件不存在。
第三个是gtk_text_buffer_set_text(buffer, content, length)。它接收的是 UTF-8 编码的字符串和字节长度。如果文件是 GBK 编码,直接把content传进去,显示时就会出现乱码。这个问题我在后面专门放一节讲。
4.2 保存文件:把缓冲区内容写回磁盘
保存文件的回调与打开文件的结构对称,但多了一个关键操作:从缓冲区取出文本。代码一般是:
static void save_file(GtkWidget *widget, gpointer data) { GtkWidget *dialog; GtkFileChooserAction action = GTK_FILE_CHOOSER_ACTION_SAVE; gint res; dialog = gtk_file_chooser_dialog_new("保存文件", GTK_WINDOW(window), action, "_取消", GTK_RESPONSE_CANCEL, "_保存", GTK_RESPONSE_ACCEPT, NULL); res = gtk_dialog_run(GTK_DIALOG(dialog)); if (res == GTK_RESPONSE_ACCEPT) { char *filename = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog)); GtkTextIter start, end; gchar *content; gtk_text_buffer_get_start_iter(buffer, &start); gtk_text_buffer_get_end_iter(buffer, &end); content = gtk_text_buffer_get_text(buffer, &start, &end, FALSE); GError *error = NULL; if (!g_file_set_contents(filename, content, -1, &error)) { g_printerr("保存文件出错: %s\n", error->message); g_error_free(error); } g_free(content); g_free(filename); } gtk_widget_destroy(dialog); }这里涉及 GTK+ 文本操作的经典套路:用迭代器(iter)标记范围。文本缓冲区里的位置不是简单的整数下标,而是通过GtkTextIter这样的迭代器来描述的。你可以把迭代器想象成文本里的一个“光标”,它在字符之间移动。gtk_text_buffer_get_start_iter让光标跳到缓冲区开头,gtk_text_buffer_get_end_iter让光标跳到结尾,然后用gtk_text_buffer_get_text把这两个光标之间的所有字符拷出来。
为什么不用整数索引?因为 GTK+ 的文本缓冲区内部还要处理多字节字符、标签、嵌入图片等复杂结构,单纯用字节偏移很容易出错。迭代器这个抽象虽然第一次接触有点绕,但用两三次就会习惯。
这里还需要注意g_file_set_contents这个函数:它不是简单的fwrite,而是一个原子性写入操作,会先把内容写进临时文件,再替换原文件。好处是如果写入到一半崩溃,原文件不会被破坏。在字处理程序里,用这个函数比手动fopen再fwrite稳得多。
4.3 剪切、复制、粘贴:缓冲区与剪贴板的交互
菜单栏里的“剪切”“复制”“粘贴”可以说是字处理程序的灵魂。很多参考代码在实现它们时会走GtkEditable接口,比如gtk_editable_cut_clipboard()。但如果例子是基于缓冲区体系,更常见的做法是用GtkTextBuffer提供的剪贴板方法:
static void cut_clipboard(GtkWidget *widget, gpointer data) { GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_cut_clipboard(buffer, clipboard, TRUE); } static void copy_clipboard(GtkWidget *widget, gpointer data) { GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_copy_clipboard(buffer, clipboard); } static void paste_clipboard(GtkWidget *widget, gpointer data) { GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); gtk_text_buffer_paste_clipboard(buffer, clipboard, NULL, TRUE); }这里的GTK_RESPONSE_ACCEPT与文件对话框里的gtk_dialog_run返回值是一套,统一处理控件反馈。而gtk_text_buffer_cut_clipboard的最后一个布尔参数表示“剪切后是否删除选中区域”。传TRUE才是剪切,如果误传FALSE,那实际就变成了“复制并覆盖剪贴板”,操作语义就错了。
剪贴板本身是 X 窗口系统里的一个概念,gdk_clipboard_get(GDK_SELECTION_CLIPBOARD)用来获取系统剪贴板。在 Wayland 会话下,这个抽象依然有效。理解这一点对你排查“为什么剪贴板里复制了却无法粘贴到别处”会很有帮助。
比较容易被忽略的是:当文本缓冲区为空时,剪切和复制按钮仍然可点击,只是什么都不做。要做得更完善,需要在选中状态变化时更新按钮的敏感状态。这需要监听缓冲区上的changed信号或mark-set信号,再配合gtk_text_buffer_get_selection_bounds判断是否有选中文本。书上这个例子可能没有处理,但如果作为扩展练习,这是非常值得加的功能。
4.4 新建与退出:简单背后的细节
“新建”的回调往往只是清空缓冲区:
static void new_file(GtkWidget *widget, gpointer data) { gtk_text_buffer_set_text(buffer, "", -1); }这里特别注意-1这个参数表示“忽略长度,字符串以\0结尾”。GTK+ 的很多函数接受length = -1的惯用法,但这种“魔法值”也容易让新手困惑。如果你改成其他正数,比如0,那么缓冲区就会被设成空串,效果类似。但最好还是理解-1的含义:它告诉 GTK+ 别管长度了,自己用strlen去求。
“退出”的代码一般就一行:
gtk_main_quit();但如果窗口的destroy信号已经和gtk_main_quit相连,那么退出菜单项也可以直接调用gtk_widget_destroy(window),触发destroy信号,间接退出主循环。两种方式都行,但前者更直接,后者更有“事件驱动”的味道。书里的例子若是通过菜单退出,多半会用前者,因为代码更短,也免去窗口指针的传递。
5. 阅读源码过程中的常见问题与排查
5.1 编译通不过:pkg-config 的坑
我在很多群里看人问“我照着书抄的代码为什么编译报错”,贴出来的编译命令十有八九是直接gcc editor.c -o editor。这种报错通常是一大片红色的“未定义的引用”,因为链接器找不到 GTK 的函数实现。解决方案就是前面说的,使用pkg-config提供编译和链接参数。
还有一个隐蔽问题:不同发行版的包名完全不同。在 Arch Linux 上,包名是gtk3;在 Alpine Linux 里,需要的是gtk3-dev;如果你用的是嵌入式环境,可能还要交叉编译。所以当你拿着别人给的编译命令在自己机器上跑不通时,先检查pkg-config --modversion gtk+-3.0能不能输出版本号。这一步能排查掉绝大多数环境问题。
5.2 菜单点了没反应:信号连接与回调类型
菜单项点击后没有任何反应,第一反应是检查g_signal_connect的参数。最常见的问题是G_CALLBACK宏漏了,或者回调函数类型不匹配。在 C 语言里裸传函数指针虽然能过编译,但一旦函数签名对不上,回调时参数错位,轻则无反应,重则崩溃。正确的写法是:
g_signal_connect(menu_item, "activate", G_CALLBACK(open_file), NULL);菜单项的activate信号是在用户激活菜单项时发出的。如果你不小心连到了clicked信号上,在菜单项上可能永远不触发。因为GtkMenuItem不像按钮那样有clicked信号,它的激活信号才是正确的。
此外,回调函数必须定义在g_signal_connect之前,或者至少先有函数声明。C 编译器的要求是“先声明后使用”,很多例子代码里把回调函数放在前面,main放在最后,就是这个原因。
5.3 打开文件乱码:字符编码问题
这是这个例子最容易踩中的坑。GTK+ 内部统一使用 UTF-8 编码。如果磁盘上的文件是 GBK/GB2312 编码,直接g_file_get_contents读出来,再传给gtk_text_buffer_set_text,缓冲区里的字节序列是原封不动的 GBK 字节,窗口显示自然就乱了。
解决办法是在设置文本之前做编码转换。GLib 提供了g_convert函数,但直接使用有些繁琐。也可以先用g_utf8_validate判断读入的文本是否合法 UTF-8,如果不是,再用g_convert从 GBK 转到 UTF-8。简单场景下,可以这样写:
if (!g_utf8_validate(content, length, NULL)) { gsize bytes_read = 0, bytes_written = 0; GError *conv_error = NULL; gchar *utf8_content = g_convert(content, length, "UTF-8", "GBK", &bytes_read, &bytes_written, &conv_error); if (utf8_content) { gtk_text_buffer_set_text(buffer, utf8_content, -1); g_free(utf8_content); } else { g_printerr("编码转换失败: %s\n", conv_error->message); } } else { gtk_text_buffer_set_text(buffer, content, length); }注意g_convert是 GLib 做编码转换的通用接口,它依赖 GIO 模块里支持的具体编码格式。在大多数 Linux 发行版上,GBK 是内置支持的;如果遇到G_CONVERT_ERROR_NOT_SUPPORTED,要么是 GLib 编译时没启用 iconv,要么是你用了错别的编码名字。
5.4 滚动条不出现或区域大小不对
文本视图放进GtkScrolledWindow后,滚动条还是不出现,通常是两个原因。一是GtkScrolledWindow本身没有获得可扩展的空间。如果你把它放进盒容器时把expand参数设成了FALSE,那么即使内容超出,滚动窗口也可能被压缩成一块很小的区域。解决办法是把滚动窗口的扩展和填充参数都设为TRUE,让它占据可用空间。
另一个原因是滚动条策略设置成了GTK_POLICY_NEVER而不是GTK_POLICY_AUTOMATIC。即使内容超出,滚动条也强制不显示。这种情况多见于从别的代码片段里复制来的默认值。检查一下gtk_scrolled_window_set_policy那两行即可。
5.5 剪贴板相关操作失灵
复制/粘贴功能偶尔出现“复制了却粘不上”的现象,多半不是代码问题,而是剪贴板机制本身。在 Linux 图形环境下,剪贴板内容由持有它的进程负责提供。如果你的程序被关闭,但剪贴板里还保存着从它复制的内容,其他程序可能无法读取。这是 X11 时代的经典行为。现代桌面环境下,剪贴板管理器会介入,问题不再明显,但在某些精简窗口管理器里依然存在。排查时可以先试试往其他 GTK+ 应用里粘贴,比如gedit,来确认剪贴板内容是否还在。
代码层面还有一个容易漏掉的点:复制时没有判断缓冲区里是否真的有选中文本。使用gtk_text_buffer_get_selection_bounds可以检查:
GtkTextIter start, end; if (gtk_text_buffer_get_selection_bounds(buffer, &start, &end)) { // 有选中内容才执行复制 }如果没做这个判断,复制操作可能只是静默地把旧剪贴板内容保留下来,给用户造成“我明明复制了新东西”的错觉。
6. 从例子出发:如何做出更专业的编辑器
6.1 用 GtkTextTag 实现简单的高亮
读懂第160页的例子后,一个自然的进阶方向是给文本编辑器增加“高亮”能力。比如让特定关键字变色、加粗。GTK+ 里这件事靠GtkTextTag完成。它本质上是一个“文本标记”,你可以创建一个标签,再把这个标签应用到缓冲区里的一段范围上:
GtkTextTag *tag = gtk_text_buffer_create_tag(buffer, "keyword", "foreground", "red", "weight", PANGO_WEIGHT_BOLD, NULL);然后通过迭代器把标签应用到选中范围:
gtk_text_buffer_apply_tag(buffer, tag, &start, &end);这相当于给文字“刷了一层颜色”。很多代码编辑器里的语法高亮,本质上就是不断扫描文本,给不同 token 套上不同的GtkTextTag。当然,真正的生产级编辑器不会用GtkTextView裸写,而是使用GtkSourceView这类扩展库。但从理解标签系统的角度,这个小实验非常有价值。
6.2 撤销/重做:一个需要自己维护的工程
GTK+ 的GtkTextBuffer没有内置撤销/重做功能,这一点和很多人的直觉相反。要实现撤销,常见的做法是每在缓冲区上执行一次修改操作之前,把修改涉及的范围和旧文本记录到一个栈里。当用户按 Ctrl+Z 时,弹栈还原。
这个工作量不小,而且需要对信号有更深的理解,比如监听insert-text和delete-range信号。如果你想锻炼 GTK+ 的实战能力,在这个例子的基础上做撤销/重做是一个绝佳的练习:你不仅要处理多次连续输入的合并,还要处理光标位置的恢复,甚至要考虑撤销操作自身又触发修改信号的递归问题。能把这套逻辑写稳,说明你对 GTK+ 的文本体系已经有相当深入的理解。
6.3 从 GTK+ 3 迁移视角看这份源码
如果这本书稍老一些,例子可能写的是 GTK+ 2。API 差异并不小。比如 GTK+ 2 里填充分区用的是gtk_vbox_new,到了 GTK+ 3 就统一成gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);GTK+ 2 里按钮创建用gtk_button_new_from_stock,GTK+ 3 则改为直接传字符串。到了 GTK+ 4,显示函数变成了gtk_widget_show之外的gtk_widget_set_visible,容器 API 也有不小的调整。所以如果你抄的是旧书上默认 GTK+ 2 的代码,在pkg-config --modversion gtk+-3.0的环境下编译,报错是大概率事件。
这时候需要做的是“按新 API 翻译”而不是“硬适配”。比如把gtk_vbox_new替换为gtk_box_new,把gtk_signal_connect替换为g_signal_connect,把GTK_WINDOW_TOPLEVEL这种常量检查是否在新版本里还适用。翻译的过程,比直接抄新代码更能加深对 GTK+ 演进方向的理解。
最后分享一点我自己的经验:读源码不要只看,也不要只抄。把这页例子改成“汉字字体变大”“打开文件时默认目录是家目录”“退出前弹出确认框”这些无伤大雅的小需求,每次改一个边角,然后立刻编译运行。这种“故意折腾”的练习,比连续抄十个例子都管用。等你能随手在回钩子里增加一个新菜单项,并且知道它为什么工作、为什么不工作的时候,这本第160页的例子的价值就已经远超它的篇幅了。