这是一个标准的 GTK Hello World, 只有一个按钮构件的程序
#include <gtk/gtk.h> /* 这是一个回调函数。data 参数在本示例中被忽略。 * 后面有更多的回调函数示例。*/ void hello( GtkWidget *widget, gpointer data ) { g_print ("Hello World\n"); } gint delete_event( GtkWidget *widget, GdkEvent *event, gpointer data ) { /* 如果你的 "delete_event" 信号处理函数返回 FALSE,GTK 会发出 "destroy" 信号。 * 返回 TRUE,你不希望关闭窗口。 * 当你想弹出“你确定要退出吗?”对话框时它很有用。*/ g_print ("delete event occurred\n"); /* 改 TRUE 为 FALSE 程序会关闭。*/ return TRUE; } /* 另一个回调函数 */ void destroy( GtkWidget *widget, gpointer data ) { gtk_main_quit (); } int main( int argc, char *argv[] ) { /* GtkWidget 是构件的存储类型 */ GtkWidget *window; GtkWidget *button; /* 这个函数在所有的 GTK 程序都要调用。参数由命令行中解析出来并且送到该程序中*/ gtk_init (&argc, &argv); /* 创建一个新窗口 */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); /* 当窗口收到 "delete_event" 信号 (这个信号由窗口管理器发出,通常是“关闭” * 选项或是标题栏上的关闭按钮发出的),我们让它调用在前面定义的 delete_event() 函数。 * 传给回调函数的 data 参数值是 NULL,它会被回调函数忽略。*/ g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (delete_event), NULL); /* 在这里我们连接 "destroy" 事件到一个信号处理函数。 * 对这个窗口调用 gtk_widget_destroy() 函数或在 "delete_event" 回调函数中返回 FALSE 值 * 都会触发这个事件。*/ g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL); /* 设置窗口边框的宽度。*/ gtk_container_set_border_width (GTK_CONTAINER (window), 10); /* 创建一个标签为 "Hello World" 的新按钮。*/ button = gtk_button_new_with_label ("Hello World"); /* 当按钮收到 "clicked" 信号时会调用 hello() 函数,并将NULL传给 * 它作为参数。hello() 函数在前面定义了。*/ g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (hello), NULL); /* 当点击按钮时,会通过调用 gtk_widget_destroy(window) 来关闭窗口。 * "destroy" 信号会从这里或从窗口管理器发出。*/ g_signal_connect_swapped (G_OBJECT (button), "clicked", G_CALLBACK (gtk_widget_destroy), window); /* 把按钮放入窗口 (一个 gtk 容器) 中。*/ gtk_container_add (GTK_CONTAINER (window), button); /* 最后一步是显示新创建的按钮和窗口 */ gtk_widget_show (button); gtk_widget_show (window); /* 所有的 GTK 程序必须有一个 gtk_main() 函数。程序运行停在这里 * 等待事件 (如键盘事件或鼠标事件) 的发生。*/ gtk_main (); return 0; } |
编译 Hello World 程序www.freedesktop.org 得到。这个程序读取 GTK 附带的 .pc 文件来决定编译 GTK 程序需要的编译选项。pkg-config --cflags gtk+-2.0 列出 include 目录,pkg-config --libs gtk+-2.0 列出编译连接库,也可以合在一起,像这样:pkg-config --cflags --libs gtk+-2.0。
编译命令是:
gcc -Wall -g helloworld.c -o helloworld `pkg-config --cflags gtk+-2.0` \
`pkg-config --libs gtk+-2.0`
这里使用了程序 pkg-config,可以从
注意上面编译命令中使用的单引号类型是很重要的。是tab键上那个键。
信号和回调函数的原理事件
Hello World 详解设置构件属性这一章还有其它类似函数。
现在我们知道基本理论了,让我们来详细分析helloworld示例程序。
这是按钮被点击时要调用的回调函数。在这个示例中我们忽略了参数 widget 和 data,但是使用这些参数也不难。下一个示例会使用 data 参数来告诉我们哪个按钮被按下了。
void hello( GtkWidget *widget, gpointer data ) { g_print ("Hello World\n"); } |
接下来的一个回调函数有点特殊。"delete_event" 在窗口管理器发送这个事件给应用程序时发生。我们在这里可以选择对这些事件做什么。可以忽略它们,可以做一点响应,或是简单地退出程序。
这个回调函数返回的值让 GTK 知道该如何去做。返回 TRUE,让它知道我们不想让 "destroy" 信号被发出,保持程序继续运行。返回 FALSE,我们让 "destroy" 信号发出,这接着会调用 "destroy" 信号处理函数。
gint delete_event( GtkWidget *widget, GdkEvent *event, gpointer data ) { g_print ("delete event occurred\n"); return TRUE; } |
这里是另一个回调函数,它通过调用 gtk_main_quit() 来退出程序。这个函数告诉 GTK 当控制权返回给它时就从 gtk_main 退出。
void destroy( GtkWidget *widget, gpointer data ) { gtk_main_quit (); } |
我假设你知道 main() 函数...是的,像其它程序一样,所有的 GTK 程序有一个 main() 函数。
int main( int argc, char *argv[] ) { |
接下来声明两个指向 GtkWidget 类型的结构的指针。它们被用于创建一个窗口和一个按钮。
GtkWidget *window; GtkWidget *button; |
这里又是 gtk_init()。跟前面一样,这个初始化工具包,分析命令行里的参数。它从参数列表中删除任何可以识别的参数,并且修改 argc 和 argv,使这些被删除的参数好像从来就不存在一样,而允许你的程序分析剩余的参数。
gtk_init (&argc, &argv); |
创建一个新窗口。这个很直观。它为 GtkWidget *window 结构分配了内存,这样 window 现在指向了一个有效的结构。它建立了一个新窗口,但是这个窗口直到在程序后面部分我们调用 gtk_widget_show(window) 后才会显示出来。
window = gtk_window_new (GTK_WINDOW_TOPLEVEL); |
这有两个连接一个信号处理函数到一个对象 (本例中,就是 window ) 的示例。这里,"delete_event" 和 "destroy" 信号被捕获。当我们用窗口管理器去关闭窗口或调用函数 gtk_widget_destroy() 并将 window 构件作为对象传给它来销毁时,"delete_event" 信号发出。当我们在 "delete_event" 信号处理函数中返回 FALSE 值时,"destroy" 信号发出。G_OBJECT 和 G_CALLBACK 是宏,为我们执行类型转换和检测,同时也增加了代码的可读性。
g_signal_connect (G_OBJECT (window), "delete_event", G_CALLBACK (delete_event), NULL); g_signal_connect (G_OBJECT (window), "destroy", G_CALLBACK (destroy), NULL); |
接下来这个函数用于设置容器对象的属性。设置窗口边框宽度为10个象素。在
再次,GTK_CONTAINER也是一个执行类型转换的宏。
gtk_container_set_border_width (GTK_CONTAINER (window), 10); |
这个函数调用创建一个新按钮。在内存中分配空间给一个新的 GtkWidget 结构,初始化它,并使 button 指针指向它。它显示后上面会有个 "Hello World" 标签。
button = gtk_button_new_with_label ("Hello World"); |
在这,我们让这个按钮做一些有用的事。我们给按钮设置信号处理函数,因此当按钮发出 "clicked" 信号时,hello() 函数被调用。我们忽略了 data 参数,简单地传送 NULL 给 hello() 回调函数。显而易见,当我们用鼠标点击按钮时,信号 "clicked" 被发出。
g_signal_connect (G_OBJECT (button), "clicked", G_CALLBACK (hello), NULL); |
我们也要使用这个按钮退出程序。这将演示 "destroy" 信号怎样由窗口管理器引发,或由我们的程序引发。当我们按下按钮时,和上面一样,它首先调用 hello() 回调函数,然后是这个函数,这依赖于它们被设置连接的顺序。你可以拥有许多回调函数,所有的回调按你设置连接的顺序依次执行。因为 gtk_widget_destroy() 函数只接受 GtkWidget *widget 作为唯一的参数,我们这里用 g_signal_connect_swapped() 函数代替正统的 g_signal_connect()。
g_signal_connect_swapped (G_OBJECT (button), "clicked", G_CALLBACK (gtk_widget_destroy), G_OBJECT (window)); |
这是一个组装调用,在组装构件这一章将作深入讲解。不过它相当容易理解。它简单地告诉 GTK 要把按钮放在窗口里,也就是它显示的地方。注意一个 GTK 容器只能包含一个构件。还有其它的构件,在后面介绍,设计为用来以各种方法布局多个构件。
gtk_container_add (GTK_CONTAINER (window), button); |
一切准备就绪。所有信号处理函数连接好了,按钮也放进了窗口,我们让 GTK 在屏幕上“显示”这些构件。窗口构件最后显示,这样整个窗口会一下弹出,而不是先见到窗口弹出后再见到按钮。虽然这个简单的示例中,你不会注意到。
gtk_widget_show (button); gtk_widget_show (window); |
接着,当然,我们调用 gtk_main() 函数来等待来自 X 服务器的事件,当这些事件到来时,调用构件发出信号。
gtk_main (); |
最后返回,调用函数 gtk_quit() 后控制权返回到这里。
return 0; |
现在,当我们用鼠标点击一个 GTK 按钮,构件发出一个 "clicked" 信号。为了让我们利用这个信息,程序设置了一个信号处理器来捕获那个信号,它按我们的选择依次调用函数。在我们的示例中,当按下按钮时,以 NULL 作为参数调用函数 hello(),然后调用该信号的下一个处理函数,该函数调用 gtk_widget_destroy() 函数,把窗口构件作为参数传递给它,销毁窗口构件。这导致窗口发出 "destroy" 信号,它被捕获,并且调用我们的 destroy() 回调函数,简单地退出 GTK。
如果用窗口管理器去关闭窗口,它会引发 "delete_event"。这会调用我们的 "delete_event" 处理函数。如果我们在函数中返回 TRUE,窗口还是留在那里,什么事也不发生。返回 FALSE,会使 GTK 发出 "destroy" 信号,它当然会调用 "destroy" 回调,退出 GTK。
数据类型
你或许发现前述示例中有几个地方需要解释。gint、gchar 等等。你看到的这些被分别定义类型别名(typedefs)到 int 和 char,它们是 GLib 系统的一部分。这用来避免在计算时对简单数据类型的大小(size)的依赖。
一个好的示例是,"gint32" 被定义为任何平台的32位整数,无论是64位的 alpha 还是32位的 i386。该类型定义非常直观。它们都在 glib/glib.h 里定义 (这个文件被gtk.h包含了 )。
你也将注意到 GTK 有在函数要一个 GtkObject 作为参数时传入 GtkWidget 的能力。GTK 的设计是面向对象的,一个构件是一个对象。
在 2.0 版,信号系统已从 GTK 移到 GLib,因此在函数和类型的说明中有前缀 "g_" 而不是 "gtk_"。我们不打算介绍 GLib 2.0 信号系统相对 GTK 1.2 信号系统扩展的细节。 |
在我们详细分析 helloworld 程序之前,我们会讨论信号和回调函数。GTK 是一个事件驱动的工具包,意味着它会等在 gtk_main() 那里,直到下一个事件发生,才把控制权传给适当的函数。
控制权的传递是使用“信号”的办法来完成的。(注意这里的信号并不等同于 Unix 系统里的信号,并且也不是用它们实现的,虽然使用的术语是一样的。) 当一个事件发生时,如按一下鼠标键,所按的构件会“发出”适当的信号。这就是 GTK 的工作机制。有所有构件都继承的信号,如 "destroy",有构件专有的信号,如开关 (toggle) 按钮发出的 "toggled" 信号。
要使一个按钮执行一个动作,我们需设置信号和信号处理函数之间的连接。可以这样使用函数来设置连接:
gulong g_signal_connect( gpointer *object, const gchar *name, GCallback func, gpointer func_data ); |
第一个参数是要发出信号的构件,第二个参数是你想要连接的信号的名称,第三个参数是信号被捕获时所要调用的函数,第四个参数是你想传递给这个函数的数据。
第三个参数指定的函数叫做回调函数,一般为下面的形式:
void callback_func( GtkWidget *widget, gpointer callback_data ); |
第一个参数是一个指向发出信号的构件的指针,第二个参数是一个指向数据的指针,就是上面 g_signal_connect() 函数的最后一个参数传进来的数据。
注意上面回调函数的声明只是一般的形式,有些构件的特殊信号会用不同的调用参数。
另一个在 helloworld 示例中使用的调用,是:
gulong g_signal_connect_swapped( gpointer *object, const gchar *name, GCallback func, gpointer *slot_object ); |
g_signal_connect_swapped() 和 g_signal_connect() 相同,只是回调函数只用一个参数,一个指向 GTK 对象的指针。所以当使用这个函数连接信号时,回调函数应该是这样的形式
void callback_func( GtkObject *object ); |
这个对象通常是一个构件。然而我们一般不用函数 g_signal_connect_swapped() 设置回调。它们常用来调用一个只接受一个单独的构件或者对象作为参数的 GTK 函数,如同我们的 helloworld 示例中那样。
拥有两个函数来设置信号连接的目的只是为了允许回调函数有不同数目的参数。GTK 库中许多函数仅接受一个单独的构件指针作为其参数,所以对于这些函数你要用 g_signal_connect_swapped(),然而对你自己定义的函数,你可能需要附加的数据提供给你的回调函数。
除有前面描述的信号机制外,还有一套 events 反映 X 事件机制。回调函数可以与这些事件连接。这些事件是:
- event
- button_press_event
- button_release_event
- scroll_event
- motion_notify_event
- delete_event
- destroy_event
- expose_event
- key_press_event
- key_release_event
- enter_notify_event
- leave_notify_event
- configure_event
- focus_in_event
- focus_out_event
- map_event
- unmap_event
- property_notify_event
- selection_clear_event
- selection_request_event
- selection_notify_event
- proximity_in_event
- proximity_out_event
- visibility_notify_event
- client_event
- no_expose_event
- window_state_event
为了连接一个回调函数到这些事件之一,你使用函数 g_signal_connect(),像前面介绍的一样,用上面事件名之一作为 name 参数。事件的回调函数与信号的回调函数有一点点不同:
gint callback_func( GtkWidget *widget, GdkEvent *event, gpointer callback_data ); |
GdkEvent 是一个 C 联合结构,它的类型依赖于上述事件中的哪个事件发生了。为了让我们得知发生了哪个事件,每个可能的类型都有一个 type 成员来反映发生的事件。事件结构的其它部分将依赖于这个事件的类型。类型的可能的值有:
GDK_NOTHING GDK_DELETE GDK_DESTROY GDK_EXPOSE GDK_MOTION_NOTIFY GDK_BUTTON_PRESS GDK_2BUTTON_PRESS GDK_3BUTTON_PRESS GDK_BUTTON_RELEASE GDK_KEY_PRESS GDK_KEY_RELEASE GDK_ENTER_NOTIFY GDK_LEAVE_NOTIFY GDK_FOCUS_CHANGE GDK_CONFIGURE GDK_MAP GDK_UNMAP GDK_PROPERTY_NOTIFY GDK_SELECTION_CLEAR GDK_SELECTION_REQUEST GDK_SELECTION_NOTIFY GDK_PROXIMITY_IN GDK_PROXIMITY_OUT GDK_DRAG_ENTER GDK_DRAG_LEAVE GDK_DRAG_MOTION GDK_DRAG_STATUS GDK_DROP_START GDK_DROP_FINISHED GDK_CLIENT_EVENT GDK_VISIBILITY_NOTIFY GDK_NO_EXPOSE GDK_SCROLL GDK_WINDOW_STATE GDK_SETTING |
所以,连接一个回调函数到这些事件之一,我们会这样用:
g_signal_connect (G_OBJECT (button), "button_press_event", G_CALLBACK (button_press_callback), NULL); |
这里假定 button 是一个按钮构件。现在,当鼠标位于按钮上并按一下鼠标时,函数 button_press_callback() 会被调用。这个函数应该声明为:
static gint button_press_callback( GtkWidget *widget, GdkEventButton *event, gpointer data ); |
注意,我们可以把第二个参数类型声明为 GdkEventButton,因为我们知道哪个类型的事件会发生。
这个函数的返回值指示这个事件是否应该由 GTK 事件处理机制做进一步的传播。返回 TRUE 指示这个事件已经处理了,且不应该做进一步传播。返回 FALSE 继续正常的事件处理。