GTK+ 事件和信号
最后修改于 2023 年 10 月 18 日
在 GTK+ 编程教程的这一部分,我们将讨论事件系统。
GTK+ 是一个事件驱动系统。所有 GUI 应用程序都是事件驱动的。应用程序启动一个主循环,该循环持续检查新生成的事件。如果没有事件,应用程序将等待并且不做任何事情。在 GTK+ 中,一个事件是来自 X 服务器的消息。当事件到达一个控件时,它可以通过发出一个信号来响应这个事件。GTK+ 程序员可以将一个特定的回调连接到信号。回调是一个响应信号的处理器函数。
按钮点击
当按钮被触发时,它会发送一个 clicked
信号。按钮可以通过鼠标指针或 空格键 触发(前提是按钮具有焦点)。
#include <gtk/gtk.h> void button_clicked(GtkWidget *widget, gpointer data) { g_print("clicked\n"); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *halign; GtkWidget *btn; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "GtkButton"); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); gtk_container_set_border_width(GTK_CONTAINER(window), 15); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); halign = gtk_alignment_new(0, 0, 0, 0); btn = gtk_button_new_with_label("Click"); gtk_widget_set_size_request(btn, 70, 30); gtk_container_add(GTK_CONTAINER(halign), btn); gtk_container_add(GTK_CONTAINER(window), halign); g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(button_clicked), NULL); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_widget_show_all(window); gtk_main(); return 0; }
在应用程序中,我们有两个信号:clicked
信号和 destroy
信号。
g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(button_clicked), NULL);
我们使用 g_signal_connect
函数将 clicked
信号连接到 button_clicked
回调。
void button_clicked(GtkWidget *widget, gpointer data) { g_print("clicked\n"); }
回调将 "clicked" 字符串打印到控制台。回调函数的第一个参数是发出信号的对象。在我们的例子中,它是 Click 按钮。第二个参数是可选的。我们可以向回调发送一些数据。在我们的例子中,我们没有发送任何数据;我们为 g_signal_connect
函数的第四个参数提供了 NULL
值。
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
如果按下位于标题栏右上角的 x 按钮,或者按下 Alt+F4,则会发出 destroy
信号。将调用 gtk_main_quit
函数,该函数终止应用程序。
移动窗口
下一个例子展示了我们如何响应窗口移动事件。
#include <gtk/gtk.h> void configure_callback(GtkWindow *window, GdkEvent *event, gpointer data) { int x, y; GString *buf; x = event->configure.x; y = event->configure.y; buf = g_string_new(NULL); g_string_printf(buf, "%d, %d", x, y); gtk_window_set_title(window, buf->str); g_string_free(buf, TRUE); } int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); gtk_widget_add_events(GTK_WIDGET(window), GDK_CONFIGURE); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), G_OBJECT(window)); g_signal_connect(G_OBJECT(window), "configure-event", G_CALLBACK(configure_callback), NULL); gtk_widget_show(window); gtk_main(); return 0; }
在这个例子中,我们在标题栏中显示窗口左上角的当前位置。
gtk_widget_add_events(GTK_WIDGET(window), GDK_CONFIGURE);
控件的事件掩码决定了特定控件将接收的事件类型。一些事件是预先配置的,其他事件必须添加到事件掩码中。gtk_widget_add_events
将 GDK_CONFIGURE
事件类型添加到掩码。GDK_CONFIGURE
事件类型包含所有大小、位置和窗口更改的堆叠顺序。
g_signal_connect(G_OBJECT(window), "configure-event", G_CALLBACK(configure_callback), NULL);
当控件窗口的大小、位置或堆叠发生变化时,将发出 configure-event
。
void configure_callback(GtkWindow *window, GdkEvent *event, gpointer data) { int x, y; GString *buf; x = event->configure.x; y = event->configure.y; buf = g_string_new(NULL); g_string_printf(buf, "%d, %d", x, y); gtk_window_set_title(window, buf->str); g_string_free(buf, TRUE); }
回调函数有三个参数:发出信号的对象,GdkEvent
和可选数据。我们确定 x, y 坐标,构建一个字符串,并将其设置为窗口标题。

enter 信号
以下示例展示了我们如何响应 enter
信号。当我们用鼠标指针进入控件区域时,会发出 enter 信号。
#include <gtk/gtk.h> void enter_button(GtkWidget *widget, gpointer data) { GdkColor col = {0, 27000, 30000, 35000}; gtk_widget_modify_bg(widget, GTK_STATE_PRELIGHT, &col); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *halign; GtkWidget *btn; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); gtk_container_set_border_width(GTK_CONTAINER(window), 15); gtk_window_set_title(GTK_WINDOW(window), "Enter signal"); halign = gtk_alignment_new(0, 0, 0, 0); btn = gtk_button_new_with_label("Button"); gtk_widget_set_size_request(btn, 70, 30); gtk_container_add(GTK_CONTAINER(halign), btn); gtk_container_add(GTK_CONTAINER(window), halign); g_signal_connect(G_OBJECT(btn), "enter", G_CALLBACK(enter_button), NULL); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_widget_show_all(window); gtk_main(); return 0; }
在这个例子中,当我们用鼠标指针悬停在按钮控件上时,按钮控件的背景颜色会发生变化。
g_signal_connect(G_OBJECT(btn), "enter", G_CALLBACK(enter_button), NULL);
当发生 enter
信号时,我们调用 enter_button
用户函数。
void enter_button(GtkWidget *widget, gpointer data) { GdkColor col = {0, 27000, 30000, 35000}; gtk_widget_modify_bg(widget, GTK_STATE_PRELIGHT, &col); }
在回调函数内部,我们通过调用 gtk_widget_modify_bg
函数来更改按钮的背景。
断开回调连接
我们可以将回调与信号断开连接。下一个代码示例演示了这种情况。
#include <gtk/gtk.h> gint handler_id; void button_clicked(GtkWidget *widget, gpointer data) { g_print("clicked\n"); } void toogle_signal(GtkWidget *widget, gpointer window) { if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) { handler_id = g_signal_connect(G_OBJECT(window), "clicked", G_CALLBACK(button_clicked), NULL); } else { g_signal_handler_disconnect(window, handler_id); } } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *hbox; GtkWidget *vbox; GtkWidget *btn; GtkWidget *cb; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); gtk_container_set_border_width(GTK_CONTAINER(window), 15); gtk_window_set_title(GTK_WINDOW(window), "Disconnect"); hbox = gtk_hbox_new(FALSE, 15); btn = gtk_button_new_with_label("Click"); gtk_widget_set_size_request(btn, 70, 30); gtk_box_pack_start(GTK_BOX(hbox), btn, FALSE, FALSE, 0); cb = gtk_check_button_new_with_label("Connect"); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(cb), TRUE); gtk_box_pack_start(GTK_BOX(hbox), cb, FALSE, FALSE, 0); vbox = gtk_vbox_new(FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0); gtk_container_add(GTK_CONTAINER(window), vbox); handler_id = g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(button_clicked), NULL); g_signal_connect(G_OBJECT(cb), "clicked", G_CALLBACK(toogle_signal), (gpointer) btn); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_widget_show_all(window); gtk_main(); return 0; }
在代码示例中,我们有一个按钮和一个复选框。复选框连接或断开回调与按钮的 clicked
信号的连接。
handler_id = g_signal_connect(G_OBJECT(btn), "clicked", G_CALLBACK(button_clicked), NULL);
g_signal_connect
返回处理程序 ID,该 ID 唯一标识回调。
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget))) { handler_id = g_signal_connect(G_OBJECT(window), "clicked", G_CALLBACK(button_clicked), NULL); } else { g_signal_handler_disconnect(window, handler_id); }
此代码确定复选框的状态。根据状态,它使用 g_signal_connect
函数连接回调,或使用 g_signal_handler_disconnect
函数断开连接。

拖放示例
在下一个例子中,我们展示了无边框窗口,并学习如何拖动和移动这样的窗口。
#include <gtk/gtk.h> gboolean on_button_press(GtkWidget* widget, GdkEventButton *event, GdkWindowEdge edge) { if (event->type == GDK_BUTTON_PRESS) { if (event->button == 1) { gtk_window_begin_move_drag(GTK_WINDOW(gtk_widget_get_toplevel(widget)), event->button, event->x_root, event->y_root, event->time); } } return TRUE; } int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 250, 200); gtk_window_set_title(GTK_WINDOW(window), "Drag & drop"); gtk_window_set_decorated(GTK_WINDOW(window), FALSE); gtk_widget_add_events(window, GDK_BUTTON_PRESS_MASK); g_signal_connect(G_OBJECT(window), "button-press-event", G_CALLBACK(on_button_press), NULL); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), G_OBJECT(window)); gtk_widget_show(window); gtk_main(); return 0; }
该示例演示了无边框窗口的拖放操作。
gtk_window_set_decorated(GTK_WINDOW(window), FALSE);
我们使用 gtk_window_set_decorated
函数删除窗口装饰。这意味着窗口将没有边框和标题栏。
g_signal_connect(G_OBJECT(window), "button-press-event", G_CALLBACK(on_button_press), NULL);
我们将窗口连接到 button-press-event
信号。
gboolean on_button_press(GtkWidget* widget, GdkEventButton *event, GdkWindowEdge edge) { if (event->type == GDK_BUTTON_PRESS) { if (event->button == 1) { gtk_window_begin_move_drag(GTK_WINDOW(gtk_widget_get_toplevel(widget)), event->button, event->x_root, event->y_root, event->time); } } return TRUE; }
在 on_button_press
函数内部,我们执行拖放操作。我们检查是否按下了鼠标左键。然后我们调用 gtk_window_begin_move_drag
函数,该函数开始移动窗口。
计时器示例
以下示例演示了计时器示例。当我们需要一些重复的任务时,可以使用计时器。它可以是时钟、倒计时、视觉效果或动画。
#include <cairo.h> #include <gtk/gtk.h> gchar buf[256]; gboolean on_expose_event(GtkWidget *widget, GdkEventExpose *event, gpointer data) { cairo_t *cr; cr = gdk_cairo_create(widget->window); cairo_move_to(cr, 30, 30); cairo_set_font_size(cr, 15); cairo_show_text(cr, buf); cairo_destroy(cr); return FALSE; } gboolean time_handler(GtkWidget *widget) { if (widget->window == NULL) return FALSE; GDateTime *now = g_date_time_new_now_local(); gchar *my_time = g_date_time_format(now, "%H:%M:%S"); g_sprintf(buf, "%s", my_time); g_free(my_time); g_date_time_unref(now); gtk_widget_queue_draw(widget); return TRUE; } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *darea; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); darea = gtk_drawing_area_new(); gtk_container_add(GTK_CONTAINER(window), darea); g_signal_connect(darea, "expose-event", G_CALLBACK(on_expose_event), NULL); g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); gtk_window_set_title(GTK_WINDOW(window), "Timer"); g_timeout_add(1000, (GSourceFunc) time_handler, (gpointer) window); gtk_widget_show_all(window); time_handler(window); gtk_main(); return 0; }
该示例在窗口上显示当前的本地时间。也使用了 Cairo 2D 库。
g_signal_connect(darea, "expose-event", G_CALLBACK(on_expose_event), NULL);
我们在 on_expose_event
回调函数内部绘制时间。回调函数连接到 expose-event
信号,当窗口将要重新绘制时,会发出该信号。
g_timeout_add(1000, (GSourceFunc) time_handler, (gpointer) window);
此函数注册计时器。time_handler
函数以固定的间隔重复调用;在我们的例子中,每秒调用一次。计时器函数被调用直到它返回 FALSE。
time_handler(window);
这会立即调用计时器函数。否则,将延迟一秒钟。
cairo_t *cr; cr = gdk_cairo_create(widget->window); cairo_move_to(cr, 30, 30); cairo_set_font_size(cr, 15); cairo_show_text(cr, buf); cairo_destroy(cr);
此代码在窗口上绘制当前时间。有关 Cairo 2D 库的更多信息,请参阅 ZetCode 的 Cairo 图形教程。
if (widget->window == NULL) return FALSE;
当窗口被销毁时,可能会调用计时器函数。此行阻止对已销毁的控件进行操作。
GDateTime *now = g_date_time_new_now_local(); gchar *my_time = g_date_time_format(now, "%H:%M:%S"); g_sprintf(buf, "%s", my_time);
这些行确定当前的本地时间。时间存储在全局 buf
变量中。
gtk_widget_queue_draw(widget);
gtk_widget_queue_draw
函数使窗口区域无效,然后发出 expose-event
信号。
本章是关于 GTK+ 中的事件。