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 的新列表部件时,您很可能会遇到选择模型,因为它们都期望其模型是选择模型。

重要的模型

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

在 GTK 3.99 的准备阶段,我们在这两个模型上花费了大量精力,并使它们能够增量地完成工作,以避免在处理大型模型时长时间阻塞 UI。

GtkListView 单词演示展示了对 50 万个单词列表的交互式过滤

剩余的模型

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 的第一个显著区别,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 作为参数,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”呢?在上面的示例中,绑定回调获取了项的属性(MyObject:string 属性)并使用它们的值设置了部件的属性(GtkLabel:label 属性)。换句话说,“bind”处理程序正在执行属性绑定。为简单起见,我们在这里仅创建了一次性绑定,但是我们也可以使用 g_object_bind_property() 来创建持久绑定。

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

这就是 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 中自定义部件的系列文章的第五部分。第 1 部分第 2 部分第 3 部分第 4 部分)。

激活所有内容

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

在 GTK 内部,有多种方式可以触发操作:可以发出信号(::activate 或 ::mnemonic-activate,或键盘快捷键信号),可以调用回调函数,或者可以激活 GAction。这些在 GTK 4 中都不是全新的概念,但我们正在转向使用 GActions 作为连接操作的主要机制。

操作(Actions)

操作可以以各种形式出现在 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 属性的值。

可操作对象和菜单(Actionables and Menus)

仅声明操作是不够的,你还需要以某种形式将操作连接到 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 中的控件如何使用渲染节点进行自己的绘制。

GTK4 中的数据传输

桌面应用程序之间用户启动的数据传输的传统方法是剪贴板或拖放。自最初以来,GTK+ 就一直支持这些方法,但在 GTK3 之前,我们用于此类数据传输的 API 是对相应 X11 API 的薄弱伪装:选择、属性和原子。这并不太令人惊讶,因为整个 GDK API 都是以 X11 为模型的。不幸的是,该实现包括诸如增量传输和字符串格式转换之类的恐怖之处。

对于 GTK4,我们正在抛弃这些东西,因为我们正在将 GDK 中的内容移动到更接近 Wayland API 的位置。数据传输是最迫切需要这种现代化的领域之一。值得庆幸的是,它在这一点上几乎完成,因此值得一看已更改的内容以及未来的工作方式。

概念

如果您的应用程序要发送的数据不是字符串,则它可能是一些对象,例如 GFile、GdkTexture 或 GdkRGBA。接收端的应用程序可能不使用 GTK 或 GLib,因此不会知道这些类型。即使它知道,也没有办法一次性地将对象从一个进程传输到另一个进程。

在核心层面,数据传输的工作方式是从源应用程序发送一个文件描述符,目标应用程序从中读取字节流。剪贴板和 DND 的协议使用 mime 类型(如 text/uri-list、image/png 或 application/x-color)来标识字节流的格式。

发送对象涉及协商双方都支持的数据格式,将源端的对象序列化为该格式的字节流,传输数据,并在目标端反序列化对象。

本地情况

在继续讨论具体的 API 之前,值得花一点时间考虑另一种常见的数据传输情况:在单个应用程序内部。使用相同的剪贴板和 DND 机制将数据从应用程序的一侧发送到另一侧是很常见的。由于在这种情况下我们没有跨越进程边界,因此我们可以避免字节流以及相关的序列化和反序列化开销,只需传递对该对象的引用即可传输数据。

API

我们在上一节中提到的对象由 GType(如 G_TYPE_FILE 或 GDK_TYPE_TEXTURE)描述,而如上所述,Wayland 和 X11 协议中的数据交换格式由 mime 类型描述。

我们引入的第一个处理这些类型的 API 是 GdkContentFormats 对象。它可以包含格式列表,这些格式可以是 GType 或 mime 类型。我们使用 GdkContentFormats 对象来描述应用程序可以提供数据的格式,以及应用程序可以接收数据的格式。

您可能想知道为什么我们将 GType 和 mime 类型混合在同一个对象中。答案是,我们希望使用相同的 API 处理跨进程和本地情况。虽然我们需要为跨进程情况匹配 mime 类型,但我们需要为本地情况匹配 GType。

转换

我们仍然需要一种方法来关联 GType 和 mime 类型,以便我们可以将它们相互转换。这由 GdkContentSerializer 和 GdkContentDeserializer API 处理。这些本质上是转换函数的注册表:GdkContentSerializer 知道如何将 GType 转换为 mime 类型,而 GdkContentDeserializer 处理另一个方向。

GDK 具有常用类型的预定义转换,但该系统可以使用 gdk_content_register_serializer 和 gdk_content_register_deserializer 进行扩展。

内容

现在我们知道如何描述格式并在它们之间进行转换,但是为了将所有这些放在一起,我们仍然需要一个方便的 API,它在一方获取一个对象,并在另一方提供一个字节流。为此,我们添加了 GdkContentProvider API。

GdkContentProvider 允许您将一个对象与输入端的格式描述相结合,并在输出端提供一个异步写入器 API,该 API 可以连接到我们想要通过其发送数据的文件描述符。

典型的 content provider 是这样创建的:

gdk_content_provider_new_for_value (gvalue)
gdk_content_provider_new_for_bytes (gbytes, mimetype)

它采用的 GValue 包含对象及其类型的信息,因此如果我们以 GBytes(本质上只是一段内存)的形式提供对象,则不需要额外的类型信息,我们需要单独提供类型信息。

剪贴板

GTK3 有一个 GtkClipboard 对象,它提供了复制/粘贴操作的实现。在 GTK 中拥有此对象并非理想之举,因为它需要在 GTK 支持的平台上具有不同的实现。因此,GTK4 将该对象移动到 GDK,并因此将其重命名为 GdkClipboard。它也已移植到上述新的数据传输 API。要将数据放在 GTK4 的剪贴板上,您可以使用“set” API 之一。

gdk_clipboard_set_content()
gdk_clipboard_set_value()
gdk_clipboard_set_text()

最终,所有这些函数都会将一个 GdkContentProvider 与剪贴板关联起来。

要在 GTK4 中从剪贴板读取数据,您可以使用异步的 ‘read’ API 之一。

gdk_clipboard_read_async()
gdk_clipboard_read_value_async()
gdk_clipboard_read_text_async()

拖放

GTK3 的拖放 API 涉及监听 GtkWidget 上的多个信号,并为拖动源和目标调用一些特殊的设置函数。它很灵活,但通常被认为令人困惑,我们不会在这里详细描述它。

在 GTK4 中,拖放 API 已围绕内容提供程序和事件控制器的概念进行了重组。要启动拖放操作,您需要创建一个 GtkDragSource 事件控制器来响应拖动手势(您也可以通过直接调用 gdk_drag_begin 来启动“一次性”拖放操作),并为要传输的数据提供一个 GdkContentProvider。要接收拖放操作,您需要创建一个 GtkDropTarget 事件控制器,并在其发出 ::drop-done 信号时调用异步读取方法。

gdk_drop_read_value_async()
gdk_drop_read_text_async()

约束布局

什么是约束

最基本来说,约束是两个值之间的关系。这种关系
可以被描述为线性方程

target.attribute = source.attribute × multiplier + constant

例如,这个

可以被描述为

blue.start = red.end × 1.0 + 8.0

  • 目标 “blue” 的属性 “start”,该属性将由约束设置;这是等式的左侧
  • 等式左侧和右侧之间的关系,在本例中为相等;关系也可以是大于或等于
    小于或等于
  • “red” 的属性 “end”,该属性将由约束读取;这是等式的右侧
  • 应用于源属性的乘数,“1.0”
  • 常量 “8.0”,添加到属性的偏移量

约束布局是一系列类似于上述的等式,描述了 UI 各个部分之间的所有关系。

重要的是要注意,关系不是赋值,而是相等(或不等):等式的两边都将以满足约束的方式求解;这意味着约束列表可以重新排列;例如,上面的示例可以重写为

red.end = blue.start × 1.0 - 8.0

一般来说,为了方便和可读性,您应该按阅读顺序排列约束,从前导边缘到后导边缘,从上到下。您还应该优先使用整数作为乘数,并使用正数作为常量。

求解布局

线性方程组可以有一个解、多个解,甚至没有解。此外,出于性能原因,您并不想每次都重新计算所有解。

早在 1998 年,Greg J. Badros 和 Alan Borning 发表了用于求解线性算术约束的 Cassowary 算法,以及其在 C++、Smalltalk 和 Java 中的实现。Cassowary 算法尝试通过找到最优解来求解线性方程组;此外,它还以增量方式进行,这使其对于用户界面非常有用。

在过去的十年中,各种平台和工具包开始提供基于约束的布局管理器,其中大多数都使用了 Cassowary 算法。第一个是 Apple 的 AutoLayout,于 2011 年发布;2016 年,Google 在 Android SDK 中添加了 ConstraintLayout。

2016 年,Endless 在一个名为 Emeus 的库中为 GTK 3 实现了约束布局。从这项工作开始,GTK 4 现在为应用程序和小部件开发人员提供了一个可用的 GtkConstraintLayout 布局管理器。

实现约束求解器的机制是 GTK 私有的,但公共 API 提供了一个布局管理器,您可以将其分配给您的 GtkWidget 类,以及一个不可变的 GtkConstraint 对象,该对象描述您希望添加到布局的每个约束,将两个小部件绑定在一起。

引导约束

约束使用小部件作为源和目标,但在某些情况下,您希望将小部件属性绑定到屏幕上不绘制任何内容的矩形区域。您可以向布局添加一个虚拟小部件,然后将其不透明度设置为 0 以避免渲染它,但这会给场景增加不必要的开销。相反,GTK 提供了 GtkConstraintGuide,该对象的唯一工作是为布局做出贡献。

An example of the guide UI element

在上面的示例中,只有标记为“子项 1”和“子项 2”的小部件将可见,而引导线将是一个空白区域。

引导线具有最小、自然(或首选)和最大尺寸。它们都是约束,这意味着您可以将引导线不仅用作对齐的辅助工具,还可以用作布局中可以增长和收缩的灵活空间。

在布局中描述约束

约束可以以编程方式添加,但像 GTK 中的许多内容一样,为了方便起见,它们也可以在 GtkBuilder UI 文件中描述。如果向 UI 文件添加 GtkConstraintLayout,则可以在特殊的“<constraints>”元素中列出约束和引导线。

  <object class="GtkConstraintLayout">
    <constraints>
      <constraint target="button1" target-attribute="width"
                     relation="eq"
                     source="button2" source-attribute="width" />
      <constraint target="button2" target-attribute="start"
                     relation="eq"
                     source="button1" source-attribute="end"
                     constant="12" />
      <constraint target="button1" target-attribute="start"
                     relation="eq"
                     source="super" source-attribute="start"
                     constant="12" />
      <constraint target="button2" target-attribute="end"
                     relation="eq"
                     source="super" source-attribute="end"
                     constant="-12"/>
    </constraints>
  </object>

您还可以使用“<guide>”自定义元素描述引导线。

  <constraints>
    <guide min-width="100" max-width="500" />
  </constraints>

可视化格式语言

除了 XML 之外,约束还可以使用一种称为“可视化格式语言”的紧凑语法来描述。VFL 描述是面向行和列的:您可以使用一条在视觉上类似于您正在实现的布局的行来描述布局中的每一行和每一列,例如

|-[findButton]-[findEntry(<=250)]-[findNext][findPrev]-|

描述了一个水平布局,其中 findButton 小部件与布局管理器的前导边缘之间由一些默认空间分隔,然后是相同的默认空间;然后是 findEntry 小部件,该小部件的宽度最多为 250 像素。在 findEntry 小部件之后,我们再次获得一些默认空间,然后是两个小部件 findNextfindPrev,它们彼此紧挨着;最后,这两个小部件与布局管理器的后导边缘之间由默认数量的空间分隔。

使用 VFL 表示法,GtkConstraintLayout 将创建所有必需的约束,而无需手动描述它们。

重要的是要注意,VFL 不能描述所有可能的约束;在某些情况下,您需要使用 GtkConstraint 的 API 创建它们。

约束布局的限制

约束布局非常灵活,因为它们可以实现任何布局策略。这种灵活性是有代价的。

  • 您的布局可能有太多解决方案,这会使其模糊且不稳定;如果您的布局非常复杂,这可能会有问题。
  • 您的布局可能没有任何解决方案。这通常发生在您没有使用足够约束时;一个经验法则是每个目标每个维度至少使用两个约束,因为所有小部件都应具有定义的位置和大小。
  • 相同的布局可以通过不同的约束系列来描述;在某些情况下,几乎不可能说哪种方法更好,这意味着您将不得不进行实验,尤其是在涉及到动态添加或删除 UI 元素或允许用户交互(例如拖动 UI 元素)的布局时。

此外,在更大规模上,本地的、临时的布局管理器可能比基于约束的布局管理器更有效;如果您有一个可以增长到未知行数的列表框,则不应将其替换为约束布局,除非您预先衡量了性能影响。

演示

当然,自从我们添加了这个新的 API 之后,我们还在 GTK 演示应用程序中添加了一些演示。

A constraints demo
约束演示窗口,作为 GTK 演示应用程序的一部分。

以及一个完整的约束编辑器演示。

The GTK constraints editor demo
GTK 约束编辑器演示应用程序的屏幕截图,显示了左侧边栏中的 UI 元素、引导线和约束列表,以及窗口右侧的结果。

更多信息