容器秘密:尺寸分配,第 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;
}

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

请注意,在这种情况下,orientation 是 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()。

该度量实现仅仅是测量了三个子部件,水平方向上累加了最小和自然尺寸,垂直方向上取它们的最大值。用粗略的伪代码表示为:

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. 我们开始的代码