Rust UI 框架:用 Rust 和 Slint 制作一个简易计算器(Live coding )

8,012 阅读6分钟

Slint 背景知识

Slint 是一个 GUI 工具包,可以高效地为任何显示器开发流畅的图形用户界面:嵌入式设备和桌面应用程序。支持多种编程语言,例如 Rust 、 C ++或 JavaScript 。 Slint 作为开源商业化产品,取得了很大的进展。

2022年 slint 发展回顾:

  • 将SixFPS 改名为 slint ,定义了品牌。
  • 在2022第一季度被公认为增长最快的 OSS 初创公司之一,年增长率为470%!
  • slint 的免费专有许可证开始受到关注:已有15位大使正在使用此免费许可证构建他们的项目
  • 参加嵌入式世界展览和会议,在那里展示了 Slint 在各种嵌入式设备上的强大功能,包括功率非常低的 Raspberry Pi Pico
  • 与多家设计和服务公司建立了合作伙伴关系计划
  • 获得了六位数的种子资金来扩大团队:用于产品开发、销售和业务发展.作为白银会员加入 Rust 基金会
  • 参加了 EuroRust 2022会议, Tobias 在会上介绍了 Rust 和 C ++的互操作性
  • Florian 在将 Slint 移植到 Redox OS 方面取得了惊人的进展,一些实用程序已经基于 Slint
  • 与 PopOS 合作,使 Slint 成为开发人员的替代工具包

2023 年 Slint 1.0 正式版发布,标志着项目已顺利从开发阶段“毕业”,可正式用于生产环境。

Slint 创始团队都是来自于 Qt 社区(使用 Slint 有没有是曾相识的感觉)。

注:全球知名 Qt 咨询和 UI/UX 设计服务公司 tQCS 的合作伙伴有两家都加入了 Rust 基金会银牌会员(QT 的两个好逆子🐶🐱,既合作又竞争⛽️,目标指向蚕食 Qt 市场 🏴‍☠️,真是“父慈子孝”,“兄恭弟让”😂)。分别是:

  • Slint: 极大地简化了取代 Qt 需求的嵌入式平台的 GUI 开发。支持 Rust/Cpp/Javascript ,有设计友好的 UI 标记语言。其创始人同样来自 Qt 项目主要贡献者,QtQml 引擎的主要开发者。
  • KDAB :在嵌入式系统、3D 图形以及跨桌面、嵌入式和移动平台的工作方面拥有多年经验, KDAB( Slint 的竞争对手/合作伙伴) 是 Qt 项目的主要贡献者。他们正在研究 CXX-Qt 以使 Qt 和 Rust 更容易地一起使用。CXX-Qt 可用于使用 CMake 将 Rust 集成到 C ++应用程序中,或用于使用 Cargo 构建 Rust 应用程序,支持在 C ++、 QML 和 JavaScript 中使用。

目前 CXX - Qt 处于早期开发阶段, API 经常更改,可以参考 CXX-Qt book 来了解更多,而 Slint 发展迅猛, Slint 1.0 正式版已于2023年正式发布。

Slint 使用了声明式编程来简化 UI 的开发,优化应用程序开发和性能的方法是:

  • 用声明式语言来描述 UI,使用的语法提供了一种广泛的方式来描述各种图形元素,同时易于阅读、编写和学习
  • Slint 编译器对描述 UI 的代码进行优化并翻译成原生代码
  • 采用任何语言编写的业务逻辑,可通过使用 Slint 提供的特定于语言的 API 与 UI 连接

Slint 整体架构

详见:github.com/slint-ui/sl…

Slint 控件支持

Slint 的控件还是比较丰富的,基本够用了,官方提供的控件分为需要从"std-widgets.slint"文件导入(import)的小部件(Widgets)和不需要导入直接可用的内置部分(Builtin Elements And Enums),它们的属性、用途、以及使用方式可以查阅官方文档,非常简单(声明式编程都大同小异,通过属性控制显示行为,比如通用的位置xyz和布局相关的宽高、spacing、padding、alignment ,及背景,动画等),不用强行记忆,看几遍文档有个大概印象,大多数控件和属性都是"观其名,就知其大体用途",用到时候查阅具体如何使用即可:

Slint GUI 应用程序示例:一个简易计算器

一视频胜过千言万语(一定要看完视频,视频才是本文分享的重点),视频中,博主 Live coding 使用 Rust 和 Slint GUI 套件制作一个小型 GUI 应用程序:一个简易计算器,通过一步一步 Live coding 来示范开发一个简易计算器应用程序,学习如何使用 Slint 制作一些简单 GUI 程序的全过程,包括 Slint 的自定义组件,组件的属性和使用,全局单例和回调 ,Rust 代码和 Slint 交互,可以跟着博主思路和操作自己敲一遍代码,可能也许一定会有不一样的收获。

Youtube 视频:Live coding to create a small simple GUI application with Rust and the Slint toolkit (https://slint.rs)

注:无法观看Youtube视频的同学,可以这里看完整视频mp.weixin.qq.com/s/ZmnG4Cu4R…,我给视频添加了中英文字幕,当然中文字幕是机译的,有些地方可能翻译不太准确,读者见谅,不过视频中操作和代码,即使不用字幕大体都能看懂(掘金不能上传视频😓)

完整代码:

创建一个新的 cargo 项目:

cargo new slint-calculator
cd slint-calculator

可以命令行添加 slint 的依赖项:

cargo add slint@1.0.0

最终 Cargo.toml 文件显示

[package]
name = "slint-calculator"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
slint = "1.0.0"

main.rs 中添加代码:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
   // 创建窗体应用程序
   let app =  App::new().unwrap();
   let weak = app.as_weak();// as_weak避免内存泄露
   let state: Rc<RefCell<CalcState>> = Rc::new(RefCell::new(CalcState::default()));
   // 处理.slint中 CalcLogic 全局单例的回调
   app.global::<CalcLogic>().on_button_pressed(move |value| {
      let app = weak.unwrap();
      let mut state = state.borrow_mut();
      println!("pressed value: {}", value);
      // 只处理输入的数字,保存到 state 中
      if let Ok(val) = value.parse::<i32>() {
        state.current_value *=10;
        state.current_value += val;
        app.set_value(state.current_value);
        return;
      }
      // 处理等号"="逻辑
      if value.as_str() == "=" {
        let reslut = match state.operator.as_str() {
            "+" => state.prev_value + state.current_value,
            "-" => state.prev_value - state.current_value,
            "*" => state.prev_value * state.current_value,
            "/" => state.prev_value / state.current_value,
            _=> state.current_value,
        };
        // 输出结果
        app.set_value(reslut);
        println!("{} {} {} = {}", state.prev_value, state.operator, state.current_value, reslut);
        // 重置 state
        state.current_value = 0;
        state.prev_value = reslut;
        state.operator = Default::default();
      } else {
        state.operator = value.clone();
        state.prev_value = state.current_value;
        state.current_value = 0;
      }
   });
   // 运行窗体程序
   app.run().unwrap();
   println!("Hello, world! Hello, Slint!");

}

// 保存输入数据的结构体
#[derive(Default)]
struct CalcState {
   prev_value: i32,
   current_value: i32,
   operator:slint::SharedString,
}

// Slint 宏构建 UI
slint::slint! {
    import { VerticalBox } from "std-widgets.slint";

    // 导出全局单例:Rust 代码可以操作
    export global CalcLogic {
        callback button-pressed(string);
    }
    // 自定义 Button 组件
    component Button {
        in property <string> text;
        min-height:30px;
        min-width: 30px;
        in property <brush> background: @linear-gradient(-20deg, #a0a3e4, #3c58e3);
        Rectangle {
            background: ta.pressed ? red :ta.has-hover? background.darker(10%) : background;
            animate background {duration: 100ms; }
            border-radius: 4px;
            border-width: 2px;
            border-color: self.background.darker(20%);
            ta := TouchArea {
                // Button初始化
                init => { debug("Button init"); }
                // Button点击事件,回传给 Rust 处理
                clicked => {
                    debug("Button clicked");
                    CalcLogic.button-pressed(root.text)
                }
            }
            Text {text: root.text;}
        }
    }
    export component App inherits Window {
        title: "Slint Calculator";
        in property <int> value: 0 ;
        // Slint 内嵌的网格组件
        GridLayout {
            padding: 10px;
            spacing: 5px;
            Text {text: value; colspan: 3;}
            Row {
                Button {text: "1";}
                Button {text: "2";}
                Button {text: "3";}
                Button {text: "+"; background: yellow;}
            }
            Row {
                Button {text: "4";}
                Button {text: "5";}
                Button {text: "6";}
                Button {text: "-"; background: yellow;}
            }
            Row {
                Button {text: "7";}
                Button {text: "8";}
                Button {text: "9";}
                Button {text: "*"; background: yellow;}
            }
            Row {
                Button {text: "0";}
                Button {text: "="; col: 2; background: green;}
                Button {text: "/"; background: yellow;}
            }
        }
    }
}

执行命令:cargo run,运行效果如下图(注:以上所有程序开发均在vs code下完成,依赖slint官方插件)

QQ20230518-122450@2x.png

视频和代码只是学习 Slint + Rust 的一个简单示例,目的是通过开发一个应用程序的来学习 Slint 相关知识,当然这只是一个简易版的计算器,自己可以按照真实版计算器来继续完善功能,让它更像一个完整的计算器。

Slint 资料