容器的秘密:尺寸分配,第6部分

基线

我们正在进入 GTK+ 尺寸分配中更神秘的领域之一。基线将窗口部件从一个简单的具有宽度和高度的模型转变为一种窗口部件可以在垂直方向以更有趣的方式对齐的模型。这主要关系到文本。当你在文本行中移动时,读者对单词上下移动非常敏感。基线就是为了避免这种情况。

 

由于这是关于子项之间垂直对齐的,因此只有当容器处于水平方向时,基线才相关。

测量上方和下方

由于子项现在可以有“强制”对齐,简单地取子项的最大高度不再足够。对齐可能会导致子项在顶部或底部“突出”,需要更大的整体高度。为了处理这个问题,我们分别测量“基线上方”的部分和“基线下方”的部分,并分别最大化它们。

for (i = 0; i < 3; i++) {
  gtk_widget_measure (child[i],
                      orientation,
                      sizes[i].minimum_size,
                      &child_min, &child_nat,
                      &child_min_baseline, &child_nat_baseline);

   below_min = MAX (below_min, child_min - child_min_baseline);
   above_min = MAX (above_min, child_min_baseline);
   below_nat = MAX (below_nat, child_nat - child_nat_baseline);
   above_nat = MAX (above_nat, child_nat_baseline);
}

total_min = above_min + below_min;
total_nat = above_nat + below_nat;

此代码省略了一些细节,例如处理不返回基线的子项。

在基线上分配

在分配方面,有两种情况:要么我们被赋予一个必须将子项对齐的基线,要么我们必须自己确定一个基线。在后一种情况下,我们需要做与我们已经为测量所做的基本相同的事情:分别确定下方和上方的大小,并使用它们来找到我们的基线

for (i = 0; i < 3; i++) {
  if (gtk_widget_get_valign (child[i]) != GTK_ALIGN_BASELINE)
    continue;

  gtk_widget_measure (child[i],
                      GTK_ORIENTATION_VERTICAL,
                      child_size[i],
                      &child_min, &child_nat,
                      &child_min_baseline, &child_nat_baseline);

  below_min = MAX (below_min, child_min - child_min_baseline);
  below_nat = MAX (below_nat, child_nat - child_nat_baseline);
  above_min = MAX (above_min, child_min_baseline);
  above_nat = MAX (above_nat, child_nat_baseline);
}

在确定基线时,我们再次需要做出选择。当可用空间大于最小值时,我们将基线放置得尽可能高、尽可能低,还是中间的某个位置?GtkBox 有一个 ::baseline-position 属性,将此选择留给用户,我们在这里也这样做。

switch (baseline_position) {
  case GTK_BASELINE_POSITION_TOP:
    baseline = above_min;
    break;
  case GTK_BASELINE_POSITION_CENTER:
    baseline = above_min + (height - (above_min + below_min)) / 2;
    break;
  case GTK_BASELINE_POSITION_BOTTOM:
    baseline = height - below_min;
    break;
}
展开,基线位置:居中
压缩,基线位置:顶部
压缩,基线位置:居中
压缩,基线位置:底部

总结

这结束了我们在 GTK+ 尺寸分配机制中的旅程。我希望你喜欢它。

参考

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第2部分
  3. 容器的秘密:尺寸分配,第3部分
  4. 容器的秘密:尺寸分配,第4部分
  5. 容器的秘密:尺寸分配,第5部分
  6. 带有这些更改的代码

容器的秘密:尺寸分配,第5部分

方向

GTK+ 中的许多窗口部件可以水平或垂直定向。从分隔符到工具栏的任何内容都在实现 GtkOrientable 接口,以允许通过设置 ::orientation 属性在运行时更改此设置。因此,很明显,GtkCenterBox 也应该遵循这种模式。

我不会详细解释如何添加接口和实现属性。对我们来说有趣的部分是我们将如何在尺寸分配期间使用方向属性。

值得庆幸的是,我们的许多机制已经用单维编写,并且可以像宽度一样应用于高度。剩下的就是遍历所有函数,并确保我们在执行任何取决于它的操作时都考虑方向。例如,我们引入了一个小助手来查询适当的展开属性。

static gboolean
get_expand (GtkWidget *widget,
            GtkOrientation orientation)
{
  if (orientation == GTK_ORIENTATION_HORIZONTAL)
    return gtk_widget_get_hexpand (widget);
  else
    return gtk_widget_get_vexpand (widget);
}

要记住的一件事是,我们在这里实现的一些功能仅在水平方向上适用,例如从右到左翻转或基线。

measure() 函数更改为避免硬编码水平方向

if (orientation == self->orientation)
  gtk_center_box_measure_orientation (widget, orientation, for_size,
                                      minimum, natural,
                                      min_baseline, nat_baseline);
else
  gtk_center_box_measure_opposite (widget, orientation, for_size,
                                   minimum, natural,
                                   min_baseline, nat_baseline);

size_allocate() 函数调用 distribute() 以根据方向分配宽度或高度

if (self->orientation == GTK_ORIENTATION_HORIZONTAL) {
  size = width;
  for_size = height;
} else {
  size = height;
  for_size = width;
}
distribute (self, for_size, size, sizes);

经过这些直接但乏味的更改后,我们可以垂直定向中心框

参考

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第2部分
  3. 容器的秘密:尺寸分配,第3部分
  4. 容器的秘密:尺寸分配,第4部分
  5. 带有这些更改的代码

容器的秘密:尺寸分配,第4部分

高度换宽度

在这里,我们进入 GTK+ 尺寸分配的更深层次部分。高度换宽度意味着窗口部件没有单个最小尺寸,但它可能会为了获得更大的高度而容纳更小的宽度。大多数窗口部件不是这样的。这种行为的典型示例是可以将其文本包装为多行的标签

  

高度换宽度使尺寸分配更加昂贵,因此容器必须通过设置请求模式来显式启用它。通常,容器应查看其子项并使用大多数子项首选的请求模式。为简单起见,我们在此处硬编码高度换宽度

static GtkSizeRequestMode
gtk_center_box_get_request_mode (GtkWidget *widget)
{
  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
}

双向测量

编写可以处理高度换宽度的 measure() 函数的惯用方法是将其分解为两种情况:一种是我们沿着布局的方向进行测量的情况,另一种是我们沿着相反方向进行测量的情况。

if (orientation == GTK_ORIENTATION_HORIZONTAL)
  measure_orientation (widget, for_size,
                       orientation,
                       minimum, natural,
                       minimum_baseline, natural_baseline);
else
  measure_opposite (widget, for_size,
                    orientation,
                    minimum, natural,
                    minimum_baseline, natural_baseline);

沿着方向进行测量就像我们一直以来所做的 measure() 函数一样:我们得到一个高度,因此我们询问所有子项它们对于该高度需要多少宽度,然后我们将答案加起来。

沿着相反方向进行测量意味着回答问题:给定这个宽度,你需要多少高度?我们想问子项相同的问题,但是我们应该给每个子项多少宽度?我们不能只是将全宽度传递给每个子项,因为我们不希望它们重叠。

分配

为了解决这个问题,我们需要在子项之间分配可用宽度。这正是我们的 size_allocate() 函数正在做的事情,因此我们需要将 size_allocate() 的核心分解为一个单独的函数。

毫不奇怪,我们将新函数称为 distribute()。

static void
distribute (GtkCenterBox *self,
            int for_size,
            int size,
            GtkRequestedSize *sizes)
{
   /* Do whatever size_allocate() used to do
    * to determine sizes
    */

  sizes[0].minimum_size = start_size;
  sizes[1].minimum_size = center_size;
  sizes[2].minimum_size = end_size;
}

既然我们知道如何获取子项的候选宽度,我们可以完成该函数以沿相反方向进行测量。与之前一样,我们最终会返回子项的所需高度的最大值,因为我们的布局是水平的。

请注意,在这种情况下,方向为 GTK_ORIENTATION_VERTICAL,因此 gtk_widget_measure() 调用返回的 min 和 nat 值是高度。

distribute (self, -1, width, sizes);

gtk_widget_measure (start_widget,
                    orientation,
                    sizes[0].minimum_size,
                    &start_min, &start_nat,
                    &min_baseline, &nat_baseline);

gtk_widget_measure (center_widget,
                    orientation,
                    sizes[1].minimum_size,
                    &center_min, &center_nat,
                    &min_baseline, &nat_baseline);

gtk_widget_measure (end_widget,
                    orientation,
                    sizes[2].minimum_size,
                    &end_min, &end_nat,
                    &min_baseline, &nat_baseline);

*minimum = MAX (start_min, center_min, end_min);
*natural = MAX (start_nat, center_nat, end_nat);

由于我们现在已经将 size_allocate() 的大部分内容分解为 distribute() 函数,因此我们可以直接从那里调用它,然后执行其余的必要工作以将位置分配给子项(因为 distribute 已经为我们提供了大小)。

展开
略小于自然尺寸
更小
以及更小
以及更小

参考

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第2部分
  3. 容器的秘密:尺寸分配,第3部分
  4. 带有这些更改的代码
  5. 高度换宽度几何管理的文档

容器的秘密:尺寸分配,第3部分

展开子项

我们对功能强大的尺寸分配的追求的下一站是 ::expand 属性。实际上有两个属性,::hexpand 和 ::vexpand,它们在窗口部件层次结构中具有向上传播的有趣的某种行为。但这并不是我们今天要讨论的内容,我们只是想在中心框窗口部件的子项设置了 ::hexpand 标志时,为它们提供所有可用空间。

再一次,measure() 实现可以保持不变。再一次,我们需要决定如果多个子项设置了展开标志,则首先展开哪个子项。GtkBox 试图以相同的方式对待其所有子项,并在所有展开的子项之间均匀分配可用的额外空间。另一方面,我们更喜欢中心子项,因为它是最重要的子项。

但是我们该怎么做呢?经过一些实验,我发现如果由于中心子项不适合而我们已经必须将其向左或向右推,那么使其变得更大毫无意义。因此,如果不是这种情况,我们才尊重展开标志

center_expand = gtk_widget_get_hexpand (center);

if (left_size > center_x)
  center_x = left_size;
else if (width - right_size < center_pos + center_size)
  center_x = width - center_width - right_size;
else if (center_expand) {
  center_width = width - 2 * MAX (left_size, right_size);
  center_x = (width / 2) - (center_width / 2);
}

执行此操作后,如果外部子项正在展开,则可能仍有一些空间可以提供给它们

if (left_expand)
  left_size = center_pos - left_pos;
if (right_expand)
  right_size = pos + width - (center_pos + center_width);
没有展开的子项
中心子项正在展开
末尾子项正在展开
中心和末尾子项正在展开

参考

  1. 容器的秘密:尺寸分配
  2. 容器的秘密:尺寸分配,第2部分
  3. 带有这些更改的代码

本周 GTK+ – 36

在上周,GTK+ 的主分支有 22 次提交,增加了 1165 行代码,删除了 904 行代码。

计划和状态
  • GTK+ 的路线图可在 wiki 上找到
重要更改

在主分支上

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

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

容器的秘密:尺寸分配,第2部分

从右到左的语言

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

如果您只习惯用英语编写软件,这可能会有点令人惊讶,但 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+ 的主分支提交了 33 次 commit,新增了 5011 行代码,移除了 8140 行代码。

计划和状态
  • GTK+ 的路线图可在 wiki 上找到
  • Patrick Griffis 正在尝试使用 一个特性分支 来弃用并移除 gtk_dialog_run();请参阅 这条评论,其中提到了关于 UI 线程、IPC 线程和 I/O 线程嵌套主循环的缺陷。
  • Matthias Clasen 正在 尝试 在图标浏览器中重用 libdazzle 中的模糊搜索功能。
重要更改

在主分支上

  • 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 – 当顶层窗口 set_transient_for 设置为另一个顶层窗口时,在 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 文档