1 引言(1.6)

1.6 构建应用程序
一个应用程序由多个文件组成:
二进制文件:该文件安装在 /usr/bin 目录下。
桌面文件:桌面文件向桌面外壳提供应用程序的重要信息,如名称、图标、D-Bus 名称、启动命令等。它安装在 /usr/share/applications 目录下。
图标:图标安装在 /usr/share/icons/hicolor/48x48/apps 目录下,这样无论当前使用什么主题,都能找到该图标。
设置模式:如果应用程序使用 GSettings,它会将其模式安装在 /usr/share/glib-2.0/schemas 目录下,以便 dconf-editor 等工具能够找到它。
其他资源:其他文件(如 GtkBuilder 的 ui 文件)最好从存储在应用程序二进制文件本身的资源中加载。这减少了传统上需要安装在 /usr/share 中特定于应用程序位置的大多数文件的需求。
GTK 包含基于 GApplication 构建的应用程序支持。在本教程中,我们将从零开始构建一个简单的应用程序,逐步添加更多内容。在此过程中,我们将了解 GtkApplication、templates(模板)、resources(资源)、application menus(应用程序菜单)、settings(设置)、GtkHeaderBar、GtkStack、GtkSearchBar、GtkListBox 等。
这些示例的完整可构建源代码可以在 GTK 源代码分发的 examples/ 目录中找到,或者在 GTK 的 git 仓库在线查看。你可以使用 Makefile.example 文件通过 make 单独构建每个示例。有关更多信息,请参见 examples 目录中包含的 README。
一个简单的应用程序
使用 GtkApplication 时,main() 函数可以非常简单。我们只需调用 g_application_run() 并向其传递应用程序类的实例即可。

#include <gtk/gtk.h>

#include "exampleapp.h"

int
main (int argc, char *argv[])
{
  return g_application_run (G_APPLICATION (example_app_new()), argc, argv);
}

所有应用程序逻辑都位于应用程序类中,该类是 GtkApplication 的子类。我们的示例目前还没有任何有趣的功能。它所做的只是:当不带参数启动时,打开一个窗口;如果带参数启动,则打开所提供的文件。
为了处理这两种情况,我们重写了 activate() 虚函数(当应用程序不带命令行参数启动时会调用该函数)和 open() 虚函数(当应用程序带命令行参数启动时会调用该函数)。
要了解更多关于 GApplication 入口点的信息,请参考 GIO 文档。

#include <gtk/gtk.h>

#include "exampleapp.h"
#include "exampleappwin.h"

struct _ExampleApp
{
  GtkApplication parent;
};

G_DEFINE_TYPE(ExampleApp, example_app, GTK_TYPE_APPLICATION);

static void
example_app_init (ExampleApp *app)
{
}

static void
example_app_activate (GApplication *app)
{
  ExampleAppWindow *win;

  win = example_app_window_new (EXAMPLE_APP (app));
  gtk_window_present (GTK_WINDOW (win));
}

static void
example_app_open (GApplication  *app,
                  GFile        **files,
                  int            n_files,
                  const char    *hint)
{
  GList *windows;
  ExampleAppWindow *win;
  int i;

  windows = gtk_application_get_windows (GTK_APPLICATION (app));
  if (windows)
    win = EXAMPLE_APP_WINDOW (windows->data);
  else
    win = example_app_window_new (EXAMPLE_APP (app));

  for (i = 0; i < n_files; i++)
    example_app_window_open (win, files[i]);

  gtk_window_present (GTK_WINDOW (win));
}

static void
example_app_class_init (ExampleAppClass *class)
{
  G_APPLICATION_CLASS (class)->activate = example_app_activate;
  G_APPLICATION_CLASS (class)->open = example_app_open;
}

ExampleApp *
example_app_new (void)
{
  return g_object_new (EXAMPLE_APP_TYPE,
                       "application-id", "org.gtk.exampleapp",
                       "flags", G_APPLICATION_HANDLES_OPEN,
                       NULL);
}

GTK 应用程序支持中另一个重要的类是 GtkApplicationWindow。它通常也会被子类化。我们的子类目前还没有实现任何功能,所以我们只会得到一个空窗口。

#include <gtk/gtk.h>

#include "exampleapp.h"
#include "exampleappwin.h"

struct _ExampleAppWindow
{
  GtkApplicationWindow parent;
};

G_DEFINE_TYPE(ExampleAppWindow, example_app_window, GTK_TYPE_APPLICATION_WINDOW);

static void
example_app_window_init (ExampleAppWindow *app)
{
}

static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
}

ExampleAppWindow *
example_app_window_new (ExampleApp *app)
{
  return g_object_new (EXAMPLE_APP_WINDOW_TYPE, "application", app, NULL);
}

void
example_app_window_open (ExampleAppWindow *win,
                         GFile            *file)
{
}

在应用程序的初始设置中,我们还会创建一个图标和一个桌面文件。

05

图 5 一个图标
[Desktop Entry]
Type=Application
Name=Example
Icon=exampleapp
StartupNotify=true
Exec=bindir@/exampleapp
需要注意的是,在使用这个桌面文件之前,bindir@ 需要替换为二进制文件的实际路径。
到目前为止,我们已经完成了以下工作:

06

图 6 一个应用程序

虽然目前来看它还不是很引人注目,但我们的应用程序已经能在会话总线上呈现自己,具备单实例语义,并且可以接受文件作为命令行参数。

填充窗口
在这一步中,我们使用 GtkBuilder 模板将一个 GtkBuilder ui 文件与我们的应用程序窗口类相关联。
我们这个简单的 ui 文件为窗口设置了标题,并将一个 GtkStack 控件作为主要内容。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="ExampleAppWindow" parent="GtkApplicationWindow">
    <property name="title" translatable="yes">Example Application</property>
    <property name="default-width">600</property>
    <property name="default-height">400</property>
    <child>
      <object class="GtkBox" id="content_box">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkStack" id="stack"/>
        </child>
      </object>
    </child>
  </template>
</interface>

为了在应用程序中使用这个文件,我们重新处理 GtkApplicationWindow 的子类,在类初始化函数中调用 gtk_widget_class_set_template_from_resource(),将该 ui 文件设置为这个类的模板。我们还在实例初始化函数中添加对 gtk_widget_init_template() 的调用,为该类的每个实例实例化模板。

...

static void
example_app_window_init (ExampleAppWindow *win)
{
  gtk_widget_init_template (GTK_WIDGET (win));
}

static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                               "/org/gtk/exampleapp/window.ui");
}

 ...

(完整代码)
你可能已经注意到,我们使用了设置模板的函数的 _from_resource() 变体。现在我们需要使用 Glib 的资源功能将 ui 文件包含到二进制文件中。通常的做法是在 .gresource.xml 文件中列出所有资源,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<gresources>
  <gresource prefix="/org/gtk/exampleapp">
    <file preprocess="xml-stripblanks">window.ui</file>
  </gresource>
</gresources>

这个文件必须转换为 C 源文件,然后与其他源文件一起编译并链接到应用程序中。要完成此操作,我们使用 glib-compile-resources 工具:
glib-compile-resources exampleapp.gresource.xml --target=resources.c --generate-source
meson 构建系统的 gnome 模块提供了 gnome.compile_resources() 方法来完成这项任务。
我们的应用程序现在看起来是这样的:

07

图 7 应用程序

打开文件
在这一步中,我们让应用程序显示命令行中提供的所有文件的内容。
为此,我们在应用程序窗口子类的结构体中添加一个成员,并在其中保留对 GtkStack 的引用。结构体的第一个成员应当是该类所继承的父类型。这里,ExampleAppWindow 继承自 GtkApplicationWindow。gtk_widget_class_bind_template_child() 函数会进行相关设置,以便在实例化模板后,结构体的 stack 成员将指向模板中同名的控件。

...

struct _ExampleAppWindow
{
  GtkApplicationWindow parent;

  GtkWidget *stack;
};

G_DEFINE_TYPE (ExampleAppWindow, example_app_window, GTK_TYPE_APPLICATION_WINDOW)

...

static void
example_app_window_class_init (ExampleAppWindowClass *class)
{
  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                               "/org/gtk/exampleapp/window.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), ExampleAppWindow, stack);
}

...

(完整代码)
现在我们重新看一下 example_app_window_open () 函数,该函数会针对每个命令行参数被调用。我们将创建一个 GtkTextView,并把它作为一个页面添加到栈(stack)中:

...

void
example_app_window_open (ExampleAppWindow *win,
                         GFile            *file)
{
  char *basename;
  GtkWidget *scrolled, *view;
  char *contents;
  gsize length;

  basename = g_file_get_basename (file);

  scrolled = gtk_scrolled_window_new();
  gtk_widget_set_hexpand (scrolled, TRUE);
  gtk_widget_set_vexpand (scrolled, TRUE);
  view = gtk_text_view_new();
  gtk_text_view_set_editable (GTK_TEXT_VIEW (view), FALSE);
  gtk_text_view_set_cursor_visible (GTK_TEXT_VIEW (view), FALSE);
  gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (scrolled), view);
  gtk_stack_add_titled (GTK_STACK (win->stack), scrolled, basename, basename);

  if (g_file_load_contents (file, NULL, &contents, &length, NULL, NULL))
    {
      GtkTextBuffer *buffer;

      buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
      gtk_text_buffer_set_text (buffer, contents, length);
      g_free (contents);
    }

  g_free (basename);
}

...

(完整代码)
最后,我们在 ui 文件的标题栏区域添加一个 GtkStackSwitcher,并让它显示关于我们的栈的信息。
栈切换器(stack switcher)所需的所有用于显示标签页的信息都来自它所关联的栈。在这里,我们通过 gtk_stack_add_titled() 函数的最后一个参数,为每个文件指定了要显示的标签。
我们的应用程序开始初具雏形了:

08

图 8应用程序窗口

菜单
菜单显示在标题栏的右侧,用于汇集那些不常使用但会影响整个应用程序的操作。
与窗口模板类似,我们在一个 ui 文件中定义菜单,并将其作为资源添加到二进制文件中。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <menu id="menu">
    <section>
      <item>
        <attribute name="label" translatable="yes">_Preferences</attribute>
        <attribute name="action">app.preferences</attribute>
      </item>
    </section>
    <section>
      <item>
        <attribute name="label" translatable="yes">_Quit</attribute>
        <attribute name="action">app.quit</attribute>
      </item>
    </section>
  </menu>
</interface>

要让菜单显示出来,我们必须加载 ui 文件,并将生成的菜单模型与我们添加到标题栏的菜单按钮按钮相关联。由于菜单通过激活 GAction 来工作,我们还必须向应用程序添加一组合适的动作。
添加这些动作最好在 startup() 虚函数中完成,该函数确保会为每个主应用程序实例调用一次:

...

static void
preferences_activated (GSimpleAction *action,
                       GVariant      *parameter,
                       gpointer       app)
{
}

static void
quit_activated (GSimpleAction *action,
                GVariant      *parameter,
                gpointer       app)
{
  g_application_quit (G_APPLICATION (app));
}

static GActionEntry app_entries[] =
{
  { "preferences", preferences_activated, NULL, NULL, NULL },
  { "quit", quit_activated, NULL, NULL, NULL }
};

static void
example_app_startup (GApplication *app)
{
  GtkBuilder *builder;
  GMenuModel *app_menu;
  const char *quit_accels[2] = { "&lt;Ctrl&gt;Q", NULL };

  G_APPLICATION_CLASS (example_app_parent_class)->startup (app);

  g_action_map_add_action_entries (G_ACTION_MAP (app),
                                   app_entries, G_N_ELEMENTS (app_entries),
                                   app);
  gtk_application_set_accels_for_action (GTK_APPLICATION (app),
                                         "app.quit",
                                         quit_accels);
}

static void
example_app_class_init (ExampleAppClass *class)
{
  G_APPLICATION_CLASS (class)->startup = example_app_startup;
  ...
}

...

(完整代码)
我们的“偏好设置”菜单项目前还没有任何功能,但“退出”菜单项已经可以正常使用了。需要注意的是,它还可以通过常用的 Ctrl-Q 快捷键来激活。这个快捷键是通过 gtk_application_set_accels_for_action() 函数添加的。
应用程序菜单的样子如下:

09

图 9 应用程序窗口

偏好设置对话框
一个典型的应用程序会有一些偏好设置,这些设置需要在多次运行之间保持不变。即便是我们这个简单的示例应用程序,也可能需要更改用于显示内容的字体。
我们将使用 GSettings 来存储这些偏好设置。GSettings 需要一个描述我们设置的模式(schema):

<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
  <schema path="/org/gtk/exampleapp/" id="org.gtk.exampleapp">
    <key name="font" type="s">
      <default>'Monospace 12'</default>
      <summary>Font</summary>
      <description>The font to be used for content.</description>
    </key>
    <key name="transition" type="s">
      <choices>
        <choice value='none'/>
        <choice value='crossfade'/>
        <choice value='slide-left-right'/>
      </choices>
      <default>'none'</default>
      <summary>Transition</summary>
      <description>The transition to use when switching tabs.</description>
    </key>
  </schema>
</schemalist>

在应用程序中使用此模式之前,我们需要将其编译为 GSettings 所需的二进制形式。GIO 为基于 autotools 的项目提供了用于此操作的宏,而 meson 构建系统的 gnome 模块则提供了 gnome.compile_schemas() 方法来完成这项任务。
接下来,我们需要将设置与它们所要控制的控件关联起来。一种便捷的方式是使用 GSettings 的绑定功能,将设置键(settings keys)绑定到对象属性上,就像我们在处理过渡(transition)设置时所做的那样。

...

static void
example_app_window_init (ExampleAppWindow *win)
{
  gtk_widget_init_template (GTK_WIDGET (win));
  win->settings = g_settings_new ("org.gtk.exampleapp");

  g_settings_bind (win->settings, "transition",
                   win->stack, "transition-type",
                   G_SETTINGS_BIND_DEFAULT);
}

...

(完整代码)
连接字体设置的代码会稍微复杂一些,因为没有与之直接对应的简单对象属性,所以这里我们就不深入探讨了。
到目前为止,如果你修改了某项设置(例如使用 gsettings 命令行工具),应用程序已经能够做出响应。当然,我们希望应用程序能为这些设置提供一个偏好设置对话框。所以现在就来实现它。我们的偏好设置对话框将是 GtkDialog 的子类,并且会使用我们已经学过的那些技术:模板、私有结构体、设置绑定。
让我们从模板开始。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="ExampleAppPrefs" parent="GtkDialog">
    <property name="title" translatable="yes">Preferences</property>
    <property name="resizable">0</property>
    <property name="modal">1</property>
    <child internal-child="content_area">
      <object class="GtkBox" id="content_area">
        <child>
          <object class="GtkGrid" id="grid">
            <property name="margin-start">12</property>
            <property name="margin-end">12</property>
            <property name="margin-top">12</property>
            <property name="margin-bottom">12</property>
            <property name="row-spacing">12</property>
            <property name="column-spacing">12</property>
            <child>
              <object class="GtkLabel" id="fontlabel">
                <property name="label">_Font:</property>
                <property name="use-underline">1</property>
                <property name="mnemonic-widget">font</property>
                <property name="xalign">1</property>
                <layout>
                  <property name="column">0</property>
                  <property name="row">0</property>
                </layout>
              </object>
            </child>
            <child>
              <object class="GtkFontButton" id="font">
                <layout>
                  <property name="column">1</property>
                  <property name="row">0</property>
                </layout>
              </object>
            </child>
            <child>
              <object class="GtkLabel" id="transitionlabel">
                <property name="label">_Transition:</property>
                <property name="use-underline">1</property>
                <property name="mnemonic-widget">transition</property>
                <property name="xalign">1</property>
                <layout>
                  <property name="column">0</property>
                  <property name="row">1</property>
                </layout>
              </object>
            </child>
            <child>
              <object class="GtkComboBoxText" id="transition">
                <items>
                  <item translatable="yes" id="none">None</item>
                  <item translatable="yes" id="crossfade">Fade</item>
                  <item translatable="yes" id="slide-left-right">Slide</item>
                </items>
                <layout>
                  <property name="column">1</property>
                  <property name="row">1</property>
                </layout>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

接下来是对话框子类的实现。

#include <gtk/gtk.h>

#include "exampleapp.h"
#include "exampleappwin.h"
#include "exampleappprefs.h"

struct _ExampleAppPrefs
{
  GtkDialog parent;

  GSettings *settings;
  GtkWidget *font;
  GtkWidget *transition;
};

G_DEFINE_TYPE (ExampleAppPrefs, example_app_prefs, GTK_TYPE_DIALOG)

static void
example_app_prefs_init (ExampleAppPrefs *prefs)
{
  gtk_widget_init_template (GTK_WIDGET (prefs));
  prefs->settings = g_settings_new ("org.gtk.exampleapp");

  g_settings_bind (prefs->settings, "font",
                   prefs->font, "font",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (prefs->settings, "transition",
                   prefs->transition, "active-id",
                   G_SETTINGS_BIND_DEFAULT);
}

static void
example_app_prefs_dispose (GObject *object)
{
  ExampleAppPrefs *prefs;

  prefs = EXAMPLE_APP_PREFS (object);

  g_clear_object (&prefs->settings);

  G_OBJECT_CLASS (example_app_prefs_parent_class)->dispose (object);
}

static void
example_app_prefs_class_init (ExampleAppPrefsClass *class)
{
  G_OBJECT_CLASS (class)->dispose = example_app_prefs_dispose;

  gtk_widget_class_set_template_from_resource (GTK_WIDGET_CLASS (class),
                                               "/org/gtk/exampleapp/prefs.ui");
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), ExampleAppPrefs, font);
  gtk_widget_class_bind_template_child (GTK_WIDGET_CLASS (class), ExampleAppPrefs, transition);
}

ExampleAppPrefs *
example_app_prefs_new (ExampleAppWindow *win)
{
  return g_object_new (EXAMPLE_APP_PREFS_TYPE, "transient-for", win, "use-header-bar", TRUE, NULL);
}

现在我们回过头来处理应用程序类中的 preferences_activated () 函数,让它打开一个新的偏好设置对话框。

...

static void
preferences_activated (GSimpleAction *action,
                       GVariant      *parameter,
                       gpointer       app)
{
  ExampleAppPrefs *prefs;
  GtkWindow *win;

  win = gtk_application_get_active_window (GTK_APPLICATION (app));
  prefs = example_app_prefs_new (EXAMPLE_APP_WINDOW (win));
  gtk_window_present (GTK_WINDOW (prefs));
}

...

(完整代码)
经过所有这些工作,我们的应用程序现在可以显示如下所示的偏好设置对话框:
(网站图片无法加载)
图 10 偏好设置对话框

添加搜索栏
我们继续完善应用程序的功能。现在,我们添加搜索功能。GTK 通过 GtkSearchEntry 和 GtkSearchBar 支持这一功能。搜索栏是一个可以从顶部滑入的控件,用于显示搜索输入框。
我们在标题栏中添加一个切换按钮,通过该按钮可以使搜索栏从标题栏下方滑出。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="ExampleAppWindow" parent="GtkApplicationWindow">
    <property name="title" translatable="yes">Example Application</property>
    <property name="default-width">600</property>
    <property name="default-height">400</property>
    <child type="titlebar">
      <object class="GtkHeaderBar" id="header">
        <child type="title">
          <object class="GtkStackSwitcher" id="tabs">
            <property name="stack">stack</property>
          </object>
        </child>
        <child type="end">
          <object class="GtkMenuButton" id="gears">
            <property name="direction">none</property>
          </object>
        </child>
        <child type="end">
          <object class="GtkToggleButton" id="search">
            <property name="sensitive">0</property>
            <property name="icon-name">edit-find-symbolic</property>
          </object>
        </child>
      </object>
    </child>
    <child>
      <object class="GtkBox" id="content_box">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkSearchBar" id="searchbar">
            <child>
              <object class="GtkSearchEntry" id="searchentry">
                <signal name="search-changed" handler="search_text_changed"/>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkStack" id="stack">
            <signal name="notify::visible-child" handler="visible_child_changed"/>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

实现搜索功能需要不少代码改动,这里我们就不逐一详细说明了。搜索实现的核心部分是一个信号处理器,它会监听搜索输入框(search entry)中的文本变化。

...

static void
search_text_changed (GtkEntry         *entry,
                     ExampleAppWindow *win)
{
  const char *text;
  GtkWidget *tab;
  GtkWidget *view;
  GtkTextBuffer *buffer;
  GtkTextIter start, match_start, match_end;

  text = gtk_editable_get_text (GTK_EDITABLE (entry));

  if (text[0] == '\0')
    return;

  tab = gtk_stack_get_visible_child (GTK_STACK (win->stack));
  view = gtk_scrolled_window_get_child (GTK_SCROLLED_WINDOW (tab));
  buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));

  /* Very simple-minded search implementation */
  gtk_text_buffer_get_start_iter (buffer, &start);
  if (gtk_text_iter_forward_search (&start, text, GTK_TEXT_SEARCH_CASE_INSENSITIVE,
                                    &match_start, &match_end, NULL))
    {
      gtk_text_buffer_select_range (buffer, &match_start, &match_end);
      gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &match_start,
                                    0.0, FALSE, 0.0, 0.0);
    }
}

static void
example_app_window_init (ExampleAppWindow *win)
{

...

  gtk_widget_class_bind_template_callback (GTK_WIDGET_CLASS (class), search_text_changed);

...

}

...

(完整代码)
有了搜索栏后,我们的应用程序现在看起来是这样的:

11

图 11 搜索栏

添加侧边栏
作为另一项功能,我们将添加一个侧边栏,这将展示 GtkMenuButton、GtkRevealer 和 GtkListBox 的用法。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <template class="ExampleAppWindow" parent="GtkApplicationWindow">
    <property name="title" translatable="yes">Example Application</property>
    <property name="default-width">600</property>
    <property name="default-height">400</property>
    <child type="titlebar">
      <object class="GtkHeaderBar" id="header">
        <child type="title">
          <object class="GtkStackSwitcher" id="tabs">
            <property name="stack">stack</property>
          </object>
        </child>
        <child type="end">
          <object class="GtkToggleButton" id="search">
            <property name="sensitive">0</property>
            <property name="icon-name">edit-find-symbolic</property>
          </object>
        </child>
        <child type="end">
          <object class="GtkMenuButton" id="gears">
            <property name="direction">none</property>
          </object>
        </child>
      </object>
    </child>
    <child>
      <object class="GtkBox" id="content_box">
        <property name="orientation">vertical</property>
        <child>
          <object class="GtkSearchBar" id="searchbar">
            <child>
              <object class="GtkSearchEntry" id="searchentry">
                <signal name="search-changed" handler="search_text_changed"/>
              </object>
            </child>
          </object>
        </child>
        <child>
          <object class="GtkBox" id="hbox">
            <child>
              <object class="GtkRevealer" id="sidebar">
                <property name="transition-type">slide-right</property>
                <child>
                  <object class="GtkScrolledWindow" id="sidebar-sw">
                    <property name="hscrollbar-policy">never</property>
                    <child>
                      <object class="GtkListBox" id="words">
                        <property name="selection-mode">none</property>
                      </object>
                    </child>
                  </object>
                </child>
              </object>
            </child>
            <child>
              <object class="GtkStack" id="stack">
                <signal name="notify::visible-child" handler="visible_child_changed"/>
              </object>
            </child>
          </object>
        </child>
      </object>
    </child>
  </template>
</interface>

用按钮填充侧边栏(这些按钮对应每个文件中找到的单词)的代码比较复杂,这里就不深入讲解了。但我们会看一下在菜单中为这个新功能添加复选按钮的代码。

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <menu id="menu">
    <section>
      <item>
        <attribute name="label" translatable="yes">_Words</attribute>
        <attribute name="action">win.show-words</attribute>
      </item>
      <item>
        <attribute name="label" translatable="yes">_Preferences</attribute>
        <attribute name="action">app.preferences</attribute>
      </item>
    </section>
    <section>
      <item>
        <attribute name="label" translatable="yes">_Quit</attribute>
        <attribute name="action">app.quit</attribute>
      </item>
    </section>
  </menu>
</interface>

为了将菜单项与 show-words 设置关联起来,我们使用一个与指定 GSettings 键对应的 GAction。

...

static void
example_app_window_init (ExampleAppWindow *win)
{

...

  builder = gtk_builder_new_from_resource ("/org/gtk/exampleapp/gears-menu.ui");
  menu = G_MENU_MODEL (gtk_builder_get_object (builder, "menu"));
  gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (priv->gears), menu);
  g_object_unref (builder);

  action = g_settings_create_action (priv->settings, "show-words");
  g_action_map_add_action (G_ACTION_MAP (win), action);
  g_object_unref (action);
}

...

(完整代码)
我们的应用程序现在是这个样子的:

12

图 12 侧边栏

属性
控件和其他对象具有许多有用的属性。
这里我们展示一些以新颖且灵活的方式使用这些属性的方法:通过 GPropertyAction 将它们包装为动作,或者通过 GBinding 进行绑定。
为了实现这一点,我们在窗口模板的标题栏中添加两个标签,分别命名为 lines_label 和 lines,并将它们绑定到私有结构体的成员中,这是我们已经见过好几次的操作了。
我们在齿轮菜单中添加一个新的“lines”菜单项,它会触发 show-lines 动作:

<?xml version="1.0" encoding="UTF-8"?>
<interface>
  <menu id="menu">
    <section>
      <item>
        <attribute name="label" translatable="yes">_Words</attribute>
        <attribute name="action">win.show-words</attribute>
      </item>
      <item>
        <attribute name="label" translatable="yes">_Lines</attribute>
        <attribute name="action">win.show-lines</attribute>
      </item>
      <item>
        <attribute name="label" translatable="yes">_Preferences</attribute>
        <attribute name="action">app.preferences</attribute>
      </item>
    </section>
    <section>
      <item>
        <attribute name="label" translatable="yes">_Quit</attribute>
        <attribute name="action">app.quit</attribute>
      </item>
    </section>
  </menu>
</interface>

为了让这个菜单项发挥作用,我们为lines标签的 visible 属性创建一个属性动作(property action),并将其添加到窗口的动作中。这样一来,每次激活该动作时,标签的可见性就会切换。
由于我们希望两个标签同时显示或同时隐藏,因此我们将 lines_label 控件的 visible 属性与 lines 控件的同一属性进行绑定。

...

static void
example_app_window_init (ExampleAppWindow *win)
{
  ...

  action = (GAction*) g_property_action_new ("show-lines", win->lines, "visible");
  g_action_map_add_action (G_ACTION_MAP (win), action);
  g_object_unref (action);

  g_object_bind_property (win->lines, "visible",
                          win->lines_label, "visible",
                          G_BINDING_DEFAULT);
}

...

(完整代码)
我们还需要一个函数来计算当前活动标签页的行数,并更新lines标签。如果您想了解详情,可以查看完整源代码。
这使我们的示例应用程序呈现出如下外观:

13

图 13 完整的应用程序

通过网盘分享的知识:GTK 4 Reference Manual
链接: https://pan.baidu.com/s/57mKoejMZPrxs_wh3a7Kj9g
--来自百度网盘超级会员v7的分享

posted @ 2025-07-28 20:15  Hoijuon  阅读(26)  评论(0)    收藏  举报