关于 GTK 4 中列表的更多信息

上一篇文章介绍了 3.98.5 版本中引入的整体列表视图框架,并展示了一些示例。在本文中,我们将深入探讨一些技术领域,并研究一些相关主题。

GtkTreeView 的一个重要功能是它可以显示树。毕竟,它的名字就是由此而来。GtkListView 侧重于普通列表,并使处理这些列表更容易。特别是,GListModel API 比 GtkTreeModel 简单得多,这就是为什么自定义树模型实现相对较少的原因,但 GTK 4 已经有了大量的列表模型。

但我们仍然需要显示树,有时。这其中存在一些复杂性,但我们已经找到了一种方法来解决它。

模型

我们需要的第一要素是模型。由于 GListModel 表示一个线性项目列表,我们需要付出更多的努力来使其处理树。

GtkTreeListModel 通过按需创建新的子列表模型来添加展开项目的方法。GtkTreeListModel 中的项目是 GtkTreeListRow 的实例,它包装了模型中的实际项目,并且有一些函数(例如 gtk_tree_list_row_get_children())用于获取子模型的项目,以及 gtk_tree_list_row_get_item() 用于获取原始项目。GtkTreeListRow 具有一个 :expanded 属性,用于跟踪子项当前是否显示。

树列表模型的核心是 GtkTreeListModelCreateModelFunc,它从列表中获取一个项目,并返回一个新的列表模型,其中包含应作为给定项目子项的相同类型的项目。

以下是 GSettings 对象的树列表模型的示例。该函数枚举给定 GSettings 对象的子设置,并为其返回一个新的列表模型

static GListModel *
create_settings_model (gpointer item,
                       gpointer unused)
{
  GSettings *settings = item;
  char **schemas;
  GListStore *result;
  guint i;

  schemas = g_settings_list_children (settings);

  if (schemas == NULL || schemas[0] == NULL)
    {
      g_free (schemas);
      return NULL;
    }

  result = g_list_store_new (G_TYPE_SETTINGS);
  for (i = 0; schemas[i] != NULL; i++)
    {
      GSettings *child = g_settings_get_child (settings, schemas[i]);
      g_list_store_append (result, child);
      g_object_unref (child);
    }

  g_strfreev (schemas);

  return G_LIST_MODEL (result);
}

展开器

我们需要的下一个要素是一个小部件,它显示用户可以单击以控制 :expanded 属性的展开箭头。这由 GtkTreeExpander 小部件提供。就像 GtkTreeListRow 项目包装模型中的底层项目一样,您可以使用 GtkTreeExpander 小部件来包装用于显示项目的窗口小部件。

以下是 GSettings 示例中树形展开器在操作中的外观

完整的示例可以在此处找到。

排序

在我们离开树之前要谈的最后一个主题是排序。列表通常有多种排序方式:a-z、z-a、忽略大小写等等。列视图通过允许您将排序器与列相关联来支持此功能,用户可以通过单击列标题来激活排序器。此功能的 API 是

gtk_column_view_column_set_sorter (column, sorter)

在对树进行排序时,通常希望排序顺序应用于树中给定级别的项目,而不是跨级别,因为那样会扰乱树结构。GTK 通过 GtkTreeListRowSorter 支持此功能,它包装现有的排序器并使其尊重树结构

sorter = gtk_column_view_get_sorter (view);
tree_sorter = gtk_tree_list_row_sorter_new (sorter);
sort_model = gtk_sort_list_model_new (tree_model,           
                                      tree_sorter);
gtk_column_view_set_model (view, sort_model);

总之,树在新列表小部件中的地位有所降低,它们为该机制增加了很多复杂性,但是它们在列表视图和列视图中都得到了完全支持

组合框

单元格渲染器使用的更麻烦的领域之一是我们的单选控件:GtkComboBox。这从来都不是一个很好的选择,特别是与嵌套菜单结合使用时。因此,我们渴望在新列表视图机制上尝试 GtkComboBox 的替代品。

从设计方面来说,长期以来也希望改进组合框,正如 2015 年的这个模型所见

五年后,我们终于有了一个替代小部件。它被称为 GtkDropDown。新小部件的 API 尽可能简单,几乎所有工作都由列表模型和项目工厂机制完成。基本上,您使用 gtk_drop_down_new() 创建一个下拉列表,然后为其提供一个工厂和一个模型,就可以了。

由于大多数选择都由简单的字符串组成,因此有一种方便的方法可以从字符串数组中为您创建模型和工厂

const char * const times[] = {
  "1 minute",
  "2 minutes",
  "5 minutes",
  "20 minutes",
  NULL
};

button = drop_down_new ();
gtk_drop_down_set_from_strings (GTK_DROP_DOWN (button), times);

这种方便的 API 与 GtkComboBoxText 非常相似,GtkBuilder 支持也非常相似。您可以在 ui 文件中指定字符串列表,如下所示

<object class="GtkDropDown">
  <items>
    <item translatable="yes">Factory</item>
    <item translatable="yes">Home</item>
    <item translatable="yes">Subway</item>
  </items>
</object>

以下是一些 GtkDropDowns 的实际应用

摘要

再次强调这一点:所有这些都是全新的 API,我们很乐意听到您对哪些方面效果良好、哪些方面效果不佳以及缺少哪些方面的反馈。

GTK 4 中的可伸缩列表

GTK 4 中最后缺失的重要部分之一是用于新列表和小部件的基础架构。它刚刚被合并,并包含在 3.98.5 版本中。因此,是时候仔细看看了。

历史:树视图和列表框

自古以来(即 GTK 2),GtkTreeView 一直是 GTK 中常用的数据显示小部件。它使用模型-视图模式来保持数据与显示基础架构分离。多年来,它衍生出一个网格显示同级 (GtkIconView) 和一个选择表兄弟 (GtkComboBox),它们使用相同的基础架构(树模型和单元格渲染器)。

不幸的是,在 GtkTreeView 中使用单元格渲染器进行渲染的方法使我们陷入了分裂:小部件使用一组 vfunc 和技术进行大小分配和渲染,而单元格渲染器使用另一组。这种分裂的一个不幸后果是,很难在树视图中进行动画(因为单元格渲染器不保持状态)。另一个后果是,GTK CSS 渲染机制的大部分进展在单元格渲染器中不可用。

因此,我们真的希望使用小部件来显示列表中的数据。在 GTK 3 时代,我们为此目的引入了许多容器:用于列表的 GtkListBox 和用于网格的 GtkFlowBox。它们不使用单元格渲染器,因此上述限制不是问题。它们甚至可以使用列表模型来保存数据。但是,它们为每个数据项生成一个小部件,这严重限制了它们的可伸缩性。根据经验,GtkListBox 可以很好地处理 1000 个项目,而 GtkTreeView 可以很好地处理 100000 个项目。

克服可伸缩性限制,同时仍然使用小部件进行所有渲染,一直是我们的长期目标。

可伸缩性限制
小部件 可伸缩性
GtkIconView 100
GtkListBox 1 000
GtkTreeView 100 000
GtkListView 无限制

新基础架构

使用列表视图系列小部件,我们希望最终实现这一目标。目标是很好地处理无限数量的项目。如果您使用过 Android recycler view 之类的东西,您会认识到列表视图背后的基本思想

  • 项目的数据以模型(包含对象)的形式提供
  • 仅为可查看的项目范围创建小部件
  • 可以通过将小部件绑定到不同的项目来回收它们
  • 尽可能避免迭代模型中的所有项目,而只处理绑定到小部件的可视范围内的项目

模型

对于模型-视图架构的模型端,我们已经放弃了 GtkTreeModel,现在使用的是 GListModel。这有几个原因。其中一个是我们希望项目是具有属性的对象,因此我们可以使用属性绑定。另一个原因是 GtkTreeModel 本身的可伸缩性并不高(例如,请参阅此案例中的意外二次行为)。

GTK 4 附带了丰富的 GListModel 实现,从各种组合或修改现有模型的方法到筛选和排序。多个 GtkFilter 和 GtkSorter 类支持筛选和排序。选择也在模型端处理,使用 GtkSelectionModel 及其子类。

最后,对于我们正在处理的各种数据,有具体的模型:用于文件的 GtkDirectoryList,用于字体的 PangoFontMap。

传统上返回各种项目 GLists 的 API 已更改或补充为返回 GListModel 的 API,例如 gdk_display_get_monitors()gtk_window_get_toplevels()gtk_stack_get_pages()

工厂

由于我们正在讨论按需自动创建小部件,因此将涉及工厂。 GtkListItemFactory 是负责为模型中的项目创建小部件的对象。

此工厂有不同的实现。其中一个实现 GtkBuilderListItemFactory 使用 UI 文件作为列表项小部件的模板。这是一个典型的示例:

<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <property name="xalign">0</property>
        <property name="wrap">1</property>
        <property name="width-chars">50</property>
        <property name="max-width-chars">50</property>
        <binding name="label">
          <lookup name="summary" type="SettingsKey">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

完整的示例可以在 gtk4-demo 中找到。

另一个列表项工厂的实现 GtkSignalListItemFactory,使用回调函数来设置拆卸绑定解绑来自项的小部件。

static void
setup_listitem_cb (GtkListItemFactory *factory,
                   GtkListItem        *list_item)
{
  GtkWidget *image;

  image = gtk_image_new ();
  gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
  gtk_list_item_set_child (list_item, image);
}

static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem *list_item)
{
  GtkWidget *image;
  GAppInfo *app_info;

  image = gtk_list_item_get_child (list_item);
  app_info = gtk_list_item_get_item (list_item);
  gtk_image_set_from_gicon (GTK_IMAGE (image),
                            g_app_info_get_icon (app_info));
}

static void
activate_cb (GtkListView  *list,
             guint         position,
             gpointer      unused)
{
  GListModel *model;
  GAppInfo *app_info;

  model = gtk_list_view_get_model (list);
  app_info = g_list_model_get_item (model, position);
  g_app_info_launch (app_info, NULL, NULL, NULL);
  g_object_unref (app_info);
}

...

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);

list = gtk_list_view_new_with_factory (factory);
g_signal_connect (list, "activate", activate_cb, NULL);

model = create_application_list ();
gtk_list_view_set_model (GTK_LIST_VIEW (list), model);
g_object_unref (model);

gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);

完整的示例可以在这里找到。

表达式

为了将工厂创建的小部件绑定到你的项中的数据,我们需要一个灵活的机制来绑定属性。GObject 的 GBinding 机制朝着正确的方向发展,但是它不够灵活,无法处理需要绑定子对象或小部件(位于你的小部件层次结构深处)的属性的情况,以及当设置绑定时,这些对象甚至可能不存在的情况。

为了处理这种情况,我们引入了 GtkExpression 作为更灵活的绑定系统,它可以表达如下内容:

label = this->item->value

其中 this 是一个 GtkListItem,它有一个 item 属性(类型为 SettingsKey),我们希望将它的 value 属性绑定到 label 属性。在 GtkBuilder UI 文件中表达相同的内容看起来有点笨拙:

<binding name="label">
  <lookup name="value" type="SettingsKey">
    <lookup name="item">GtkListItem</lookup>
  </lookup>
</binding>

新的小部件

GtkListView 是一个简单的列表,没有列或标题。使用此类列表的一个示例是 GtkFontChooser。GtkListView 的一个小突破是,它可以设置为水平列表以及通常的垂直方向。

GtkGridView 将小部件放在一个重新排列的网格中,很像 GtkFlowBox 或 GtkIconView。

GtkColumnView 相当于一个完整的 GtkTreeView,具有多列和标题,以及诸如交互式调整大小和重新排序等功能。就像 GtkTreeView 具有 GtkTreeViewColumns 一样,GtkColumnView 具有 GtkColumnViewColumns 的列表。每列都有一个工厂,为每个项生成一个单元格。然后将单元格组合到该项的行中。

示例

复杂 GTK 对话框中的许多列表(尽管并非全部)已被新的列表小部件替换。例如,字体选择器现在使用 GtkListView,而 GTK 检查器中的大多数列表使用 GtkColumnView。

但是 gtk4-demo 包含许多新小部件的示例。这里有一些:

时钟示例显示了具有小部件渲染的完整灵活性的优势。

颜色示例显示了以各种方式渲染的中等大小的数据集。

设置示例表明,列视图在功能上或多或少地与 GtkTreeView 相匹配。

摘要

这篇文章介绍了新的列表小部件。还有更多我们在这里没有涉及的内容,例如树或组合框的替代品。要了解有关新 API 的更多信息,请访问 GTK 文档中的 详细介绍

我们现在已经将 listview 基础结构合并到主分支了。但这并不意味着它已经完成。但是我们认为它已经准备好进行更广泛的的使用,我们希望收到您的反馈,了解哪些有效,哪些无效以及缺少哪些内容。

并且,需要明确的是,这也并不意味着我们正在从 GTK 4 中删除树形视图和组合框——为时已晚,并且它们仍然在 GTK 内的许多地方使用。这可能是 GTK 5 的目标。

GTK 4 中的媒体

显示移动图像越来越重要。GTK 4 将使 GTK 应用程序更容易显示动画;无论是程序化的动画、webm 文件还是直播。

一切皆可绘制

在研究动画之前,值得花一些时间研究 GTK 用于可绘制内容的底层抽象。在 GTK 2 和 3 中,主要是 GdkPixbuf:加载文件,你会得到一个像素数据块(或多或少采用单一格式)。如果你想为其添加动画,可以使用 GdkPixbufAnimation,但可以公平地说它不是一个非常成功的 API。

GTK 4 引入了一个新的 API,名为 GdkPaintable,它受到 CSS Houdini 工作的启发。它非常灵活——任何你认为可以绘制的东西都可以是一个 GdkPaintable。内容可以是可调整大小的(如 svg),也可以随着时间变化(如 webm)。

通常显示图像内容的窗口小部件,如 GtkImage 或 GtkPicture,知道如何使用可绘制对象。并且过去会以某种形式产生像素数据的许多东西现在可以表示为可绘制对象:纹理图标,甚至是 小部件

如果你有更特殊的需求,任何可以在 GtkSnapshot 中捕获的内容都可以使用 gtk_snapshot_to_paintable() 转换为可绘制对象。如果你创建一个想要绘制可绘制对象的自定义窗口小部件,这非常简单。只需调用 gdk_paintable_snapshot()

实现动画

正如我之前所说,可绘制对象可以随着时间的推移更改其内容。它们所需要做的就是发出 ::contents-changed 信号,像 GtkPicture 这样的窗口小部件就会做正确的事情并更新其显示。

那么,我们从哪里获得一个会更改其内容的 GdkPaintable?我们可以使用 GTK 4 的内置 GtkMediaFile api 从文件中加载它。这是一个高层 API,类似于 GstPlayer:你传入一个 URI,你会得到一个具有 play() 函数和 pause() 函数的对象,并且可以作为可绘制对象使用。

GTK 附带了两个 GtkMediaFile 的实现,一个使用 gstreamer,另一个使用 ffmpeg。由于我们不想让它们中的任何一个成为 GTK 的硬依赖项,因此它们是可加载的模块。

你可以打开 GTK 检查器来找出正在使用的是哪一个

保持控制

GtkMediaFile API 是 gtk4-widget-factory 在其首页上使用动画 GTK 徽标进行演示的 API

如你所见,它不仅仅是一张动态图片,那里也有媒体控件——通过使用 GtkVideo 小部件可以免费获得这些控件。

超越基础知识

从文件中加载动画可能不是那么令人兴奋,所以这里有另一个 示例 更进一步。这是一个周末小项目,它结合了 GtkVideo、libportalpipewire,演示如何在 GTK 应用程序中显示视频流。

坏消息是,我们尚未找到支持粘合代码的永久位置(GstSink、GdkPaintable 和 GtkMediaStream)。它不适合 GTK,因为如上所述,我们不想依赖 gstreamer,它也不适合 gstreamer,因为 GTK 4 尚未发布。我们肯定会在不久之后解决这个问题,因为用几行代码将 gstreamer 管道转换为可绘制对象非常方便。

好消息是,代码的核心只有几行

fd = xdp_portal_open_pipewire_remote_for_camera (portal);
stream = gtk_gst_media_stream_new_for_pipewire_fd (fd, NULL);
gtk_video_set_media_stream (video, stream);

 

GTK 4 中的自定义窗口小部件 - 操作

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

激活所有内容

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

在 GTK 内部,可以通过多种方式进行操作:可以发出信号(::activate、::mnemonic-activate 或 keybinding 信号)、可以调用回调函数或可以激活 GAction。这在 GTK 4 中都不是全新的,但我们正在朝着使用 GActions 作为连接操作的主要机制的方向发展。

操作

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

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

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

在 GTK 4 中创建动作的新方法是在 `class_init` 函数中,通过 `gtk_widget_class_install_action()` 声明动作,类似于使用 `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。使用现成的窗口部件可确保你获得所有预期的行为,例如选择处理、上下文菜单或 hi-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 中的部件如何使用渲染节点进行自己的绘图。

GTK 3.98.2

当我们发布 3.98.0 时,我们承诺会提供更频繁的快照,因为剩下的 GTK 4 功能正在落地。几周后,3.98.1 和 3.98.2 快照已悄然发布。

那么,有什么新功能呢?

功能

还有一些工作要做,但一些更大的功能已经落地。

首先是我们已经完成了将 GtkPopover 重新实现为 xdg-popup surface,并将 GdkSurface API 分割为单独的 GdkToplevelGdkPopup 接口(还有一个 GdkDragSurface 接口),它们反映了 surface 的不同角色

  • 顶层是用户放置的主权窗口,可以最大化、全屏等。
  • 弹出窗口相对于父 surface 定位,并且通常会捕获输入,例如用于菜单时。

在 GTK 中,弹出窗口失去了 :relative-to 属性,因为它们现在像任何其他部件一样是常规层次结构的一部分,而 GtkWindow 失去了 :window-type 属性,因为所有 GTK_WINDOW_POPUP 的实例都已转换为弹出窗口,而窗口仅用于正确的顶层。

另一个主要功能是新的键盘快捷键基础设施。过去,GTK 有大量的 API 来实现按键绑定、助记符和加速键。在 GTK 4 中,所有这些都由事件控制器处理。GtkShortcutController 比典型的事件控制器稍微复杂一些,因为它使用统一的 API 处理所有不同类型的快捷键。

值得庆幸的是,大多数复杂性都被隐藏了。对于部件实现者来说,重要的 API 是 gtk_widget_class_add_shortcut() 的变体,用于添加按键绑定。对于应用程序,助记符和全局加速键(使用 gtk_application_set_accels_for_action())的工作方式与以前相同。此外,可以在 ui 文件中创建快捷键控制器和快捷键。

一系列较小的功能以一些 GtkTextTag 属性的形式落地,这些属性公开了新的pango 功能,例如上划线、空格的可见渲染以及对连字符的控制。现在可以通过标签在 GtkTextView 中控制它们。在条目中,它们可以通过直接添加 pango 属性来控制。

完成

当我写关于 3.98 的文章时,我说拖放重构已完成。事实证明这不太正确,自那时以来又进行了一轮 DND 工作。这些更改是由开发人员对拖放 API 的反馈提供的。为用户测试欢呼!

我们引入了单独的 GtkDropTarget 和 GtkDropTargetAsync 事件控制器,前者被简化为避免所有异步 API,这使得处理本地情况非常容易。

我们还清理了 DND 实现的内部结构,将 DND 事件分组为事件序列,以与处理正常运动事件相同的方式处理它们,并引入了 GtkDropControllerMotion,这是一个事件控制器,旨在处理 DND 操作期间的选项卡切换等操作。

最后,我们可以删除 X11 样式属性和选择 API 的残余;GtkSelectionData 和 GdkAtom 已被删除。

清理和修复

与往常一样,发生了大量的较小清理和修复。

最大的清理组发生在文件选择器中,其中许多边缘有用的 API(额外的部件、覆盖确认、:local-only、GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER 等)已被删除。为了弥补这一点,本机文件选择器的门户实现现在支持选择文件夹。

另一个大清理是 GdkEvent 现在是一个不可变的装箱类型。这主要是内部清理;对应用程序级 API 的影响很小,因为事件控制器在很大程度上取代了直接事件处理。

一个新的此类事件控制器是 GdkEventControllerFocus,它从按键事件控制器中分离出来,仅提供焦点处理。

GtkMenuButton 在从 GtkButton 子类转变为普通部件时,失去了拥有助记符的能力。此功能已恢复,具有 :use-underline 属性。

GTK 中包含的 HighContrast 和 HighContrastInverse 主题现在源自 Adwaita,以大大减轻维护负担并提高质量。现在在 gtk4-widget-factory 中尝试这些主题更容易了,因为我们添加了一个样式菜单。

新的 HighContrast 主题也已反向移植到 GTK 3。

未来展望

我们将继续发布快照,并希望获得更多开发人员对上述新 API 和功能的反馈。

以下是我们仍然希望在 GTK 4 之前集成的内容

  • 行回收列表和网格视图
  • 改进的可访问性基础设施
  • 动画 API

如果你想关注 GTK 4 的工作,请转到此处

GTK 3.98

几天前,我发布了 GTK 3.98 的 tarball。这是迈向 GTK 4 的又一步。它稍微落后于计划,并且没有完全包含我们想要包含的所有内容,但它更接近于我们希望在 GTK 4 中发布的内容。

自 3.96 快照以来已经过去了将近 9 个月,所以有很多新东西值得关注。太多了,无法全部涵盖,但这里有一些亮点

性能

GL 渲染器已经看到了持续的优化和性能改进。

在去年的西海岸 黑客节之后,通过为可见范围缓存渲染节点,GtkTextView 的滚动性能得到了极大的提升。在同一个黑客节上,文本插入符的闪烁被改为平滑动画,这与性能无关,但看起来很酷。

新年以来,一个重点是提高 CSS 机制的性能。CSS 值实现已得到优化,以尽可能避免计算值。CSS 查找现在使用布隆过滤器。并且图标加载的 IO 已被移动到线程中。

最近的大部分工作都是通过在性能黑客节之后添加的 sysprof 分析支持实现的,并且最近已得到增强以报告更多信息。要使用它,只需在环境中用 GTK_TRACE=1 启动 GTK 应用程序,并使用 sysprof 加载生成的 syscap 文件。

DND

DND 重构已完成。DND 的 GTK API 已转换为事件控制器:GtkDragSourceGtkDropTarget。为 DND 和剪贴板添加了通过文件传输门户支持文件传输的功能。数据传输的底层新基础架构已在之前详细介绍过。

GDK

GDK 向 Wayland 概念的迁移正在继续。此清理尚未 100% 完成。

子表面已被删除。GDK 现在仅支持顶层和弹出表面。客户端窗口实现也被删除了。全局位置和相关 API(例如 gdk_surface_move())不再可用。

抓取不再作为 API 公开。作为替代,可以将弹出表面配置为在外部单击时隐藏。

构建 X11 后端时,XI2 现在是强制性的,并且为了支持 IBus,删除了对 xim 输入法的支持。

Wayland 后端不再依赖 libwayland-cursor 来加载光标主题,而是按需加载单个光标。

GTK 删除

许多类已被明确设置为不可子类化,并且通过在可能的情况下使小部件直接从 GtkWidget 派生,简化了小部件层次结构。

GtkMenu、GtkMenuBar、GtkToolbar 和相关类已被删除。它们正在被 GMenu 和基于弹出窗口的变体所取代。弹出菜单现在可以执行传统的、嵌套的菜单,并且还可以显示快捷键。

上下文菜单不再使用 ::populate-popup 信号创建,而是也使用菜单模型和操作。使用诸如 gtk_widget_class_install_action() 之类的 API 在 class_init 中创建这些操作已经变得更容易。

GtkGestureMultiPress 已被重命名为 GtkGestureClick,以更清楚地说明此事件控制器的用途。

GTK 添加

我们不仅仅删除了东西。还添加了一些新东西。

为具有自己表面的小部件引入了 GtkNative 接口。它已从 GtkRoot 接口中分离出来,后者专用于没有父级的顶层小部件。

添加了一个基于约束的布局管理器。很高兴看到人们尝试一下。如果您这样做,请给我们反馈。

GtkTextView 和其他文本小部件已获得一个简单的撤消堆栈,该堆栈可以与 Ctrl-Z 一起使用。

Emoji 选择器小部件已公开。

未来展望

在 3.98 之后,我计划进行更频繁的快照,因为其余的未完成项正在着陆。您在问那些项目是什么吗?

以下是我们希望在 GTK 4 之前整合的内容

– 键盘快捷键的事件控制器
– 可移动的弹出窗口
– 行回收列表和网格视图
– 改进的辅助功能基础架构
– 动画 API