ZetCode

GTK+ 事件和信号

最后修改于 2023 年 10 月 18 日

在 GTK+ 编程教程的这一部分,我们将讨论事件系统。

GTK+ 是一个事件驱动系统。所有 GUI 应用程序都是事件驱动的。应用程序启动一个主循环,该循环持续检查新生成的事件。如果没有事件,应用程序将等待并且不做任何事情。在 GTK+ 中,一个事件是来自 X 服务器的消息。当事件到达一个控件时,它可以通过发出一个信号来响应这个事件。GTK+ 程序员可以将一个特定的回调连接到信号。回调是一个响应信号的处理器函数。

按钮点击

当按钮被触发时,它会发送一个 clicked 信号。按钮可以通过鼠标指针或 空格键 触发(前提是按钮具有焦点)。

buttonclick.c
#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 函数,该函数终止应用程序。

移动窗口

下一个例子展示了我们如何响应窗口移动事件。

moveevent.c
#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_eventsGDK_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 坐标,构建一个字符串,并将其设置为窗口标题。

Move event
图:移动事件

enter 信号

以下示例展示了我们如何响应 enter 信号。当我们用鼠标指针进入控件区域时,会发出 enter 信号。

entersignal.c
#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 函数来更改按钮的背景。

断开回调连接

我们可以将回调与信号断开连接。下一个代码示例演示了这种情况。

disconnect.c
#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 函数断开连接。

Disconnect
图:断开连接

拖放示例

在下一个例子中,我们展示了无边框窗口,并学习如何拖动和移动这样的窗口。

dragdrop.c
#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 函数,该函数开始移动窗口。

计时器示例

以下示例演示了计时器示例。当我们需要一些重复的任务时,可以使用计时器。它可以是时钟、倒计时、视觉效果或动画。

timer.c
#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+ 中的事件。