容器的秘密

我最近花了一些时间来追踪 GTK+ 容器和绘制方面的问题。以下是我发现的内容。

CSS 绘制

在 GTK+ 3.22 中,大多数(如果不是全部)容器都支持完整的 CSS 绘制模型,具有多层背景和边框。例如,GtkFrame 现在就是这样绘制其框架的。但是,通常只排列其子项的容器(如 GtkBox)可以绘制背景和边框。可能性是无限的!

例如,我们可以使用 GtkBox 将一个框架围绕列表框和一个标签,使标签在视觉上看起来像是列表的一部分。您甚至可以使用一些 CSS 使其色彩丰富且有趣,例如

box.frame {
 border: 5px solid magenta;
}

分配和调整大小

传统上,GTK+ 中的大多数容器都不进行自己的绘制,而只是排列其子项,因此当它们的大小发生变化时,没有真正需要进行完全重绘的必要——重绘子项就足够了。这就是 gtk_container_set_reallocate_redraws() 的作用。在 GTK+ 3 中,它的默认值为 FALSE,因为我们不想在分配更改时冒添加过多重绘的风险。

您可以看到这是怎么回事:如果我使用删除按钮从配料列表中删除黄油和盐,则列表的分配,以及围绕它的框的分配将会缩小,并且我们会遇到重绘问题。

解决方案

如果您计划使纯布局容器绘制背景或边框,请确保为正确的窗口小部件(在这种情况下,是趣味框的级)将 reallocate-redraws 设置为 TRUE。

gtk_container_reallocate_redraws (GTK_CONTAINER (parent), TRUE);

请注意,gtk_container_reallocate_redraws() 在 GTK+ 3.22 中已弃用,因为我们将在 GTK+ 4 中删除它并自动执行正确的操作。但这不应阻止您使用它来修复此问题。

另一种(也许更好)的替代方法是使用旨在绘制边框的容器,例如 GtkFrame。

日志记录及更多

前段时间,GLib 获得了一个新的“结构化日志记录”工具。与此同时,它还获得了对将日志写入 systemd 日志的支持。显然,GLib 中的日志记录变得更加复杂,并且可能会有点令人困惑。

本文试图澄清一些问题。

结构化与否

传统的 GLib 日志记录工具是 g_message()g_debug() 等宏,它们最终会调用 g_log() 函数,然后该函数使用使用 g_log_set_handler() 设置的日志处理程序来执行实际的写入。您可以将任何您喜欢的信息放入日志中,但必须全部格式化为单个字符串,即消息。

g_debug ("You have %d eggs", 12 + 2);

g_log (G_LOG_DOMAIN,
       G_LOG_LEVEL_DEBUG,
       "You have %d eggs", 12 + 2);

使用新的结构化日志记录工具,您可以调用 g_log_structured(),然后该函数使用日志写入器函数来执行写入。到目前为止,这与旧的日志记录工具非常相似。结构化日志的优点是您可以将多个字段放入日志中,而无需将其全部格式化为字符串。相反,您传递一个日志字段数组,这些字段是键值对。

g_log_structured (G_LOG_DOMAIN,
                  G_LOG_LEVEL_DEBUG,
                  "CODE_FILE", "mysource.c",
                  "CODE_LINE", 312,
                  "MESSSAGE_ID", "06d4df59e6c24647bfe69d2c27ef0b4e",
                  "MESSAGE", "You have %d eggs", 12 + 2);

这里的 CODE_FILECODE_LINEMESSAGE_ID 只是“标准”字段的示例。您也可以发明自己的字段。请注意,您仍然可以对 MESSAGE 字段使用 printf 样式格式。

因此,GLib 现在有两个单独的日志记录工具。为了使事情变得更有趣,我们允许您重定向 g_message()g_debug() 等包装器宏以在底层使用 g_log_structured() 而不是 g_log()。为此,请在包含 glib.h 之前定义 G_LOG_USE_STRUCTURED 宏。

这有什么用?首先,它可以省去您替换所有 g_debug() 的麻烦,并且仍然可以让您利用结构化日志记录的某些优势——以这种方式使用时,传统宏将使用单独的字段来表示日志域、代码文件和行以及其他一些字段,这有助于在生成的日志中进行过滤和搜索,尤其是在 systemd 日志中。

另一个优点是您可以使用单个后端(日志写入器函数)来控制新旧日志调用的最终位置。

我的日志都去哪儿了?

结构化日志记录通常与 systemd 日志相关联。因此,人们期望 g_log_structured() 的输出进入日志并不奇怪。对于服务或您从桌面图标启动应用程序时,这确实非常有用。但是,如果您从终端运行它,您可能希望在那里看到它的输出。

为了满足这些相互竞争的需求,GLib 默认日志写入器函数尝试变得智能。如果它检测到 stderr 被重定向到 journald 套接字,则它将其结构化输出写入日志。否则,它会格式化消息并将其写入 stderr

GNOME Shell 和 DBus 都会安排在启动应用程序或服务时将 stderr 重定向到日志。将 stderr 显式重定向到日志的一种方法是在 systemd-cat 下运行您的应用程序。

systemd-cat my-app-that-logs

如果您确定希望日志始终进入日志,您可以告诉 GLib 使用执行此操作的日志写入器

g_log_set_writer_func (g_log_writer_journald, NULL, NULL)

超越默认值

即使 GLib 默认提供的日志写入器函数应该可以满足许多需求,您可能仍然需要编写自己的函数。在这种情况下,GLib 有许多有用的函数可以为您提供帮助,例如 g_log_writer_format_fields()g_log_writer_is_journald()g_log_writer_supports_color()

快乐地记录日志!

参考

GSettings 的第一步

我们有时会在 #gtk+ 中收到有关 GSettings 的问题,以及是否在“简单”情况下使用此 API 是一个好主意。我认为答案非常明确,是的,本文试图解释原因。

好处

GSettings 的优点之一是它是一个高级 API,具有适用于各种本机配置系统的后端。因此,如果您将来将应用程序移植到 OS X 或 Windows,您的应用程序将自动使用预期的平台 API 来存储其设置(Windows 上的注册表和 OS X 上的 plists)。

即使您的应用程序永远不会移植到这些平台,Linux 上使用的 dconf 后端也具有强大的功能,例如配置文件和锁定,使系统管理员可以配置您的应用程序,而无需您担心它。

GSettings 的文档不幸地使它看起来比实际情况更复杂,因为它并没有真正尝试隐藏可供您使用的强大功能(配置文件、供应商覆盖、已翻译的默认值、复杂类型、绑定等)。

因此,这是在简单情况下使用 GSettings 的第一步指南。

开始入门

让我们从最简单的设置开始:布尔值。

要克服的最大障碍是 GSettings 坚持拥有一个架构,该架构定义每个键的数据类型和默认值。

<schemalist>
  <schema path="/org/gnome/recipes/"       
         id="org.gnome.Recipes">
    <key type="b" name="use-metric">
      <default>true</default>
      <summary>Prefer metric units</summary>
      <description>
        This key determines whether values
        such as temperatures or weights will
        be displayed in metric units.
      </description>
    </key>
  </schema>
</schemalist>

需要安装架构(这样做的好处是像 dconf-editor 这样的工具可以使用架构信息)。如果您使用 autotools,则宏支持此操作。只需添加

GLIB_GSETTINGS

到您的 configure.ac,以及

gsettings_SCHEMAS = org.gnome.Recipes.gschema.xml
@GSETTINGS_RULES@

到 Makefile.am。使用 meson 的设置类似。

现在我们已经定义了我们的键,我们可以像这样获取它的值

s = g_settings_new ("org.gnome.Recipes");
if (g_settings_get_boolean (s, "use-metric"))
  g_print ("Using metric units");

我们可以像这样设置它

s = g_settings_new ("org.gnome.Recipes");g_settings_set_boolean (s, "use-metric", TRUE);

对整数、浮点数、字符串等其他基本类型使用 GSettings 非常相似。只需使用适当的 getter 和 setter 即可。

您可能想知道我们在这里创建的设置对象。在需要时创建它们即可。如果愿意,也可以创建一个全局单例,但除非您想监视设置的更改,否则没有必要这样做。

下一步:复杂类型

除了基本类型之外,您还可以使用 GVariant 类型系统的全部功能来存储复杂类型。例如,如果需要存储有关平面中圆形的信息,则可以将其存储为类型为 (ddd) 的三元组,分别存储中心的 x、y 坐标和半径。

要在代码中处理具有复杂类型的设置,请使用 g_settings_get() 和 g_settings_set(),它们以 GVariant 的形式返回和接收值。

下一步:可重定位的模式

如果您的应用程序使用帐户,您可能需要查看可重定位的模式。当您需要同一配置的多个实例,并将其单独存储时,就需要可重定位的模式。一个典型的例子是帐户:您的应用程序允许创建多个帐户,并且每个帐户都具有与之关联的相同类型的配置信息。

GSettings 通过省略模式中的路径来处理此问题

<schemalist>
  <schema id="org.gnome.Recipes.User">
    <key type="b" name="use-metric">
      <default>true</default>
    </key>
  </schema>
</schemalist>

相反,在创建 GSettings 对象时,我们需要指定一个路径

s = g_settings_new_with_path ("org.gnome.Recipes.User",
                              "/org/gnome/Recipes/Account/mclasen");
if (g_settings_get_boolean (s, "use-metric"))
  g_print ("User mclasen is using metric units");

如何设计模式来将您的帐户映射到唯一路径,这取决于您。

绊脚石

使用 GSettings 时需要注意一些事项。其中之一是 GLib 需要能够在运行时找到已编译的模式。当在未安装的情况下从构建目录运行应用程序时,这可能会成为一个问题。要解决这种情况,您可以设置 GSETTINGS_SCHEMA_DIR 环境变量来告诉 GLib 在哪里可以找到已编译的模式。

GSETTINGS_SCHEMA_DIR=build/data ./build/src/gnome-recipes

另一个绊脚石是 GSettings 以序列化的 GVariant 形式读取 XML 中的默认值。对于常见的字符串情况,这可能会有点令人惊讶,因为它意味着我们需要在字符串周围加上引号

<default>'celsius'</default>

但这只是小问题,一旦您了解了它们,就可以轻松避免。

列表中的拖放

我最近有机会通过拖放(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")

作为额外的改进,我们可以在 surface 上设置偏移量,以防止在拖动开始时出现视觉“跳跃”,方法是在 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+ – 23

在过去的一周中,GTK+ 的主分支提交了 146 次,增加了 10020 行代码,删除了 16435 行代码。

计划和状态
  • Benjamin Otte 致力于阐明 GTK+ 和 gsk 中使用的各种坐标系之间的关系。
  • Benjamin 还在整个树中进行更改,为使所有小部件无窗口做好准备。
  • Emmanule Bassi 正在为 CSS 渲染开发更精细的 gsk 渲染节点。
  • GTK+ 路线图可在 wiki 上找到。
值得注意的更改
  • Benjamin Otte 向 GTK+ 检查器添加了一个帧记录器。有关更多信息,请参见此帖子
  • Timm Bäder 和 Benjamin 将更多的小部件转换为使用带有 gsk 渲染节点的间接渲染。
  • Matthias Clasen 使 GtkTextAttributes 结构私有化,从而可以进行进一步的清理并删除已弃用的 GdkColor 类型。
  • Benjamin 删除了视觉效果。
已修复的错误
  • 773885 按下 Escape 键时弹出 GtkPopover
  • 773299确保 GTK+-4.x 在 Windows 上构建和工作
  • 773274[Wayland] gdk_wayland_window_attach_image() 下的崩溃
  • 768081为 GDK-Win32 启用 HiDPI 支持
  • 773857 – X11: 添加对 gdk_window_fullscreen_on_monitor 的支持
  • 773799GtkLevelBar 在离散模式下不更新块
  • 773954gtkstylecontext: 澄清 getter 的内存分配行为
  • 773903gtk.h 和 gtktextiter.h 包含私有的 gtktextattributes.h
参与其中

有兴趣参与 GTK+ 的开发吗?请查看适合新手的错误列表,并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

本周 GTK+ – 8

在上周,GTK+ 的主分支有 23 次提交,添加了 1154 行代码,删除了 121 行代码。

计划和状态
  • Matthias 继续处理 GLibGTK+ 的门户分支。
  • Emmanuele 继续在 gsk-renderer 分支上使 gsk 渲染小部件。
  • Philip Withnall 正在处理一个补丁系列,以使 GLib 支持将结构化日志记录到 systemd 日志 这里
值得注意的更改
  • Carlos Garnacho 提高了 Wayland 剪贴板处理与旧版 X 客户端的兼容性。
  • Matthias 使在打印对话框中隐藏预览按钮成为可能。
  • Eric Koegel 为 GtkApplication 添加了对 Xfce 会话管理器的支持
  • Georges Basile Stavracas Neto 为 GTK+ CSS 机制添加了对 background-blend-mode 的支持。
  • 在 GLib 中,Matthias 添加了一个新的 gio 实用程序,它将各种 gvfs 命令行工具的功能组合成一个。
已修复的错误
  • Bug 767965改进检测远程文件系统的启发式方法
  • Bug 768184headerbar:如果标题小部件被隐藏,则不抛出警告
  • Bug 768082 – C从 Wayland 复制到 NEdit 无效
  • Bug 768177 在 PRIMARY 请求超时后,CLIPBOARD 目标请求超时
  • Bug 768142 Makefile.example 中 $(LIBS) 和 $(OBJS) 的顺序不正确导致“未定义的引用”
  • Bug 693203GtkApplication 不支持 Xfce 会话管理器
  • Bug 768305Gtk+ 应该支持 background-blend-mode
参与其中

有兴趣参与 GTK+ 的开发吗?请查看适合新手的错误列表,并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

本周 GTK+ – 7

在上周,GTK+ 的主分支有 29 次提交,添加了 4744 行代码,删除了 4340 行代码。

计划和状态
  • GTK+ 3.21.3 已发布
  • Matthias 继续处理 GLibGTK+ 的门户分支。
  • William Hua 正在 重构 bug 756579 中的菜单定位 API,遵循了黑客马拉松的讨论。
  • Emmanuele 推出了 gsk 分支的新修订版。
  • Carlos Soriano 正在为文件选择器和 nautilus 开发新的路径栏实现
值得注意的更改
  • Emmanuele 修复了 Firefox 中因引入 GdkDrawingContext 而导致的一些问题。
  • Ray Strode 清理了一些 headerbar 代码并添加了对扩展子项的支持
  • Matthias 修复了 GtkColorChooser 中的一个崩溃和 GtkInspector 中的另一个崩溃
已修复的错误
  • Bug 767851 – 弹出窗口箭头在某些方向上损坏
  • Bug 767849 – 焦点处理中的崩溃
  • Bug 724332 – GtkHeaderBar 需要支持 expand 属性
  • Bug 768025 – entry.warning 和 entry.error 损坏
参与其中

有兴趣参与 GTK+ 的开发吗?请查看适合新手的错误列表,并加入 irc.gnome.org 上的 IRC 频道 #gtk+。

GTK+ 中的光标

历史

传统上,光标在 Linux 中一直是一个大问题。

X11 光标字体从远古时代流传至今,并为我们提供了诸如 gumby () 或 trek () 之类的经典光标。不幸的是,这种情况被冻结到了 GDK API 中,通过 GdkCursorType 枚举和 gdk_cursor_new() 函数实现。

后来,出现了 Xcursor 库。它发明了自己的图像格式来存储光标,并为我们带来了光标主题,但没有回答“我的光标主题应该提供哪些光标?”这个问题。

由于没有官方推荐的光标名称列表,光标主题经常提供在野外发现的所有光标名称变体。例如,这里是 oxygen 光标主题中包含的光标列表。如果您想知道,此列表中的十六进制字符串是 Xcursor 的一个巧妙技巧,用于在上述使用光标字体的核心 X11 应用程序下改造主题光标。

CSS 来救援

大约一年前,我们决定最终改进 GTK+ 光标的故事。值得庆幸的是,CSS3 规范包含一个合理的 光标名称列表,可以合理地预期在各个平台上都可用。

标准光标由于 GdkCursorType 枚举包含太多无意义的东西并且不容易扩展,我们决定使 gdk_cursor_new_from_name() 成为获取光标的推荐 API。此函数的文档现在列出了 CSS 光标名称(请点击上面的链接查看),并且各个 GDK 后端中的光标处理代码会努力为您提供所有这些名称的有意义的光标。

在某些平台(例如使用随机光标主题的 X11)上,如果主题中不存在某个特定光标,我们可能不得不回退到默认的箭头光标。作为光标代码的整体改造的一部分,Windows 后端增加了对光标主题的支持。

GTK+ 本身现在完全使用 gdk_cursor_new_from_name() 以及标准光标名称。gtk3-demo 包含一个演示,其中显示了所有标准光标并允许您尝试它们。上面的屏幕截图显示了它。

这里描述的更改已进入 GTK+ 3.18,该版本大约在 9 个月前发布。

您在应用程序中应该怎么做

很可能,您无需执行任何操作!GTK+ 小部件自行使用合适的光标,您可以从中受益,而无需任何额外的工作。

如果您的应用程序因任何原因正在创建自己的光标,您应该仔细检查上面显示的某个标准光标是否适合您。使用标准光标可确保无论您的应用程序在哪个平台上运行,也无论用户选择了哪个光标主题,您都将获得合适的光标。

请使用 gdk_cursor_new_from_name() 来生成主题光标,因为这现在是此任务的首选 API。