GTK+ 字处理程序示例源码剖析:从控件到回调机制
我翻到一本 GTK 编程书的第160页时说实话第一眼觉得这例子也太“素”了一个窗口、一块能输入文字的白色区域、几个菜单项。但如果只是把它当成“照着敲一遍就完事”的入门练习那就太浪费了。书里这个简单的字处理程序其实把 GTK 的窗口布局、文本缓冲区、信号回调、文件对话框这些核心机制全串了起来。源代码篇幅不长但每一段都值得逐行抠开看读懂之后你后面写任何图形界面工具都会顺手很多。这篇文章就和你一起把这个例子从控件到代码逻辑整个拆一遍。1. 这个例子到底在做什么1.1 “字处理程序”的最小闭环先明确一个概念这里的“字处理程序”不是 Word 那种排版、插图、样式齐全的编辑器而是一个最基础的文本编辑器。它要解决的需求很简单创建一个窗口里面有一块可以编辑文字的空白区域然后让用户通过菜单完成打开文件、保存文件、剪切、复制、粘贴、新建、退出这几项基本操作。可别小看这个“最小闭环”。文本编辑器的骨架不外乎就是这些一个编辑区、一条菜单栏、一个文件选择对话框、一组文本缓冲区操作。无论你以后是做记事本、写代码编辑器、做日志分析工具都得跟这套机制打交道。很多初学者看完教程觉得自己都会了但一动手就发现窗口怎么都弹不出来、菜单点了没反应、打开文件全是乱码。这些问题的根源基本都藏在这个例子里。书里把这个例子放在第160页也有讲究。前面的章节大概率已经讲完了 GTK 的基础控件、信号与回调、容器布局。到这一页正好需要把那些零零碎碎的知识组装成一个小项目。它不追求华丽而是在有限篇幅里把“窗口 容器 控件 信号 文件操作”这条完整链路展示出来。对这个例子做源代码阅读和分析收益最大的点不在控件 API 本身而在于理解“GTK 程序是如何被组织起来的”。1.2 例子里反复出现的几个核心概念从一个读者的角度我建议你拿到源代码后先别急着敲先用几分钟把文件从头到尾扫一遍找出它的骨架开头通常是一堆#include然后是全局变量和函数声明接着是若干个静态回调函数最后是main()。这个结构几乎在所有 GTK 程序里都一样。例子里反复出现的几个核心概念值得先建立印象控件WidgetGTK 里所有可见元素都是控件窗口、按钮、菜单、文本框都是。你可以把控件理解成“乐高积木块”程序做的就是拼装它们。容器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没有内置撤销/重做功能这一点和很多人的直觉相反。要实现撤销常见的做法是每在缓冲区上执行一次修改操作之前把修改涉及的范围和旧文本记录到一个栈里。当用户按 CtrlZ 时弹栈还原。这个工作量不小而且需要对信号有更深的理解比如监听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_stockGTK 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页的例子的价值就已经远超它的篇幅了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →