GtkColumnView

在我最近关于 GTK 4 中列表视图模型的系列文章中,我有一个未完成的部分,即详细介绍 GtkColumnView。这很可能是本系列中最复杂的部分。我们正在进入 GtkTreeView 的核心地带——任何旨在取代其大部分功能的组件都将是一个复杂的野兽。

概述

正如我们对 GtkListView 所做的那样,我们将从一个高层次的概述和一个图片开始。

如果你回顾一下listview图片,你会记得我们使用列表项工厂为模型中需要显示的每个项目创建一个部件。

在列视图中,我们需要为每个项目提供多个部件——每个列一个。我们通过为每个列提供自己的列表项工厂来实现这一点。每当我们需要显示一个新项目时,我们会将每个列工厂的部件组合成新项目的一行。

在内部,列视图实际上是使用列表视图来保存行的。这很好,因为我在之前的文章中解释的所有关于项目重用以及如何使用列表项工厂的内容都同样适用。

当然,有些东西不同的。例如,列视图必须组织大小分配,以便所有行中的部件对齐以形成适当的列。

注意:就像 GtkListView 一样,列视图仅为当前在视图中的模型部分创建部件,因此它共享垂直可扩展性。在水平方向上则不是这样——每一行都完全填充了每个列的部件,即使它们在左侧或右侧的视图之外。因此,如果你添加大量列,速度会变慢。

标题和其他复杂情况

列对象还包含其他数据,例如标题。列视图使用这些来显示每个列的标题。如果列视图被标记为可重新排序,则可以通过拖放标题部件来重新排列列。如果列被标记为可调整大小,则可以拖动两列之间的边界来调整其大小。

如果你注意到了,你现在可能会想知道这种调整大小与行中的单元格可以是任意部件并且希望至少有其最小大小可用于绘制其内容的事实如何结合起来。答案是我们正在使用 GTK 4 渲染机制的另一个新功能:部件可以控制如何处理其边界之外(由子部件)的绘制,通过

 gtk_widget_set_overflow (cell, GTK_OVERFLOW_HIDDEN)

排序、选择以及对树视图对等性的追求

由于我们希望在功能上与 GtkTreeview 相匹配,因此我们尚未完成。用户喜欢在树视图中做的另一件事是单击标题,按该列对内容进行排序。GtkColumnView 标题也允许这样做。

你可能还记得上一篇文章中,排序是通过将你的数据包装在 GtkSortListModel 中,并为其提供合适的排序器对象来完成的。由于我们希望根据你单击的列标题具有不同的排序顺序,因此我们为每个列提供自己的排序器,你可以使用以下代码设置:

gtk_column_view_column_set_sorter (column, sorter)

但是我们如何从你刚刚单击的列中获取正确的排序器,并将其附加到排序模型?请记住,排序模型会是我们传递给列视图的最外层模型,因为它始终是选择模型,因此列视图本身无法切换排序列表模型上的排序器。

我们想出的解决方案是让列视图提供一个在内部使用列排序器的排序器,通过:

gtk_column_view_get_sorter (view)

你可以在设置模型时将此排序器一次性提供给排序模型,然后当用户单击列标题以激活不同的列排序器时,一切都会自动更新。

这听起来很复杂,但它运行得非常好。这种方法的一个好处是,我们实际上可以一次按多个列进行排序——因为我们拥有所有可用的列排序器,并且我们知道你最后单击了哪个列。

相比之下,选择处理很容易。它的工作方式与 GtkListView 中相同。

总结

GtkColumnView 是一个复杂的部件,但我希望这一系列文章将使你更容易开始使用它。

关于列表模型

在之前的帖子中,我承诺深入了解列表模型以及 GTK 4 在这方面提供的功能。让我们首先看看GListModel接口

struct _GListModelInterface
{
  GTypeInterface g_iface;

  GType    (* get_item_type) (GListModel *list);
  guint    (* get_n_items)   (GListModel *list);
  gpointer (* get_item)      (GListModel *list,
                              guint       position);
};

实现接口的一个重要部分是,你需要发出
在需要时使用 ::items-changed 信号,使用该信号的辅助函数
GLib 为此目的而提供的

void g_list_model_items_changed (GListModel *list,
                                 guint       position,
                                 guint       removed,
                                 guint       added)

关于此接口的一些注意事项

  • 非常简单; 这使得它易于实现
  • API 是根据位置定义的,并且只处理列表成员资格的更改——跟踪项目本身的更改取决于你

列表模型动物园

GTK 提供了一个相当大的列表模型实现集合。仔细检查后,它们可以分为几个不同的组。

列表模型构建套件

第一组可以称为列表模型构建套件:允许你通过修改或组合已有的模型来构建新模型的模型。

此组中的第一个模型是GtkSliceListModel,它获取现有模型的一部分切片,给定一个偏移量和一个大小,并创建一个仅包含这些项目的新模型。如果你想在分页视图中呈现一个大列表,这将非常有用——前进和后退按钮只会将偏移量增加或减少大小。切片模型还可以用于通过随着时间的推移使切片变大来增量填充列表。GTK 在某些地方使用了这种技术。

此组中的下一个模型GtkFlattenListModel,它将几个列表模型组合成一个。由于这一切都与列表模型有关,因此要组合的模型以列表模型的形式传递给展平模型。当需要组合来自多个来源的数据时,这非常有用,例如 GTK 在打印对话框中对纸张大小所做的那样。

Paper size list in print dialog
展平的列表

请注意,原始模型继续存在于展平模型之后,它们的更新将按预期由展平列表模型传播。

有时,你的数据在一个列表模型中,但它不是完全正确的形式。在这种情况下,你可以使用 GtkMapListModel 将原始模型中的每个项目替换为不同的项目。

具体模型

GTK 及其依赖项包含许多我们自己处理的数据类型的具体模型。

这里的第一个示例是为其数据实现列表模型接口的 Pango 对象:PangoFontMap 是 PangoFontFamily 对象的列表模型,而 PangoFontFamily 是 PangoFontFace 对象的列表模型。字体选择器正在使用这些模型。

font chooser dialog
Pango 列表模型

下一个示例是 GtkDirectoryListGtkBookmarkList 对象,它们将在文件选择器中用于表示目录内容和书签。关于这些对象的一个有趣的细节是,它们都需要执行 IO 来填充其内容,并且它们异步执行此操作以避免长时间阻塞 UI。

此组中的最后一个模型有点不太具体:GtkStringList 是围绕非常常见的字符串数组的简单列表模型包装器。经常使用这种列表模型的一个示例是 GtkDropDown。这非常常见,以至于 GtkDropDown 有一个方便的构造函数,该构造函数接受一个字符串数组并为你创建 GtkStringList

GtkWidget *
    gtk_drop_down_new_from_strings (const char * const * strings)

选择

下一组模型使用新接口扩展了 GListModel:GtkSelectionModel。对于底层模型中的每个项目,GtkSelectionModel 都会维护该项目是否被选中的信息。

我们不会详细讨论该接口,因为你不太可能需要自己实现它,但最重要的点是

gboolean gtk_selection_model_is_selected (GtkSelectionModel *model)
                                          guint              pos)
GtkBitset *
       gtk_selection_model_get_selection (GtkSelectionModel *model)

因此,你可以获取单个项目的选择信息,也可以以位集的形式获取整体信息。当然,还有一个 ::selection-changed 信号,其工作方式与 GListModel 的 ::items-changed 信号非常相似。

GTK 有三种 GtkSelectionModel 实现:GtkSingleSelectionGtkMultiSelectionGtkNoSelection,它们在可以同时选择的项目数(1、多个或 0)方面有所不同。

GtkGridView 颜色演示显示了使用橡皮筋的多选

 

使用 GTK 的新列表部件时,你很可能会遇到选择模型,因为它们都期望其模型是选择模型。

重要的模型

我想提到的最后一组模型是执行列表中你期望的典型操作的模型:过滤和排序。这些模型是GtkFilterListModel GtkSortListModel。它们都使用辅助对象来实现其操作:GtkFilter 和 GtkSorter。两者都有子类来处理常见情况:排序和过滤字符串或数字,或使用回调。

在 GTK 3.99 之前,我们在这两个模型上投入了大量精力,并使它们能够增量地完成工作,以避免在处理大型模型时长时间阻塞 UI。

GtkListView 单词演示展示了对 500,000 个单词的列表进行交互式过滤

剩余内容

GTK 中还有一些列表模型实现,它们不完全适合上述任何组,例如 GtkTreeListModelGtkSelectionFilterModelGtkShortcutController。今天我就跳过这些了。

无处不在的模型

最后,我将简要列出返回列表模型的 GTK API

  • gdk_display_get_monitors
  • gtk_widget_observe_children
  • gtk_widget_observe_controllers
  • gtk_constraint_layout_observe_constraints
  • gtk_constraint_layout_observe_guides
  • gtk_file_chooser_get_files
  • gtk_drop_down_get_model
  • gtk_list_view_get_model
  • gtk_grid_view_get_model
  • gtk_column_view_get_model
  • gtk_column_view_get_columns
  • gtk_window_get_toplevels
  • gtk_assistant_get_pages
  • gtk_stack_get_pages
  • gtk_notebook_get_pages

总而言之,列表模型在 GTK 4 中无处不在。它们灵活且有趣,您应该使用它们!

GtkListView 入门

一些 GTK4 的早期采用者指出,新的列表小部件不容易学习。特别是,GtkExpression 和 GtkBuilderListItemFactory 很难理解。这并不太令人惊讶 - 一个完整的列表小部件,包含列、选择和排序以及树结构等,是一个复杂的野兽。

但是,让我们看看是否可以逐个解开,并使其更易于理解。

概述

让我们从相关组件及其交互的高级视图开始:模型、列表项工厂和列表视图。

它们是创建列表视图时发生的三个事情

view = gtk_list_view_new (model, factory);

我们使用的模型是 GListModel。这些模型始终包含 GObject,因此您必须以对象的形式提供数据。这是与 GtkTreeview 的第一个显著区别,后者直接使用包含基本类型的 GtkTreeModel。

对于一些简单的情况,GTK 提供了现成的模型,例如 GtkStringList。但通常,您必须创建自己的模型。幸运的是,GListModel 的接口比 GtkTreeModel 简单得多,因此这并不太难。

列表项工厂的职责是生成一个行小部件,并在列表视图需要时将其连接到模型中的一个项。

列表视图将创建比填充其可见区域所需的行多一些的行,以便更好地估计滚动条的大小,并且在您决定滚动视图时有一些“缓冲”。

一旦您滚动,我们就不一定需要让工厂制作更多行 —我们可以回收在另一端滚动出视图的行。

值得庆幸的是,所有这些都在幕后自动发生。您所要做的就是提供一个列表项工厂。

创建项

GTK 提供了两种不同的创建项的方法。您可以使用 GtkSignalListItemFactory 手动执行,也可以使用 GtkBuilderListItemFactory 从 ui 文件实例化行小部件。

手动方法更容易理解,所以让我们先看看。

factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", setup_listitem_cb, NULL);
g_signal_connect (factory, "bind", bind_listitem_cb, NULL);

当工厂需要创建新的行小部件时,会发出“setup”信号;当行小部件需要连接到模型中的项时,会发出“bind”信号。

这两个信号都以 GtkListItem 作为参数,这是一个包装对象,允许您访问模型项(使用 gtk_list_item_get_item())并允许您传递新的行小部件(使用 gtk_list_item_set_child())。

static void
setup_listitem_cb (GtkListItemFactory *factory,
                   GtkListItem        *list_item)
{
  GtkWidget *label = gtk_label_new ("");
  gtk_list_item_set_child (list_item, label);
}

通常,您的行会比单个标签更复杂。您可以创建复杂的小部件并将它们分组在容器中,根据需要。

static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem        *list_item)
{
  GtkWidget *label;
  MyObject *obj;

  label = gtk_list_item_get_child (list_item);
  obj = gtk_list_item_get_item (list_item);
  gtk_label_set_label (GTK_LABEL (label),
                       my_object_get_string (obj));
}

如果您的“bind”处理程序连接到项上的信号或执行其他需要清理的操作,则可以使用“unbind”信号来执行该清理。“setup”信号有一个类似的对应项,称为“teardown”。

构建器方式

我们的“setup”处理程序基本上是创建小型小部件层次结构的配方。GTK 有一种更具声明性的方法来执行此操作:GtkBuilder ui 文件。这就是 GtkBuilderListItemFactory 的工作方式:您给它一个 ui 文件,它会在需要创建行时实例化该 ui 文件。

ui = "<interface><template class="GtkListItem">...";
bytes = g_bytes_new_static (ui, strlen (ui));
gtk_builder_list_item_factory_new_from_bytes (scope, bytes);

您现在可能想知道:等一下,您正在为我的模型中的数千个项解析 xml 文件,这不是很昂贵吗?

对此有两个答案

  • 我们不是为每个项文字解析 xml;我们解析一次,存储回调序列,稍后再重放它。
  • GtkBuilder 最昂贵的部分实际上不是 xml 解析,而是对象的创建;回收行对此有帮助。

相对容易看出 ui 文件如何替换“setup”处理程序,但“bind”呢?在上面的示例中,bind 回调正在获取项的属性(MyObject:string 属性)并使用它们的值来设置小部件的属性(GtkLabel:label 属性)。换句话说,“bind”处理程序正在进行属性绑定。为了简单起见,我们在这里仅创建了一次绑定,但我们也可以使用 g_object_bind_property() 来创建持久绑定。

GtkBuilder ui 文件可以在对象之间设置属性绑定,但有一个问题:模型项在 ui 文件中不是“存在”的,它只会在稍后的“bind”时与行小部件关联。

这就是 GtkExpression 的用武之地。GtkExpression 的核心是一种描述尚未存在的对象之间绑定的方法。 在我们的例子中,我们想要实现的是

label->label = list_item->item->string

不幸的是,当它作为 ui 文件的一部分转换为 xml 时,会变得有点笨拙

<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <binding name="label">
          <lookup name="string">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

请记住,ui 模板中的类名(GtkListItem)用作引用正在实例化的对象的“this”指针。

因此,<lookup name="item">GtkListItem</lookup> 表示:已创建的列表项的“item”属性的值。<lookup name="string"> 表示:该对象的“string”属性。<binding name="label"> 表示将小部件的“label”属性设置为该属性的值。

由于表达式的工作方式,当列表项工厂将列表项上的“item”属性设置为新值时,所有这些都将被重新评估,这正是我们需要使行小部件回收工作的方式。

专家级困惑

当您嵌套事物时,GtkBuilderListItemFactory 和 GtkExpression 可能会变得非常令人困惑——列表项工厂本身可以在 ui 文件中构建,并且它们可以将其自己的 UI 文件作为属性给出,因此您最终会得到如下结构

<object class="GtkListView">
  <property name="factory">
    <object class="GtkBuilderListItemFactory">
      <property name="bytes"><![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<template class="GtkListItem">
  <property name="child">
  ...
]]>
      </property>
    </object>
  </property>
...

即使对于 GTK 专家来说,这也可能令人困惑。

我的建议是,在刚开始使用 GtkListView 时避免这种情况——您不必在 UI 文件中创建列表项工厂,并且您可以将其 UI 模板指定为资源,而不是直接嵌入它。

深入了解

我们今天在这里描述的所有内容也适用于网格视图,只需进行最小的调整。

到目前为止,我们专注于事物的视图方面。关于模型也有很多要说的。

然后是列视图,它值得单独写一篇帖子。

GTK 4 中的自定义小部件 – 操作

(这是关于 GTK 4 中自定义小部件的系列文章的第五部分。 第一部分, 第二部分, 第三部分, 第四部分)。

激活所有内容

GTK 中的许多内容都可以激活:按钮、复选框、开关、菜单项等等。通常,可以通过多种方式实现相同的任务,例如,通过 Control-C 快捷方式和上下文菜单中的项目都可以将选择复制到剪贴板。

在 GTK 内部,事情可以通过多种方式进行:可以发出信号(::activate 或 ::mnemonic-activate 或键绑定信号),可以调用回调,或者可以激活 GAction。这在 GTK 4 中并非全新的,但我们正在转向使用 GActions 作为连接操作的主要机制。

操作

操作可以以各种形式出现在 GTK 应用程序中。

首先,有全局应用程序操作,添加到 GtkApplication 或 GtkApplicationWindow(这两者都实现了 GActionGroup 接口)。这是操作首次出现在 GTK 3 中的地方,主要目的是在会话总线上导出它们以用于应用程序菜单。

我们还允许通过调用 gtk_widget_insert_action_group() 将操作与小部件关联。以这种方式添加的操作仅在它源自层次结构中小部件下方时才会被考虑激活。

在 GTK 4 中创建操作的一种新方法是通过 gtk_widget_class_install_action() 在 class_init 函数中声明操作,类似于使用 g_object_class_install_property() 声明属性的方式。以这种方式创建的操作可用于小部件的每个实例。

这是来自 GtkColorSwatch 的示例

gtk_widget_class_install_action (widget_class,
                                 "color.customize", "(dddd)",
                                 customize_color);

当 color.customize 操作被激活时,会调用 customize_color 函数。如您所见,操作可以声明它们期望参数。这是使用 GVariant 语法;您需要提供四个双精度值。

一个方便的简写允许您创建一个有状态的操作来设置您的小部件类的属性

gtk_widget_class_install_property_action (widget_class,
                                         "misc.toggle-visibility",
                                         "visibility");

这声明了一个名为 misc.toggle-visibility 的操作,它会切换布尔值 visibility 属性的值。

可操作项和菜单

声明操作只能走这么远,您还需要以某种形式将您的操作连接到 UI。对于像按钮或开关这样实现可操作接口的小部件,这就像设置 action-name 属性一样简单

gtk_actionable_set_action_name (GTK_ACTIONABLE (button),
                                "misc.toggle-visibility");

当然,您也可以在 ui 文件中执行此操作。

如果您想从菜单中激活您的操作,您可能会使用从 XML 构建的菜单模型,例如这个

<menu id="menu">
  <section>
    <item>
      <attribute name="label">Show text</attribute>
      <attribute name="action">misc.toggle-visibility</attribute>
    </item>
  </section>
</menu>

在 GTK 3 中,您将连接到 ::populate-popup 信号,以将项目添加到标签或条目的上下文菜单中。在 GTK 4 中,这是通过向小部件添加菜单模型来完成的

gtk_entry_set_extra_menu (entry, menu_model);

深入了解

要了解有关 GTK 4 中操作的更多信息,您可以阅读 GTK 文档中的操作概述

GTK 4 中的自定义小部件 – 输入

(这是关于 GTK 4 中自定义窗口部件的系列文章的第四部分。第一部分第二部分第三部分)。

事件处理程序接管

在前面的部分中,我们已经看到了一些将 GtkWidget 信号处理替换为某些辅助对象的示例。这种趋势在输入区域更加明显,在输入区域我们传统上需要处理许多信号:::button-press-event、::key-press-event、::touch-event 等。所有这些信号在 GTK 4 中都已消失,取而代之的是,你需要向窗口部件添加事件控制器,并监听它们的信号。例如,有 GtkGestureClick、GtkEventControllerKey、GtkGestureLongPress 等等。

事件控制器可以在 ui 文件中创建,但更常见的是在 init() 函数中创建。

static void click_cb (GtkGestureClick *gesture,
                      int              n_press,
                      double           x,
                      double           y)
{
  GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
  GtkWidget *widget = gtk_event_controller_get_widget (controller);

  if (x < gtk_widget_get_width (widget) / 2.0 &&
      y < gtk_widget_get_height (widget) / 2.0)
     g_print ("Red!\n");
}

...

  controller = gtk_gesture_click_new ();
  g_signal_handler_connect (controller, "pressed",
                            G_CALLBACK (click_cb), NULL);
  gtk_widget_add_controller (widget, controller);

gtk_widget_add_controller() 会获取控制器的所有权,并且 GTK 会在窗口部件被最终确定时自动清理控制器,因此无需再做任何操作。

复杂的事件处理程序

前面章节中的事件处理程序示例很简单,一次只处理单个事件。手势则更复杂一些,因为它们处理相关事件的序列,并且通常会保持状态。

更复杂的事件处理程序的示例包括诸如 DND 和键盘快捷键之类的内容。我们可能会在以后的文章中介绍其中一些。

深入了解

所有不同事件处理程序背后的统一原则是,GTK 将它从窗口系统接收到的事件从窗口部件树的根部传播到目标窗口部件,然后再返回,这种模式通常被称为捕获-冒泡。

对于键盘事件,目标窗口部件是当前焦点。对于指针事件,它是指针下悬停的窗口部件。

要了解更多关于 GTK 中输入处理的信息,请访问 GTK 文档中的输入处理概述。

展望

我们已经完成了本系列准备好的材料。如果有兴趣,它可能会在未来的某个时间继续。可能的主题包括:快捷键、动作和激活、拖放、焦点处理或可访问性。

GTK 4 中的自定义窗口部件 – 布局

(这是关于 GTK 4 中自定义窗口部件的系列文章的第三部分。第一部分第二部分)。

推荐使用窗口部件

正如我们之前所说,“一切都是一个窗口部件”。例如,我们建议你使用 GtkLabel 而不是手动渲染 pango 布局,或者使用 GtkImage 而不是手动加载和渲染 pixbuf。使用现成的窗口部件可以确保你获得所有预期的行为,例如选择处理、上下文菜单或高 dpi 支持。并且它比你自己完成所有事情要容易得多。

委托布局

snapshot() 和 measure() 函数的默认实现会自动处理子窗口部件。自定义窗口部件的主要职责是根据需要排列子窗口部件。在 GTK 3 中,这可以通过实现 size_allocate() 函数来完成。你仍然可以这样做。但是在 GTK 4 中,更方便的选择是使用布局管理器。GTK 带有许多预定义的布局管理器,例如 GtkBoxLayout、GtkCenterLayout、GtkGridLayout 等等。

布局管理器可以通过多种方式设置,最简单的方法是在你的 class_init 函数中设置布局管理器类型

gtk_widget_class_set_layout_manager_type (widget_class, 
                                          GTK_TYPE_GRID_LAYOUT);

然后,GTK 将自动实例化并使用此类型的布局管理器。

布局管理器将你的子窗口部件包装在它们自己的“布局子”对象中,这些对象可以具有影响布局的属性。这是子属性的替代品。就像子属性一样,你可以在 ui 文件中设置这些“布局属性”。

<child>
  <object class="GtkLabel">
    <property name="label">Image:</property>
    <layout>
      <property name="left-attach">0</property>
    </layout>
  </object>
</child>

添加子项

使用模板是将子项添加到窗口部件的最便捷方法。在 GTK 4 中,这适用于任何窗口部件,而不仅仅是容器。如果出于某种原因,你需要手动创建子窗口部件,最好在你的 init() 函数中完成。

void
demo_init (DemoWidget *demo)
{
  demo->label = gtk_label_new ("Image:");
  gtk_widget_set_parent (demo->label, GTK_WIDGET (demo));
}

执行此操作时,重要的是设置正确的父子关系,使你的子窗口部件成为整个窗口部件层次结构的一部分。并且此设置需要在你的 dispose() 函数中撤消。

void
demo_dispose (GObject *object)
{
  DemoWidget *demo = DEMO_WIDGET (object);

  g_clear_pointer (&demo->label, gtk_widget_unparent);

  GTK_WIDGET_CLASS (demo_widget_parent_class)->dispose (object);
}

新的可能性

布局管理器将布局任务与窗口部件机制的其余部分很好地隔离,这使得更容易尝试新的布局。

例如,GTK 4 包括 GtkConstraintLayout,它使用约束求解器根据对窗口部件大小和位置的一组约束来创建布局。

要了解有关 GTK 4 中约束的更多信息,请阅读 GtkConstraintLayout 的文档。

展望

在下一篇文章中,我们将研究 GTK 4 中的窗口部件如何处理输入。

GTK 4 中的自定义窗口部件 – 绘图

(这是关于 GTK 4 中自定义窗口部件的系列文章的第二部分。第一部分)。

老式的绘图方式

在查看窗口部件如何进行自身绘制之前,值得指出的是,如果只需要一些独立的 cairo 绘图,GtkDrawingArea 仍然是一个有效的选择。

GTK 3 和 GTK 4 之间唯一的区别是,你调用 gtk_drawing_area_set_draw_func() 来提供你的绘图函数,而不是将信号处理程序连接到 ::draw 信号。其他一切都相同:GTK 为你提供了一个 cairo 上下文,你可以直接在其中绘制。

void
draw_func (GtkDrawingArea *da,
           cairo_t        *cr,
           int             width,
           int             height,
           gpointer        data)
{
  GdkRGBA red, green, yellow, blue;
  double w, h;

  w = width / 2.0;
  h = height / 2.0;

  gdk_rgba_parse (&red, "red");
  gdk_rgba_parse (&green, "green");
  gdk_rgba_parse (&yellow, "yellow");
  gdk_rgba_parse (&blue, "blue");

  gdk_cairo_set_source_rgba (cr, &red);
  cairo_rectangle (cr, 0, 0, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &green);
  cairo_rectangle (cr, w, 0, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &yellow);
  cairo_rectangle (cr, 0, h, w, h);
  cairo_fill (cr);

  gdk_cairo_set_source_rgba (cr, &blue);
  cairo_rectangle (cr, w, h, w, h);
  cairo_fill (cr);
}

...

gtk_drawing_area_set_draw_func (area, draw, NULL, NULL);

渲染模型

GTK 3 和 GTK 4 之间的主要区别之一是,我们现在以 GL / Vulkan 而不是 cairo 为目标。作为此切换的一部分,我们已经从即时模式渲染模型转换为保留模式渲染模型。在 GTK 3 中,我们使用 cairo 命令在表面上渲染。在 GTK 4 中,我们创建一个包含渲染节点的场景图,并且这些渲染节点可以传递给渲染器,或者以某种其他方式处理,或者保存到文件中。

在窗口部件 API 中,这种变化反映在以下两者之间的差异中

gboolean (* draw) (GtkWidget *widget, cairo_t *cr)

 void (* snapshot) (GtkWidget *widget, GtkSnapshot *snapshot)

GtkSnapshot 是一个辅助对象,它将你的绘图命令转换为渲染节点,并将它们添加到场景图中。

窗口部件的 CSS 样式信息描述了如何渲染其背景、边框等等。GTK 将此转换为一系列函数调用,这些调用在窗口部件内容的渲染节点之前和之后将合适的渲染节点添加到场景图中。因此,你的窗口部件会自动符合 CSS 绘图模型,而无需任何额外的工作。

为内容提供渲染节点是窗口部件 snapshot() 实现的责任。GtkSnapshot 具有方便的 API,可以使其易于使用。例如,使用 gtk_snapshot_append_texture() 来渲染纹理。使用 gtk_snapshot_append_layout() 来渲染文本。如果要使用自定义 cairo 绘图,可以使用 gtk_snapshot_append_cairo()。

绘图窗口部件

要实现一个执行一些自定义绘图的窗口部件,你需要实现 snapshot() 函数,该函数会为你的绘图创建渲染节点

void
demo_snapshot (GtkWidget *widget, GtkSnapshot *snapshot)
{
  GdkRGBA red, green, yellow, blue;
  float w, h;

  gdk_rgba_parse (&red, "red");
  gdk_rgba_parse (&green, "green");
  gdk_rgba_parse (&yellow, "yellow");
  gdk_rgba_parse (&blue, "blue");

  w = gtk_widget_get_width (widget) / 2.0;
  h = gtk_widget_get_height (widget) / 2.0;

  gtk_snapshot_append_color (snapshot, &red,
                             &GRAPHENE_RECT_INIT(0, 0, w, h));
  gtk_snapshot_append_color (snapshot, &green,
                             &GRAPHENE_RECT_INIT(w, 0, w, h));
  gtk_snapshot_append_color (snapshot, &yellow,
                             &GRAPHENE_RECT_INIT(0, h, w, h));
  gtk_snapshot_append_color (snapshot, &blue,
                             &GRAPHENE_RECT_INIT(w, h, w, h));
}

...

widget_class->snapshot = demo_snapshot;

此示例生成四个颜色节点

如果你的绘图需要特定的大小,你也应该实现 measure() 函数

void
demo_measure (GtkWidget      *widget,
              GtkOrientation  orientation,
              int             for_size,
              int            *minimum_size,
              int            *natural_size,
              int            *minimum_baseline,
              int            *natural_baseline)
{
  *minimum_size = 100;
  *natural_size = 200;
}

...

widget_class->measure = demo_measure;

GTK 会保留你的 snapshot() 函数生成的渲染节点,并在你通过调用 gdk_widget_queue_draw() 告知它你的窗口部件需要重新绘制之前重复使用它们。

深入了解

GTK 文档提供了 GTK 绘图模型的概述,如果你有兴趣阅读有关此主题的更多信息。

展望

在下一篇文章中,我们将研究 GTK 4 中的窗口部件如何处理子窗口部件。

GTK 4 中的自定义窗口部件 – 简介

随着 GTK 4 越来越接近完成,现在是概述 GTK 4 中自定义窗口部件外观的好时机。

本系列文章将探讨编写小部件的主要方面,以及它们与 GTK 3 相比的变化。这些文章将提供一个高层次的概述;有关将应用程序移植到 GTK 4 的详细清单,请查看迁移指南

引言

我们的 API 更改的总体方向是强调委托而不是子类化。这样做的一个动机是使编写自己的小部件更容易且不易出错。因此,您将看到更多辅助对象接管核心小部件类的功能。许多小部件现在是最终类——直接从 GtkWidget 派生是预期的。

我们的 API 的另一个总体趋势是“一切都是小部件”。在 GTK 3 中,我们逐渐将复杂的小部件分解为其组成部分,首先是 CSS 节点,然后是小工具。在 GTK 4 中,例如,GtkScale 的槽和滑块是完全形成的小部件,它们可以维护自己的状态并像任何其他小部件一样接收输入。

GTK 4 过渡中的一个大输家是 GtkContainer 基类。它已经变得不那么重要了。现在,任何小部件都可以有子小部件。子属性已被布局子项及其属性取代。并且所有焦点处理都已从 GtkContainer 移至 GtkWidget。

另一个大输家是 GtkWindow。在 GTK 3 中,所有“弹出窗口”(条目完成、菜单、工具提示等)都在使用底层的 GtkWindow。在 GTK 4 中,它们中的大多数已转换为弹出窗口,并且 GtkPopover 的实现已从 GtkWindow 中分离出来。此外,许多顶层特定功能已分解为名为 GtkRoot 和 GtkNative 的单独接口。

展望

在下一篇文章中,我们将了解 GTK 4 中的小部件如何使用渲染节点进行自己的绘制。

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