GTK+:helloword详解
系列文章
- GTK+:搭建开发环境
- GTK+:helloword详解【当前文章】
麻雀虽小,五张俱全。本文通过一个非常简单的helloword,来探寻到GTK+应用程序的基本结构。后面我们再一步一步地深入了解GTK+的方方面面。如果您还没有GTK+的开发环境,可以参考一下该系列的上一篇文章。
源码
在上一篇文章中,写了一个helloword来验证GTK+的开发环境是否正常。这一篇,我们一起来详细看一下这个程序的细节。代码如下:
#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;
window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), "Window");
gtk_window_set_default_size(GTK_WINDOW(window), 480, 360);
gtk_container_set_border_width(GTK_CONTAINER(window), 28);
button = gtk_button_new_with_label("Hello World");
g_signal_connect(button, "clicked", G_CALLBACK(print_hello), NULL);
gtk_container_add(GTK_CONTAINER(window), button);
gtk_widget_show_all(window);
}
int main(int argc, char **argv)
{
GtkApplication *app;
int status;
app = gtk_application_new("org.gtk.helloword", G_APPLICATION_FLAGS_NONE);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}
头文件
首当其冲的就是include一下GTK+的头文件。如果你去gtk的头文件目录msys64\mingw64\include\gtk-3.0\gtk
中看一下就会发现,gtk有很多很多的头文件。在以后写程序的过程中,我们会经常跟这些头文件打交道。比如你实现一个窗口,就一定会用到gtkwindow.h
,实现菜单就会用到gtkmenu.h
、gtkmenubar.h
、gtkmenubutton.h
...不过,我们不会也不允许直接include这些头文件,而是直接include gtk.h
。这个头文件再把其他的头文件include进来。
入口函数
然后,我们定义了三个函数,main
就不用多讲了,这是C/C++语言所写应用程序的入口函数。需要注意的是,通过main入口的应用程序其实是控制台界面的。虽然我们写的gtk程序是图形界面的,但是,在Windows下,依旧会出现一个console窗口。这个需要通过用WinMain入口函数解决。代码如下:
#ifdef WIN32
#include <windows.h>
extern "C" int APIENTRY WinMain(HINSTANCE, HINSTANCE, LPSTR /*cmdParamarg*/, int /* cmdShow */) {
int argc;
wchar_t **argvW = CommandLineToArgvW(GetCommandLineW(), &argc);
if (!argvW)
return -1;
char **argv = new char *[argc + 1];
for (int i = 0; i < argc; ++i) {
auto iSize = WideCharToMultiByte(CP_ACP, 0, argvW[i], -1, nullptr, 0, nullptr, nullptr);
argv[i] = (char *) malloc(iSize * sizeof(char));
WideCharToMultiByte(CP_ACP, 0, argvW[i], -1, argv[i], iSize, nullptr, nullptr);
}
argv[argc] = nullptr;
LocalFree(argvW);
const int exitCode = main(argc, argv);
for (int i = 0; i < argc && argv[i]; ++i) {
free(argv[i]);
}
delete[] argv;
return exitCode;
}
#endif
上面这个函数,就是windows下图形程序的入口函数。我们这儿仅仅是用它做入口,然后,再调用控制台入口函数main。所以,整个函数被包在WIN32宏里面,仅仅在Windows下使用。
当然,并不是仅仅定义这个函数就可以了,还需要在编译时做一点点工作。
target_link_libraries(${PROJECT_NAME} -Wl,-subsystem,windows)
很简单,就是告诉编译器,我们使用的是windows
子系统,程序启动时会首先调用WinMain
函数。如果不指定就是console
,程序启动时会首先调用main
函数。
在main
函数中,首先需要创建一个GtkApplication
对象。不过,需要留意的是GTK+是C语言库,没有类和对象的语法。所以,GTK提供了gtk_application_new
函数来创建GtkApplication
对象。后面,调用g_signal_connect
、g_application_run
等函数时,第一个参数就是对象指针。其实,gtk_application_new
就是C++里面new+类构造函数,而对象指针就是C++里面的this指针。虽然GTK时C语言写的,没有类和对象,但是,还是使用了类和对象的概念的。关于gtk_application_new
的详细用法可以参考:GTK文档。这儿就不再深入讨论了。
信号
接下来我们通过,g_signal_connect将函数activate
关联到了GtkApplication
对象的activate
信号中。换句话说,当发生activate
信号时,gtk就会调用咱们定义的函数activate
。
这儿出现了一个新的概念---信号
。信号是图形编程里绕不开的概念。包括Qt、MFC、GTK+在内的绝大多数图形界面库,都是使用的事件驱动
。所谓,事件驱动就是,程序中的函数,是通过相应事件的方式来执行的。而事件是否用户、系统或者程序自身发起的。比如用户点击鼠标这个动作。鼠标将电信号转化为数字信号,通过USB、蓝牙或者wifi,传递到操作系统内核驱动中,操作系统内核驱动在将这个信号传递给X11等图形子系统,接着X11处理这个信号,判断需要将这个信号传递给窗口管理器还是某个应用程序窗口。如果是传递给应用程序窗口,就会被GTK+的主循环
程序接收到。GTK+的主循环
程序再将这个信号传递到该信号的处理函数。于是,咱们写的消息处理函数就被调用执行,完成了事件响应。
当GtkApplication
对象被激活时就会触发activate
信号,于是,就会调用咱们关联的信号处理函数activate
了。那么,啥时候会触发activate
信号呢。文档中说是在g_application_activate
函数中。单独看文档会很困惑,咱们的helloword程序中并没有调用这个函数。其实,这个函数是在GTK+内部被调用的。对我们而言,需要调用的是g_application_run
函数。
GtkApplication
有很多信号,具体列表可以在文档。
主循环
我们可以将g_application_run
函数理解GTK程序真正的入口函数。这个函数比较复杂。当前,我们只需要记得,它解析了命令行参数,然后实现了一个事件主循环。关于命令行参数,这儿先不展开讲。咱们先聊一聊,GTK+的事件主循环。
每个图形应用程序都是为了响应用户的操作而设计的。当用户不做任何动作时,应用程序只会做两件事件——后台任务和等待用户操作。如时钟事件、工作线程等等,都是后台任务。界面的程序的主要任务时等待用户的操作,比如点击鼠标、键盘等等。在g_application_run
函数中就会有一个类似的代码块:
while(true){
等待信号;
获取信号;
分发并处理信号;
}
这个循环是所有图形程序的核心处理逻辑。在g_application_run
中,就会调用GTK+文档中说的g_application_activate
函数,这个函数触发了activate
信号。再由事件主循环块获取到并处理。
我们一直没有解释事件
的概念。我查阅了不少资料,并没有找到事件的详细定义。通常,我们事件
理解为信号
及信号处理函数组成的整体。换句话说,信号是个名词,事件是个动作,用来描述信号及其处理过程。
信号处理函数
前面我们提到函数activate
是g_application_run
的activate
信号处理函数。信号处理函数可以有多个,当信号发生时,GTK+会依次调用这些信号处理函数。一个信号处理函数也关联多次,当信号发生时,GTK+也会多次调用该信号处理函数。
函数print_hello
也是一个信号处理函数。不同的是,函数print_hello
是button对象的信号clicked
处理函数。
在函数activate
中,首先使用gtk_application_window_new
创建了一个应用级别的窗口。单纯讲创建窗口,一般使用的是gtk_window_new
。但是,在GTK+框架中,至少要有一个应用级别的窗口,否则主循环就会自动推出。
然后,通过gtk_window_set_title
设置窗口标题,通过gtk_window_set_default_size
设置窗口的默认大小,通过gtk_container_set_border_width
设置容器内容的边界。注意:这个设置的是容器内边界,而不是窗口边框。gtk_window_set_title
和gtk_window_set_default_size
都是GtkWindow
对象的操作函数。类似的操作函数还有很多,具体可以参考官方文档。另外,通过官方文档我们可以看到它的继承关系为:
所以,我们也可以调用GtkContainer
、GtkWidget
等父类的操作函数。不过,因为是C语言,我们需要通过GTK_CONTAINER
、GTK_WIDGET
将GtkWindow
指针对应操作函数需要的指针。
接下来是通过gtk_button_new_with_label
创建一个带标签的按钮,然后,将函数print_hello
关联到按钮的clicked
信号上面。这样当用户点击按钮时就会调用函数print_hello
。
不过,按钮只是创建了,还需要通过函数gtk_container_add
将其添加到窗口中,否则,按钮不会被显示出来。但是,仅仅添加到窗口中还不够,因为,GTK+中所有的控件默认都是不可见的,我们还需要通过调用函数gtk_widget_show_all
将窗口中所有的子控件显示出来。在我们这个例子中函数gtk_widget_show_all
可以用下面的代码代替:
gtk_widget_show(GTK_WIDGET(button));
gtk_widget_show(GTK_WIDGET(window));
如果你并不想把窗口中所有的子控件都显示出来,就可以通过这种方法。
系列文章
- GTK+:搭建开发环境
- GTK+:helloword详解【当前文章】