GObject Introspection) 的应用, 可以轻松地, 创建其他语言的绑定
这使你能利用不同语言编写GUI, 比如Rust, Nim, Vala, Python, Javascript... (不会真的有人用C语言写Gtk吧?)
基本上名气稍微大点的语言, 都有对应的GTK绑定
目前有个项目, 叫做 gtk-rs, 负责GTK的Rust绑定, 在 crates.io 上搜索 gtk, 所看到的 gtk 与 gtk4, 就属于gtk-rs
gtk4这个crate, 是Rust语言的绑定, 而非同纯C编写的那个gtk4本身
所以进行接下来的步骤前, 请确保环境内, 已有gtk4本身的正确版本, 不然crate会装不上
首先, 你需要创建一个新项目, 随后修改 Cargo.toml, 请输入以下命令:
cargo add gtk4@0.5.2 --rename gtk
cargo add libadwaita@0.2 --rename adw
我们将 gtk4(crate), 重命名为 gtk, 方便之后编写, 随后, 请run一下, 安装依赖 (请确保安装了gtk4/libadwaita, 如 p1 中所述)
(adw 这个库之后会提的, 现在先忽略掉即可) 下面就是正式的编码环节了
有个非常快速的名为 mold (github.com/rui314/mold) 的链接器, 非常适合加速 gtk 项目的编译, 以便快速调试
直接用你的包管理器下载 mold
, 或者根据 github 的指示获取, 随后像下面这样简单的添加前缀即可:
mold -run cargo run
一个GTK4应用的创建, 需要用到 gtk::Application
同时还需 use gtk::prelude::*
, 原因与 std::prelude 一样
先创建一个应用, 它目前连窗口也没有:
use gtk::prelude::*;
use gtk::Application;
const APP_ID: &str = "xyz.jedsek.myapp";
fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.run();
}
很简单吧? 但别急着运行, 先看看下面两处:
application_id:
每个GTK应用, 都带有一个id, 即 application_id
, 它必须是全世界唯一的, 一般使用反域名作为id, 如 "org.gnome.gedit"
app_id
作为App的唯一标识符, 用于App之间的通信与识别, 因此要慎重考虑, 最好第一次就定好, 避免之后的麻烦!
Builder Pattern (一种Rust中常见的设计模式):
我们利用它进行链式构造, 让构造的过程更加清晰, 这里我们在链式中, 指定了程序的id: application_id(id: &str)
在gtk4中, 它被高频次地使用, 最后调用的 build
会生成比如窗口, 按钮等部件的实例
当然, 也可以通过编写XML文件, 来构造对应的部件, 指定其布局, 但这是后话
当你跃跃欲试, mold -run cargo run
之后, 会看到:
GLib-GIO-WARNING : Your application does not implement g_application_activate()
and has no handlers connected to the 'activate' signal.
It should do one of these.
靠, 这是啥子玩意儿? 实际上, 我们还要添加一个名为 activate 的信号量(Signal):
use gtk::prelude::*;
use gtk::Application;
const APP_ID: &str = "xyz.jedsek.myapp";
fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
todo!()
}
当然, 现在还无法执行, 请注意 build_ui
函数中的 todo!()
, 我们还没有填充这个函数!
因为我想逐步地讲解一下... 先来讲什么是 signal 吗:
当某件事情发生时, signal 就会被发送, 交由Gtk的全局处理系统, 由它进行调度
比如窗口打开, 按钮被点击, 拖动控件...这些事件都会发送一个 signal 来告诉处理系统, 让它知道有事情发生了
你可以为每个 signal 绑定一个函数, 当处理系统接受到某个 signal, 就可以调用对应的处理函数了
handler
connect
Gtk 的理念是: App只关心在特定的时候需要做的事情
事件发送的时间
, 即 signal 什么时候发送已经定好了, 至于 事情的内容
, 即 handler 的具体实现, 由开发者自己负责
下面是四个待响应的信号, 也对应着四个待实现的虚函数/handler:
正如上面第三个信号所说, 一个GtkApp至少得有一个窗口
当某个App试图打开第二个窗口, Gtk全局系统, 会发送 signal(activate/open) 给第一个窗口
所有的初始化工作都应在startup中完成, 哪怕是第二个窗口相关的初始化
这些只是Gtk给我们的 signal 中最为常见的几个, 实际上还有很多信号, 帮你自定义设计App, 本节后面, 会再帮你加深下印象
回到之前的代码, 如下:
use gtk::prelude::*;
use gtk::Application;
const APP_ID: &str = "xyz.jedsek.myapp";
fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
todo!()
}
Your application does not implement g_application_activate()
and has no handlers connected to the 'activate' signal.
现在你应该明白, 为什么之前会报出这个警告了吧? connect_xxxx
, 代表着连接某个 signal , 即实现对应的虚函数/handler
connect_activate
, 要求传入一个具有静态lifetime的函数作为参数, 且被传入函数只有唯一的参数, 其类型是 &Application
我们创建的这个函数, 名为 build_ui
, 正是为了消除该警告, 真正显示窗口, 毕竟一个GUI程序若无任何显示, 这...啧, 不太聪明?
直接上 build_ui
的代码, 注意 use 那边多了东西:
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow};
const APP_ID: &str = "xyz.jedsek.myapp";
fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
let win = ApplicationWindow::builder()
.application(app)
.title("My App")
.build();
win.present();
}
我们用builder模式, 指定了窗口对应的App是谁, 标题是啥, 并在最后显示它
如果你处于GNOME-DE, 并启用了相关主题, 将会看见一个具有对应主题的空白窗口, 标题是 My App
在App启动之后的某时刻, activate
信号对应的虚函数被调用, 之前因为没有实现它, 一个警告出现
但现在, 我们已经将它覆盖成自己的 build_ui
了, 因此警告不再出现
让我们再创建一个按钮, 提示信息是 Press me!
, 在按下之后, 变成显示 Hello World!
:
use gtk::{prelude::*, Button};
use gtk::{Application, ApplicationWindow};
const APP_ID: &str = "xyz.jedsek.myapp";
fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
let btn = Button::builder().label("Press me!").build();
btn.connect_clicked(|btn| btn.set_label("Hello World!"));
let win = ApplicationWindow::builder()
.application(app)
.title("My App")
.child(&btn)
.build();
win.present();
}
我在这里使用Button, 而不是Label, 单纯是为了复习先前学过的概念, 仅此而已
之前说过, 我们可以 connect 的 signal 远不止那四个, 比如这里的 connect_clicked, 就代表按钮被点击后的handler
点击事情发生时, 本来就能啥也不做, 因此即使不调用 connect_clicked 也不会出现警告, 不像 activate 强制要求你处理
然后, 我们在使用 Builder Pattern
构造窗口时, 调用child, 将该按钮的引用传入, 让按钮也出现在窗口中
注意: 这里会使得按钮的强引用数量+1, 因为Button是GObject的子类, 而GObject具有引用计数的特点, 下节会讲
稍微再水一点字数
let btn = Button::builder()
.label("Press me!")
.margin_top(12)
.margin_bottom(12)
.margin_start(12)
.margin_end(12)
.build();
let win = ApplicationWindow::builder()
.application(app)
.title("My Gtk App")
.opacity(0.8) // 窗口不透明度, 越低越透明, 越高越不透明
.build();
let win = ApplicationWindow::builder()
.application(app)
.title("My Gtk App")
.default_height(100)
.default_width(200)
.build();
至此, 本小节结束, 又要愉快地鸽鸽鸽了呢 :)
上一篇: p1~> 系列说明
下一篇: p3~> GObject-内存管理