我最近有机会通过拖放(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 曲面,将行部件渲染到其中(信号是在手柄上发出的,所以我们必须找到作为祖先的行)。
不幸的是,这还没有产生完美的结果,因为列表框行通常不会渲染背景或框架。为了解决这个问题,我们可以暂时向该行的样式上下文添加一个自定义样式类,并使用一些自定义 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+ 源代码树中的完整示例:listbox-dnd.c
- GTK+ DND 文档
不错的文章。
我也在 Builder 颜色选择器调色板列表中完成了列表框拖放操作,在行之间有放置高亮显示,如果人们感兴趣的话。但是代码可能不太通用,也难以理解。
感谢这篇文章。
我曾经尝试通过基本重新实现整个列表框来解决这个问题,但这要简单得多。(虽然它看起来有点不太优雅,但我更喜欢标签移动的方式进行拖放。)
我制作了一个简单的变体(用 Vala 编写),带有滚动和行边缘高亮显示:https://gist.github.com/JMoerman/6f2fa1494847ce7b7044b99787ccc769
感谢您的教程。也感谢 Jonathan Moerman 的 Vala 示例。