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

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

激活所有功能

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

在 GTK 内部,有多种方式可以进行:可能会发出信号(::activate,或 ::mnemonic-activate,或快捷键信号),可能会调用回调,或者可能会激活 GAction。这些在 GTK 4 中都不是全新的,但我们正在朝着使用 GAction 作为连接操作的主要机制发展。

操作

操作可以在 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 中,我们创建一个包含渲染节点的场景图,这些渲染节点可以传递给渲染器,或者以其他方式处理,或者保存到文件中。

在 widget API 中,此更改反映在以下两者之间的差异中:

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

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

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

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

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

一个绘图 widget

要实现一个进行一些自定义绘图的 widget,您需要实现 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() 告知它您的 widget 需要重新绘制之前重复使用它们。

深入了解

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

展望

在下一篇文章中,我们将了解 GTK 4 中的 widget 如何处理子 widget。

GTK 4 中的自定义 widget – 简介

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

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

简介

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

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

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

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

展望

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

控制 GtkScrolledWindow 中的内容大小

GtkScrolledWindow widget 是 Gtk+ 应用程序开发人员的老朋友;它的目的是通过使用滚动条来使大型 widget 适合小空间。

GtkScrolledWindow Example
正在运行的垂直 GtkScrolledWindow

自 Gtk+ 3.0 以来,GtkScrolledWindow 能够通过 GtkScrolledWindow:min-content-widthGtkScrolledWindow:min-content-height 属性及其相关函数来设置最小内容大小(宽度和高度)。

从下一个稳定版本开始,Gtk+ 也将提供这些属性的最大尺寸对应项。

它们的作用是什么?

顾名思义,最小尺寸属性定义了可滚动区域将具有的最小尺寸(无论是宽度还是高度)——即使其子项没有完全填充可用空间。

scrolledwindow min-content-height
即使子 widget 没有填充可用空间,也会分配滚动窗口。

另一方面,最大内容尺寸定义了可滚动区域在内容开始滚动之前允许增长的程度。

让我们看看它的实际效果

scroll animation
演示最小和最大内容尺寸的示例。滚动窗口永远不会小于 110 像素,也永远不会高于 250 像素。
在哪里以及如何使用它们

每当您想要限制可滚动区域的大小时,您都希望使用新的属性。例如,GtkPopover 总是将其子 widget 缩小到它们的最小尺寸。以下部分举例说明如何使内容最多增长到 300 像素(宽度和高度方向)。

<template>
  <object class="GtkPopover">
    <child>
      <object class="GtkScrolledWindow">
        <property name="visible">True</property>
        <property name="max-content-width">300</property>
        <property name="max-content-height">300</property>
      </object>
    </child>
  </object>
</template>

或者,如果您想以编程方式实现相同的目的,可以调用 gtk_scrolled_window_set_max_content_width()gtk_scrolled_window_set_max_content_height()