Gtk 入门 [src]
GTK 是 小部件工具包。由 GTK 创建的每个用户界面都包含小部件。这是在 C 中使用 GObject
实现的,后者是一种面向 C 对象的框架。小部件按照层次结构进行组织。窗口小部件是主容器。然后通过向窗口中添加按钮、下拉菜单、输入字段和其他小部件来构建用户界面。如果你正在创建复杂的用户界面,建议使用 GtkBuilder 及其特定于 GTK 的标记描述语言,而非手动组装界面。
GTK 是事件驱动的。该工具包可以侦听事件(如点击按钮),并将该事件传递给你的应用程序。
本节包含一些教程信息,可助你开始 GTK 编程。假定你已经准备好使用并安装了 GTK、其依赖项和 C 编译器。如果你需要首先构建 GTK 本身,请参阅本参考中的 编译 GTK 库 部分。
基础知识
为了开始了解 GTK,我们将从一个非常简单的应用程序开始。此程序将创建一个 200 × 200 像素的空窗口。
使用以下内容创建一个名为 example-0.c
的新文件。
#include <gtk/gtk.h>
static void
activate (GtkApplication* app,
gpointer user_data)
{
GtkWidget *window;
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Window");
gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
gtk_window_present (GTK_WINDOW (window));
}
int
main (int argc,
char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
你可以使用 GCC 编译上述程序,方法为使用
gcc $( pkg-config --cflags gtk4 ) -o example-0 example-0.c $( pkg-config --libs gtk4 )
有关如何编译 GTK 应用程序的详细信息,请参阅本参考中的 编译 GTK 应用程序 部分。
当然,所有 GTK 应用程序都会包含 gtk/gtk.h
,其中声明了 GTK 应用程序所需的功能、类型和宏。
即使 GTK 安装了多个头文件,第三方代码也只能直接包含顶级 gtk/gtk.h
头。如果直接包含任何其他头,编译器将中止并显示一条错误消息。
在 GTK 应用程序中,main()
函数的目的是创建一个 GtkApplication
对象并运行它。在此示例中,声明了一个名为 app
的 GtkApplication
指针,然后使用 gtk_application_new()
对其进行初始化。
在创建 GtkApplication
时,你需要选择应用程序标识符(一个名称),并将它作为参数传递给 gtk_application_new()
。在此示例中使用了 org.gtk.example
。有关为你的应用程序选择标识符的信息,请参阅 本指南。最后,gtk_application_new()
将 GApplicationFlags
作为应用程序的输入,如果你的应用程序有特殊需求。
接下来,将 activate 信号 连接到与 main()
函数在同一行的 activate()
函数。在下一行上使用 g_application_run()
启动应用程序时,将发出 activate
信号。g_application_run()
调用还将命令行参数(argc
计数和 argv
字符串数组)作为参数。你的应用程序可以覆盖命令行处理,例如打开在命令行上传递的文件。
在 g_application_run()
内将发送激活信号,然后我们继续进入应用程序的 activate()
函数。这是我们构建 GTK 窗口的位置,以便在应用程序启动时显示一个窗口。gtk_application_window_new()
的调用将创建一个新的 GtkApplicationWindow
并将其存储在 window
指针内。该窗口将具有框架、标题栏和窗口控件,具体取决于平台。
窗口标题使用 gtk_window_set_title()
设置。此函数将 GtkWindow
指针和字符串作为输入。由于我们的 window
指针是 GtkWidget
指针,我们需要将它转换为 GtkWindow
;不必通过典型 C 转换(如 (GtkWindow*)
)来转换 window
,可以使用宏 GTK_WINDOW()
来转换 window
。GTK_WINDOW()
将在转换之前检查该指针是否是 GtkWindow
类的一个实例,如果检查失败,则发出一个警告。有关此惯例的更多信息可以在 GObject 文档 中找到。
最后,使用 gtk_window_set_default_size()
设置窗口大小,然后通过 gtk_widget_show()
由 GTK 显示该窗口。
当你关闭窗口(例如按下 X 按钮)时,g_application_run()
调用将返回一个数字,该数字保存在名为 status
的整数变量内。随后,GtkApplication
对象将使用 g_object_unref()
从内存中释放。最后,状态整数将返回,应用程序退出。
在程序运行时,GTK 正在接收事件。这些通常是由于用户与您的程序交互而导致的输入事件,但也包括来自窗口管理器或其他应用程序的消息。 GTK 将处理这些信息,因此您的窗口小部件上可能会发出信号。连接这些信号的处理程序是您通常使程序对用户输入做出响应的方式。
下面的示例稍微复杂一些,并试图展示 GTK 的一些功能。
你好,世界
在编程语言和库的悠久传统中,此示例称为你好,世界。
你好,C 中的世界
创建一个包含以下内容的新文件,名称为 example-1.c
。
#include <gtk/gtk.h>
static void
print_hello (GtkWidget *widget,
gpointer data)
{
g_print ("Hello World\n");
}
static void
activate (GtkApplication *app,
gpointer user_data)
{
GtkWidget *window;
GtkWidget *button;
GtkWidget *box;
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Window");
gtk_window_set_default_size (GTK_WINDOW (window), 200, 200);
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_halign (box, GTK_ALIGN_CENTER);
gtk_widget_set_valign (box, GTK_ALIGN_CENTER);
gtk_window_set_child (GTK_WINDOW (window), box);
button = gtk_button_new_with_label ("Hello World");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);
g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_window_destroy), window);
gtk_box_append (GTK_BOX (box), button);
gtk_window_present (GTK_WINDOW (window));
}
int
main (int argc,
char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
你可以使用 GCC 编译上述程序,方法为使用
gcc $( pkg-config --cflags gtk4 ) -o example-1 example-1.c $( pkg-config --libs gtk4 )
如上所述,example-1.c
通过在我们的窗口中添加一个标记为“你好,世界”的按钮,进一步构建在 example-0.c
的基础上。声明了两个新的 GtkWidget
指针来完成此操作,button
和 box
。box 变量被创建为存储一个 GtkBox
,这是 GTK 的控制按钮大小和布局的方式。
GtkBox
小部件使用 gtk_box_new()
创建,它需要 GtkOrientation
枚举值作为参数。此框将包含的按钮可以水平或垂直排列。在这种情况中无关紧要,因为我们只处理一个按钮。使用新创建的 GtkBox
初始化框后,代码使用 gtk_window_set_child()
将框小部件添加到窗口小部件。
接下来以类似的方式初始化 button
变量。调用 gtk_button_new_with_label()
,它返回一个 GtkButton
存储在 button
中。之后 button
添加到我们的 box
中。
使用 g_signal_connect()
,按钮连接到我们应用程序中名为 print_hello()
的函数,这样当按钮被单击时,GTK 将调用此函数。由于 print_hello()
函数不使用任何数据作为输入,因此将向其传递 NULL
。print_hello()
使用字符串 “Hello World” 调用 g_print()
,如果 GTK 应用程序是从一个终端启动的,这将在终端中打印 Hello World。
连接完 print_hello()
之后,使用 g_signal_connect_swapped()
连接到按钮的 “clicked” 状态的另一个信号。此函数类似于 g_signal_connect()
,区别在于回调函数的处理方式;g_signal_connect_swapped()
允许您指定回调函数应该将什么作为参数,方法是让您将它作为数据传递。在这种情况下,回调函数是 gtk_window_destroy()
,并且 window
指针传递给了它。这样做的效果是,当按钮被单击时,整个 GTK 窗口将被销毁。相反,如果使用正常的 g_signal_connect()
将 “clicked” 信号与 gtk_window_destroy()
连接,那么该函数将被调用 button
(这将不太好,因为该函数需要一个 GtkWindow
作为参数)。
example-1.c
中代码的其余部分与 example-0.c
相同。下一节将进一步阐述如何在您的 GTK 应用程序中添加多个 GtkWidget
。
打包
在创建应用程序时,您需要在一个窗口中放置多个小部件。当您这样做时,控制每个部件如何定位和调整大小变得很重要。这就是打包的用武之地。
GTK 带有种类众多的布局容器,其目的是控制添加到其中的子小部件的布局,比如
以下示例展示了 GtkGrid
容器如何让您排列多个按钮
打包按钮
创建一个新文件,内容如下,并将其命名为 example-2.c
。
#include <gtk/gtk.h>
static void
print_hello (GtkWidget *widget,
gpointer data)
{
g_print ("Hello World\n");
}
static void
activate (GtkApplication *app,
gpointer user_data)
{
GtkWidget *window;
GtkWidget *grid;
GtkWidget *button;
/* create a new window, and set its title */
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Window");
/* Here we construct the container that is going pack our buttons */
grid = gtk_grid_new ();
/* Pack the container in the window */
gtk_window_set_child (GTK_WINDOW (window), grid);
button = gtk_button_new_with_label ("Button 1");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);
/* Place the first button in the grid cell (0, 0), and make it fill
* just 1 cell horizontally and vertically (ie no spanning)
*/
gtk_grid_attach (GTK_GRID (grid), button, 0, 0, 1, 1);
button = gtk_button_new_with_label ("Button 2");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);
/* Place the second button in the grid cell (1, 0), and make it fill
* just 1 cell horizontally and vertically (ie no spanning)
*/
gtk_grid_attach (GTK_GRID (grid), button, 1, 0, 1, 1);
button = gtk_button_new_with_label ("Quit");
g_signal_connect_swapped (button, "clicked", G_CALLBACK (gtk_window_destroy), window);
/* Place the Quit button in the grid cell (0, 1), and make it
* span 2 columns.
*/
gtk_grid_attach (GTK_GRID (grid), button, 0, 1, 2, 1);
gtk_window_present (GTK_WINDOW (window));
}
int
main (int argc,
char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
你可以使用 GCC 编译上述程序,方法为使用
gcc $( pkg-config --cflags gtk4 ) -o example-2 example-2.c $( pkg-config --libs gtk4 )
自定义绘图
许多小部件(如按钮)都会自己完成所有绘图。您只需告诉它们您希望看到的标签,它们就会弄清楚要使用哪种字体,绘制按钮轮廓和焦点矩形等。有时,需要进行一些自定义绘图。在这种情况下,GtkDrawingArea
可能是要使用的正确的部件。它提供了一个画布,您可以通过设置其绘制函数来对其进行绘制。
小组件的内容通常需要部分或完全重新绘制,例如,当另一个窗口被移动且覆盖了小组件的一部分时,或包含它的窗口被调整大小时。还可通过调用 gtk_widget_queue_draw()
来明确地使小组件重新绘制。GTK 会提供即用型 cairo 上下文来进行绘图,从而处理大部分细节。
以下示例展示了如何将绘图函数与 GtkDrawingArea
结合使用。这比之前的示例稍复杂一些,因为它还演示了使用事件控制器进行输入事件处理。
响应输入进行绘制
创建名为 example-3.c
的新文件,内容如下。
#include <gtk/gtk.h>
/* Surface to store current scribbles */
static cairo_surface_t *surface = NULL;
static void
clear_surface (void)
{
cairo_t *cr;
cr = cairo_create (surface);
cairo_set_source_rgb (cr, 1, 1, 1);
cairo_paint (cr);
cairo_destroy (cr);
}
/* Create a new surface of the appropriate size to store our scribbles */
static void
resize_cb (GtkWidget *widget,
int width,
int height,
gpointer data)
{
if (surface)
{
cairo_surface_destroy (surface);
surface = NULL;
}
if (gtk_native_get_surface (gtk_widget_get_native (widget)))
{
surface = gdk_surface_create_similar_surface (gtk_native_get_surface (gtk_widget_get_native (widget)),
CAIRO_CONTENT_COLOR,
gtk_widget_get_width (widget),
gtk_widget_get_height (widget));
/* Initialize the surface to white */
clear_surface ();
}
}
/* Redraw the screen from the surface. Note that the draw
* callback receives a ready-to-be-used cairo_t that is already
* clipped to only draw the exposed areas of the widget
*/
static void
draw_cb (GtkDrawingArea *drawing_area,
cairo_t *cr,
int width,
int height,
gpointer data)
{
cairo_set_source_surface (cr, surface, 0, 0);
cairo_paint (cr);
}
/* Draw a rectangle on the surface at the given position */
static void
draw_brush (GtkWidget *widget,
double x,
double y)
{
cairo_t *cr;
/* Paint to the surface, where we store our state */
cr = cairo_create (surface);
cairo_rectangle (cr, x - 3, y - 3, 6, 6);
cairo_fill (cr);
cairo_destroy (cr);
/* Now invalidate the drawing area. */
gtk_widget_queue_draw (widget);
}
static double start_x;
static double start_y;
static void
drag_begin (GtkGestureDrag *gesture,
double x,
double y,
GtkWidget *area)
{
start_x = x;
start_y = y;
draw_brush (area, x, y);
}
static void
drag_update (GtkGestureDrag *gesture,
double x,
double y,
GtkWidget *area)
{
draw_brush (area, start_x + x, start_y + y);
}
static void
drag_end (GtkGestureDrag *gesture,
double x,
double y,
GtkWidget *area)
{
draw_brush (area, start_x + x, start_y + y);
}
static void
pressed (GtkGestureClick *gesture,
int n_press,
double x,
double y,
GtkWidget *area)
{
clear_surface ();
gtk_widget_queue_draw (area);
}
static void
close_window (void)
{
if (surface)
cairo_surface_destroy (surface);
}
static void
activate (GtkApplication *app,
gpointer user_data)
{
GtkWidget *window;
GtkWidget *frame;
GtkWidget *drawing_area;
GtkGesture *drag;
GtkGesture *press;
window = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (window), "Drawing Area");
g_signal_connect (window, "destroy", G_CALLBACK (close_window), NULL);
frame = gtk_frame_new (NULL);
gtk_window_set_child (GTK_WINDOW (window), frame);
drawing_area = gtk_drawing_area_new ();
/* set a minimum size */
gtk_widget_set_size_request (drawing_area, 100, 100);
gtk_frame_set_child (GTK_FRAME (frame), drawing_area);
gtk_drawing_area_set_draw_func (GTK_DRAWING_AREA (drawing_area), draw_cb, NULL, NULL);
g_signal_connect_after (drawing_area, "resize", G_CALLBACK (resize_cb), NULL);
drag = gtk_gesture_drag_new ();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (drag), GDK_BUTTON_PRIMARY);
gtk_widget_add_controller (drawing_area, GTK_EVENT_CONTROLLER (drag));
g_signal_connect (drag, "drag-begin", G_CALLBACK (drag_begin), drawing_area);
g_signal_connect (drag, "drag-update", G_CALLBACK (drag_update), drawing_area);
g_signal_connect (drag, "drag-end", G_CALLBACK (drag_end), drawing_area);
press = gtk_gesture_click_new ();
gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (press), GDK_BUTTON_SECONDARY);
gtk_widget_add_controller (drawing_area, GTK_EVENT_CONTROLLER (press));
g_signal_connect (press, "pressed", G_CALLBACK (pressed), drawing_area);
gtk_window_present (GTK_WINDOW (window));
}
int
main (int argc,
char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
你可以使用 GCC 编译上述程序,方法为使用
gcc $( pkg-config --cflags gtk4 ) -o example-3 example-3.c $( pkg-config --libs gtk4 )
构建用户界面
在构建包含几十甚至几百个小组件的复杂用户界面时,在 C 代码中完成所有设置工作非常繁琐,几乎不可能进行更改。
值得庆幸的是,GTK 支持将用户界面布局与其业务逻辑分离开来,方法是使用 GtkBuilder
类解析的 XML 格式的 UI 说明。
使用 GtkBuilder 来填充按钮
创建名为 example-4.c
的新文件,内容如下。
#include <gtk/gtk.h>
#include <glib/gstdio.h>
static void
print_hello (GtkWidget *widget,
gpointer data)
{
g_print ("Hello World\n");
}
static void
quit_cb (GtkWindow *window)
{
gtk_window_close (window);
}
static void
activate (GtkApplication *app,
gpointer user_data)
{
/* Construct a GtkBuilder instance and load our UI description */
GtkBuilder *builder = gtk_builder_new ();
gtk_builder_add_from_file (builder, "builder.ui", NULL);
/* Connect signal handlers to the constructed widgets. */
GObject *window = gtk_builder_get_object (builder, "window");
gtk_window_set_application (GTK_WINDOW (window), app);
GObject *button = gtk_builder_get_object (builder, "button1");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);
button = gtk_builder_get_object (builder, "button2");
g_signal_connect (button, "clicked", G_CALLBACK (print_hello), NULL);
button = gtk_builder_get_object (builder, "quit");
g_signal_connect_swapped (button, "clicked", G_CALLBACK (quit_cb), window);
gtk_widget_set_visible (GTK_WIDGET (window), TRUE);
/* We do not need the builder any more */
g_object_unref (builder);
}
int
main (int argc,
char *argv[])
{
#ifdef GTK_SRCDIR
g_chdir (GTK_SRCDIR);
#endif
GtkApplication *app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
int status = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return status;
}
创建名为 builder.ui
的新文件,内容如下。
<?xml version="1.0" encoding="UTF-8"?>
<interface>
<object id="window" class="GtkWindow">
<property name="title">Grid</property>
<child>
<object id="grid" class="GtkGrid">
<child>
<object id="button1" class="GtkButton">
<property name="label">Button 1</property>
<layout>
<property name="column">0</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object id="button2" class="GtkButton">
<property name="label">Button 2</property>
<layout>
<property name="column">1</property>
<property name="row">0</property>
</layout>
</object>
</child>
<child>
<object id="quit" class="GtkButton">
<property name="label">Quit</property>
<layout>
<property name="column">0</property>
<property name="row">1</property>
<property name="column-span">2</property>
</layout>
</object>
</child>
</object>
</child>
</object>
</interface>
你可以使用 GCC 编译上述程序,方法为使用
gcc $( pkg-config --cflags gtk4 ) -o example-4 example-4.c $( pkg-config --libs gtk4 )
请注意,也可以使用 GtkBuilder
来构建非小组件对象,例如树模型、调整项等。这就是我们此处使用 gtk_builder_get_object()
方法且其返回 GObject
而不是 GtkWidget
的原因。
通常,你应将完整路径传递给 gtk_builder_add_from_file()
,以使得程序的执行独立于当前目录。安装 UI 说明和类似数据的常见位置是 /usr/share/appname
。
还可以将 UI 说明作为字符串嵌入源代码并使用 gtk_builder_add_from_string()
加载它。但将 UI 说明保存在单独文件中具有多个优点:
- 无需重新编译程序就能对 UI 进行微小调整
- 能够更轻松地将 UI 代码从应用程序的业务逻辑中分离出来
- 能够更轻松地使用复合小组件模板将 UI 重构为单独的 类
使用 GResource,既可以鱼与熊掌兼得:你可以将 UI 定义文件独立保存在源代码存储库中,然后再将其嵌入应用程序中进行发布。
构建应用程序
应用程序由多个文件组成:
- 二进制文件
- 该文件安装在
/usr/bin
。 - 桌面文件
- 桌面文件会向桌面 shell 提供应用程序的重要信息,例如名称、图标、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
、模板、资源、应用程序菜单、设置、GtkHeaderBar
、GtkStack
、GtkSearchBar
、GtkListBox
等内容。
可以从 GTK 源分布包的 examples
目录或 GTK 源代码存储库中的 在线版本 找到这些示例的可构建完整源代码。你可以使用 Makefile.example
文件通过 make 单独构建每个示例。有关更多信息,请参阅示例目录中包含的 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)
{
}
作为应用程序初始设置的一部分,我们还会创建一个图标和一个桌面文件。
[Desktop Entry]
Type=Application
Name=Example
Icon=exampleapp
StartupNotify=true
Exec=@bindir@/exampleapp
请注意,在使用此桌面文件之前,需要将 `bindir
@` 替换为二进制文件的实际路径。
以下是我们迄今为止取得的成果
这看起来还不算很令人印象深刻,但我们的应用程序已经在会话总线上显示了自己,它具有单实例语义,并且接受文件作为命令行参数。
填充窗口
在此步骤中,我们使用 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
子类,并从 class init 函数中调用 gtk_widget_class_set_template_from_resource()
将 ui 文件设置为该类的模板。我们还会在实例 init 函数中添加对 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
文件(如 this(此文件))中列出所有资源完成的
<?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()
方法来执行这项 task。
现在,我们的应用程序如下 this
打开文件
在此步骤中,我们让应用程序显示它在命令行上获取的所有文件的内容。
注意:对于示例应用 3-9 来说,根据屏幕截图所示进行显示需要在命令行中提供文件名(例如,./exampleapp examplewin.c examplewin.h
)。
为此,我们将一个成员添加到应用程序窗口子类的结构中,并在其中保留一个对 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 的信息。
堆栈切换器会获取它需要用来从其所属的堆栈显示选项卡的所有信息。此处,我们传递标签以将每个文件显示为 gtk_stack_add_titled()
函数的最后一个参数。
我们的应用程序开始成 shape
菜单
菜单显示在页眉栏的右侧。其目的是收集不常使用的、影响整个 应用程序的操作。
与窗口模板一样,我们在一个 UI 文件中指定菜单,并将它作为资源添加到我们的 binary。
<?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 文件并将生成的 menu model 与我们添加到页眉栏中的 menu 按钮关联起来。由于菜单通过激活 GActions 起作用,所以我们还必须向我们的 application 添加一组合适的 action。
最好在 vfunc startup()
中添加操作,该 vfunc 保证针对每个主要应用程序 instance 调用一次
...
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] = { "<Ctrl>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()
添加的。
应用程序菜单如下 this
首选项对话框
典型的应用程序会有一些首选项,这些首选项需要从一次运行记住到下一次运行。即使对于我们简单的示例应用程序来说,我们可能也想更改用于 content 的字体。
我们将使用 GSettings
来存储首选项。GSettings
要求使用模式来描述我们的 settings
<?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
绑定功能将设置键绑定到对象属性,就像我们在此处对过渡设置所做的那样。
...
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));
}
...
(完整源)
完成所有这些工作后,我们的应用程序现在可以显示首选项对话框了,如下所示
添加搜索栏
我们继续完善应用程序的功能。现在,我们添加搜索。 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>
实现搜索需要相当多的代码更改,此处我们不会完全介绍。搜索实现的核心部分是一个信号处理程序,用于监听搜索条目中的文本更改。
...
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);
...
}
...
(完整源)
使用搜索栏,我们的应用程序现在看起来像下面这样
添加侧边栏
作为功能的另一部分,我们正在添加侧边栏,它演示了 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);
}
...
(完整源)
我们应用程序现在的样子
属性
小部件和其他对象具有许多有用的属性。
在这里,我们展示了将它们包装成 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(可见)属性创建了一个属性操作,并将其添加到窗口的操作中。这样,每次激活该操作都会切换该标签的可见性。
由于我们希望两个标签同时显示和消失,因此我们将 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
标签。如果您对详细信息感兴趣,请参见全部源代码。
这使得我们的示例应用程序呈现出以下外观