本周 GTK+ – 36

在过去的一周中,GTK+ 的 master 分支有 22 次提交,增加了 1165 行代码,删除了 904 行代码。

规划和状态
  • GTK+ 路线图可在维基上找到
重要变更

在 master 分支上

  • Robert Ancell 更新了图标浏览器实用程序,以改进加载图标失败时的错误消息
  • Matthias Clasen 改进了新添加的 GtkCenterBox 小部件;您可以在“容器秘密”系列文章中关注他的工作
修复的错误
  • 783552 – 翻译解释
  • 759308 – 打印对话框中的即时应用(份数)
  • 783445 – gtk_widget_insert_after/before() 的文档不完整
参与其中

有兴趣参与 GTK+ 的开发吗?请查看新手错误列表并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

容器秘密:大小分配,第二部分

从右到左的语言

作为本系列的第一件事,我们重新添加了对从右到左语言的支持。

如果您只习惯于用英语编写软件,可能会有点惊讶,但 GTK+ 一直试图为从右向左书写的语言(如希伯来语)自动“做正确的事”。具体来说,这意味着我们将在这些语言中将水平布局的起始位置解释为在右侧,即,我们“翻转”水平排列。当然,可以通过设置容器的 ::text-direction 属性来覆盖这一点。默认情况下,文本方向由区域设置确定。

这当然很容易做到。我在第一篇文章中展示的代码假定子项的顺序是从左到右。我们保持这种方式,并在必要时重新排序子项。请注意,measure() 代码不需要任何更改,因为它根本不依赖于顺序。因此,我们只更改 size_allocate()

if (text_direction == rtl) {
  child[0] = last;
  child[1] = center;
  child[2] = first;
} else {
  child[0] = first;
  child[1] = center;
  child[2] = last;
}

我还没有提到的一个小陷阱:CSS 假定 :first-child 始终是最左边的元素,而不管文本方向如何。因此,当我们在 RTL 上下文中将子小部件从左侧移动到右侧时,我们需要在文本方向更改时重新排序相应的 CSS 节点。

if (direction == GTK_TEXT_DIR_LTR) {
  first = gtk_widget_get_css_node (start_widget);
  last = gtk_widget_get_css_node (end_widget);
} else {
  first = gtk_widget_get_css_node (end_widget);
  last = gtk_widget_get_css_node (start_widget);
}

parent = gtk_widget_get_css_node (box);
gtk_css_node_insert_after (parent, first, NULL);
gtk_css_node_insert_before (parent, last, NULL);

自然大小

既然这很容易,我们将继续努力,使我们的大小分配也尊重自然大小。

在 GTK+ 中,每个小部件不仅有最小大小,即它可以有效呈现自身的最小大小,而且还有一个首选或自然大小。 measure() 函数返回这两个大小。

对于自然大小,我们可以做得比上次展示的代码稍微好一点。当时,我们只是通过将所有子项的自然大小相加来计算框的自然大小。但我们希望将中间的子项居中,所以让我们要求足够的空间来让所有子项都具有自然大小将中间的子项放置在中心

*natural = child2_nat + 2 * MAX (child1_nat, child3_nat);

size_allocate() 函数需要更多的工作,在这里我们需要做出一些决定。在 3 个子项争夺可用空间以及居中施加的额外约束的情况下,我们可能更喜欢给外部子项更多空间,或者使中心子项更大。由于居中是此小部件的定义特征,所以我选择了后者。

那么,我们可以给中心小部件多少空间?我们不希望它小于最小大小,也不希望它大于自然大小,并且我们需要为外部子项至少保留所需的最小空间。

center_size = CLAMP (width - (left_min + right_min),
                     center_min, center_nat);

接下来,让我们计算一下我们可以给外部子项多少空间。显然,我们不能在给中心子项分配空间后给出比剩余空间更多的空间,并且我们必须均匀分割剩余空间才能使居中工作(这就是下面代码中 avail 的含义)。同样,希望尊重子项的最小和自然大小。

avail = MIN ( (width - center_size) / 2,
              width - (center_size + right_min));
left_size = CLAMP (avail, left_min, left_nat);

对于右侧子项也是如此。在确定子项大小之后,剩下的就是以我们在第一部分中看到的方式分配位置:将外部子项放置在最左侧和最右侧,然后将中间子项居中并将其向右或向左推以避免重叠。

扩展
自然大小
低于自然大小
较小
最小大小

参考

  1. 容器秘密,大小分配
  2. 具有这些更改的代码

容器秘密:大小分配

我最近有机会为支持 GTK+ 的所有功能的容器重新实现大小分配

  • RTL 支持
  • 自然大小
  • 对齐和展开
  • 高度适应宽度
  • 方向支持
  • 基线

如果您在应用程序中进行一次性容器操作,则其中大多数可能无关紧要,但在通用的 GTK+ 小部件中,所有这些都可能会迟早变得相关。

由于这涵盖了很多内容,因此需要几篇文章才能完成。让我们开始吧!

起点

GtkCenterBox 是一个简单的小部件,可以包含三个子小部件 – 它不是一个 GtkContainer,至少目前不是。在 GTK+ 4 中,任何小部件都可以是其他小部件的父级。 GtkCenterBox 应用于其子项的布局是使中间子项居中,只要有可能。这是 GtkBox 在 GTK+ 3 中提供的功能,但中心子项处理使已经复杂的容器更加复杂。因此,我们将其移动到 GTK+ 4 中的单独小部件中。

扩展
自然大小
低于自然大小
最小大小

当我开始查看 GtkCenterBox 大小分配代码时,它非常简单。需要查看的两个方法是 measure()size_allocate()

measure 实现只是测量三个子项,在水平方向上将最小和自然大小相加,并在垂直方向上取最大值。用粗略的伪代码表示

if (orientation == GTK_ORIENTATION_HORIZONTAL) {
  *minimum = child1_min + child2_min + child3_min;
  *natural = child1_nat + child2_nat + child3_nat;
} else {
  *minimum = MAX(child1_min, child2_min, child3_min);
  *natural = MAX(child1_min, child2_min, child3_min);
}

size_allocate 实现是将第一个子项放在左侧,将最后一个子项放在右侧,然后将中间的子项放在中心,通过根据需要将其向右或向左推来消除重叠。

child1_width = child1_min;
child2_width = child2_min;
child3_width = child3_min;
child1_x = 0;
child3_x = total_width - child3_width;
child2_x = total_width/2 - child2_width/2;
if (child2_x < child1_x + child1_width)
  child2_2 = child1_x + child1_width;
else if (child2_x + child2_width > child3_x)
  child2_x = child3_x - child2_width;

如您所见,这非常简单。遗憾的是,它没有任何我上面列出的功能

  • 子项始终获取其最小大小
  • 未考虑 ::expand 属性
  • 第一个子项始终放置在左侧,而不管文本方向如何
  • 没有垂直方向
  • 不支持高度适应宽度
  • 基线被忽略

在接下来的几篇文章中,我将尝试展示如何将这些功能添加回去,希望能澄清 GTK+ 大小分配工作方式的一些奥秘。

参考

  1. 本系列中的第一个提交
  2. GtkWidget 大小分配的文档
  3. 我们从中开始的代码

本周 GTK+ – 35

在过去的一周中,GTK+ 的 master 分支有 33 次提交,增加了 5011 行代码,删除了 8140 行代码。

规划和状态
  • GTK+ 路线图可在维基上找到
  • Patrick Griffis 正在尝试使用一个功能分支来弃用和删除 gtk_dialog_run();请参阅此评论,了解关于 UI 线程、IPC 线程和 I/O 线程的嵌套主循环的缺陷
  • Matthias Clasen 正在尝试在图标浏览器中重复使用 libdazzle 中的模糊搜索
重要变更

在 master 分支上

  • Matthias Clasen 添加了将图标名称复制到剪贴板的功能到图标浏览器实用程序
  • Matthias 还公开了 GtkCenterBox 小部件;此小部件取代了 GtkBox 的等效功能来拥有居中的小部件
  • Olivier Fourdan 修复了 Wayland 后端的各种错误,并将修复程序向后移植到 gtk-3-22 稳定分支
  • Chun-wei Fan 推送了各种修复程序,以确保 GTK+ 继续在 Windows 上使用 MSVC 构建
  • Emmanuele Bassi 修改了 Meson 构建,以确保在构建 GTK+ 时,如果安装了 sassc,则会重新生成所有基于 SASS 的主题;Lapo Calamandrei 删除了 Ruby/Sass 的 Gem 文件,因此 GTK+ 切换到 sassc 作为首选的 SASS 编译器
修复的错误
  • 770513 – 全屏模式下的 MainToolbar 具有圆角,这显示了它下面的视频像素渗色
  • 783347 – gtkfilechoosernativewin32:修复对非 ASCII 路径的支持
  • 781945 – 当 toplevel 窗口 set_transient_for 设置为另一个 toplevel 时,在 Wayland 上拖动窗口时出现 SIGSEGV
  • 782283 – Wayland:当工具提示可见时,关闭菜单时崩溃
  • 781285 – Wayland 下的按键重复取消应取决于重复的按键
  • 783397 – 删除 gtktextdisplay.c 中未使用的代码
参与其中

有兴趣参与 GTK+ 的开发吗?请查看新手错误列表并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

列表中的拖放操作,再次探讨

我之前关于列表中的拖放操作的文章为了展示最简单、功能齐全的实现做了一些妥协。其中之一就是我们只能在整行周围绘制拖放目标高亮,而实际上拖放操作是在行之间插入拖放的行。让我们尝试做得更好!

更改目标

在简化版本中,我们将每一行都设为拖放目标。这一次,我们将更改此设置,只让列表本身接受拖放。

gtk_drag_dest_set (list,
                   GTK_DEST_DEFAULT_MOTION|GTK_DEST_DEFAULT_DROP, 
                   entries, 1,
                    GDK_ACTION_MOVE);
g_signal_connect (list, "drag-data-received",
                  G_CALLBACK (drag_data_received), NULL);

如果你将此与之前所做的进行比较,你可能会注意到另一个变化:我们现在只请求移动和拖放的默认行为,而不是 GTK_DEST_DEFAULT_ALL。我们不再请求高亮的默认行为,因为我们希望自己处理它。

为了做到这一点,我们需要连接到 drag-motion 和 drag-leave 信号。这些信号在拖放目标(即列表)上发出。

 g_signal_connect (list, "drag-motion",
                   G_CALLBACK (drag_motion), NULL);
 g_signal_connect (list, "drag-leave",
                   G_CALLBACK (drag_leave), NULL);

注意间隙

我们改进的拖动高亮的基本思想是,我们跟踪拖放将要发生的两个相邻行,并使用 GTK+ CSS 机制来创建合适的间隙高亮。

这需要太多代码才能在此处完整显示,但思路如下:使用 gtk_list_box_get_row_at_y() 查找当前位于光标下的行。查看其分配以找出光标是在该行的上半部分还是下半部分。根据这一点,我们选择后续行或前一行作为我们的行对中的另一个成员。

row = gtk_list_box_get_row_at_y (list, y);
gtk_widget_get_allocation (row, &alloc);
if (y < alloc.y + alloc.height/2)
  {
    row_after = row;
    row_before = get_row_before (list, row);
  }
else
  {
    row_before = row;
    row_after = get_row_after (list, row);
  }

这里省略了一些特殊情况,例如当悬停的行是第一行或最后一行,或者当我们悬停在列表中的“空白区域”时。

CSS 高亮

我说过我们将使用 CSS 来创建高亮。最简单的方法是将样式类添加到我们找到的两行中

gtk_style_context_add_class (
                      gtk_widget_get_style_context (row_before),
                      "drag-hover-bottom");
gtk_style_context_add_class (
                      gtk_widget_get_style_context (row_after),
                      "drag-hover-top");

然后我们将使用一些自定义 CSS 来创建我们的高亮。如果您想知道:#4e9a06 是 Adwaita 主题中使用的拖动高亮颜色。

.row.drag-hover-top {
  border-top: 1px solid #4e9a06; 
}
.row.drag-hover-bottom {
  border-bottom: 1px solid #4e9a06; 
}

这再次省略了我之前提到过的特殊情况。

我们是怎么到这里的?

将行拖放到它原来的位置没有任何作用。以某种方式标记拖动开始的位置以使其显而易见是有意义的。我们可以再次使用 CSS 来实现这一点,并在我们的 drag_begin() 方法中将样式类添加到拖动的行中

gtk_style_context_add_class (gtk_widget_get_style_context (row),
                             "drag-row");

为了在鼠标悬停在其上时给行添加一点高亮,我们在 drag_motion() 处理程序中向其添加一个额外的样式类

if (row == drag_row)
  {
    gtk_style_context_add_class (gtk_widget_get_style_context (row),
                                 "drag-hover");
    row_before = get_row_before (row);
    row_after = get_row_after (row);
  }
else …

这是这些类的 CSS

.row.drag-row {
  color: gray;
  background: alpha(gray,0.2);
 }
 .row.drag-row.drag-hover {
  border-top: 1px solid #4e9a06;
  border-bottom: 1px solid #4e9a06;
  color: #4e9a06;
}

将它们放在一起

(抱歉,光标已损坏。我们真的应该在 gnome-shell 的屏幕录像机中修复此问题。)

参考

  1. 完整的示例,testlist3.c
  2. GTK+ DND 文档

本周 GTK+ – 34

在 GNOME 3.24 版本发布后经过相当长的中断后,我们终于回来了。抱歉久等了!

在上周,GTK+ 的主分支提交了 103 次,添加了 2355 行代码,删除了 5482 行代码。

规划和状态
  • GTK+ 路线图可在维基上找到。
  • Matthias Clasen 发布了 GTK+ 3.91.0,这是将导致 3.92 版本的开发周期的第一个快照。这仍然是迈向 API 稳定版 4.0 的开发周期的一部分。
  • Timm Bäder 正在他的 drawing 分支 上工作,该分支旨在将所有内部 CSS 小工具替换为真正的 widget。有关更多信息,请参阅本博客上的这篇文章
重要变更

在 master 分支上

  • Carlos Garnacho 合并了他的 event-delivery 分支,该分支将事件处理从 GDK 窗口层次结构移动到 GTK widget 层次结构;这是朝着移除顶级窗口之外的所有 GdkWindow 实例的第一步,最终将改进输入处理。
修复的错误
  • 745289 – wayland:不要在连接错误时使用 g_error()
参与其中

有兴趣参与 GTK+ 的开发吗?请查看新手错误列表并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

容器秘密

我最近花了一些时间追踪 GTK+ 容器和绘图问题。以下是我发现的。

CSS 绘图

在 GTK+ 3.22 中,大多数(如果不是全部)容器都支持具有多层背景和边框的完整 CSS 绘图模型。例如,GtkFrame 现在就是这样绘制其框架的。但是,通常只排列其子项的容器(例如 GtkBox)可以绘制背景和边框。可能性是无限的!

例如,我们可以使用 GtkBox 将框架放置在列表框和标签周围,使标签在视觉上看起来像是列表的一部分。您甚至可以使用一些 CSS 让它变得丰富多彩且有趣,例如

box.frame {
 border: 5px solid magenta;
}

分配和调整大小

传统上,GTK+ 中的大多数容器都不进行自己的绘制,而只是排列它们的子项,因此当它们的大小发生变化时,它们实际上不需要进行完全重绘 - 重绘子项就足够了。这就是 gtk_container_set_reallocate_redraws() 的作用。它在 GTK+ 3 中默认为 FALSE,因为我们不想在分配发生变化时冒添加过多重绘的风险。

你可以看到这种情况的走向:如果我使用删除按钮从配料列表中删除黄油和盐,则列表的分配(以及它周围的框)将缩小,并且会出现重绘问题。

解决方案

如果你计划使普通的布局容器绘制背景或边框,请确保为正确的 widget 设置 reallocate-redraws 为 TRUE(在本例中为有趣框的父级)。

gtk_container_reallocate_redraws (GTK_CONTAINER (parent), TRUE);

请注意,gtk_container_reallocate_redraws() 在 GTK+ 3.22 中已弃用,因为我们将在 GTK+ 4 中将其删除并自动执行正确的操作。但这不应该阻止你使用它来解决此问题。

另一种(也许更好)的选择是使用旨在绘制边框的容器,例如 GtkFrame。

日志记录及更多

前段时间,GLib 获得了一个新的“结构化日志记录”功能。与此同时,它还获得了将日志写入 systemd 日志的支持。显然,GLib 中的日志记录变得更加复杂,并且可能会让人感到困惑。

本文试图澄清一些问题。

结构化与否

传统的 GLib 日志记录工具是g_message()g_debug()等宏,它们最终调用g_log()函数,该函数然后使用使用g_log_set_handler()设置的日志处理程序来执行实际写入。您可以在日志中放入您喜欢的任何信息,但必须全部格式化为单个字符串,即消息。

g_debug ("You have %d eggs", 12 + 2);

g_log (G_LOG_DOMAIN,
       G_LOG_LEVEL_DEBUG,
       "You have %d eggs", 12 + 2);

使用新的结构化日志记录工具,您调用g_log_structured(),然后该函数使用日志编写器函数来执行写入。到目前为止,这与较旧的日志记录工具非常相似。结构化日志的优点是,您可以在日志中放入多个字段,而无需将其全部格式化为字符串。相反,您传递一个日志字段数组,这些字段是键值对。

g_log_structured (G_LOG_DOMAIN,
                  G_LOG_LEVEL_DEBUG,
                  "CODE_FILE", "mysource.c",
                  "CODE_LINE", 312,
                  "MESSSAGE_ID", "06d4df59e6c24647bfe69d2c27ef0b4e",
                  "MESSAGE", "You have %d eggs", 12 + 2);

此处的 CODE_FILECODE_LINEMESSAGE_ID 只是“标准”字段的示例。您也可以发明自己的字段。请注意,您仍然可以对 MESSAGE 字段使用 printf 样式的格式设置。

因此,GLib 现在有两个独立的日志记录工具。为了使事情更有趣,我们允许您将 g_message()g_debug() 等包装宏重定向为在底层使用 g_log_structured() 而不是 g_log()。要做到这一点,请在包含 glib.h 之前定义 G_LOG_USE_STRUCTURED 宏。

为什么这很有用?一方面,它可以省去你替换所有 g_debug() 的麻烦,并且仍然允许你利用结构化日志的一些优势——当以这种方式使用时,传统的宏会为日志域、代码文件和行以及其他一些字段使用单独的字段,这对于在生成的日志中进行过滤和搜索非常有用,特别是使用 systemd journal 时。

另一个优点是,你可以使用一个单独的后端,即日志写入函数,来控制新旧日志调用最终的去向。

我的所有日志都去哪儿了?

结构化日志通常与 systemd journal 相关联。因此,人们期望 g_log_structured() 的输出进入 journal 也就不足为奇了。这对于服务,或者当你从桌面图标启动应用程序时非常有用。但是如果你从终端运行它,你可能希望在那里看到它的输出。

为了满足这些相互竞争的需求,GLib 默认的日志写入函数会尝试变得智能。如果它检测到 stderr 被重定向到 journald 套接字,那么它会将结构化输出写入 journal。否则,它会格式化一条消息并将其写入 stderr

GNOME Shell 和 DBus 在启动应用程序或服务时都会安排将 stderr 重定向到 journal。将 stderr 显式重定向到 journal 的一种方法是在 systemd-cat 下运行你的应用程序。

systemd-cat my-app-that-logs

如果你确定希望日志始终进入 journal,你可以告诉 GLib 使用一个执行此操作的日志写入器。

g_log_set_writer_func (g_log_writer_journald, NULL, NULL)

超越默认值

即使 GLib 默认提供的日志写入函数应该满足许多需求,你可能仍然需要编写自己的函数。在这种情况下,GLib 有许多有用的函数可以帮助你,例如 g_log_writer_format_fields()g_log_writer_is_journald()g_log_writer_supports_color()

祝你日志记录愉快!

参考

  • Philipps 关于结构化日志的 演讲
  • GLib 日志记录 文档
  • Systemd journal 文档

GSettings 的第一步

我们有时会在 #gtk+ 中收到关于 GSettings 的问题,以及在“简单”情况下使用此 API 是否是个好主意。在我看来,答案非常明确,是肯定的,本文试图解释原因。

好处

GSettings 的一个优点是它是一个高级 API,具有适用于各种本机配置系统的后端。因此,如果你曾经将你的应用程序移植到 OS X 或 Windows,你的应用程序将自动使用预期的平台 API 来存储其设置(Windows 上的注册表和 OS X 上的 plist)。

即使你的应用程序永远不会移植到这些平台,Linux 上使用的 dconf 后端也具有强大的功能,例如配置文件和锁,这些功能使系统管理员可以配置你的应用程序,而无需你担心。

不幸的是,GSettings 的文档使其看起来比实际情况更复杂,因为它并没有真正试图隐藏可供你使用的强大功能(配置文件、供应商覆盖、翻译后的默认值、复杂类型、绑定等)。

因此,这里有一个关于 GSettings 的第一步的指南,在简单的情况下。

入门

让我们从最简单的设置开始:布尔值。

要克服的最大障碍是 GSettings 坚持使用模式,该模式定义每个键的数据类型和默认值。

<schemalist>
  <schema path="/org/gnome/recipes/"       
         id="org.gnome.Recipes">
    <key type="b" name="use-metric">
      <default>true</default>
      <summary>Prefer metric units</summary>
      <description>
        This key determines whether values
        such as temperatures or weights will
        be displayed in metric units.
      </description>
    </key>
  </schema>
</schemalist>

模式需要安装(这样做的好处是 dconf-editor 等工具可以使用模式信息)。如果你正在使用 autotools,则使用宏支持此功能。只需添加

GLIB_GSETTINGS

到你的 configure.ac,和

gsettings_SCHEMAS = org.gnome.Recipes.gschema.xml
@GSETTINGS_RULES@

到 Makefile.am。使用 meson 的设置类似。

现在我们已经定义了我们的键,我们可以像这样获取它的值

s = g_settings_new ("org.gnome.Recipes");
if (g_settings_get_boolean (s, "use-metric"))
  g_print ("Using metric units");

我们可以像这样设置它

s = g_settings_new ("org.gnome.Recipes");g_settings_set_boolean (s, "use-metric", TRUE);

为其他基本类型(如整数、浮点数、字符串等)使用 GSettings 非常相似。只需使用适当的 getter 和 setter。

你可能想知道我们在这里创建的设置对象。只要你需要它们,就可以创建它们。如果你愿意,也可以创建一个全局单例,但除非你想监视设置的更改,否则没有真正需要这样做。

下一步:复杂类型

除了基本类型之外,你还可以使用 GVariant 类型系统的全部功能来存储复杂类型。例如,如果你需要存储关于平面中一个圆的信息,你可以将其存储为类型为 * (ddd) * 的三元组,存储圆心的 x、y 坐标和半径。

要处理代码中具有复杂类型的设置,请使用 g_settings_get() 和 g_settings_set(),它们以 GVariant 的形式返回和接受值。

下一步:可重定位的模式

如果你的应用程序使用帐户,你可能需要查看可重定位的模式。可重定位的模式是你需要单独存储同一配置的多个实例时所需要的。这方面的一个典型例子是帐户:你的应用程序允许创建多个帐户,并且每个帐户都具有与之相关的相同类型的配置信息。

GSettings 通过省略模式中的路径来处理此问题

<schemalist>
  <schema id="org.gnome.Recipes.User">
    <key type="b" name="use-metric">
      <default>true</default>
    </key>
  </schema>
</schemalist>

相反,我们需要在创建 GSettings 对象时指定路径

s = g_settings_new_with_path ("org.gnome.Recipes.User",
                              "/org/gnome/Recipes/Account/mclasen");
if (g_settings_get_boolean (s, "use-metric"))
  g_print ("User mclasen is using metric units");

你需要自己设计一个模式,将你的帐户映射到唯一的路径。

绊脚石

使用 GSettings 时需要注意一些事项。其中之一是 GLib 需要能够在运行时找到已编译的模式。在不安装的情况下从构建目录运行应用程序时,这可能是一个问题。要处理这种情况,你可以设置 GSETTINGS_SCHEMA_DIR 环境变量,以告知 GLib 在哪里可以找到已编译的模式

GSETTINGS_SCHEMA_DIR=build/data ./build/src/gnome-recipes

另一个绊脚石是 GSettings 以序列化 GVariant 的形式读取 XML 中的默认值。对于常见的字符串情况,这可能会有点令人惊讶,因为它意味着我们需要在字符串周围加上引号

<default>'celsius'</default>

但是这些都是小问题,一旦你了解了它们,就可以轻松避免它们。

GTK+ 4.0 中的小部件层次结构

今天,我们将邀请客座作者 Timm Bäder,Corebird 的维护者和 GTK+ 的贡献者,来谈论在 GTK+ 4.0 中编写复合小部件的更改。

(注意:这里的一些信息基于尚未合并到 master 中的分支,但我相信它们在不久的将来会合并)

在 GTK+3 中,只有 GtkContainer 子类可以拥有子小部件。这对于我们知道的“公共”容器子级(例如 GtkBox)来说很有意义——也就是说,开发人员可以任意添加、删除和重新排序子小部件,而容器只是进行布局。

但是,GTK+3 中还有更复杂的小部件不继承自 GtkContainer,例如 GtkSpinButtonGtkSwitch。这些永远没有真正的 GtkWidget 子级。例如,考虑 GtkSpinButton 中的两个可单击区域。我在这里不称它们为“按钮”是有原因的,因为在 GTK+3 中,它们不是实际的 GtkButton 实例,因为 GtkSpinButton 不是 GtkContainer。相反,GtkSpinButton 必须解决这个问题,并为上/下区域创建两个 GdkWindow,然后在其中渲染两个图标;关心悬停和 CSS 状态;各种按钮向上/向下事件;以及 GdkWindow 的生命周期等。为了解决 GtkContainer 的要求,我们在 GTK+3 中引入了*gadgets* (GtkCssGadget)。在样式方面,gadget 对应于 CSS 框,因此表示 CSS 树中的一个节点。在小部件方面,它们被用于为非容器小部件提供“类似小部件”、可 CSS 设置样式的子级。

GtkWidget 更改

当然,为了在 GTK+4 中支持这些用例,需要进行大量的更改。我不会在这里列出所有这些更改(特别是那些比较丑陋的更改,比如焦点处理),但我认为其中大多数对于应用程序开发者和自定义小部件作者来说都非常有趣和重要。总的来说,我们正在努力摆脱特殊情况,并通过尽可能地重用小部件来走一条更通用的道路。因此,不要使用 PangoLayout 来显示文本,小部件应该使用 GtkLabel。如果您有一个具有类似按钮语义的可点击区域,请尝试使用 GtkButton。如果您想以水平或垂直方向布局小部件,请使用 GtkBox。这样,我们就有一个输入和渲染都可以操作的小部件树。在实践中,这意味着主要要摆脱小部件内部使用的所有小工具,以及独立的 GtkCssNode 实例。

迭代子小部件

在 GTK+3 中,每个容器都必须实现 GtkContainer::forall,并且可以使用 gtk_container_foreach()gtk_container_forall() 轻松地迭代所有子小部件。而在 GTK+4 中,每个 GtkWidget 都可以有子小部件,因此每个小部件都可能有一个我们需要绘制、测量、尺寸分配等的子列表。在小部件层次结构中,可以通过以下方式访问这些子小部件和同级小部件:
  • gtk_widget_get_first_child()
  • gtk_widget_get_last_child()
  • gtk_widget_get_prev_sibling()
  • gtk_widget_get_next_sibling()
因此,迭代给定 GtkWidget 的子小部件的最简单方法是(在 C 中):
GtkWidget *widget;
GtkWidget *child;
for (child = gtk_widget_get_first_child (widget);
     child != NULL;
     child = gtk_widget_get_next_sibling (child))
  {
    /* Do stuff with @child */
    g_assert (gtk_widget_get_parent (child) == widget);
  }

向非容器父级添加小部件

GTK+ 中的每个小部件(包括 3 和 4)都会保存一个指向其父小部件的指针。可以使用 gtk_widget_set_parent() 设置此父级,并且所有 GtkContainer::add 实现最终都必须使用此函数来设置给定子小部件的父级。

在 GTK+4 中,gtk_widget_set_parent() 仍然有效,并将小部件添加到父级子小部件列表的末尾。但是,我们显然也希望管理子小部件的顺序,以及我们在列表中添加新子小部件的位置,因此我们有:

  • gtk_widget_insert_before()
  • gtk_widget_insert_after()
在父级列表中已有的子小部件之前或之后添加新的子小部件。这些也可以用于通过传递一个已经设置了给定父级的子项来重新排序子小部件。
由于 GTK+ 中的许多小部件目前使用 XML 形式的复合小部件模板,GtkWidget 现在也有其自己的 GtkBuildable::add_child() 实现来支持此用例。例如,GtkFileChooserWidget 就使用了此功能,它几乎完全在 XML 中定义。

小部件 CSS 名称

由于我们经常需要为任意小部件使用任意 CSS 节点名称,GtkWidget 现在有一个名为 GtkWidget:css-name 的构造时只读属性,如果指定了该属性,它将用作 CSS 节点名称。如果没有指定,则将使用传递给小部件 gtk_widget_class_set_css_name() 调用的名称。如果这两个都没有使用,则 CSS 节点将简单地命名为“widget”。

已转换小部件的示例

当前主分支(和侧分支)中已经有一些小部件从使用各种 GtkCssGadgetGdkWindowPangoLayout 转换为使用实际的小部件。最终目标当然是尽可能多地重用小部件,从而减少代码大小和维护负担。

GtkSwitch

在 GTK+3 中,GtkSwitch 是一个直接的 GtkWidget 子类(好耶!),它使用一个 GdkWindow 用于输入(单击开关将启用/禁用它),一个 GtkCssGadget 用于小部件本身,两个 PangoLayout 用于 ON/OFF 文本,以及另一个 GtkCssGadget 用于滑块。
在 GTK+ 主分支中,开关仍然有其小部件级的 GtkCssGadget,因此它支持 min-width/min-height CSS 属性和 CSS 边距,但滑块小工具已被 GtkButton 取代,并且两个 PangoLayout 已被 GtkLabel 取代。这样,我们可以节省 gtkswitch.c 中大约 300 行代码。理论上,我们还拥有更多功能,例如可以使用 GtkLabel 支持的对 text-decoration CSS 属性的有限支持,但我只是怀疑这是否非常有用。

GtkSpinButton

如前所述,GtkSpinButton 可以轻松地为向上/向下区域使用实际的 GtkButton,并且在 GTK+ 主分支(将在某个时候成为 GTK+4)中也是这样做的。这消除了 gtkspinbutton.c 中的另外 300 行代码。通过使用 GtkButton,旧的图标助手小工具也变成了实际的 GtkImage 实例。不幸的是,我们必须在这里自己实现一些 GtkGesture 魔法,因为 GtkSpinButton 还支持在其按钮上进行鼠标中键和右键单击,而 GtkButton::clicked 仅对单次鼠标主按钮单击做出反应。

GtkLevelBar

GtkLevelBar 管理一组块,并为它们分配不同的样式类。在 GTK+3 中,这些块都是 GtkCssGadget 实例。所有这些块都是“哑”的,也就是说它们不执行任何特殊操作,它们只是 CSS 框而已。这就是为什么将其转换为对所有块使用 GtkWidget 并没有减少太多文件大小的原因。

 

GtkProgressBar

GtkProgressBar 使用小工具来表示槽和进度节点。它还使用 PangoLayout 来显示百分比或用户给定的字符串。

在主分支中,槽和进度都是小部件,并且 PangoLayout 当然是一个 GtkLabel。无需监听 GtkWidget::style-changed(小部件会自动完成此操作),并且无需自己绘制 PangoLayoutGtkLabel 现在负责此操作)可以节省大约 200 行代码的不错代码大小。

 

GtkExpander

GtkExpander 比看起来更复杂。在 GTK+3 中,它由 2 个 GtkBoxGadget(类似于 GtkBox,但不是小部件...)、一个用于标题小部件左侧的箭头的工具、标题小部件和实际内容小部件组成。在主分支中,这是使用实际的 GtkBox 和一个 GtkIcon(内部小部件)来表示箭头的。我不确定这是否是表达 GtkExpander 功能的最佳方式,例如,我们也可以为箭头+标题小部件组合使用 GtkButton
由于 GtkBoxGadget 已经几乎是一个完美的 GtkBox 克隆,因此这里的代码节省不是很有趣,但是不必再次监听 GtkWidget::direction-changed 可以节省大约 30 行代码。

意外的 GtkBox 和 GtkButton 子类

GTK+3 包含许多仅为了看起来和行为类似于另一个小部件而从该小部件继承的小部件。这里的问题是,这些小部件还会继承父类的所有 API,而这很少是想要的。
对于 GtkBox 来说,GTK+3 中的几乎所有子类都是“偶然的”,因为实际上将它们用作 GtkBox 没有任何意义,人们通常也不会这样做,但它们必须是 GtkBox 的子类,才能满足 GTK+ 的 GtkContainer 要求。一个这样的控件示例是 GtkFileChooserWidget。这已经是存在的最复杂的控件之一,但你是否曾考虑过使用 gtk_container_add()gtk_box_pack_{start,end}() 向其中添加控件?这没什么意义。它是一个具有自身 API 的封闭实体。因此,在 GTK+4 中,它将是一个直接的 GtkWidget 子类,其中包含一个 GtkBox。或者可能不是。这只是一个你无需关心的实现细节。(一个有趣的题外话:GtkFileChooserButton 在 GTK+3 中是一个 GtkBox
同样的情况也适用于 GtkButton。在 GTK+3 中,GtkButton 有很多子类继承了所有 GtkButton 的 API,但实际上并不支持它。如果你从 GtkLinkButton 中移除子控件会发生什么?如果你设置 GtkFontButtonGtkButton:label 属性会发生什么?同样,这些都是封闭的实体,它们有自己的 API 来设置和获取各种数据,并根据这些数据更改行为和/或外观,但这并不意味着它们支持所有的 GtkButton/GtkContainer 诡计。

总体重构规则和未来

对于此重构工作,我们尽量保持 CSS 节点结构与 GTK+3 中的结构相同,即,我们尽量不破坏目前在 testsuite/css/nodes.c 中的 CSS 节点测试。
GTK+ 中一些更复杂的控件仍然严重依赖于小工具,将它们移植为仅使用实际的控件将需要大量的工作。GtkRange 从历史上看是 GTK+ 中最复杂的非容器控件之一。它既用于滚动条又用于滑块,因此将其移植到控件可能首先需要另一轮重构。
另一个有趣的例子是 GtkNotebook,它结合了小工具和控件的使用。在这里,例如,我们可以使用一个真正的 GtkStack 来切换页面,并轻松支持页面切换过渡。
展望未来的另一个激动人心的方向当然是 Carlos 的 wip/carlosg/event-delivery 分支,它摆脱了大量的 GdkWindow 实例,并使控件输入比以往任何时候都更容易。