参考资料
https://github.com/ToshioCP/Gtk4-tutorial/blob/main/gfm
第一个程序
1. 创建一个程序
#include <gtk/gtk.h>
int main (int argc, char **argv)
{
GtkApplication *app = gtk_application_new ("com.hello", G_APPLICATION_DEFAULT_FLAGS);
int stat = g_application_run (app, argc, argv);
g_object_unref (app);
return stat;
}
这样就是最简程序了:创建一个程序并运行它。
其中"com.hello"
是app的id,规则就是域名倒过来,和java的包名规则一样。
然而,编译运行后你会看到这样的警告:
(main:34508): GLib-GIO-WARNING **: 17:21:46.809: Your application does not implement g_application_activate() and has no handlers connected to the 'activate' signal. It should do one of these.
不用急,程序至少已经正常运行了。
这个警告告诉我们:
- You didn’t implement
g_application_activate()
- And no handlers connected to the
activate
signal - You should do one of these.
这两个错误原因都与信号有关。因此,先解释一下。
发生某些事情时会发出信号,比如创建/销毁一个窗口。 当程序被activated时(被激活),会发出activate
信号。(激活并不等于启动,但是可以暂且认为相同)
如果信号连接到一个函数,这个函数被称为信号处理程序。
流程是这样的:
- 有事情发生。
- 如果它与某个信号有关,那么该信号就会被发射。
- 如果信号已经预先连接到处理程序,则调用处理程序。
信号在对象中定义。例如,activate
信号属于GApplication
对象,它是GtkApplication
对象的父对象。
GApplication
对象是 GObject
对象的子对象。GObject
是所有对象层次结构中的顶级对象。
GObject -- GApplication -- GtkApplication
<---parent --->child
子对象从其父对象继承信号、函数、属性等。所以,GtkApplication 也有activate
信号。
2. 处理activate信号
#include <gtk/gtk.h>
static void
app_activate (GApplication *app, gpointer *user_date)
{
g_print ("hello world\n");
}
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("com.hello", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
int stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
g_signal_connect()
有4个参数:
- 信号所属的实例。
- 信号的名称。
- 处理函数(也称为回调),需要用
G_CALLBACK
来强制转换. - 要传递给处理程序的数据。如果不需要数据,则应给出 NULL。
3. GtkWindow
前面还是没有图形话界面。
所以这里加上:
#include <gtk/gtk.h>
static void
on_app_activate (GApplication *app, gpointer user_data)
{
GtkWidget *win = gtk_window_new();
gtk_window_set_application (GTK_WINDOW (win), GTK_APPLICATION (app));
gtk_window_present (GTK_WINDOW (win));
}
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("com.hello", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (G_APPLICATION (app), "activate", G_CALLBACK (on_app_activate), NULL);
int stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
Widget 是一个抽象的概念,包括所有的GUI 界面,如窗口、对话框、按钮、多行文本、容器等。GtkWidget 是一个基础对象,所有 GUI 对象都从中派生。
parent <-----> child
GtkWidget -- GtkWindow
GtkWindow
在其对象的顶部包含 GtkWidget
。
该函数gtk_window_new定义如下。
GtkWidget *
gtk_window_new (void);
根据这个定义,它返回一个指向 GtkWidget
的指针,而不是 GtkWindow
。它实际上创建了一个新的 GtkWindow
实例(不是 GtkWidget
),但返回一个指向 GtkWidget
的指针。但是,该指针指向 GtkWidget
,同时它也指向其中包含 GtkWidget
的 GtkWindow
。
如果要用作win
指向 GtkWindow
类型实例的指针,则需要对其进行强制转换。
(GtkWindow *) win
它有效,但通常不使用。相反,使用宏GTK_WINDOW
。
GTK_WINDOW (win)
建议使用宏,因为它不仅会转换指针,还会检查类型。
您可以使用gtk_widget_set_visible (win, true)
而不是gtk_window_present
。但是这两者的行为是不同的。假设屏幕上有win1和win2两个窗口,win1在win2的后面。两个窗口都可见。该函数gtk_widget_set_visible (win1, true)
不执行任何操作,因为 win1 已经可见。因此,win1 仍然落后于 win2。另一个函数gtk_window_present (win1)
将 win1 移动到窗口堆栈的顶部。因此,如果你想呈现窗口,你应该使用gtk_window_present
.
两个函数gtk_widget_show
,gtk_widget_hide
自 GTK 4.10 起已弃用。你应该改用gtk_widget_set_visible
。
4. GtkApplicationWindow
GtkApplicationWindow
是 GtkWindow
的子对象。它有一些额外的功能可以更好地与 GtkApplication
集成。建议将其用作应用程序的顶层窗口,而不是 GtkWindow
。
1 static void
2 app_activate (GApplication *app, gpointer user_data) {
3 GtkWidget *win;
4
5 win = gtk_application_window_new (GTK_APPLICATION (app));
6 gtk_window_set_title (GTK_WINDOW (win), "pr4");
7 gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
8 gtk_window_present (GTK_WINDOW (win));
9 }
当创建 GtkApplicationWindow
时,需要将 GtkApplication
实例作为参数提供。然后它会自动连接这两个实例。所以不需要再调用gtk_window_set_application
。
GtkLabel, GtkButton, GtkBox
1.GtkLabel
显示一个标签
#include <gtk/gtk.h>
static void
app_activate (GApplication *app)
{
GtkWindow *win = GTK_WINDOW (gtk_application_window_new (GTK_APPLICATION (app)));
gtk_window_set_title (win, "hello window");
gtk_window_set_default_size (win, 400, 300);
GtkLabel *label = GTK_LABEL (gtk_label_new ("hello label"));
gtk_window_set_child (win, GTK_WIDGET (label));
gtk_window_present (win);
}
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("top.hello", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
int stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
2. GtkButton
点击按钮关闭窗口。
#include <gtk/gtk.h>
static void
click_btn (GtkButton *btn, GtkWindow *win)
{
gtk_window_destroy (win);
}
static void
app_activate (GtkApplication *app)
{
GtkWindow *win = GTK_WINDOW (gtk_application_window_new (app));
gtk_window_set_title (win, "hello window");
gtk_window_set_default_size (win, 400, 300);
GtkWidget *btn = gtk_button_new();
gtk_button_set_label (GTK_BUTTON (btn), "button");
gtk_window_set_child (win, btn);
g_signal_connect (btn, "clicked", G_CALLBACK (click_btn), win);
gtk_window_present (win);
}
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("top.hello", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
int stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
3. GtkBox
点击hello button
会变为hello button!!!
,点击close
会关闭。
这里需要明白的是,一个window只能有一个孩子,如果要添加多个组件,就要用布局容器。
#include <gtk/gtk.h>
#include <string.h>
static void
btn_1_clicked (GtkButton *btn)
{
const char *text = gtk_button_get_label (btn);
if (strcmp (text, "hello button") == 0)
gtk_button_set_label (btn, "hello button !!!");
else
gtk_button_set_label (btn, "hello button");
}
static void
btn_2_clicked (GtkButton *btn, GtkWindow *win)
{
gtk_window_destroy (win);
}
static void
app_activate (GtkApplication *app)
{
GtkWidget *win = gtk_application_window_new (app);
gtk_window_set_title (GTK_WINDOW (win), "hello window");
gtk_window_set_default_size (GTK_WINDOW (win), 400, 300);
GtkWidget *btn_1,
*btn_2,
*box;
btn_1 = gtk_button_new_with_label ("hello button");
btn_2 = gtk_button_new_with_label ("close");
box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 5);
gtk_box_set_homogeneous (GTK_BOX (box), TRUE);
gtk_box_append (GTK_BOX (box), btn_1);
gtk_box_append (GTK_BOX (box), btn_2);
gtk_window_set_child (GTK_WINDOW (win), box);
g_signal_connect (GTK_BUTTON (btn_1), "clicked", G_CALLBACK (btn_1_clicked), NULL);
g_signal_connect (GTK_BUTTON (btn_2), "clicked", G_CALLBACK (btn_2_clicked), win);
gtk_window_present (GTK_WINDOW (win));
}
int main (int argc, char **argv)
{
GtkApplication *app =
gtk_application_new ("com.hello", G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect (app, "activate", G_CALLBACK (app_activate), NULL);
int stat = g_application_run (G_APPLICATION (app), argc, argv);
g_object_unref (app);
return stat;
}
gtkmm
学到这里,已经感觉出来不对劲了,使用成本太高了,要写一堆强制转换宏真的很烦。
虽然不加宏实际上也没什么,但是编译器会警告,会淹没重要信息。
弃了,后面学gtkmm了,用C++。
gtkmm参考资料:
https://gnome.pages.gitlab.gnome.org/gtkmm-documentation/chapter-basics.html
基础知识
1. hello world
#include <gtkmm.h>
class MyWindow : public Gtk::Window {
public:
MyWindow() {
set_title ("Hello world window");
set_default_size (200, 200);
}
};
int main (int argc, char **argv)
{
auto app = Gtk::Application::create ("com.hello");
return app->make_window_and_run<MyWindow> (argc, argv);
}
g++ simple.cc -o simple `pkg-config --cflags --libs gtkmm-4.0` -std=c++17
运行就显示一个空白窗口,标题是hello world window
。
需要注意的是,直接用#include <gtkmm.h>
会引入gtkmm的所有套件,这个头文件有1MB。
更好的方案是引入需要的部分,这里为了学习,我们直接全部包含就行了。
多妙啊,比tmd C语言看起来舒服多了。
2. 组件(控件)
除了可见的组件,还有一些不可见的组件用于布局。例如Gtk::Box
, Gtk::Frame
。
下面是一个添加Gtk::Button
到Gtk::Box
的例子:
#include <gtkmm/application.h>
#include <gtkmm/box.h>
#include <gtkmm/window.h>
#include <gtkmm/frame.h>
#include <gtkmm/button.h>
class MyWindow : public Gtk::Window {
public:
MyWindow() {
set_title ("Hello world window");
set_default_size (200, 200);
set_child (m_box);
m_box.append (m_button_1);
m_box.append (m_button_2);
m_box.set_orientation (Gtk::Orientation::VERTICAL);
}
private:
Gtk::Button m_button_1;
Gtk::Button m_button_2;
Gtk::Box m_box;
};
int main (int argc, char **argv)
{
auto app = Gtk::Application::create ("com.hello");
return app->make_window_and_run<MyWindow> (argc, argv);
}
3.信号
与大多数 GUI 工具包一样,gtkmm是事件驱动的。
- 当事件发生时,例如按下鼠标按钮,相应的信号将由被按下的 组件发出。
- 每个组件都有一组不同的信号可以发出。
- 为了使按钮点击产生动作,我们需要设置一个信号处理程序来捕捉按钮的“clicked”信号。
gtkmm使用 libsigc++ 库来实现信号。下面是连接 Gtk::Button
的"clicked"
信号和名为“on_button_clicked”
的信号处理程序的示例代码行
m_button1.signal_clicked().connect( sigc::mem_fun(*this,
&HelloWorld::on_button_clicked) );
信号的更多内容见附录:https://gnome.pages.gitlab.gnome.org/gtkmm-documentation/chapter-signals.html
4. Glib::ustring
您可能会惊讶地发现gtkmm并未std::string
在其界面中使用。相反,它使用Glib::ustring
,它是如此相似且不引人注目,以至于您实际上可以假设每个Glib::ustring
都是 std::string
并忽略本节的其余部分。但如果您想在申请中使用英语以外的语言,请继续阅读。
std::string
每个字符使用 8 位,但 8 位不足以对阿拉伯语、中文和日语等语言进行编码。尽管这些语言的编码已由Unicode Consortium指定,但 C 和 C++ 语言尚未为 UTF-8 编码提供任何标准化的 Unicode 支持。GTK 和 GNOME 选择使用 UTF-8 实现 Unicode,这就是 Glib::ustring
所包装的内容。它提供与 std::string 几乎完全相同的接口,以及与 std::string
的自动转换。
UTF-8 的好处之一是除非您愿意,否则您不需要使用它,因此您不需要一次改造所有代码。std::string
仍然适用于 7 位 ASCII 字符串。但是,当您尝试将您的应用程序本地化为中文等语言时,您将开始看到奇怪的错误,并可能出现崩溃。然后你需要做的就是开始使用Glib::ustring
。
请注意,UTF-8 与 8 位编码(如 ISO-8859-1)不兼容。例如,德语变音符号不在 ASCII 范围内,在 UTF-8 编码中需要超过 1 个字节。如果您的代码包含 8 位字符串文字,则必须将它们转换为 UTF-8(例如,巴伐利亚问候语“Grüß Gott”将是“Gr\xC3\xBC\xC3\x9F Gott”)。
您应该避免使用 C 风格的指针算法和 strlen()
等函数。在 UTF-8 中,每个字符可能需要 1 到 6 个字节,因此不能假设下一个字节是另一个字符。Glib::ustring
为您担心这方面的细节,因此您可以使用诸如 Glib::ustring::substr()
之类的方法,同时仍然根据字符而不是字节来思考。
与 Windows UCS-2 Unicode 解决方案不同,这不需要任何特殊的编译器选项来处理字符串文字,并且不会导致与 ASCII 不兼容的 Unicode 可执行文件和库。