列表中的拖放

我最近有机会通过拖放 (DND) 实现 GtkListBox 的重新排序。这并不复杂。由于我没有看到拖放在列表框中被广泛使用,这里快速总结一下使基本功能正常工作所需的内容。

设置拖动源

有两种方法可以将 GTK+ 小部件设置为拖动源(即,单击并拖动将启动 DND 操作的位置)。您可以通过调用 gtk_drag_begin() 动态决定启动拖动。但在这里我们采用更简单的方法:我们只是静态地声明列表行应该是拖动源,并让 GTK+ 处理所有细节

handle = gtk_event_box_new ();
gtk_container_add (GTK_CONTAINER (handle),
        gtk_image_new_from_icon_name ("open-menu-symbolic", 1));
gtk_drag_source_set (handle,
        GDK_BUTTON1_MASK, entries, 1, GDK_ACTION_MOVE);

请注意,我选择在这里创建一个可见的拖动手柄,而不是允许拖动从行中的任何位置开始。它看起来像这样

这些条目告诉 GTK+ 我们想通过此源的拖动提供什么数据。在我们的例子中,我们将不提供像 text/plain 这样的标准 MIME 类型,而是创建我们自己的私有类型,并提示 GTK+ 我们不想支持拖动到其他应用程序

static GtkTargetEntry entries[] = {
   { "GTK_LIST_BOX_ROW", GTK_TARGET_SAME_APP, 0 }
};

这里一个小陷阱是,您设置为拖动源的小部件必须具有 GdkWindow。GtkButton 或 GtkEventBox(如本例中所示)将起作用。GTK4 将提供不同的 API 来创建拖动源,从而避免对窗口的需求。

有了这段代码,您已经可以拖动您的行了,但到目前为止,还没有地方可以放置它们。让我们解决这个问题。

接受放置

与拖动相反,在拖动中我们创建了一个可见的拖动手柄来提示用户支持拖放,我们希望只接受列表中的任何位置的放置。最简单的方法是使每一行都成为一个放置目标(即可能接受放置的位置)。

gtk_drag_dest_set (row,
        GTK_DEST_DEFAULT_ALL, entries, 1, GDK_ACTION_MOVE);

这些条目与我们上面讨论的相同。GTK_DEST_DEFAULT_ALL 告诉 GTK+ 为我们处理 DND 操作的所有方面,因此我们可以使此示例保持简单。

现在我们可以开始在手柄上拖动,并且可以将其放置在其他行上。但是之后什么也没有发生。我们需要做一些额外的工作才能使重新排序发生。让我们接下来做这件事。

传输数据

拖放通常用于在应用程序之间传输数据。GTK+ 为此使用一个名为 GtkSelectionData 的数据持有者对象。要发送和接收数据,我们需要连接到源和目标端的信号

g_signal_connect (handle, "drag-data-get",
        G_CALLBACK (drag_data_get), NULL);
g_signal_connect (row, "drag-data-received",
        G_CALLBACK (drag_data_received), NULL);

在源端,当 GTK+ 需要数据将其发送到放置目标时,会发出 drag-data-get 信号。在我们的例子中,该函数只会将指向源小部件的指针放入选择数据中

gtk_selection_data_set (selection_data,
        gdk_atom_intern_static_string ("GTK_LIST_BOX_ROW"),
        32,
        (const guchar *)&widget,
        sizeof (gpointer));

在目标端,当 GTK+ 将其收到的数据传递给应用程序时,会在放置目标上发出 drag-data-received。在我们的例子中,我们将从选择数据中拉出指针,并重新排列行。

handle = *(gpointer*)gtk_selection_data_get_data (selection_data);
source = gtk_widget_get_ancestor (handle, GTK_TYPE_LIST_BOX_ROW);

if (source == target)
  return;

source_list = gtk_widget_get_parent (source);
target_list = gtk_widget_get_parent (target);
position = gtk_list_box_row_get_index (GTK_LIST_BOX_ROW (target));

g_object_ref (source);
gtk_container_remove (GTK_CONTAINER (source_list), source);
gtk_list_box_insert (GTK_LIST_BOX (target_list), source, position);
g_object_unref (source);

这里唯一的技巧是,我们需要在从其父容器中删除小部件之前对其进行引用,以防止其被最终确定。

有了这个,我们就有了可重新排序的行。耶!

作为最后一步,让我们让它看起来更好。

一个漂亮的拖动图标

到目前为止,在拖动期间,您只能看到光标,这没什么帮助,也不太漂亮。预期的行为是拖动行的视觉表示。

为了实现这一点,我们连接到拖动源上的 drag-begin 信号

g_signal_connect (handle, "drag-begin",
        G_CALLBACK (drag_begin), NULL);

...并做一些额外的工作来创建一个漂亮的“拖动图标”

row = gtk_widget_get_ancestor (widget, GTK_TYPE_LIST_BOX_ROW);
gtk_widget_get_allocation (row, &alloc);
surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
                                      alloc.width, alloc.height);
cr = cairo_create (surface);
gtk_widget_draw (row, cr);

gtk_drag_set_icon_surface (context, surface);

cairo_destroy (cr);
cairo_surface_destroy (surface);

这看起来比实际情况复杂 - 我们正在创建一个大小合适的 cairo surface,将行小部件渲染到其中(信号在手柄上发出,所以我们必须找到作为祖先的行)。

不幸的是,这还没有产生完美的结果,因为列表框行通常不渲染背景或框架。为了解决这个问题,我们可以临时为行的样式上下文添加自定义样式类,并使用一些自定义 CSS 来确保我们获得背景和框架

context = gtk_widget_get_style_context (row);
gtk_style_context_add_class (context, "drag-icon");
gtk_widget_draw (row, cr);
gtk_style_context_remove_class (context, "drag-icon")

作为额外的改进,我们可以在表面上设置一个偏移量,以防止在拖动开始时出现视觉“跳跃”,方法是将此代码放在 gtk_drag_set_icon_surface() 调用之前

gtk_widget_translate_coordinates (widget, row, 0, 0, &x, &y);
cairo_surface_set_device_offset (surface, -x, -y);


瞧!

下一步

本文仅展示了通过拖放进行行重新排序的最简单的设置。许多改进是可能的,有些很容易,有些则不太容易。

一个明显的增强是允许在同一应用程序中的不同列表之间拖动。这只是在 drag_data_received() 调用中小心处理列表小部件的问题,我在这里展示的代码应该已经可以正常工作了。

另一个改进是根据哪个边缘更近,将行放置在目标行之前或之后。与此同时,您可能希望修改放置目标高亮显示,以指示放置将发生的边缘。这可以通过不同的方式完成,但所有这些都需要监听 drag-motion 事件并处理事件坐标,这不是我想要在这里深入研究的内容。

最后,在拖动期间滚动列表。如果您想将一行从顶部拖到底部,这对于长列表很重要 - 如果列表不滚动,您必须以页面增量进行此操作,这太麻烦了。通过将拖动目标移动到列表小部件本身,而不是单个行,可以最容易地实现此目的。

参考资料

GTK+ 检查器

许多 GTK+ 用户和开发人员已经听说过 GTK+ 检查器,这是一个用于检查、修改和理解 GTK+ 应用程序的工具。该检查器非常强大,允许主题设计人员即时测试 CSS 更改并放大窗口小部件以查看甚至最小的细节,让开发人员检查应用程序窗口小部件及其属性,并让用户玩(并最终破坏)应用程序。

在本文中,我们将探索 GTK+ 检查器并展示您可以用它做什么。

前言

由于检查器是一个调试工具,因此默认情况下是禁用的。要开始使用检查器,您必须首先启用它。您可以使用 DConf 编辑器轻松完成此操作

Enabling the Gtk+ Inspector with DConf Editor
使用 DConf 编辑器启用 GTK+ 检查器

或者,您可以使用终端启用它。为此,请运行以下命令

$ gsettings set org.gtk.Settings.Debug enable-inspector-keybinding true

完成!检查器现在已启用!

打开检查器

现在启用了检查器,您想运行它。检查器始终与应用程序相关联。让我们以 GNOME 日历为例

GNOME Calendar
GNOME 日历应用程序

有多种方法可以启动检查器。您可以在使用应用程序时通过键入 <Ctrl> + <Shift> + D(或 <Ctrl> + <Shift> + I 自动选择鼠标指针下的窗口小部件)打开它。或者,您可以使用环境变量GTK_DEBUG=interactive.从终端启动应用程序。

检查器将打开,您将看到以下窗口

Inspector on Calendar
GNOME 日历上的检查器窗口

这就是您需要做的全部。现在,让我们探索检查器提供的各种功能。

探索检查器

起初,大量的按钮和选项卡可能会使那些不熟悉检查应用程序艺术的人感到困惑。按顺序快速解释一下选项卡

  • 对象:公开应用程序的窗口小部件,并允许编辑属性和查看有关每个窗口小部件的详细信息。下面解释。
  • 统计信息:显示应用程序的各种统计信息。您需要使用 GOBJECT_DEBUG=instance-count 运行应用程序。
  • 资源:显示嵌入在应用程序二进制文件中的各种资源,例如自定义图标或 GtkBuilder 文件等。
  • CSS:允许实时测试 CSS。下面解释。
  • 视觉:控制应用程序的一些视觉方面,例如文本方向、深色/浅色变体、主题、缩放因子等。
  • 常规:显示有关 GTK+ 应用程序(以及它运行所在的会话)的各种信息。

让我们剖析 GTK+ 检查器的主窗口

Inspector window
检查器主窗口

检查器的这 4 个注释部分是最常用的部分。主题设计人员会想要检查 (3) 和 (4),而开发人员通常使用 (1) 和 (2)。

检查窗口小部件

对于开发人员来说,检查器通过让您更改屏幕上任何窗口小部件的属性来显示其有用性。让我们从单击第一个按钮并使用鼠标光标选择一个窗口小部件开始

Selecting widgets
使用检查器选择窗口小部件

您现在可以通过浏览 对象 > 属性 选项卡轻松更改该窗口小部件的属性。例如,您可以更改窗口小部件的可见性、标签的文本等等!

Editing a widget property
编辑窗口小部件属性

既然您知道如何检查 GTK+ 应用程序,请尝试一下并探索有多少应用程序是组织起来的。更改窗口小部件的属性,看看会发生什么。大多数时候,这是安全的,不会破坏您的 GNOME 会话或冻结您的计算机!

编辑 CSS

检查器也是设计师的强大工具。它拥有的最强大的功能之一是实时 CSS 编辑器。让我们从转到 CSS 选项卡开始

CSS Editor
检查器 CSS 编辑器视图

让我们来玩一下 CSS!粘贴以下 CSS 代码,看看会发生什么

window stack {
    background-color: orange;
}

哇!窗口变成了外星人!该 CSS 代码更改了 GtkWindow 内任何 GtkStack 小部件的背景颜色。如果您想了解有关 CSS 选择器以及 GTK+ 如何使用 CSS 进行主题化的更多信息,请参阅本文末尾的一些有用链接。

谨慎的读者可能会问:CSS 元素的层次结构是什么?如何查看哪些 CSS 元素可用?

别怕!GTK+ 检查器允许您在 对象 > CSS 节点 选项卡中轻松检查 CSS 层次结构。

CSS Nodes
CSS 节点选项卡

GTK+ 部件有文档记录的 CSS 名称。您可以浏览 GTK+ 文档,了解部件是如何组织的,以及如何使用 CSS 控制部件的各个方面。

不确定您的 CSS 更改是否完美?让我们放大部件以确保不会遗漏任何细节

Zooming widget using Magnifier
使用放大镜选项卡缩放部件

看起来不错?加入 -design 并与社区分享您出色的 CSS 代码片段!

总结

虽然本文探讨了 GTK+ Inspector 的一些最大方面,但这远非 Inspector 所有功能的详尽列表。但是,在阅读本文之后,您应该能够打开 Inspector 并自行探索更多它的强大功能。

有疑问?评论?建议?请过来留言,加入 GNOME IRC 网络中的 #gtk+ 频道,让我们知道您的想法!

实用链接

控制 GtkScrolledWindow 中的内容大小

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

GtkScrolledWindow Example
正在运行的垂直 GtkScrolledWindow

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

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

它们的作用是什么?

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

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

另一方面,最大内容大小定义了可滚动区域在内容开始滚动之前允许增长多少。

让我们看看它的实际效果

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

每当您想要限制可滚动区域的大小时,您都应该使用新属性。例如,GtkPopover 总是将其子部件缩小到其最小尺寸。以下部分举例说明如何使内容在宽度和高度方向上最多增长到 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()