Rust Slint实现Qt Dial源码分享

一、源码分享

1、效果展示

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

2、源码分享

2.1、工程搭建

参考我这篇博文:【Rust 使用Slint库开发UI界面工程搭建详细教程】

2.2、工程结构

在这里插入图片描述

2.3、main.rs


use slint::{PlatformError};
slint::include_modules!();
use tokio;


#[tokio::main]
async fn main() -> Result<(), PlatformError> {
    let app: MainWindow = MainWindow::new()?;
    let weak: slint::Weak<MainWindow> = app.as_weak();

    app.global::<DataAdapter>().on_btn_clicked({
        let weak: slint::Weak<MainWindow> = weak.clone();
        move |value| {
            if let Some(strong) = weak.upgrade() {
                let _adapter: DataAdapter<'_> = strong.global::<DataAdapter>();

                
            }
        }
    });
    let _ = app.run();

    Ok(())
}


2.4、main.slint

import { AboutSlint, VerticalBox, LineEdit, HorizontalBox, Button, GroupBox, GridBox, 
    ComboBox, Spinner, Slider, ListView, Palette, ProgressIndicator, CheckBox, Switch, StandardListView,ScrollView } from "std-widgets.slint";
import { DataAdapter} from "models.slint";
export { DataAdapter}
import "images/01 Digitall.ttf";
component Dot {
    in property <bool> isBig: false;
    in property <length> radius;
    in property <color> color: #06bddd;

    width: 0;
    height: 0;
    Rectangle {
        y: isBig? radius - 6px: radius;
        x: -(self.width / 2);
        width: isBig? 5px: 3px;
        height: isBig? 14px: 6px;
        background: color;
        border-radius: self.width / 2;
    }    
}


component ScaleNumText {
    in-out property <string> text <=> t.text;
    in property <int> value;
    in property <length> radius;
    in property <brush> color <=>t.color;
    width: 0;
    height: 0;
    t := Text {
        y: radius;
        width: 36px;
        font-size: 18px;
        transform-rotation: -root.transform-rotation - 30deg;
        horizontal-alignment: center;
    }
}

component ValueTextDigit inherits Text {
    in property <bool> active: true;
    in property <int> digit;
    text: active ? digit : 0;
    color: active ? white : #555454;
    width: 36px;
    font-size: 52px;
    font-family: "01 Digitall";
}

component ValueText {
    in property <int> value;
    in property <string> unit:"%";
    HorizontalLayout {
        property <length> default-width: 18px;
        ValueTextDigit {
            digit: (value / 100).floor();
            active: value > 99;
        }

        ValueTextDigit {
            digit: (value.mod(100) / 10).floor();
            active: value > 9;
        }

        ValueTextDigit {
            digit: value.mod(10);
        }
        Text {
            y:self.height/2;
            text: root.unit;
            color: white;
            font-size: 18px;
            font-family: "01 Digitall";
        }
    }
}

component DynamicDial inherits Rectangle{
    width: 500px;
    height: 500px;
   // background: #046621;
    out property <int> value: 0;
    in property <int> minimum: 0;
    in property <int> maximum: 100;
    property <int> total_dots: 51;
    
    
    private property <angle> start_angle: 0deg;
    private property <angle> end_angle: 300deg;
    private property <angle> current_angle: start_angle;

    callback value_changed(val:int);
    Rectangle {
        width: parent.width;
        height: parent.height;
        transform-rotation: 30deg;
       
        for i in total_dots : Dot {
            isBig: Math.mod(i, 5) == 0;
            radius:root.width / 2 - 20px;
            transform-rotation: 306deg * (i / total_dots);
        }
    }
    Rectangle {
        transform-rotation: 30deg;
        property <int> per : (Math.abs(minimum) + Math.abs(maximum)) / 10;
        for i in 11: ScaleNumText {
            text: per*i;
            radius:root.width / 2 - 50px;
            color: white;
            transform-rotation: 330deg * (i / 11);
        }
    }

    Image {
        width: parent.width*0.77;
        height: self.width;
        source: @image-url("images/dial.png");

        Image {
            width: parent.width*0.823;
            height: self.width;
            source: @image-url("images/lines.png");
            colorize: #ffffff42;
            transform-rotation: root.current_angle;
            opacity: 1;
        } 
        ta := TouchArea {
            property <length> centerX: self.width / 2;
            property <length> centerY: self.height / 2;
            property <length> relativeX;
            property <length> relativeY;
            property <angle> newAngle;
            property <angle> deltaDegrees;
            property <bool> firstTouch: false;
            width: parent.width;
            height: parent.height;
            enabled: true;
            pure public function normalizeAngle(angle: angle) -> angle {
                return (angle + 360deg).mod(360deg);
            }
            public function map(x:int,in_min:int,in_max:int,out_min:int,out_max:int) -> int {
                return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
            }
            changed pressed => {
                if !self.pressed {
                    firstTouch = false;
                }
                else{
                    relativeX = ta.mouse-x - centerX;
                    relativeY = ta.mouse-y - centerY;
                    newAngle = normalizeAngle(atan2(relativeY / 1px, relativeX / 1px));
                    root.current_angle = normalizeAngle(newAngle - 120deg).clamp(start_angle, end_angle);

                    root.value = map(root.current_angle/1deg,root.start_angle/1deg,root.end_angle/1deg,root.minimum,root.maximum);
                    root.value_changed(root.value);
                }
            }
            moved => {
                relativeX = ta.mouse-x - centerX;
                relativeY = ta.mouse-y - centerY;
                newAngle = normalizeAngle(atan2(relativeY / 1px, relativeX / 1px));
                newAngle -= 120deg;
                
                if newAngle < root.start_angle && newAngle > -30deg {
                    root.current_angle = root.start_angle;
                }
                else if newAngle >root.end_angle && newAngle < 330deg {
                    root.current_angle = root.end_angle;
                }
                else{
                    root.current_angle = normalizeAngle(newAngle).clamp(start_angle, end_angle);
                }
                    

                root.value = map(root.current_angle/1deg,root.start_angle/1deg,root.end_angle/1deg,root.minimum,root.maximum);
                root.value_changed(root.value);
            }
        }     
        Image{
            x: parent.width/2 + parent.width/2*0.73 * (root.current_angle+120deg).cos() - self.width/2;
            y: parent.height/2 + parent.height/2*0.73 * (root.current_angle+120deg).sin() - self.height/2;
            source: @image-url("images/notch.png");
        }
    }
    ValueText {
        value: root.value;
    }
}








export component MainWindow inherits Window {

    width: 800px;
    height: 600px;
    background: #34373F;

    dial:=DynamicDial {
        value_changed(val) => {
            debug(val)
        }   
    }
}

2.5、models.slint

export global DataAdapter {


    callback btn_clicked(string);
}

2.6、Cargo.toml

[package]
name = "ttt"
version = "0.1.0"
edition = "2024"
author = "<peng.xu@sf-express.com>"

[dependencies]
rand = "0.9.2"
slint = "1.14.1"
slint-interpreter = "1.14.1"
tokio = { version = "1.48.0", features = ["full"] }
[build-dependencies] 
slint-build = "1.14.1"

2.7、源码资源下载

文章顶部下载。

二、Slint详解

Slint 是一个现代的、声明式的原生 GUI 框架,旨在高效、简洁地构建跨平台的用户界面。它特别注重性能和资源消耗,非常适合嵌入式设备和桌面应用程序。下面将从多个角度深入解析:

1、核心概念与设计哲学

  • 声明式 UI: Slint 使用一种自定义的、基于标记的声明式语言(.slint 文件)来描述用户界面。开发者专注于定义 UI 的结构、外观和交互逻辑(状态、属性、绑定、回调),而不是编写命令式的绘图代码。这类似于 React 的 JSX 或 QML。
  • 原生性能: Slint 渲染引擎使用平台原生的图形 API(如 OpenGL, Vulkan, DirectX, Metal 或软件渲染)进行高效绘制,确保流畅的用户体验。
  • 轻量级: Slint 的运行时库非常小巧,编译后的应用程序体积也小,特别适合资源受限的环境(如微控制器)。
  • 跨平台: Slint 支持 Windows, macOS, Linux (X11/Wayland), WebAssembly 以及多种嵌入式平台(如运行 FreeRTOS 的微控制器)。
  • 与 Rust 深度集成: 虽然 Slint 的核心是用 C++ 编写的,但其 Rust API 是一等公民。开发者可以使用 Rust 的强大功能(安全性、并发性、生态)来编写应用程序逻辑。

2、Slint UI 描述语言 (.slint 文件)

这是定义 UI 的主要方式。该语言结构清晰:

import { VerticalBox, Button } from "std-widgets.slint";

export component MainWindow inherits Window {
    // 属性 (Property):定义组件的状态或可配置项
    property <int> counter: 0; // 定义一个整数属性 `counter`,初始值为 0

    // 回调 (Callback):定义可以被 Rust 代码或 UI 元素触发的信号
    callback increment(); // 定义一个无参数回调

    // 布局和子组件
    VerticalBox {
        Text { text: "Count: " + counter; } // 绑定:文本内容与 `counter` 属性同步
        Button {
            text: "Increment";
            clicked => { // 处理按钮点击事件
                increment(); // 触发回调
                counter += 1; // 直接修改属性(绑定会自动更新 UI)
            }
        }
    }
}

关键元素:

  • component 定义一个可重用的 UI 组件(类似于控件或窗口)。
  • property 定义组件的状态变量。类型可以是内置类型(int, float, string, bool, color, length 等)或自定义结构体。属性是响应式的,它们的改变会自动触发依赖它们的 UI 部分更新。
  • callback 定义事件信号。UI 事件(如按钮点击)或 Rust 代码可以触发它们。Rust 代码可以注册处理函数来响应这些回调。
  • 绑定 (=>=)
    • property => expression:单向绑定。当 expression 结果改变时,更新 property
    • property = expression:初始化赋值。
    • property1 <=> property2:双向绑定(较少用)。
    • 表达式可以包含算术运算、逻辑运算、字符串连接、三元运算符等。
  • 布局组件:VerticalBox, HorizontalBox, GridLayout 等,用于组织子组件的位置。
  • 内置组件: Text, Button, Slider, CheckBox, LineEdit 等。
  • 自定义组件: 开发者可以创建自己的 component 以实现复用。
  • 动画: 支持简单的属性动画(如 animate x { duration: 250ms; ... })。

3、Rust 集成与交互

Rust 代码负责应用程序的业务逻辑、数据处理、网络通信等。它与 Slint UI 通过以下方式交互:

  • 加载 .slint 文件: 使用 slint::load_file("ui/main.slint")slint::load_from_string(...) 加载 UI 定义。
  • 获取组件实例: 加载后,可以获取 UI 根组件(如 MainWindow)的实例:
    let main_window = MainWindow::new().unwrap();
    
  • 访问和修改属性: 通过生成的 getter 和 setter 方法:
    // 读取属性
    let current_count = main_window.get_counter();
    // 修改属性 (会触发 UI 更新)
    main_window.set_counter(current_count + 1);
    
  • 注册回调处理函数: 使用 on_<callback_name> 方法注册 Rust 函数来处理 UI 事件:
    main_window.on_increment(|| {
        println!("Increment callback triggered from UI!");
        // 可以在这里执行 Rust 逻辑
    });
    
  • 触发回调: Rust 代码也可以主动触发 UI 中定义的回调,通知 UI 状态变化:
    main_window.increment(); // 触发定义在 .slint 中的 `increment` 回调
    
  • 运行事件循环: 最后,调用 run() 启动主事件循环,显示窗口并处理用户输入:
    main_window.run().unwrap();
    

4、架构概览

+---------------------------------------+
|       Your Rust Application Code       |
| (Business Logic, Data, Network, etc.) |
+------------------+--------------------+
                   | (Interact via Properties & Callbacks)
                   v
+------------------+--------------------+
| Slint Runtime (Rust API)              |
| - Loads .slint files                  |
| - Manages component instances         |
| - Bridges UI events to Rust           |
| - Handles property bindings           |
+------------------+--------------------+
                   | (Internal)
                   v
+------------------+--------------------+
| Slint Compiler / Interpreter          |
| - Parses .slint DSL                   |
| - Evaluates expressions & bindings    |
| - Handles layout                      |
+------------------+--------------------+
                   | (Render Commands)
                   v
+------------------+--------------------+
| Slint Renderer                        |
| - Uses native graphics APIs           |
| (OpenGL/Vulkan/DX/Metal/Software)     |
| - Efficiently draws UI elements       |
+---------------------------------------+

5、优点

  • 高效开发: 声明式语言简化 UI 构建。
  • 高性能: 原生渲染,低资源占用。
  • 跨平台: 广泛支持各种操作系统和设备。
  • Rust 集成: 无缝结合 Rust 的安全性和性能。
  • 响应式: 基于属性的自动 UI 更新。
  • 嵌入式友好: 极小的内存和 CPU 占用。

6、学习资源与工具

  • 官方文档: https://slint.dev/docs 是最权威的资源,包含语言参考、Rust API 文档和教程。
  • 示例仓库: https://github.com/slint-ui/slint/tree/master/examples 包含各种功能的示例代码。
  • Slint VS Code 扩展: 提供语法高亮、代码片段、实时预览等功能,极大提升开发效率。
  • slint-viewer 工具: 可以快速预览 .slint 文件的效果,无需编译整个 Rust 项目。

7、适用场景

  • 嵌入式设备 GUI (MCU)
  • 桌面应用程序 (Windows, macOS, Linux)
  • 需要原生性能和轻量级的应用
  • 希望使用 Rust 构建 UI 的项目

总结: Rust Slint 是一个强大的工具,它通过声明式语言、高效的渲染引擎和与 Rust 的深度集成,为开发者提供了一种构建高性能、跨平台原生 GUI 的现代解决方案。对于追求性能、效率和 Rust 生态集成的项目来说,它是一个非常值得考虑的选择。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小灰灰搞电子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值