GTK 4 中的布局管理器

自 GTK 最初的设计以来,容器和布局策略一直是其设计的主要组成部分。如果您希望您的窗口小部件根据特定的策略来布局其子项,则必须实现 GtkContainer 来处理子窗口小部件的添加、删除和迭代,然后必须实现 GtkWidget 中的大小协商虚拟函数来测量、定位和调整每个子项的大小。

GTK 4 开发周期的一个主要主题是将更多功能委托给辅助对象,而不是将其编码到 GTK 提供的基类中。例如,我们将事件处理从 GtkWidget 描述的信号处理程序移到了事件控制器中,并将渲染委托给了 GtkSnapshot 对象。朝着这个方向的另一个步骤是将布局机制从 GtkWidget 本身分离到一个辅助类型 GtkLayoutManager

布局管理器

布局管理器是负责测量和调整窗口小部件及其子项大小的对象。每个 GtkWidget 都拥有一个 GtkLayoutManager,并使用它来代替 measure()allocate() 虚拟函数(这些函数即将消失)。更改的要点:您不是子类化 GtkWidget 来实现其布局策略,而是子类化 GtkLayoutManager,然后将布局管理器分配给窗口小部件。

就像旧的 GtkWidget 代码一样,您需要覆盖一个虚拟函数来测量布局,称为 measure(),它取代了 GTK 3 的 get_preferred_* 系列虚拟函数

static void
layout_measure (GtkLayoutManager *layout_manager,
                GtkWidget        *widget,
                GtkOrientation    orientation,
                int               for_size,
                int              *minimum,
                int              *natural,
                int              *minimum_baseline,
                int              *natural_baseline)

测量之后,您需要将大小分配给布局;这发生在 allocate() 虚拟函数中,该函数取代了先前 GTK 主要版本的 venerable size_allocate() 虚拟函数

static void
layout_allocate (GtkLayoutManager *layout_manager,
                 GtkWidget        *widget,
                 int               width,
                 int               height,
                 int               baseline)

在更深奥的方面,您还可以覆盖 get_request_mode() 虚拟函数,它允许您声明布局管理器是请求固定大小,还是其大小之一取决于相反的大小,例如高度取决于宽度或宽度取决于高度

static GtkSizeRequestMode
layout_get_request_mode (GtkLayoutManager *layout_manager,
                         GtkWidget        *widget)

您可能会注意到,每个虚拟函数都会传递布局管理器实例,以及使用布局管理器的窗口小部件。

当然,这对于 GTK 窗口小部件的工作方式的各个方面具有更大的影响,最明显的是,布局代码的所有复杂性现在可以保留在其自己的对象中,通常是不可导出的,而窗口小部件可以保持可导出并变得更简单。

这项工作的另一个特点是,如果您想更改容器的布局策略,可以在运行时更改布局管理器;您还可以拥有每个窗口小部件的布局策略,而无需向窗口小部件代码添加更多复杂性。

最后,布局管理器使我们能够摆脱 GTK 的一种特殊情况,即:容器子属性。

子属性

GtkContainer 的内部深处,是 GObject 属性相关代码的本质副本,其唯一的工作是为从 GtkContainer 派生的类型实现“子”属性。这些容器/子属性仅在子项成为特定容器类的父项时存在,并且出于各种原因使用它们——但通常是为了控制布局选项,例如盒子和类似盒子的容器中的打包方向; GtkFixed 内的固定位置;或者笔记本标签小部件的展开/填充规则。

子属性很难使用,因为它们需要特殊的 API 而不是常用的 GObject API,因此需要在 GtkBuilder、gtk-doc 和语言绑定中进行特殊处理。子属性也附加到容器的实际直接子项,因此如果窗口小部件介入一个子项(例如,GtkScrolledWindowGtkListBox 所做的那样),那么您需要保留对子项的引用,以便更改应用于您自己的窗口小部件的布局。

在 GTK 的主分支中,我们摆脱了它们中的大多数——要么在有实际窗口小部件 API 实现相同功能时直接删除它们,要么通过创建辅助 GObject 类型并将子属性移动到这些类型中。最终目标是在 GTK 4 发布时删除所有这些属性以及 GtkContainer 中的相关 API。对于与布局相关的属性,GtkLayoutManager 提供了自己的 API,以便在将子项添加到使用布局管理器的窗口小部件或从窗口小部件中删除子项时,可以自动创建和销毁对象。创建的对象是可内省的,并且在文档或绑定方面不需要特殊处理。

您首先从 GtkLayoutChild 类派生您自己的类型,并像为任何其他 GObject 类型添加属性一样添加属性。然后,您覆盖 GtkLayoutManagercreate_layout_child() 虚拟函数

static GtkLayoutChild *
create_layout_child (GtkLayoutManager *manager,
                     GtkWidget *container,
                     GtkWidget *child)
{
  // The simplest implementation
  return g_object_new (your_layout_child_get_type (),
                       "layout-manager", manager,
                       "child-widget", child,
                       "some-property", some_property_initial_state,
                       NULL);
}

之后,只要窗口小部件仍然是使用布局管理器的容器的子项,您就可以访问您的布局子对象;如果子项从其父项中删除,或者容器更改布局管理器,则布局子项将自动收集。

新的布局管理器

当然,仅仅在 GTK 中拥有 GtkLayoutManager 类对我们没有任何好处。GTK 4 为应用程序和窗口小部件开发人员引入了各种布局管理器

  • GtkBinLayout 实现了 GtkBin 的布局策略,并添加了一个额外功能,即它支持多个彼此堆叠的子项,类似于 GtkOverlay 的工作方式。您可以使用每个窗口小部件的对齐和展开属性来控制它们在分配区域内的位置,并且 GtkBinLayout 将始终要求足够的空间来分配其最大的子项。
  • GtkBoxLayoutGtkBox 实现的布局策略的直接端口;GtkBox 本身已被移植为在内部使用 GtkBoxLayout
  • GtkFixedLayoutGtkFixedGtkLayout 的固定布局定位策略的端口,并添加了让您定义通用转换的功能,而不是每个子项的纯 2D 平移;GtkFixed 已被修改为使用 GtkFixedLayout 并使用 2D 平移——并且 GtkLayout 已合并到 GtkFixed 中,因为其唯一区别特征是实现了 GtkScrollable 接口。
  • GtkCustomLayout 是一种方便的布局管理器,它采用曾经是 GtkWidget 虚拟函数覆盖的函数,并且主要用作在将现有窗口小部件移植到布局管理器未来时的桥梁。

我们仍在实现 GtkGridLayout 并使 GtkGrid 在内部使用它,遵循与 GtkBoxLayoutGtkBox 相同的模式。GTK 内部的其他窗口小部件将在此过程中获得自己的布局管理器,但在此期间它们可以使用 GtkCustomLayout

最后一步是实现一个基于约束的布局管理器,这将使我们能够创建复杂的、响应式的用户界面,而无需将窗口小部件打包到嵌套层次结构中。基于约束的布局值得单独撰写一篇博文,敬请期待!

GTK 4 中的条目

最近在 GTK 主分支中进行的一项较大重构是重新制作条目层次结构。这篇文章总结了已更改的内容以及我们认为这样做更好的原因。

GTK 3 中的条目

让我们首先看看 GTK 3 中的情况。

GtkEntry 是这里的基础类。它实现了 GtkEditable 接口。GtkSpinButtonGtkEntry 的子类。多年来,添加了更多内容。GtkEntry 获得了对条目完成、嵌入图标和显示进度的支持。我们添加了另一个子类,GtkSearchEntry

这种方法的一些问题立即显现出来。gtkentry.c 的代码超过 11100 行。不仅很难向这个庞大的代码库添加更多功能,而且也很难子类化它——这是创建您自己的条目的唯一方法,因为所有单行文本编辑功能都在 GtkEntry 内部。

GtkEditable 接口非常古老——它早在 GTK 2 之前就存在了。不幸的是,它作为一个接口并没有真正成功——GtkEntry 是唯一的实现,并且它以一种令人困惑的方式在内部使用接口函数。

GTK 4 中的条目

现在让我们看看 GTK 主分支中的情况。

我们做的第一件事是将 GtkEntry 的核心文本编辑功能移动到一个名为 GtkText 的新窗口小部件中。这基本上是一个没有所有额外功能的条目,例如图标、完成和进度。

通过向其添加一些更常见的功能(例如 width-chars 和 max-width-chars),我们使 GtkEditable 接口更有用,并使 GtkText 实现它。我们还添加了辅助 API,以便轻松地将 GtkEditable 实现委托给另一个对象。

“复杂”的条目窗口小部件(GtkEntryGtkSpinButtonGtkSearchEntry)现在都是复合窗口小部件,其中包含 GtkText 子项,并将其 GtkEditable 实现委托给此子项。

最后,我们添加了一个新的 GtkPasswordEntry 窗口小部件,它接管了 GtkEntry 曾经具有的相应功能,例如显示 Caps Lock 警告

或让用户查看内容。

为什么这样做更好?

此重构的主要目标之一是使在 GTK 之外创建自定义条目窗口小部件更容易。

过去,这需要子类化 GtkEntry,并导航一个复杂的 vfuncs 迷宫来覆盖。现在,您只需添加一个 GtkText 窗口小部件,将其 GtkEditable 实现委托给它,就可以轻松地拥有一个功能齐全的条目窗口小部件。

并且您可以在 GtkText 组件周围添加花哨的东西时拥有很大的灵活性。例如,我们向 gtk4-demo 添加了一个标记条目,现在可以在 GTK 本身之外轻松实现。

从 GTK 3 移植时会影响您吗?

在将代码移植到这种新的条目处理方式时,有几点需要注意。

GtkSearchEntryGtkSpinButton 不再派生自 GtkEntry。如果您看到有关从这些类之一强制转换为 GtkEntry 的运行时警告,则很可能需要切换到使用 GtkEditable API。

GtkEntry 和其他复杂条目窗口小部件不再可聚焦——焦点会转移到包含的 GtkText。但是 gtk_widget_grab_focus() 仍然有效,并且会将焦点移动到正确的位置。您不太可能受到此影响。

Caps Lock 警告功能已从 GtkEntry 中删除。如果您使用的是 visibility==FALSE 用于密码的 GtkEntry,则只需切换到 GtkPasswordEntry

如果您使用 GtkEntry 进行基本的编辑功能,并且不需要任何额外的条目功能,您应该考虑使用 GtkText。

纹理和可绘制对象

在 GTK4 中,我们一直在尝试为图像数据寻找更好的解决方案。在 GTK3 中,我们为此使用的对象是 pixbufsCairo surfaces。但是它们不再满足要求,所以现在我们有了 GdkTextureGdkPaintable

GdkTexture

GdkTextureGdkPixbuf 的替代品。为什么它更好?
首先,它更简单。API 看起来像这样

int gdk_texture_get_width (GdkTexture *texture);
int gdk_texture_get_height (GdkTexture *texture);

void gdk_texture_download (GdkTexture *texture,
                           guchar     *data,
                           gsize       stride);

因此,它是一个 2D 像素数组,如果需要,您可以下载像素。它还保证是不可变的,因此像素永远不会改变。存在许多构造函数来从文件、资源、数据或 pixbuf 创建纹理。

但是纹理和 pixbuf 之间的最大区别在于,它们不公开它们用来存储像素的内存。事实上,在调用 gdk_texture_download() 之前,该数据甚至不需要存在。
这在 GL 纹理中使用。例如,GtkGLArea 小部件使用此方法来传递数据。预计 GStreamer 也以 GL 纹理的形式传递视频。

GdkPaintable

但有时,您拥有的东西比不可变的一堆像素更复杂。例如,您可能有一个动画 GIF 或可缩放 SVG。这就是 GdkPaintable 的用武之地。
抽象地说,GdkPaintable 是一个接口,用于知道如何在任何尺寸下渲染自身的对象。受 CSS 图像的启发,它们可以选择提供 GTK 小部件可用于放置它们的固有尺寸信息。
因此,GdkPaintable 接口的核心是使可绘制对象自行渲染的函数,以及提供尺寸信息的 3 个函数

void gdk_paintable_snapshot (GdkPaintable *paintable,
                             GdkSnapshot  *snapshot,
                             double        width,
                             double        height);

int gdk_paintable_get_intrinsic_width (GdkPaintable *paintable);
int gdk_paintable_get_intrinsic_height (GdkPaintable *paintable);
double gdk_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable);

在此基础上,当其内容或大小更改时,可绘制对象可以发出“invalidate-contents”和“invalidate-size”信号。

为了使其更具体,让我们以可缩放 SVG 为例:可绘制对象的实现将不返回固有大小(那些尺寸函数的返回值 0 实现了这一点),并且每当绘制它时,它都会以给定的尺寸精确地绘制自身。
或者以动画 GIF 为例:它将提供其像素大小作为其固有大小,并将动画的当前帧缩放到给定的大小。并且每当应该显示动画的下一帧时,它将发出“invalidate-size”信号。
最后但并非最不重要的一点是,GdkTexture 实现了此接口。

我们目前正在更改 GTK3 中接受 GdkPixbuf 的所有代码,现在接受 GdkPaintable。当然,GtkImage 小部件已经更改,拖放图标或 GtkAboutDialog 也是如此。存在实验性补丁,允许应用程序向 GTK CSS 引擎提供可绘制对象。

现在,如果您将所有这些关于 GStreamer 可能提供由 GL 图像支持的纹理并创建可以上传到 CSS 的动画可绘制对象的信息放在一起,您或许可以 看到它的发展方向

GTK+ 4 中的输入法

GTK 对可加载模块的支持可以追溯到很久以前,这就是为什么 GTK 有很多代码来处理 GTypeModules 以及搜索路径等。后来,Alex 为 GVfs 重新审视了这个主题,并提出了扩展点和实现它们的 GIO 模块的概念。这是一个更好的框架,GTK 4 是我们切换到使用它的绝佳机会。

GTK+ 4 中的更改

因此,我最近花了一些时间在 GTK 中的模块支持上。这里的主要变化如下

  • 我们不再支持通用的可加载模块。此功能的少数剩余用户之一是 libcanberra,我们将考虑直接在 GTK+ 中实现“事件声音”功能,而不是依赖于模块来实现它。如果您依赖加载 GTK+ 模块,请与我们讨论其他实现您正在做的事情的方法。
  • 打印后端现在使用名为“gtk-print-backend”的扩展点定义,该扩展点需要 GtkPrintBackend 类型。现有的打印后端已转换为实现此扩展点的 GIO 模块。由于我们从未支持树外的打印后端,这不应影响其他任何人。
  • 输入法也使用一个名为“gtk-im-module”的扩展点定义,该扩展点需要 GtkIMContext 类型。我们已经删除了所有非平台 IM 模块,并将平台 IM 模块移动到 GTK+ 本身,同时还实现了扩展点。

调整现有的输入法

由于我们仍然支持树外的 IM 模块,我想使用这篇文章的剩余部分来快速概述一个用于 GTK+ 4 的树外 IM 模块必须是什么样子。

将传统的基于 GTypeModule 的 IM 模块转换为新的扩展点需要几个步骤。下面的示例代码取自 Broadway 输入法。

使用 G_DEFINE_DYNAMIC_TYPE

我们将从模块加载一个类型,而 G_DEFINE_DYNAMIC_TYPE 是定义此类类型的正确方法

G_DEFINE_DYNAMIC_TYPE (GtkIMContextBroadway,
                       gtk_im_context_broadway,
                       GTK_TYPE_IM_CONTEXT)

请注意,此宏定义了一个 gtk_im_context_broadway_register_type() 函数,我们将在下一步中使用它。

请注意,除了更常见的 class_init 之外,动态类型还应具有 class_finalize 函数,它可以是微不足道的

static void
gtk_im_context_broadway_class_finalize
               (GtkIMContextBroadwayClass *class)
{
}

实现 GIO 模块 API

为了可以作为 GIOModule 使用,模块必须实现三个函数:g_io_module_load()、g_io_module_unload() 和 g_io_module_query()(严格来说,最后一个是可选的,但我们无论如何都会在这里实现它)。

void
g_io_module_load (GIOModule *module)
{
  g_type_module_use (G_TYPE_MODULE (module));
  gtk_im_context_broadway_register_type  
                        (G_TYPE_MODULE (module));
  g_io_extension_point_implement
             (GTK_IM_MODULE_EXTENSION_POINT_NAME,
              GTK_TYPE_IM_CONTEXT_BROADWAY,
              "broadway",
              10);
 }
void
g_io_module_unload (GIOModule *module)
{
}
char **
g_io_module_query (void)
{
  char *eps[] = {
    GTK_IM_MODULE_EXTENSION_POINT_NAME,
    NULL
  };
  return g_strdupv (eps);
}

正确安装您的模块

GTK+ 仍将在 $LIBDIR/gtk-4.0/immodules/ 中查找要加载的输入法,但 GIO 只查找名称以“lib”开头的共享对象,因此请确保您遵循该约定。

调试

就是这样!

现在,GTK+ 4 应该加载您的输入法,如果您运行带有 GTK_DEBUG=modules 的 GTK+ 4 应用程序,您应该在调试输出中看到您的模块出现。

 

滚动入门

几年前,我写了一篇关于 GTK+ 3 中滚动的帖子。是时候再看看了!

常见情况

当时描述的更改的基本思想仍然相同。我们预计触摸板(或轨迹点)滚动是最常见的滚动形式之一,并且滚动条充当此功能的狭窄指示器。

正如您所见,我们更改光标以指示滚动,并且您可以自由地在所有方向上滚动。它也是动态的。

经典滚动

当然,仍然可以单击并拖动滑块进行经典滚动。

“经典”滚动的另一个方面是,您可以单击滑块外的槽,并将位置扭曲到您单击的位置,或以页面大小增量跳跃。

默认情况下,主单击扭曲,而 Shift-主单击按页面进行。我们刚刚添加了中键单击作为 Shift-主单击的替代方法,因为这是许多人习惯的常见替代方法。出于历史原因,GTK+ 有一个设置 *gtk-primary-button-warps-slider*,它切换了主单击和 Shift-主单击在此方面的作用。

典型的键盘快捷键(Page Up、Page Down、Home、End)允许您使用键盘控制滚动。

平滑滚动

GTK+ 中的滚动还有更多您可能不知道的内容。我们很久以前引入的一项功能是“缩放”或“微调”模式,它可以减慢滚动速度,以允许像素精确的定位。

要触发此模式,您可以在滑块中长按或 Shift-单击。正如您在视频中所见,一旦您将指针移动到滚动条的下方或上方,它将以相同的放松速度继续滚动,直到您松开。

作为此主题的一种变化,最近我们添加了平滑滚动的变速变体。

要触发它,您可以在滑块外的槽中进行辅助单击。滚动开始后,您可以通过将指针移近或远离滚动条来控制速度。一旦您发现它,就会让人上瘾!

自定义位置

作为最后一个特性,应用程序可以添加一个上下文菜单,该菜单在辅助点击滑块时触发,并使其滚动到重要的位置。

就这样,尽情滚动吧!

容器的秘密:尺寸分配,第 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. 这些更改的代码

容器的秘密:尺寸分配,第 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. 这些更改的代码