容器的秘密:尺寸分配,第 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 也应该遵循此模式。

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

值得庆幸的是,我们的大部分机制已经用单个维度编写,并且可以像应用于宽度一样应用于高度。剩下的事情是浏览所有函数,并确保在执行依赖于方向的操作时,我们考虑到方向。例如,我们引入一个小助手来查询正确的 expand 属性。

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() 实现可以保持原样。再一次,我们需要决定首先扩展哪个,如果多个子项设置了 expand 标志。GtkBox 尝试平等对待所有子项,并在所有扩展子项之间均匀分配可用的额外空间。另一方面,我们更喜欢中心子项,因为它是最重要的一个。

但是我们该怎么做呢?经过一些实验,我发现,如果我们已经必须将中心子项向左或向右推,因为它不适合,那么将其做得更大就没有意义了。因此,只有在没有这种情况时,我们才尊重 expand 标志

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. 具有这些更改的代码

容器的秘密:尺寸分配,第 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. 我们开始使用的代码