gtk-rs学习.md
gtk-rs学习
简介
本篇内容基本全部来自 ~~参考书: https://gtk-rs.org/gtk4-rs/stable/latest/book/。
GTK
是使用c
编写的跨平台图形库。GTK4
是其GTK
最新版本。gtk-rs
是提供rust
语言的GTK
相关 库绑定。
学习目的:做一个TODO的GUI程序,并将其贴到桌面层级上。因为wayland下没有我想要的这种方案。当前使用基于wlroots
的wayfire
,所以找到gtk-layer-shell
相关协议,这个协议可以实现贴在桌面的效果,当然这都是之后再说的事情了。
Hello World
导入依赖:
cargo new rgtk && cd rgtk
cargo add gtk
cargo run
基本使用:
- 构建
gtk:Application
实例 - 连接app实例
- 执行主运行函数
use gtk::prelude::*;
use gtk::Application;
use gtk::ApplicationWindow;
const APP_ID: &str = "top.nsfoxer.ToDo";
fn main() {
// Create a new application
let app = Application::builder().application_id(APP_ID).build();
// Connect to "activate" signal of `app`
app.connect_activate(build_ui);
// Run the application
app.run();
}
fn build_ui(app: &Application) {
let win = ApplicationWindow::builder()
.application(app)
.title("ToDo")
.build();
win.present();
}
简单按钮交互:
fn build_ui(app: &Application) {
let button = Button::builder()
.margin(12)
.label("Press Me")
.build();
// 设置回调函数
button.connect_clicked(|button| {
button.set_label("Hello World!");
});
let win = ApplicationWindow::builder()
.application(app)
.title("ToDo")
.child(&button) // 将button作为win的子节点
.build();
win.show_all(); // 显示所有组件
win.present();
}
组件(Widgets)
组件(Widgets)是组成GTK
应用程序的。GTK
自带很多常用的组件,像显示组件、按钮、容器(containers)和窗口(windows)等等。我们也可以自定义组件。可以从这里找到GTK提供的组件。
GTK
是一个面向对象的框架,所有的组件都继承自GObject
。如:
GObject
╰── Widget
╰── Button
gtk-rs documentation列出Button
组件所有实现的trait
,其中ButtonExt
包含了对点击事件的响应:connect_cliked
GObject概念
内存管理
GObject
在rust-gtk
中是glib::Object
,它是一个引用计数、可变类。
实现对一个数进行更改:
let mut number = 0;
let button_add = Button::new();
let button_sub = Button::new();
button_add.connect_clicked(|_| number += 1);
button_sub.connect_clicked(|_| number -= 1); # 错误
明显,number
所有权被转移到闭包内,第二个按钮无法获得它。
可以使用Rc
(引用计数)和Cell
(修改)
let number = Rc::new(Cell::new(0));
let number_copy = number.clone();
button_add.connect_clicked(move |_| number_copy.set(number_copy.get() + 1));
button_sub.connect_clicked(move |_| number.set(number.get() -1));
如果两个按钮想要互相改变:
use glib::clone;
button_add.connect_clicked(clone!(@weak number, @weak button_sub =>
move |_| {
number.set(number.get() + 1);
button_sub.set_label(&number.get().to_string());
}));
button_sub.connect_clicked(clone!(@weak button_add =>
move |_| {
number.set(number.get() - 1);
button_add.set_label(&number.get().to_string());
}
));
因为组件都继承GObject
计数引用类,所以直接使用clone!()
即可。
这里如果使用强引用,会导致循环依赖问题。所有全部使用弱引用。
子类
GObject
非常依赖继承。我们可以通过继承Button
实现自定义按钮。
基本实现
: src/custom_button/imp.rs
- 实现
ObjectSubClass
NAME
: 这个子类的名称,为了保持唯一性,使用包名+类名Type
: 之后将被创建的实际GObject
ParentType
: 父类型
- 实现祖先
trait
use gtk4::glib;
use gtk4::subclass::prelude::*;
#[derive(Default)]
pub struct CustomButton;
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
const NAME: &'static str = "MyGtkAppCustomButton";
type Type = super::CustomButton;
type ParentType = gtk4::Button;
}
impl ObjectImpl for CustomButton {
}
impl WidgetImpl for CustomButton {
}
impl ButtonImpl for CustomButton {
}
: src/custom_button/mod.rs
- 定义自定义类公共接口。
glib::wrapper!
实现父类的traits
。目前,在rust
中需要手动实现所有父类的特征。可以在这里查看Button
所需的类。
mod imp;
use glib::Object;
use gtk4::glib;
glib::wrapper! {
pub struct CustomButton(ObjectSubclass<imp::CustomButton>)
@extends gtk4::Button, gtk4::Widget,
@implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget;
}
impl CustomButton {
pub fn new() -> Self {
Object::new(&[]).expect("Failed to create `CustomButton`.")
}
pub fn with_label(label: &str) -> Self {
Object::new(&[("label", &label)]).expect("Failed to create `CustomButton`")
}
}
impl Default for CustomButton {
fn default() -> Self {
Self::new()
}
}
: main.rs
- 使用自定义类
mod custom_button;
use custom_button::CustomButton;
// ................
let button = CustomButton::with_label("Custom Button Press Me!");
button.connect_clicked(move |button| {
button.set_label("Hello, World!");
});
// ................
自定义功能
我们将数字状态放入自定义按钮中。
:src/custom_button/imp.rs
use std::cell::Cell;
use gtk4::glib;
use gtk4::subclass::prelude::*;
use gtk4::traits::ButtonExt;
#[derive(Default)]
pub struct CustomButton {
number: Cell<i32>,
}
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
const NAME: &'static str = "MyGtkAppCustomButton";
type Type = super::CustomButton;
type ParentType = gtk4::Button;
}
impl ObjectImpl for CustomButton {
fn constructed(&self, obj: &Self::Type) { // 覆盖默认构造函数,也就是new()的实现
self.parent_constructed(obj);
obj.set_label(&self.number.get().to_string());
}
}
impl WidgetImpl for CustomButton {
}
impl ButtonImpl for CustomButton {
fn clicked(&self, button: &Self::Type) {
self.number.set(self.number.get() + 1);
button.set_label(&self.number.get().to_string());
}
}
:src/main.rs
// ...........
let button = CustomButton::new();
//............
通用值(Generic Value)
GObject
相关函数的参数和返回值都是一般值,由于GObject
接口使用的c接口,所以这些函数没办法直接使用Rust
中的类型。可以通过glib::Value
和glib::Variant
进行转换后使用。
Value
Value
实际是一个枚举:
enum Value <T> {
bool(bool),
i8(i8),
i32(i32),
u32(u32),
i64(i64),
u64(u64),
f32(f32),
f64(f64),
// boxed types
String(Option<String>),
Object(Option<dyn IsA<glib::Object>>),
}
基本使用:
// i32 -> Value
let integer_value = 10.to_value();
// Value -> i32
let integer = integer_value.get::<i32>().expect("The value needs to be of type `i32`.");
// String -> Value
let string_value = "Hello".to_value();
// Value -> String
let string = string_value.get::<String>().unwrap();
// 当判断是否错误时使用
let string = string_value.get::<Option<String>>().unwrap();
Variant
数据需要被序列化(网络请求、磁盘存储等等)时,使用Variant
。原生Variant
支持任意组合类型,但目前rust
绑定有限制,可以有以下类型:bool
, u8
, i16
, u16
, i32
, u32
, i64
, u64
, f64
, &str/String
,VariantDict
。这些类型组成的包装类型也可以,像是HashMap
、Vec
、Option
,tuple
(最长16个元素)。
// i32 -> variant
let integer_variant = 10.to_variant();
// variant -> i32
let integer = integer_variant.get::<i32>().unwrap();
// vec -> variant
let variant = vec!["Hello", "there"].to_variant();
assert_eq!(variant.n_children(), 2);
// variant -> vec
let vec = &variant.get::<Vec<String>>().unwrap();
assert_eq!(vec[0], "Hello");
属性(Properties
)
属性为访问GObject
状态(数据)提供一组公共的API。
属性读写
我们看Switch
组件,它的一个属性是state
,表示开关状态。state
是可读写的。
let switch = Switch::new();
// 设置状态
switch.set_state(true);
// 读取状态
let current_state = switch.state();
println!("state {}", current_state);
// ----------------------
let switch = Switch::new();
// 也可以使用set_property取代
switch.set_property("state", &false);
// 直接读取 try_property try_set_property
let current_state = switch.property::<bool>("state");
println!("state {}", current_state);
属性绑定
属性之间可以相互绑定:
let switch1 = Switch::new();
let switch2 = Switch::new();
// 将switch1 和 switch2 的state双向绑定
switch1.bind_property("state", &switch2, "state")
.flags(BindingFlags::BIDIRECTIONAL) // 指定双向绑定
.build();
对自定义Gobject
子类添加属性
我们可以对自定义子类添加属性。这里用到lazy
加载,添加once_cell
依赖。
:src/custom_button/imp.rs
use std::cell::Cell;
use glib::BindingFlags;
use glib::{ParamSpec, ParamSpecInt};
use gtk4::glib;
use gtk4::prelude::*;
use gtk4::subclass::prelude::*;
use once_cell::sync::Lazy;
#[derive(Default)]
pub struct CustomButton {
number: Cell<i32>,
}
#[glib::object_subclass]
impl ObjectSubclass for CustomButton {
const NAME: &'static str = "MyGtkAppCustomButton";
type Type = super::CustomButton;
type ParentType = gtk4::Button;
}
impl ObjectImpl for CustomButton {
// 将number与label绑定起来
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
obj.bind_property("number", obj, "label")
.flags(BindingFlags::SYNC_CREATE)
.build();
}
// 属性初始化
fn properties() -> &'static [glib::ParamSpec] {
static PROPERTIES: Lazy<Vec<ParamSpec>> =
Lazy::new(|| vec![ParamSpecInt::builder("number").build()]);
PROPERTIES.as_ref()
}
// 设置属性值
fn set_property(&self, _obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &ParamSpec) {
match pspec.name() {
"number" => {
let input_number = value.get().unwrap();
self.number.replace(input_number);
},
_ => unimplemented!(),
}
}
// 读取属性内容
fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> glib::Value {
match pspec.name() {
"number" => self.number.get().to_value(),
_ => unreachable!(),
}
}
}
impl WidgetImpl for CustomButton {
}
impl ButtonImpl for CustomButton {
// 设置点击事件,由于之前已经将number和label属性进行绑定,这里改变number属性的值,按钮的label会跟随改变
fn clicked(&self, button: &Self::Type) {
let incremented_number = self.number.get() + 1;
button.set_property("number", &incremented_number);
}
}
:src/main.rs
let button1 = CustomButton::new();
let button2 = CustomButton::new();
// 绑定过程可以改变值。绑定也不要求两个绑定类型一致
button1
.bind_property("number", &button2, "number")
.transform_to(|_, value| { // button1 -> button2
let number = value.get::<i32>().unwrap();
let incremented_number = number + 1;
Some(incremented_number.to_value())
})
.transform_from(|_, value| { // button2 -> button1
let number = value.get::<i32>().unwrap();
let decremented_number = number - 1;
Some(decremented_number.to_value())
})
.flags(BindingFlags::BIDIRECTIONAL | BindingFlags::SYNC_CREATE)
.build();
// 当number值改变时,进行回调,打印出内容
button1.connect_notify_local(Some("number"), move |button, _| {
let number = button.property::<i32>("number");
println!("Current value: {}", number);
});
信号Signals
Gobject
信号是为指定事件的一个注册回调系统。当我们按下一个按钮时,将发送一个clicked
信号,然后所有关注此信号的回调函数将被执行。
// 一般使用
button.connect_clicked(move |button| button.set_label("Hello"));
// 可用于自定义信号
let button = Button::new();
button.connect_closure("clicked", false, closure_local!(move |button: Button| {
button.set_label("Hello");
}));
自定义信号
signals
定义一组信号。
: src/custom_button/imp.rs
// ...........
impl ObjectImpl for CustomButton {
fn signals() -> &'static [glib::subclass::Signal] {
static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
vec![Signal::builder(
"max-number-reached", // 信号名称,这种格式
&[i32::static_type().into()], // 发送信号时的值类型
<()>::static_type().into(),) // emitter得到的返回值类型,这里直接忽略
.build()]
});
SIGNALS.as_ref()
}
// ...................
}
发送信号:
:src/custom_button/imp.rs
// ..................
static MAX_NUMBER: i32 = 8;
// Trait shared by all buttons
impl ButtonImpl for CustomButton {
fn clicked(&self, button: &Self::Type) {
let incremented_number = self.number.get() + 1;
// If `number` reached `MAX_NUMBER`,
// emit "max-number-reached" signal and set `number` back to 0
if incremented_number == MAX_NUMBER {
button.emit_by_name::<()>("max-number-reached", &[&incremented_number]);
button.set_property("number", &0);
} else {
button.set_property("number", &incremented_number);
}
}
}
//.............
使用信号:
: src/main.rs
button.connect_closure(
"max-number-reached",
false,
closure_local!(move |_button: CustomButton, number: i32| {
println!("The maximum number {} has been reached", number);
}),
);
主事件循环
主循环管理所有事件,像是鼠标点击、键盘按键。这些处理都在一个线程内,快速循环事件产生一种并行处理事件的假象。
下面代码在点击按钮后会睡眠5s,我们可以观察到GUI卡死。
use std::time::Duration;
use gtk4::prelude::*;
use gtk4::{self, Application, ApplicationWindow, Button};
const APP_ID: &str = "org.gtk_rs.MainEventLoop1";
pub fn main() {
let app = Application::builder().application_id(APP_ID).build();
app.connect_activate(build_ui);
app.run();
}
fn build_ui(app: &Application) {
let button = Button::builder()
.label("Sleep Me")
.margin_end(12)
.margin_top(12)
.margin_start(12)
.margin_bottom(12)
.build();
button.connect_clicked(move |_| {
let five_seconds = Duration::from_secs(5);
std::thread::sleep(five_seconds);
});
let win = ApplicationWindow::builder()
.application(app)
.child(&button)
.build();
win.present();
}
避免事件主循环中耗时操作
新线程
为避免在事件主循环中,处理耗时程序,可以简单的将耗时程序放到新的线程中。
button.connect_clicked(move |_| {
std::thread::spawn(move || {
let five_seconds = Duration::from_secs(5);
std::thread::sleep(five_seconds);
});
});
channel
fn build_ui(app: &Application) {
let button = Button::builder()
.label("Sleep Me")
.margin_end(12)
.margin_top(12)
.margin_start(12)
.margin_bottom(12)
.build();
let (sender, receiver) = MainContext::channel(glib::PRIORITY_DEFAULT);
button.connect_clicked(move |_| {
let sender = sender.clone();
std::thread::spawn(move || {
// 请求停止按钮
sender.send(false).unwrap();
let five_seconds = Duration::from_secs(5);
std::thread::sleep(five_seconds);
// 请求激活按钮
sender.send(true).unwrap();
});
});
// 接收请求状态
receiver.attach(
None,
glib::clone!(@weak button => @default-return Continue(false),
move |enable_button| {
// 设置按钮激活状态
button.set_sensitive(enable_button);
Continue(true)
}
));
let win = ApplicationWindow::builder()
.application(app)
.child(&button)
.build();
win.present();
}
async
可以让主线程处理异步代码块。
fn build_ui(app: &Application) {
let button = Button::builder()
.label("Sleep Me")
.margin_end(12)
.margin_top(12)
.margin_start(12)
.margin_bottom(12)
.build();
let (sender, receiver) = MainContext::channel(glib::PRIORITY_DEFAULT);
button.connect_clicked(move |_| {
let main_context = MainContext::default();
// 在主循环中执行异步代码块
main_context.spawn_local(glib::clone!(@strong sender => async move {
// 请求停止激活
sender.send(false).unwrap();
glib::timeout_future_seconds(5).await;
// 请求激活
sender.send(true).unwrap();
}));
});
// 只要主循环接收到消息,就会立即执行闭包函数
receiver.attach(
None,
glib::clone!(@weak button => @default-return Continue(false),
move|enable_button| {
button.set_sensitive(enable_button);
Continue(true)
})
);
let win = ApplicationWindow::builder()
.application(app)
.child(&button)
.build();
win.present();
}
因为我们在同一线程内,所以我们甚至可以直接更改button
的状态。
button.connect_clicked(move |button| {
let main_context = MainContext::default();
main_context.spawn_local(glib::clone!(@weak button => async move {
button.set_sensitive(false);
glib::timeout_future_seconds(5).await;
button.set_sensitive(true);
}));
});
Settings
如果想要存储GUI各种组件的状态,可以使用gio::Settings
。
首先创建GSchema
的XML文件描述程序中的各种数据。
:org.gtk_rs.Settings.gschema.xml
<?xml version="1.0" encoding="utf-8"?>
<schemalist>
<!--指定id,这个id和程序id要一致
path 需要前后都有'/'-->
<schema id="org.gtk_rs.Settings" path="/org/gtk_rs/Settings/">
<!-- name 名称 type指定类型(https://docs.gtk.org/glib/gvariant-format-strings.html)-->
<key name="is-switch-enabled" type="b">
<default>false</default>
<summary>Default switch state</summary>
</key>
</schema>
</schemalist>
编译:
mkdir -p $HOME/.local/share/glib-2.0/schemas
cp org.gtk_rs.Settings.gschema.xml $HOME/.local/share/glib-2.0/schemas/
glib-compile-schemas $HOME/.local/share/glib-2.0/schemas/
:main.rs
fn build_ui2(app: &Application) {
// 初始化settings
let settings = Settings::new(APP_ID);
// 获取值
let id_switch_enabled = settings.boolean("is-switch-enabled");
let switch = Switch::builder()
.margin_bottom(48)
.margin_top(48)
.margin_start(48)
.margin_end(48)
.valign(gtk4::Align::Center)
.halign(gtk4::Align::Center)
.state(id_switch_enabled)
.build();
// 当state状态设置时触发
switch.connect_state_set(move |_, is_enabled| {
// 设置settings值
settings.set_boolean("is-switch-enabled", is_enabled)
.unwrap();
Inhibit(false) // 不禁止默认行为
});
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&switch)
.build();
window.present();
}
由于state
反应switch的状态,我们可以直接使用绑定:
fn build_ui2(app: &Application) {
let settings = Settings::new(APP_ID);
let switch = Switch::builder()
.margin_bottom(48)
.margin_top(48)
.margin_start(48)
.margin_end(48)
.valign(gtk4::Align::Center)
.halign(gtk4::Align::Center)
.build();
// 使用绑定
settings.bind("is-switch-enabled", &switch, "state")
.flags(SettingsBindFlags::DEFAULT) // 默认绑定
.build();
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.child(&switch)
.build();
window.present();
}
注: 值状态可以使用dconf编辑器
查看
保存窗口状态
如果想在窗口第二次打开时,维持上次的窗口大小,我们可以使用前面的Settings
。GTK
对此功能并未开箱即用,但是却不难实现。我们想办法保存height
、width
、is_maximized
即可。
: org.gtk_rs.SavingWindowState.gschema.xml
<?xml version="1.0" encoding="UTF-8"?>
<schemalist>
<schema id="org.gtk_rs.SavingWindowState" path="/org/gtk_rs/SavingWindowState/">
<key name="window-width" type="i">
<default>600</default>
<summary>Default window width</summary>
</key>
<key name="window-height" type="i">
<default>480</default>
<summary>Default window height</summary>
</key>
<key name="is-maximized" type="b">
<default>false</default>
<summary>Default window maximized behaviour</summary>
</key>
</schema>
</schemalist>
编译
:src/custom_window/imp.rs
use glib::subclass::object::ObjectImpl;
use glib::subclass::types::ObjectSubclass;
use gtk4::{ApplicationWindow, Inhibit};
use gtk4::gio::Settings;
use gtk4::subclass::prelude::*;
use once_cell::sync::OnceCell;
#[derive(Default)]
pub struct Window {
pub settings: OnceCell<Settings>,
}
#[glib::object_subclass]
impl ObjectSubclass for Window {
const NAME: &'static str = "MyGtkAppWindow";
type Type = super::Window;
type ParentType = ApplicationWindow;
}
impl ObjectImpl for Window {
fn constructed(&self, obj: &Self::Type) {
self.parent_constructed(obj);
obj.setup_settings();
obj.load_window_size();
}
}
impl WidgetImpl for Window {}
impl WindowImpl for Window {
fn close_request(&self, window: &Self::Type) -> glib::signal::Inhibit {
window.save_window_size().expect("Failed to save window state");
Inhibit(false)
}
}
impl ApplicationWindowImpl for Window {}
:src/custom_window/mod.rs
mod imp;
use gio::Settings;
use glib::Object;
use gtk4::prelude::*;
use gtk4::{gio, glib, Application};
use gtk4::subclass::prelude::*;
use crate::APP_ID;
glib::wrapper! {
pub struct Window(ObjectSubclass<imp::Window>)
@extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget,
@implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager;
}
impl Window {
pub fn new(app: &Application) -> Self {
Object::new(&[("application", app)]).unwrap()
}
fn setup_settings(&self) {
let settings = gio::Settings::new(APP_ID);
self.imp()
.settings
.set(settings)
.unwrap();
}
fn settings(&self) -> &Settings {
self.imp()
.settings
.get()
.unwrap()
}
pub fn save_window_size(&self) -> Result<(), glib::BoolError>{
let size = self.default_size();
self.settings().set_int("window-width", size.0)?;
self.settings().set_int("window-height", size.1)?;
self.settings()
.set_boolean("is-maximized", self.is_maximized())?;
Ok(())
}
fn load_window_size(&self) {
let width = self.settings().int("window-width");
let height = self.settings().int("window-height");
let is_maximized = self.settings().boolean("is-maximized");
self.set_default_size(width, height);
if is_maximized {
self.maximize();
}
}
}
:src/main.rs
fn build_ui2(app: &Application) {
//let settings = Settings::new(APP_ID);
let switch = Switch::builder()
.margin_bottom(48)
.margin_top(48)
.margin_start(48)
.margin_end(48)
.valign(gtk4::Align::Center)
.halign(gtk4::Align::Center)
.build();
//settings.bind("is-switch-enabled", &switch, "state")
//.flags(SettingsBindFlags::DEFAULT)
//.build();
let window = custom_window::Window::new(app);
window.set_child(Some(&switch));
window.present();
}
当然,我们可以在运行后,在dconf编辑器
中发现属性:
列表组件
注:列表组件,都是复用思想。其他GUI编写大多也是这样。
显示列表时,可以用gtk::ListBox
(垂直列表)和gtk::FlowBox
(网格列表)组件。
简单使用
fn build_ui3(app: &Application) {
// 创建list box
let list_box = ListBox::new();
for number in 0..100 {
// 添加100个label
let label = Label::new(Some(&number.to_string()));
list_box.append(&label);
}
// 添加滚动窗口
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(gtk4::PolicyType::Never) // 从不进行横屏滚动
.min_content_width(360)
.child(&list_box)
.build();
let window = ApplicationWindow::builder()
.application(app)
.default_width(600)
.default_height(480)
.child(&scrolled_window)
.build();
window.present();
}
Views
视图
上文中,我们对每个元素都创建一个组件,这太消耗资源了。如果我们想要创建无穷尽的元素,简单创建明显不可行。
可以使用可扩展列表替代:
model
: 拥有数据,负责过滤和描述元素顺序factory
:定义如何将数据转为组件。view
: 指定组件的排列方式(注:指定显示谁,指定不显示谁)
我们只需要创建比界面可展示的组件稍为多一点的组件数量就足够了。当滚动时,不可见的组件将被复用。
-
定义和填充
model
。model
是gio::ListStore
的实例化,gio::ListStore
只接受GObject
类。这里需要自定义一个继承GObject
的类。:
src/integer_object/mod.rs
mod imp; use glib::Object; use gtk4::glib; glib::wrapper! { pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>); } impl IntegerObject { pub fn new(number: i32) -> Self { Object::new(&[("number", &number)]).unwrap() } }
:
src/integer_object/imp.rs
use std::cell::Cell; use glib::{ParamSpec, ParamSpecInt}; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::glib; use once_cell::sync::Lazy; // 包含number状态的类 #[derive(Default)] pub struct IntegerObject { number: Cell<i32>, } // 子类化 GObject 的核心特征 #[glib::object_subclass] impl ObjectSubclass for IntegerObject { const NAME: &'static str = "MyGtkAppIntegerObject"; type Type = super::IntegerObject; } // 所有 GObject 共享的特征 impl ObjectImpl for IntegerObject { fn properties() -> &'static [glib::ParamSpec] { static PROPERTIES: Lazy<Vec<ParamSpec>> = Lazy::new(|| vec![ParamSpecInt::builder("number").build()]); PROPERTIES.as_ref() } fn set_property(&self, _obj: &Self::Type, _id: usize, value: &glib::Value, pspec: &ParamSpec) { match pspec.name() { "number" => { let input_number = value.get().unwrap(); self.number.replace(input_number); }, _ => unimplemented!(), } } fn property(&self, _obj: &Self::Type, _id: usize, pspec: &ParamSpec) -> glib::Value { match pspec.name() { "number" => self.number.get().to_value(), _ => unimplemented!(), } } }
-
使用 :
src/main.rs
fn build_ui4(app: &Application) { // 1. 给model填充数据,model只持有数据 // 创建vector let vector: Vec<IntegerObject> = (0..=100_000).into_iter().map(IntegerObject::new).collect(); // 创建model let model = gio::ListStore::new(IntegerObject::static_type()); // 将数据填充至model model.extend_from_slice(&vector); // 2. factory关心widgets和model之间的关系 // 创建factory let factory = SignalListItemFactory::new(); // 当一个组件必须创建时,将连接此信号 factory.connect_setup(move |_, list_item| { let label = Label::new(None); list_item.set_child(Some(&label)); }); // 3. 绑定label和数据 factory.connect_bind(move |_, list_item| { // 从list_item中获取model数据 let integer_object = list_item .item() .unwrap() .downcast::<IntegerObject>() .unwrap(); // 取出数据 let number = integer_object.property::<i32>("number"); // 从list_item获得组件 let label = list_item.child().unwrap() .downcast::<Label>().unwrap(); // 设置组件内容 label.set_label(&number.to_string()); }); // 4. 设置singleSelection,只想选中一个 let selection_model = SingleSelection::new(Some(&model)); // 将model和factory关联 let list_view = ListView::new(Some(&selection_model), Some(&factory)); // 5. 设置滚动窗口 let scrolled_window = ScrolledWindow::builder() .hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling .min_content_width(360) .child(&list_view) .build(); // Create a window let window = ApplicationWindow::builder() .application(app) .title("My GTK App") .default_width(600) .default_height(300) .child(&scrolled_window) .build(); window.present(); }
我们想点击一个label,让其增加值:
:src/integer_object_mod.rs
mod imp;
use glib::{Object, ObjectExt};
use gtk4::glib;
glib::wrapper! {
pub struct IntegerObject(ObjectSubclass<imp::IntegerObject>);
}
impl IntegerObject {
pub fn new(number: i32) -> Self {
Object::new(&[("number", &number)]).unwrap()
}
pub fn increse_number(self) {
let old_number = self.property::<i32>("number");
self.set_property("number", old_number+1);
}
}
:main.rs
fn build_ui4(app: &Application) {
let vector: Vec<IntegerObject> =
(0..=100_000).into_iter().map(IntegerObject::new).collect();
let model = gio::ListStore::new(IntegerObject::static_type());
model.extend_from_slice(&vector);
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item.set_child(Some(&label));
});
factory.connect_bind(move |_, list_item| {
let integer_object = list_item
.item()
.unwrap()
.downcast::<IntegerObject>()
.unwrap();
let number = integer_object.property::<i32>("number");
let label = list_item.child().unwrap()
.downcast::<Label>().unwrap();
integer_object.bind_property("number", &label, "label")
.flags(BindingFlags::SYNC_CREATE)
.build();
});
let selection_model = SingleSelection::new(Some(&model));
let list_view = ListView::new(Some(&selection_model), Some(&factory));
list_view.connect_activate(move |list_view, position| {
let model = list_view.model().unwrap();
let integer_object = model.item(position).unwrap().downcast::<IntegerObject>().unwrap();
integer_object.increse_number();
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
window.present();
}
当前看上去正常工作了,但会发现有点击一个按钮导致多个按钮变动的情况,这是因为组件label被公用的原因。GTK
提供一个expressions
的属性解决。
fn build_ui4(app: &Application) {
let vector: Vec<IntegerObject> =
(0..=100_000).into_iter().map(IntegerObject::new).collect();
let model = gio::ListStore::new(IntegerObject::static_type());
model.extend_from_slice(&vector);
let i = Rc::<Cell<i32>>::new(Cell::new(0));
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
let label = Label::new(None);
list_item.set_child(Some(&label));
//属性
list_item.property_expression("item")
.chain_property::<IntegerObject>("number")
.bind(&label, "label", Widget::NONE);
let i = i.clone();
i.set(i.get()+1);
println!("set up {} label", i.get());
});
let filter = CustomFilter::new(move|obj| {
let integer_object = obj.downcast_ref::<IntegerObject>().unwrap();
let number = integer_object.property::<i32>("number");
number % 2 == 0
});
let filter_model = FilterListModel::new(Some(&model), Some(&filter));
let sorter = CustomSorter::new(move |obj1, obj2| {
let integer_object_1 = obj1.downcast_ref::<IntegerObject>().unwrap();
let integer_object_2 = obj2.downcast_ref::<IntegerObject>().unwrap();
let num1 = integer_object_1.property::<i32>("number");
let num2 = integer_object_2.property::<i32>("number");
num2.cmp(&num1).into()
});
let sort_model = SortListModel::new(Some(&filter_model), Some(&sorter));
let selection_model = SingleSelection::new(Some(&sort_model));
let list_view = ListView::new(Some(&selection_model), Some(&factory));
list_view.connect_activate(move |list_view, position| {
let model = list_view.model().unwrap();
let integer_object = model.item(position).unwrap().downcast::<IntegerObject>().unwrap();
integer_object.increse_number();
filter.changed(gtk4::FilterChange::Different);
sorter.changed(gtk4::SorterChange::Different);
});
let scrolled_window = ScrolledWindow::builder()
.hscrollbar_policy(PolicyType::Never) // Disable horizontal scrolling
.min_content_width(360)
.child(&list_view)
.build();
// Create a window
let window = ApplicationWindow::builder()
.application(app)
.title("My GTK App")
.default_width(600)
.default_height(300)
.child(&scrolled_window)
.build();
window.present();
}
复合模板 Composite Templates
我们可以使用xml
标记来描述用户界面。
基本使用
-
构建
resources
文件src/resources/window.ui
<?xml version="1.0" encoding="UTF-8"?> <interface> <template class="MyGtkAppWindow" parent="GtkApplicationWindow"> <property name="title">My Gtk App</property> <child> <object class="GtkButton" id="button"> <property name="label">Press me!</property> <property name="margin-top">12</property> <property name="margin-bottom">12</property> <property name="margin-start">12</property> <property name="margin-end">12</property> </object> </child> </template> </interface>
src/resources/resources.gresource.xml
<!-- 进行压缩 --> <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gtk_rs/example/"> <file compressed="true" preprocess="xml-stripblanks">window.ui</file> </gresource> </gresources>
-
更改构建方式
Cargo.toml
[package] name = "rgtk" version = "0.1.0" edition = "2021" build = "build.rs" # 指定构建文件 # 添加构建依赖 [build-dependencies] gtk4 = "0.4.8" [dependencies] cairo = "0.0.4" glib = "0.15.12" gtk4 = "0.4.8" once_cell = "1.13.0"
build.rs
use gtk4::gio; fn main() { gio::compile_resources( "src/resources", // 资源文件目录 "src/resources/resources.gresource.xml", // gresource文件 "resources.gresource", // 构建目标 ); }
-
构建自定义window
src/window/imp.rs
use glib::subclass::InitializingObject; use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::{glib, Button, CompositeTemplate}; #[derive(CompositeTemplate, Default)] #[template(resource = "/org/gtk_rs/example/window.ui")] pub struct Window { #[template_child] pub button: TemplateChild<Button>, } #[glib::object_subclass] impl ObjectSubclass for Window { // 与window.ui文件中指定的一致 const NAME: &'static str = "MyGtkAppWindow"; type Type = super::Window; // 父类型与window.ui指定的父类型一致 type ParentType = gtk4::ApplicationWindow; fn class_init(kclass: &mut Self::Class) { kclass.bind_template(); } fn instance_init(obj: &InitializingObject<Self>) { obj.init_template(); } } impl ObjectImpl for Window { fn constructed(&self, obj: &Self::Type) { self.parent_constructed(obj); self.button.connect_clicked(move |button| { button.set_label("Hello World!"); }); } } impl WidgetImpl for Window {} impl WindowImpl for Window {} impl ApplicationWindowImpl for Window {}
src/window/mod.rs
mod imp; use glib::Object; use gtk4::{gio, glib, Application}; glib::wrapper! { pub struct Window(ObjectSubclass<imp::Window>) @extends gtk4::ApplicationWindow, gtk4::Window, gtk4::Widget, @implements gio::ActionGroup, gio::ActionMap, gtk4::Accessible, gtk4::Buildable, gtk4::ConstraintTarget, gtk4::Native, gtk4::Root, gtk4::ShortcutManager; } impl Window { pub fn new(app: &Application) -> Self { Object::new(&[("application", app)]).unwrap() } }
-
main.rs
mod window; use gtk4::prelude::*; use gtk4::{gio, Application}; use window::Window; const APP_ID: &str = "org.gtk_rs.CompositeTemplates"; fn main() { // 与build.rs的target一致 gio::resources_register_include!("resources.gresource").unwrap(); let app = Application::builder().application_id(APP_ID).build(); app.connect_activate(build_ui); app.run(); } fn build_ui(app: &Application) { let window = Window::new(app); window.present(); }
使用自定义组件
-
创建自定义组件
src/temp_buttom/mod.rs
use glib::Object; use gtk4::glib; mod imp; glib::wrapper! { pub struct CustomButton(ObjectSubclass<imp::CustomButton>) @extends gtk4::Button, gtk4::Widget, @implements gtk4::Accessible, gtk4::Actionable, gtk4::Buildable, gtk4::ConstraintTarget; } impl CustomButton { pub fn new() -> Self { Object::new(&[]).unwrap() } } impl Default for CustomButton { fn default() -> Self{ Self::new() } }
src/temp_button/imp.rs
use gtk4::glib; use gtk4::subclass::prelude::*; #[derive(Default)] pub struct CustomButton; #[glib::object_subclass] impl ObjectSubclass for CustomButton { const NAME: &'static str = "MyGtkAppCustomButton"; type Type = super::CustomButton; type ParentType = gtk4::Button; } impl ObjectImpl for CustomButton {} impl WidgetImpl for CustomButton {} impl ButtonImpl for CustomButton {}
-
修改
window.ui
<?xml version="1.0" encoding="UTF-8"?> <interface> <template class="MyGtkAppWindow" parent="GtkApplicationWindow"> <property name="title">My Gtk App</property> <child> <object class="MyGtkAppCustomButton" id="button"> <property name="label">Press me!</property> <property name="margin-top">12</property> <property name="margin-bottom">12</property> <property name="margin-start">12</property> <property name="margin-end">12</property> </object> </child> </template> </interface>
模板中定义回调函数
我们可以在复合模板中指定处理信号的函数。
-
window.ui
<?xml version="1.0" encoding="UTF-8"?> <interface> <template class="MyGtkAppWindow" parent="GtkApplicationWindow"> <property name="title">My Gtk App</property> <child> <object class="MyGtkAppCustomButton" id="button"> <signal name="clicked" handler="handle_button_clicked" /> <property name="label">Press me!</property> <property name="margin-top">12</property> <property name="margin-bottom">12</property> <property name="margin-start">12</property> <property name="margin-end">12</property> </object> </child> </template> </interface>
-
src/window/imp.rs
// ............. #[glib::object_subclass] impl ObjectSubclass for Window { const NAME: &'static str = "MyGtkAppWindow"; type Type = super::Window; type ParentType = gtk4::ApplicationWindow; fn class_init(kclass: &mut Self::Class) { kclass.bind_template(); // 绑定函数调用模板 kclass.bind_template_callbacks(); } fn instance_init(obj: &InitializingObject<Self>) { obj.init_template(); } } impl ObjectImpl for Window { } #[gtk4::template_callbacks] impl Window { #[template_callback] fn handle_button_clicked(button: &CustomButton) { button.set_label("Hello 2"); } } //..............
在定义回调函数中,访问组件状态
-
在
xml
中将swapped
定义为true,允许访问状态<?xml version="1.0" encoding="UTF-8"?> <interface> <template class="MyGtkAppWindow" parent="GtkApplicationWindow"> <property name="title">My Gtk App</property> <child> <object class="MyGtkAppCustomButton" id="button"> <signal name="clicked" handler="handle_button_clicked" swapped="true" /> <property name="label">Press me!</property> <property name="margin-top">12</property> <property name="margin-bottom">12</property> <property name="margin-start">12</property> <property name="margin-end">12</property> </object> </child> </template> </interface>
-
src/window/imp.rs
#[gtk4::template_callbacks] impl Window { #[template_callback] fn handle_button_clicked(&self,button: &CustomButton) { let number_increased = self.number.get() + 1; self.number.set(number_increased); button.set_label(&number_increased.to_string()); } }