• 【一起学Rust | 框架篇 | iced框架】rust原生跨平台GUI框架——iced



    前言

    学习一门编程语言,绝对不可以抛弃该编程语言的应用。在学习其他编程语言时,例如C++,只学习语法,数据结构与算法是相当枯燥的,这就很考虑一个人的毅力了。此时最好的办法就是让学习变得有趣起来,在我学习的时候,我的兴趣之源就是想要做出Windows上华丽的窗口,来提高自己的学习和工作效率,为此我学习过QT,MFC,这些框架都挺好,确实实现了我想要的效果,但是开发较为繁琐,后来我又学习qt python,确实开发变得很方便了,但是新的问题又出现了,就是打包不方便,为此,我在这个路上经历了曲折的探索。此时学习Rust也是一样的,要想提升自己的学习力,就要找到其中感兴趣的点,对于Rust,我很想利用Rust的优势来实现Windows的窗口,也找过相关的解决方案,比如Rust的qt,但是它比较繁琐,在后面的文章会介绍到;还有使用Rust作为后端的跨平台ui框架Tauri UI,他的思路更像是Electron这种的,前端使用html+css布局,然后后端使用Rust,再打包好app,其使用方式在本系列文章后面也会介绍;还有就是本期文章要介绍的Iced,它更像是Rust的flutter,基于Elm实现的跨平台GUI框架。

    Iced是一个我较为感兴趣的GUI框架,其开发方式对我我这种学习了Vue的人来说相当友好,且配和Rust的特点,已经是很舒服了。此外它颜值也挺高,这就是我学习它的理由。

    Iced的特点

    • 简单易用,有一系列内置API
    • 类型安全,有一套交互编程模型
    • 跨平台(支持Windows,Mac,Linux和Web)
    • 响应式布局
    • 基于widgets
    • 支持自定义widgets
    • 还有其他特性,建议去Github查看

    一、搭建项目

    1. 正常创建项目

    首先创建一个项目

    cargo new gui_iced_001
    
    • 1

    2. 导入idea

    使用idea导入项目

    3. 引入依赖

    打开Cargo.toml,然后在依赖出写入

    iced = "0.4"
    
    • 1

    注意:本人用的是2021以后的版本,如果你不是,建议去官网学习对应的处理策略,这里不解释。

    此时文件应该是这样的

    到此为止,项目就搭建完毕,接下来就是写我们的demo了。

    二、编写demo

    在Github上是有很多Iced的例子的,其中最经典,也是官网唯一写上去的例子就是Counter计数器,因从这里就实现Counter的demo。虽然说这个很简单,但是其中坑很多,好不容易才写出这个demo,就说两个比较坑的地方把,这个框架行和列不分,例子中代码太过老旧,都是我一步一步探索出来的。

    以下内容与官网会有较大的差别,官网的运行不了,请注意甄别。

    1. 编写State

    首先为程序编写一个State,这个核心概念在学习Vue和React或者Flutter的时候必然会用到,这里暂时不做解释,直接给出代码

    struct Counter {
        value: i32,
        // The local state of the two buttons
        increment_button: button::State,
        decrement_button: button::State,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里是定义了一个结构体Counter ,这个结构体就当作我们窗口的State,其成员value代表计数器计数的数值,increment_buttondecrement_button是两个按钮+和-的state。

    2. 定义消息类型

    接下来就是定义程序中用到的消息类型,程序中的交互是通过信号来进行的,这点Qt就做的很好,如果你学习过qt或者前端的Vue等框架,这个就很好理解了。

    #[derive(Debug, Clone, Copy)]
    enum Message {
        IncrementPressed,
        DecrementPressed,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这里定义了两个消息,一个是IncrementPressed,代表+按钮被点击,一个是DecrementPressed,代表-按钮被点击。

    3. 编写视图逻辑

    这里官方给出的例子是直接为Counter 实现view和update,但是经过我的探索,是不可以直接使用的。

    Iced程序运行,需要实现Application或者Sandbox,这俩是什么意思现在先不管,我们这里使用的是Sandbox,因为其足够的简单。

    一个空的Sandbox应该是这样的

    impl Sandbox for Counter {
        type Message = ();
    
        fn new() -> Self {
            todo!()
        }
    
        fn title(&self) -> String {
            todo!()
        }
    
        fn update(&mut self, message: Self::Message) {
            todo!()
        }
    
        fn view(&mut self) -> Element<'_, Self::Message> {
            todo!()
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    这里从上到下开始介绍

    Message

    代表的是当前窗口的所有消息,在使用时,需要传入定义好的消息枚举,就比如说这里时用法应该是这样的,

    #[derive(Debug, Clone, Copy)]
    enum Message {
        IncrementPressed,
        DecrementPressed,
    }
    // ....
    type Message = Message;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    new

    这里和通常编写代码一样,需要返回自身实例,就不做过多解释了

    fn new() -> Self {
            Self { value: 0, increment_button: Default::default(), decrement_button: Default::default() }
        }
    
    • 1
    • 2
    • 3

    title

    见名知义,这里就是要返回当前窗口的名字

    fn title(&self) -> String {
            String::from("Counter - Iced")
        }
    
    • 1
    • 2
    • 3

    update

    这里时处理窗口的消息逻辑,本窗口处理两个消息,一个是IncrementPressed,代表+按钮被点击,如果被点击了,State的Value就+1,一个是DecrementPressed,代表-按钮被点击,如果被点击了,State的Value就-1。这里处理相当简单,并未考虑边界值。

    fn update(&mut self, message: Message) {
            match message {
                Message::IncrementPressed => {
                    self.value += 1;
                }
                Message::DecrementPressed => {
                    self.value -= 1;
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    view

    这里要返回窗口的布局,实际上也就是要构建这个窗口,这里Counter的窗口代码如下

    fn view(&mut self) -> Element<Message> {
            Column::new().push(
                Text::new("Counter")
            ).push(
                Row::new().push(
                    Button::new(&mut self.increment_button, Text::new("+"))
                        .on_press(Message::IncrementPressed).width(Length::Fill),
                ).push(
                    Text::new(self.value.to_string()).size(22),
                ).push(
                    Button::new(&mut self.decrement_button, Text::new("-"))
                        .on_press(Message::DecrementPressed).width(Length::Fill),
                )
            )
            .align_items(Alignment::Center).into()
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    可以看到,这里使用的时链式调用,一层套一层,其代码和flutter特别的相似,如果你学习过flutter,那必然非常熟悉,这里我画一个图,来解释这段代码,首先由Column将窗口分为两行,其布局和红框是一致的,上下两行

    注意:这个框架行和列傻傻分不清,Column是列的意思,在这个框架里面是行的意思。

    第一行,只添加了个Text组件,并且给初始值Counter,这里原本是打算使用中文计数器的,奈何这玩意儿不支持中文

    Column::new().push(
                Text::new("Counter")
            )
    
    • 1
    • 2
    • 3

    第二行,其中添加了个Row组件(组件),并且加入了三个组件,分别是两个Button,就是+和-按钮,一个Text组件,用来显示当前Value

    //...
    .push(
                Row::new().push(
                    Button::new(&mut self.increment_button, Text::new("+"))
                        .on_press(Message::IncrementPressed).width(Length::Fill),
                ).push(
                    Text::new(self.value.to_string()).size(22),
                ).push(
                    Button::new(&mut self.decrement_button, Text::new("-"))
                        .on_press(Message::DecrementPressed).width(Length::Fill),
                )
            )
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    所以此时的窗口布局应该是这样的

    4. 编写main函数

    写好的窗口是无法自动运行的,需要启动才可以,通常在main函数中启动窗口,在这里会变得简单起来,这里直接贴上代码

    fn main() -> iced::Result {
        Counter::run(Settings::default())
    }
    
    • 1
    • 2
    • 3

    启动窗口只要调用窗口的run方法就好了,其中传入Settings,用来设置窗口的初始状态,这里直接用默认状态了,如果后续还要进入深入,这里会专门出一期来进行讲解。

    三、运行效果

    此时我们运行当前写好的demo

    注意:完整代码放在文章末尾,如果你懒得敲代码,可以直接复制。


    总结

    本期为大家介绍了Rust的GUI框架Iced,经过我的探索,终于是将Counter Demo搭建起来。经过我的体验,我认为这个框架其实并没有我想象中的那么好,它确实是Rust的原生GUI框架,也确实有他说的那些特点,确实有颜值,但是它有个最大的问题就是不支持中文,而且行和列傻傻分不清,官方文档太过老旧,所以搭建demo流程就变得很复杂,对开发不是很友好,唯一值得一说的就是这个代码,确实是舒服了不少,这是其他UI框架所无法比的上的,这点值得赞同,希望以后这个框架或者后继者能解决这些问题,Rust的UI才能强大起来。

    完整代码

    use iced::{Alignment, button, Button, Column, Element, Length, Row, Sandbox, Settings, Text};
    
    fn main() -> iced::Result {
        Counter::run(Settings::default())
    }
    
    struct Counter {
        value: i32,
        // The local state of the two buttons
        increment_button: button::State,
        decrement_button: button::State,
    }
    
    #[derive(Debug, Clone, Copy)]
    enum Message {
        IncrementPressed,
        DecrementPressed,
    }
    
    impl Sandbox for Counter {
        type Message = Message;
    
        fn new() -> Self {
            Self { value: 0, increment_button: Default::default(), decrement_button: Default::default() }
        }
    
        fn title(&self) -> String {
            String::from("Counter - Iced")
        }
    
        fn update(&mut self, message: Message) {
            match message {
                Message::IncrementPressed => {
                    self.value += 1;
                }
                Message::DecrementPressed => {
                    self.value -= 1;
                }
            }
        }
    
        fn view(&mut self) -> Element<Message> {
            Column::new().push(
                Text::new("Counter")
            ).push(
                Row::new().push(
                    Button::new(&mut self.increment_button, Text::new("+"))
                        .on_press(Message::IncrementPressed).width(Length::Fill),
                ).push(
                    Text::new(self.value.to_string()).size(22),
                ).push(
                    Button::new(&mut self.decrement_button, Text::new("-"))
                        .on_press(Message::DecrementPressed).width(Length::Fill),
                )
            )
            .align_items(Alignment::Center).into()
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
  • 相关阅读:
    CSP-J2022普及组题解T4:上升点列
    WebGPU助力客户端Crypto/ZK
    【NOWCODER】- Python:条件语句
    从零开始搭建前端脚手架(三)-- [动态添加、删除模板]
    (二十一)数据结构-二叉排序树、平衡二叉树、散列查找
    迷茫了3年:做完这个测试项目,我终于决定辞职
    执行上下文和闭包
    数据分析之金融数据分析
    LeetCode //C - 103. Binary Tree Zigzag Level Order Traversal
    pytest实现日志按用例输出到指定文件中
  • 原文地址:https://blog.csdn.net/weixin_47754149/article/details/127271805