容器的秘密:尺寸分配

我最近有一次机会重新实现了一个容器的尺寸分配,其中包含了 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_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+ 容器和绘图的问题。这是我发现的。

CSS 绘图

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

例如,我们可以使用 GtkBox 在列表框和标签周围放置一个框架,使标签在视觉上看起来像是列表的一部分。你甚至可以使用一些类似如下的 CSS 使其变得色彩鲜艳且有趣:

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

分配和调整大小

传统上,GTK+ 中的大多数容器都不进行任何自己的绘图,而只是排列其子项,因此当其大小发生变化时,它们没有真正需要进行完整的重绘 - 重绘子项就足够了。这就是 gtk_container_set_reallocate_redraws() 的作用。并且它在 GTK+ 3 中默认为 FALSE,因为我们不想冒着每次分配更改时都添加过多重绘的风险。

你可以看到它的发展方向:如果我使用删除按钮从配料列表中删除黄油和盐,则该列表的分配(以及它周围的框的分配)将缩小,并且我们会遇到重绘问题。

解决方案

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

gtk_container_reallocate_redraws (GTK_CONTAINER (parent), TRUE);

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

另一个(也许更好)的替代方法是使用旨在绘制边框的容器,例如 GtkFrame。

列表中的拖放

我最近有机会通过拖放 (DND) 来实现 GtkListBox 的重新排序。这并不复杂。由于我没有看到拖放在列表框中被广泛使用,因此这里简要概述了使基本功能正常工作所需的内容。

设置拖动源

有两种方法可以使 GTK+ 小部件成为拖动源(即单击和拖动将启动 DND 操作的位置)。你可以通过调用 gtk_drag_begin() 动态决定启动拖动。但是我们在这里采用更简单的方法:我们只是静态地声明我们的列表行应该是拖动源,并让 GTK+ 处理所有细节

handle = gtk_event_box_new ();
gtk_container_add (GTK_CONTAINER (handle),
        gtk_image_new_from_icon_name ("open-menu-symbolic", 1));
gtk_drag_source_set (handle,
        GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE);

请注意,我选择在此处创建一个可见的拖动手柄,而不是允许在行的任何位置开始拖动。它看起来像这样

这些条目告诉 GTK+ 我们希望通过此源的拖动操作提供哪些数据。在我们的例子中,我们不会提供像 text/plain 这样的标准 MIME 类型,而是创建我们自己的私有类型,并提示 GTK+ 我们不希望支持拖动到其他应用程序。

static GtkTargetEntry entries[] = {
   { "GTK_LIST_BOX_ROW", GTK_TARGET_SAME_APP, 0 }
};

这里有一个小问题,就是你设置为拖动源的窗口部件必须有一个 GdkWindow。一个 GtkButton 或一个 GtkEventBox(如本例中所示)都可以。GTK4 将提供一个不同的 API 来创建拖动源,避免对窗口的需求。

有了这些代码,你已经可以拖动你的行了,但到目前为止,还没有地方可以放置它们。让我们来解决这个问题。

接受放置

与拖动操作相反,我们在拖动操作中创建了一个可见的拖动手柄,以提示用户支持拖放操作,我们希望直接接受列表中的任何位置的放置。最简单的方法是将每行都设置为放置目标(即,可能接受放置的位置)。

gtk_drag_dest_set (row,
        GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE);

这些条目与我们上面讨论的相同。GTK_DEST_DEFAULT_ALL 告诉 GTK+ 为我们处理 DND 操作的所有方面,这样我们可以保持这个示例的简单性。

现在我们可以在手柄上开始拖动,并且我们可以将其放置在其他行上。但是之后什么都不会发生。我们需要做一些额外的工作来实现重新排序。接下来我们来做这件事。

传输数据

拖放操作通常用于在应用程序之间传输数据。GTK+ 使用一个名为 GtkSelectionData 的数据持有者对象来实现这一点。要发送和接收数据,我们需要连接到源端和目标端上的信号。

g_signal_connect (handle, "drag-data-get",
        G_CALLBACK (drag_data_get), NULL);
g_signal_connect (row, "drag-data-received",
        G_CALLBACK (drag_data_received), NULL);

在源端,当 GTK+ 需要数据将其发送到放置目标时,会发出 drag-data-get 信号。在我们的例子中,该函数只会将指向源窗口部件的指针放入选择数据中。

gtk_selection_data_set (selection_data,
        gdk_atom_intern_static_string ("GTK_LIST_BOX_ROW"),
        32,
        (const guchar *)&widget,
        sizeof (gpointer));

在目标端,当 GTK+ 将接收到的数据传递给应用程序时,会在放置目标上发出 drag-data-received 信号。在我们的例子中,我们将从选择数据中取出指针,并重新排序行。

handle = *(gpointer*)gtk_selection_data_get_data (selection_data);
source = gtk_widget_get_ancestor (handle, GTK_TYPE_LIST_BOX_ROW);

if (source == target)
  return;

source_list = gtk_widget_get_parent (source);
target_list = gtk_widget_get_parent (target);
position = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (target));

g_object_ref (source);
gtk_container_remove (GTK_CONTAINER (source_list), source);
gtk_list_box_insert (GTK_LIST_BOX (target_list), source, position);
g_object_unref (source);

这里唯一需要注意的是,我们需要在将窗口部件从其父容器中删除之前对其进行引用,以防止它被最终化。

有了这个,我们就有了可重新排序的行了。耶!

作为最后一步,让我们让它看起来更好。

一个漂亮的拖动图标

到目前为止,在拖动过程中,你只会看到光标,这没什么帮助,而且也不太好看。预期的行为是拖动行的可视表示。

要实现这一点,我们连接到拖动源上的 drag-begin 信号。

g_signal_connect (handle, "drag-begin",
        G_CALLBACK (drag_begin), NULL);

…并做一些额外的工作来创建一个漂亮的“拖动图标”

row = gtk_widget_get_ancestor (widget, GTK_TYPE_LIST_BOX_ROW);
gtk_widget_get_allocation (row, &alloc);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                      alloc.width, alloc.height);
cr = cairo_create (surface);
gtk_widget_draw (row, cr);

gtk_drag_set_icon_surface (context, surface);

cairo_destroy (cr);
cairo_surface_destroy (surface);

这看起来比实际情况要复杂得多——我们正在创建一个大小合适的 cairo 表面,将行窗口部件渲染到其中(信号在手柄上发出,所以我们必须找到作为祖先的行)。

不幸的是,这还不能产生完美的结果,因为列表框行通常不渲染背景或框架。为了解决这个问题,我们可以临时向行的样式上下文添加一个自定义样式类,并使用一些自定义 CSS 来确保我们获得背景和框架。

context = gtk_widget_get_style_context (row);
gtk_style_context_add_class (context, "drag-icon");
gtk_widget_draw (row, cr);
gtk_style_context_remove_class (context, "drag-icon")

作为额外的改进,我们可以在表面上设置一个偏移量,以防止在拖动开始时出现视觉上的“跳跃”,方法是在 gtk_drag_set_icon_surface() 调用之前放置此代码。

gtk_widget_translate_coordinates (widget, row, 0, 0, &x, &y);
cairo_surface_set_device_offset (surface, -x, -y);


瞧!

下一步

本文仅展示了通过拖放操作进行行重新排序的最简单的设置。许多改进是可能的,有些容易,有些不太容易。

一个明显的增强是允许在同一个应用程序中的不同列表之间进行拖动。这只是在 drag_data_received() 调用中小心处理列表窗口部件的问题,我在这里展示的代码应该已经可以用于此目的了。

另一个改进是根据哪个边缘更近,将行放置在目标行之前或之后。同时,你可能希望修改放置目标的高亮显示,以指示放置将发生的边缘。这可以通过不同的方式完成,但所有这些都需要监听 drag-motion 事件并处理事件坐标,我在这里不想深入研究这些内容。

最后,在拖动过程中滚动列表。这对于长列表来说非常重要,如果你想将一行从顶部拖动到底部——如果列表不滚动,你必须以页面增量进行操作,这太麻烦了。实现这一点最简单的方法可能是将放置目标移动到列表窗口部件本身,而不是单个行。

参考

GTK+ 检查器

许多 GTK+ 用户和开发者已经听说过 GTK+ 检查器,这是一个用于检查、修改和理解 GTK+ 应用程序的工具。检查器非常强大,允许主题设计人员即时测试 CSS 更改并放大窗口部件以查看最微小的细节,让开发者检查应用程序窗口部件及其属性,并让用户玩(并最终破坏)应用程序。

在本文中,我们将探索 GTK+ 检查器并展示你可以用它做什么。

序言

由于检查器是一个调试工具,默认情况下是禁用的。要开始使用检查器,你首先必须启用它。你可以使用 DConf 编辑器轻松做到这一点。

Enabling the Gtk+ Inspector with DConf Editor
使用 DConf 编辑器启用 GTK+ 检查器

或者,你可以使用终端来启用它。为此,请运行以下命令

$ gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true

完成!检查器现在已启用!

打开检查器

现在检查器已启用,你希望运行它。检查器始终与应用程序关联。让我们以 GNOME 日历为例。

GNOME Calendar
GNOME 日历应用程序

有多种方法可以调出检查器。你可以在使用应用程序时通过键入<Ctrl> + <Shift> + D(或<Ctrl> + <Shift> + I 自动选择鼠标指针下的窗口部件)来打开它。或者,你可以使用环境变量 GTK_DEBUG=interactive. 从终端启动应用程序。

检查器将打开,你会看到以下窗口。

Inspector on Calendar
GNOME 日历上的检查器窗口

这就是你需要做的全部。现在让我们探索检查器提供的各种功能。

探索检查器

起初,大量按钮和选项卡可能会让那些不精通检查应用程序艺术的人感到困惑。以下是对选项卡的快速说明,按顺序排列:

  • 对象:公开应用程序的窗口部件,并允许编辑属性和查看每个窗口部件的详细信息。如下所述。
  • 统计:显示应用程序的各种统计信息。你需要使用 GOBJECT_DEBUG=instance-count 运行应用程序。
  • 资源:显示嵌入在应用程序二进制文件中的各种资源,例如自定义图标或 GtkBuilder 文件等。
  • CSS:允许实时测试 CSS。如下所述。
  • 视觉:控制应用程序的一些视觉方面,例如文本方向、深色/浅色变体、主题、缩放因子等。
  • 常规:显示有关 GTK+ 应用程序(及其运行所在的会话)的各种信息。

让我们剖析 GTK+ 检查器的主窗口。

Inspector window
主检查器窗口

检查器的这 4 个带注释的部分是最常用的部分。主题设计人员会想检查 (3) 和 (4),而开发人员通常使用 (1) 和 (2)。

检查窗口部件

对于开发人员来说,检查器通过让你更改屏幕上任何窗口部件的属性来显示其用处。让我们首先单击第一个按钮并使用鼠标光标选择一个窗口部件。

Selecting widgets
使用检查器选择窗口部件

现在,你可以通过浏览对象 > 属性选项卡轻松更改该窗口部件的属性。例如,你可以更改窗口部件的可见性、标签的文本等等!

Editing a widget property
编辑窗口部件属性

现在你知道如何检查 GTK+ 应用程序了,请试用并探索有多少应用程序被组织起来。更改窗口部件的属性,看看会发生什么。大多数情况下,这是安全的,不会破坏你的 GNOME 会话,也不会冻结你的计算机!

编辑 CSS

检查器也是一个强大的设计工具。它拥有的最强大的功能之一是实时 CSS 编辑器。让我们首先转到 CSS 选项卡。

CSS Editor
检查器 CSS 编辑器视图

让我们试用 CSS!粘贴以下 CSS 代码,看看会发生什么。

window stack {
    background-color: orange;
}

哇!窗口变得不一样了!这段 CSS 代码会更改 GtkWindow 内任何 GtkStack 小部件的背景颜色。如果您想了解更多关于 CSS 选择器以及 GTK+ 如何使用 CSS 进行主题化的信息,本文末尾有一些有用的链接。

谨慎的读者可能会问:CSS 元素的层次结构是什么?我如何查看哪些 CSS 元素可用?

别担心!GTK+ Inspector 允许您在Objects(对象)> CSS Nodes(CSS 节点)选项卡中轻松检查 CSS 层次结构。

CSS Nodes
CSS 节点选项卡

GTK+ 小部件都有文档化的 CSS 名称。您可以浏览 GTK+ 文档,了解小部件的组织方式,以及如何使用 CSS 控制小部件的各个方面。

不确定您的 CSS 更改是否完美?让我们放大窗口小部件以确保我们不会遗漏任何细节

Zooming widget using Magnifier
使用 Magnifier(放大镜)选项卡放大窗口小部件

效果不错?加入 -design 并与社区分享您出色的 CSS 代码片段!

总结

虽然本文探讨了 GTK+ Inspector 的一些重要方面,但这远不是 Inspector 所有功能的详尽列表。阅读本文后,希望您能够打开 Inspector 并自行探索其更多强大的功能。

疑问?意见?建议?请留言,加入 GNOME IRC 网络上的 #gtk+ 频道,让我们知道您的想法!

有用的链接