Loading

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下没有我想要的这种方案。当前使用基于wlrootswayfire,所以找到gtk-layer-shell相关协议,这个协议可以实现贴在桌面的效果,当然这都是之后再说的事情了。

Hello World

导入依赖:

cargo new rgtk && cd rgtk
cargo add gtk
cargo run

基本使用:

  1. 构建gtk:Application实例
  2. 连接app实例
  3. 执行主运行函数
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概念

内存管理

GObjectrust-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

  1. 实现ObjectSubClass
    • NAME: 这个子类的名称,为了保持唯一性,使用包名+类名
    • Type: 之后将被创建的实际GObject
    • ParentType: 父类型
  2. 实现祖先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

  1. 定义自定义类公共接口。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

  1. 使用自定义类
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::Valueglib::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/StringVariantDict。这些类型组成的包装类型也可以,像是HashMapVecOptiontuple(最长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编辑器查看

image-20220803214024756

保存窗口状态

​ 如果想在窗口第二次打开时,维持上次的窗口大小,我们可以使用前面的SettingsGTK对此功能并未开箱即用,但是却不难实现。我们想办法保存heightwidthis_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编辑器中发现属性:

image-20220803223747407

列表组件

注:列表组件,都是复用思想。其他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: 指定组件的排列方式(注:指定显示谁,指定不显示谁)

我们只需要创建比界面可展示的组件稍为多一点的组件数量就足够了。当滚动时,不可见的组件将被复用。

img

  1. 定义和填充modelmodelgio::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!(),
            }
        }
    }
    
  2. 使用 :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标记来描述用户界面。

基本使用

  1. 构建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>
    
  2. 更改构建方式

    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", // 构建目标
        );
    }
    
  3. 构建自定义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()
        }
    }
    
  4. 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();
    }
    

使用自定义组件

  1. 创建自定义组件

    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 {}
    
  2. 修改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>
    

模板中定义回调函数

我们可以在复合模板中指定处理信号的函数。

  1. 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>
    
  2. 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");
        }
    }
    //..............
    

在定义回调函数中,访问组件状态

  1. 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>
    
  2. 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());
        }
    }
    
posted @ 2022-07-28 21:34  nsfoxer  阅读(2736)  评论(1编辑  收藏  举报