GTK+编程概述
目标
- 什么是GTK?
- 怎样做一个GTK应用程序
- 事件(Events)、信号(Signals)、回调函数(Callbacks)
- 组件(widgets)
引言
GIMP工具包(GTK+)最初被设计为一个光栅图形编辑器叫做GNU图像处理(GIMP)。GTK+作为两个最流行的Linux桌面环境GNOME和Xfce的默认图形工具包,。虽然它最初是用在Linux操作系统上,GTK+已经扩展到支持其他类似Unix的操作系统:微软Windows、Solaris、BeOS、Mac OS X等。GTK+是全部用c语言写的,和GTK+软件的大部分也是用C语言写的。幸运的是,有许多语言绑定到GTK+上,让您在您的首选语言如C++、Python、PHP、Ruby、Perl、C#、Java 中使用GTK+。我们可以通过使用GTK+在X窗口系统上进行图形界面程序的开发。
GTK+体系结构
GTK+建立在大量的其他库之上,如图1所示:
图1:GTK+体系结构
- Glib (-lglib) - 提供了底层的数据结构、类型、线程支持、事件循环和动态加载
- GObject (-lgobject) - 完全用C语言而没有用C++实现了面向对象系统
- Pango (-lpango)- 支持文本的渲染和布局
- ATK(Accessability Toolkit) - 帮助创建易使用的应用程序并允许用户运行带有屏幕渲染和其他工具的应用程序
- GDK(GIMP Drawing Kit) (-lgdk) - 处理Xlib上的底层图形渲染
- GdkPixbuf - 帮助处理GTK+程序里的图像,用于加载图像和维护图像缓存
- Xlib (-lX11) - 提供位于Linux和Unix系统上底层绘图
- GModule (-lgmodule)- 动态加载插件的便捷方法
Hello world
现在,我们将写一个简单的GTK+程序。它并不做太多,但它展现了linux上的GUI编程的基本结构。如图2所示。即使你按下'X'按钮,程序也不会被杀死。因为此程序并没有写代码去销毁程序,所以你可以使用Ctrl+C来杀死此程序。
图2:Hello world
用gedit新建hello.c文档,内容如下:
#include <gtk/gtk.h> int main (int argc, char *argv[]) { GtkWidget *window; /* Initialize the GTK+ and all of its supporting libraries. */ gtk_init (&argc, &argv); /* Create a new window, give it a title and display it to the user. */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Hello World"); gtk_widget_show (window); /* Hand control over to the main loop. */ gtk_main (); return 0; }
<gtk/gtk.h>包含了GTK+的所有可使用的组件(widgets)、变量、函数、结构以及GTK+所依赖的其他库的头文件。当你使用GTK+编写程序时,除非你想用一些高级的应用,否则仅仅使用<gtk/gtk.h>就足够了。
下面的代码声明了一个GtkWidget对象的指针变量
GtkWidget *window;
一个GTK+程序是由组件(widgets)组成。元素如窗口(windows)、按钮(buttons)、滑动条(scrolibars)都称为组件(widgets).稍后我们将详细地介绍组件(widgets).
接下来的函数初始化GTK+
/* Initialize GTK+ and all of its supporting libraries. */ gtk_init (&argc, &argv);
靠调用gtk_init(), 所有初始化的工作都将自动执行。它将设置GTK+环境变量、获取GDK显示、贮备Glib主事件(main event)循环和基本的信号(signal)处理。在其他函数调用GTK+库之前调用gtk_init()是极其重要的。否则,你的程序将不能恰当的运行,并且可能会崩溃。
接下来的代码创建了一个新的GtkWindow对象并设置了一些属性:
/* Create a new window, give it a title and display it to the user. */ window = gtk_window_new (GTK_WINDOW_TOPLEVEL); gtk_window_set_title (GTK_WINDOW (window), "Hello World"); gtk_widget_show (window);
gtk_window_new()创建了一个GtkWindow对象,它的默认宽度和高度都为200像素(pixels)。我们传递了GTK_WINDOW_TOPLEVEL给gtk_window_new()。 这使GTK+创建了一个新的顶层窗口(top-level window)。顶层窗口使用窗口管理器(window manager)装饰,有一个边界框,并且允许它们自己被窗口管理器所放置。反之,你也可以使用GTK_WINDOW_POPUP来创建一个弹出窗口。弹出窗口是来作为提示(tooltips)和菜单的。在GtkWindowType枚举型里,仅仅可使用GTK_WINDOW_TOPLEVEL和GTK_WINDOW_POPUP这两个元素。
gtk_window_set_title()函数设置了标题栏和任务栏来显示窗口的标题"Hello World"。第一参数是为GtkWindow对象,第二个是你想显示的字符串。
gtk_widget_show()函数告诉GTK+去设置组件(widget)可见。当你调用gtk_widget_show()时,组件(widget)可能不会立即可见,因为GTK+让组件(widget)排队直到它被绘在屏幕上之前的所有预处理都已完成。
下面的函数是主循环,起着控制和开始处理事件的作用。
/* Hand control over to the main loop. */ gtk_main ();
gtk_main()函数将一直运行直到你调用gtk_main_quit()或程序终止。这应该是main()函数里最后的GTK+函数。在GTK+里,信号(signals)和回调函数(callback functions)被用户的行为所触发,这些行为诸如按钮点击、异步输入输出事件、可编程的超时等。稍后将介绍信号、回调机制和事件。
目前为止,我们已经写了一个简单的GTK+程序,学习了GTK+的体系结构,理解了每个部分的意思是什么以及它是如何运作的。现在,我们将编译我们所写的代码来让它可执行。
命令行编译代码如下所示:
gcc hello.c -o hello ‘pkg-config --cflags --libs gtk+-2.0‘
注意: pkg前面的点 和 gtk+-2.0后面的点.这个点是 Tab 上面的那个点, 也是Q上面的那个1的左边。千万别弄错哦。
事件、信号、回调(Events,Signals,Callbacks)
GTK+是一个依赖于事件、信号、回调机制的系统。一个事件是由X窗口系统发出的消息。当用户执行一些动作如点击鼠标或敲击键盘,事件就被发送到你的程序,它将被GLib提供的信号系统所解释。一个信号是对一个事件的反应,这是由一个GtkObject发出的。当事件到达一个组件(widget),信号就发生了。当信号发出了,你就可以告诉GTK+运行一个函数。这就是所谓的回调函数。
注:一个GTK+信号是UNIX信号分离的
在我们初始化了我们的用户接口后,控制权就转移到了gtk_main()函数里了,这个函数休眠直到一个信号(signal)发出。当那个动作发生并且信号发出或者你已经明确发出了信号,回调函数将被调用。你也能阻止信号被发出。
g_signal_connect()函数连接了信号,也就是将信号映射到处理函数了.
gulong g_signal_connect ( gpointer object, const gchar *signal_name, GCallback handler, gpointer data);
这儿有4个参数,object是需要监听信号的组件(widget);signal_name是你要监听的信号名称;handler是回调函数,需要用G_CALLBACK()转换,当信号发出时,回调函数将被调用;data允许你发送一个指针给回调函数(应该是用来给回调函数传参数的吧?)。g_signal_connect()的返回值是信号的handler标示符。
以前的回调连接函数式gtk_signal_connect(),这个函数不应再使用,因为它已经被g_signal_connect()替代了。
-
回调函数
当组件(widget)上的信号(signal)发出时,在g_signal_connect()中指定的回调函数将被调用。回调函数有如下的形式,它是被程序员所命名的。
static void callback_function ( GtkWidget *widget, ... /* other possible arguments */ ... , gpointer data);
第一个参数widget是来自于g_signal_connect()中指定的对象。为了信号被产生,它必须总是转化为widget类型。这儿也有其它可能的参数呈现在中间,尽管这并不总有中间的参数。data参数相应于g_signal_connect()的最后的参数,它是gpointer类型。由于data被传递作为一个void指针,所有你能转为data类型为你想要的类型。
-
hello world改进
我们将扩展已经实现的简单的‘Hello world’例子。扩展是将回调函数连接到窗口信号,也就是将窗口信号映射到回调函数,程序能够结束自己,可以正常退出,你就不用使用Ctrl+C了。我们将通过这个例子来回顾信号、回调和事件。
#include <gtk/gtk.h> void destroy(GtkWidget *widget, gpointer data) { gtk_main_quit(); } int main(int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Hello World!"); gtk_widget_show (window); /* Connect the main window to the destroy */ g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(destroy), NULL); gtk_widget_show(window); gtk_main(); return 0; }
组件(Widgets)
组件是一个GUI程序的基本建造块。GTK+中的组件使用GObject层次系统,它允许你从那些已经存在的组件中派生出新组件。子组件从它们的父亲、爷爷等那继承了属性、函数和信号。GTK+中有大量的组件,所以在这儿很难都详细介绍,我将试图介绍工程上一些有用的组件,你也可他通过后面的Web链接的GTK+ API中去查询其他组件的用法。
-
窗口组件(Window widget)
GtkWindow是所有GTK+程序的基本元素。这儿有好几十个GtkWindow API调用,但现在只介绍值得注意的一些函数。
/* creates a new, empty window in memory */ GtkWidget* gtk_window_new (GtkWindowType type); /* changes the text of the tile bar by informing the window manager of the request */ void gtk_window_set_title (GtkWindow *window, const gchar *title); /* controls the position of the initial placement onscreen */ void gtk_window_set_position (GtkWindow *window, GtkWindowPosition position); /* sets the size of the window onscreen in GTK+ drawing units */ void gtk_window_set_default_size (GtkWindow *window, gint width, gint height); /* forces a resize of the window once it’s onscreen */ void gtk_window_resize (GtkWindow *window, gint width, gint height); /* sets whether the user can resize a window */ void gtk_window_set_resizable (GtkWindow *window, gboolean resizable); /* asks to maximize window, so that it becomes full-screen */ void gtk_window_maximize (GtkWindow *window);
-
容器组件(Container widgets)(用于布局)
容器类的主要目的是允许一个父组件去包含一个或多个孩子组件。我们能够用被称为布局容器的不可见组件去组织我们的组件。
-
GtkBox 组件
GtkBox是一个抽象的容器组件,它允许多个子组件放在一个维度的矩形区域。有两种类型的盒子:GtkVBox和BtkHBox。
-
GtkVBox组件
这个组件是一个单行水平排列的盒子组件
-
GtkVBox组件
这个组件是一个单列垂直排列的盒子组件
-
例子
在这个例子中,我们将实现包含hbox和vbox组件的程序
图3:layout.c
#include <gtk/gtk.h> void closeApp(GtkWidget *widget, gpointer data) { gtk_main_quit(); } int main( int argc, char *argv[]) { GtkWidget *window; GtkWidget *label1, *label2, *label3; GtkWidget *hbox; GtkWidget *vbox; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Layout"); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 300, 200); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(closeApp), NULL); label1 = gtk_label_new("Label 1"); label2 = gtk_label_new("Label 2"); label3 = gtk_label_new("Label 3"); hbox = gtk_hbox_new(TRUE, 5); vbox = gtk_vbox_new(FALSE, 10); gtk_box_pack_start(GTK_BOX(vbox), label1, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), label2, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 5); gtk_container_add(GTK_CONTAINER(window), hbox); gtk_widget_show_all(window); gtk_main(); return 0; }
程序中有3个labels和一个vbox和一个hbox。vbox容纳了label1和label2。hbox容纳了vbox和label3。最终hbox被放置在窗口window中
这些函数分别创建了一个新的hbox和vbox
hbox = gtk_hbox_new(TRUE, 5); vbox = gtk_vbox_new(FALSE, 10);
第一个参数代表所有子组件一致。如果设为TRUE,所有子组件有同样的空间分配。第二个是子组件之间的间隙大小。
这个函数添加子组件到box中。
gtk_box_pack_start(GTK_BOX(vbox), label1, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), label2, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), vbox, FALSE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox), label3, FALSE, FALSE, 5);
void gtk_box_pack_start (GtkBox *box, GtkWidget *child, gboolean expand, gboolean fill, guint padding);
这是函数的原型。第一个参数是box对象,所有的子组件将被包装在那里。第二个意味着一个子组件被加到box中。后三个参数expand、fill和padding是与子组件间的空间有关。
这个函数添加组件到容器里。
gtk_container_add(GTK_CONTAINER(window), hbox);
这个函数被用于简单容器如GtkWindow、GtkFrame和GtkButton。第一个参数是一个容器(container),第二个是一个组件(widget).
-
基本组件(Basic widgets)
-
GtkLabel组件
这个组件用来标示其它组件。当然,它们也能用来创建大块的不可编辑、格式化的、封装的文本。
-
GtkButton 组件
这个组件是一个特别类型的容器,它把它的子组件转变成可点击的实体。它仅仅可包装一个子组件。这人也有许多从GtkButton派生的组件如GtkToggleButton、GtkCheckButton、GtkRadioButton。
-
GtkEntry组件
这个组件是一个单行文本输入组件,它一般用来输入简单的文本信息。这是一个通用的方式实现的,以便它可以被塑造成适合多种类型的解决方案。它可用于文本输入、输入密码、甚至数据的选择。
-
例子
图4:entry.c
这个程序处理了几个组件和信号、回调,就像你从上面所见到的一样。
#include <gtk/gtk.h> #include <stdio.h> #include <string.h> const char *password = "secret"; void closeApp(GtkWidget *window, gpointer data) { printf("Destroy\n"); gtk_main_quit(); } void button_clicked(GtkWidget *button, gpointer data) { const char *password_text = gtk_entry_get_text(GTK_ENTRY((GtkWidget *)data)); if(strcmp(password_text, password) == 0) printf("Access granted!\n"); else printf("Access denied!\n"); } int main(int argc, char *argv[]) { GtkWidget *window; GtkWidget *username_label, *password_label; GtkWidget *username_entry, *password_entry; GtkWidget *ok_button; GtkWidget *hbox1, *hbox2; GtkWidget *vbox; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Basic Widgets"); gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); gtk_window_set_default_size(GTK_WINDOW(window), 200, 200); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(closeApp), NULL); username_label = gtk_label_new("Login: "); password_label = gtk_label_new("Password: "); username_entry = gtk_entry_new(); password_entry = gtk_entry_new(); gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE); ok_button = gtk_button_new_with_label("OK"); g_signal_connect(G_OBJECT(ok_button), "clicked", G_CALLBACK(button_clicked), password_entry); hbox1 = gtk_hbox_new(TRUE, 5); hbox2 = gtk_hbox_new(TRUE, 5); vbox = gtk_vbox_new(FALSE, 10); gtk_box_pack_start(GTK_BOX(hbox1), username_label, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox1), username_entry, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox2), password_label, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(hbox2), password_entry, TRUE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox1, FALSE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), hbox2, FALSE, FALSE, 5); gtk_box_pack_start(GTK_BOX(vbox), ok_button, FALSE, FALSE, 5); gtk_container_add(GTK_CONTAINER(window), vbox); gtk_widget_show_all(window); gtk_main(); return 0; }
在这个例子中,我们尽力使用我们所学的组件和回调函数
这个子函数起着检查输入的密码是否正确的角色。
void button_clicked(GtkWidget *button, gpointer data) { const char *password_text = gtk_entry_get_text(GTK_ENTRY((GtkWidget *)data)); if(strcmp(password_text, password) == 0) printf("Access granted!\n"); else printf("Access denied!\n"); }
我们像以往一样开发回调函数。然而,在函数中我们得到GtkEntry组件作为一个参数去获取密码信息。
gtk_label_new()创建一个新标签。
username_label = gtk_label_new("Login: ");
这个函数得到一个文本作为一个参数。
下面这些函数与entry组件有关。
password_entry = gtk_entry_new();
gtk_entry_set_visibility(GTK_ENTRY(password_entry), FALSE);
gtk_entry_get_text(GTK_ENTRY((GtkWidget *)data));
gtk_entry_new()函数创建一个新的entry组件。在创建了entry组件后,我们可以通过gtk_entry_set_visibility()设置entry组件的可见性,它有两个参数,entry和visible。最后,我们能通过gtk_entry_get_text()从entry组件获取文本信息。
下面这个函数创建了一个新的带有标签的按钮。
ok_button = gtk_button_new_with_label("OK");
-
GtkImage组件
这个组件被用来显示一个图片。
-
例子
图5:image.c
#include <gtk/gtk.h> int main( int argc, char *argv[]) { GtkWidget *window, *image; 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), 230, 150); gtk_window_set_title(GTK_WINDOW(window), "Image"); gtk_window_set_resizable(GTK_WINDOW(window), FALSE); gtk_container_set_border_width(GTK_CONTAINER(window), 2); image = gtk_image_new_from_file("pic/60cm.gif"); gtk_container_add(GTK_CONTAINER(window), image); g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL); gtk_widget_show_all(window); gtk_main(); return 0; }
下面的函数从一个文件创建了一个新的image对象。
image = gtk_image_new_from_file("pic/60cm.gif");
我们能加载各种图片格式,如JPG、BMP、GIG、TIG、PNG等。
-
例子
这个例子使用不同的方法来加载一个图片。不像以前的例子,一组二进制数据将被用来作为图片源。
图6:image2.c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <gtk/gtk.h> #define CAMERA_WIDTH 128 #define CAMERA_HEIGHT 128 int loadImage(unsigned char *data) { printf("Got image!\n"); GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, FALSE, 8, CAMERA_WIDTH, CAMERA_HEIGHT, CAMERA_WIDTH * 3, NULL, NULL); gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf); gtk_widget_queue_draw(image); printf("Loaded\n"); return 0; } unsigned char *rgbImage; GtkWidget *image; int main( int argc, char *argv[]) { GtkWidget *window; gtk_init(&argc, &argv); window = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_window_set_title(GTK_WINDOW(window), "Image2"); gtk_window_set_resizable(GTK_WINDOW(window), FALSE); gtk_container_set_border_width(GTK_CONTAINER(window), 2); image = gtk_image_new(); gtk_container_add(GTK_CONTAINER(window), image); g_signal_connect(G_OBJECT(window),"destroy",G_CALLBACK(gtk_main_quit),NULL); gtk_widget_show_all(window); loadImage(rgbImage); gtk_main(); return 0; }
这个函数创建了一个新的空的image对象。
image = gtk_image_new();
我们使用三个函数从二进制数据里生成一个image文件。
GdkPixbuf *pixbuf = gdk_pixbuf_new_from_data(data, GDK_COLORSPACE_RGB, FALSE, 8, CAMERA_WIDTH, CAMERA_HEIGHT, CAMERA_WIDTH * 3, NULL, NULL); gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf); gtk_widget_queue_draw(image);
gdk_pixbuf_new_from_data()函数从内存中创建了一个新的GdkPixbuf。
gtk_image_set_from_pixbuf()函数设置了Pixbuf数据到那个image对象。
gtk_widget_queue_draw()函数通知X窗口:一个组件的整个区域需要更新。最终,一个image组件将重绘一个图片。
有用的链接
- The GTK+ Project
- GTK+ Reference Manual
- GDK Reference Manual
- GObject Reference Manual
- GTK+ tutorial