云计算百科
云计算领域专业知识百科平台

Rust+Slint 实现个性化消息通知栏源码分享

在这里插入图片描述

Rust+Slint 实现个性化消息通知栏源码分享

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、main.rs
    • 3、build.rs
    • 4、main.slint
    • 5、Cargo.toml
  • 三、实现原理
    • 1、整体架构
    • 2、数据流设计
      • 2.1 消息数据结构
      • 2.2 双向数据绑定
    • 3、消息生命周期管理
      • 3.1 添加消息
      • 3.2 进度条计算
      • 3.3 自动过期清理
    • 4、UI 渲染原理
      • 4.1 尖角箭头形状
      • 4.2 颜色与图标映射
      • 4.3 进度条
    • 5、关闭消息的回调链路
    • 6、关键设计总结

一、效果展示

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

二、源码分享

1、工程结构

在这里插入图片描述

2、main.rs

slint::include_modules!();

use slint::{ModelRc, Timer, TimerMode, VecModel};
use std::rc::Rc;
use std::time::{Duration, Instant};

/// 消息内部数据(携带创建时间,用于计算剩余进度)
#[derive(Clone, Debug)]
struct MessageData {
id: i32,
text: String,
msg_type: MessageType,
created_at: Instant,
duration: Duration,
}

impl MessageData {
/// 转换为 Slint 可见的 Message 结构
fn to_slint(&self) -> Message {
let elapsed = self.created_at.elapsed().as_secs_f32();
let total = self.duration.as_secs_f32();
let remaining = if total > 0.0 {
(1.0 – elapsed / total).clamp(0.0, 1.0)
} else {
0.0
};
Message {
id: self.id,
text: self.text.clone().into(),
r#type: self.msg_type.clone(),
remaining,
}
}
}

fn main() {
let ui = MainWindow::new().expect("无法创建主窗口");

// 消息数据存储(含创建时间,Rust 侧维护)
let messages_data: Rc<std::cell::RefCell<Vec<MessageData>>> =
Rc::new(std::cell::RefCell::new(Vec::new()));

// 同步到 UI 的 ModelRc
let messages_model: Rc<VecModel<Message>> = Rc::new(VecModel::from(vec![]));
ui.set_messages(ModelRc::new(messages_model.clone()));

// ID 生成计数器
let next_id = Rc::new(std::cell::Cell::new(1_i32));

// 各类型消息默认显示时长
fn duration_for(t: &MessageType) -> Duration {
match t {
MessageType::Info => Duration::from_secs(5),
MessageType::Warning => Duration::from_secs(8),
MessageType::Error => Duration::from_secs(12),
}
}

// 添加一条新消息的闭包
let add_message = {
let data = messages_data.clone();
let model = messages_model.clone();
let id_gen = next_id.clone();

move |text: &str, msg_type: MessageType| {
let id = id_gen.get();
id_gen.set(id + 1);

let duration = duration_for(&msg_type);
data.borrow_mut().push(MessageData {
id,
text: text.to_string(),
msg_type,
created_at: Instant::now(),
duration,
});

// 刷新 UI 模型
let updated: Vec<Message> = data.borrow().iter().map(MessageData::to_slint).collect();
model.set_vec(updated);
}
};

// 注册添加消息的回调
{
let add = add_message.clone();
ui.on_add_info(move || {
add("这是一条提示消息", MessageType::Info);
});
}
{
let add = add_message.clone();
ui.on_add_warning(move || {
add("这是一条警告消息", MessageType::Warning);
});
}
{
let add = add_message.clone();
ui.on_add_error(move || {
add("这是一条故障消息", MessageType::Error);
});
}

// 注册关闭消息的回调(根据 id 移除)
{
let data = messages_data.clone();
let model = messages_model.clone();
ui.on_close_message(move |id| {
data.borrow_mut().retain(|m| m.id != id);
let updated: Vec<Message> = data.borrow().iter().map(MessageData::to_slint).collect();
model.set_vec(updated);
});
}

// 自动清除定时器:每 100ms 刷新进度条并移除过期消息
{
let data = messages_data.clone();
let model = messages_model.clone();
let timer = Timer::default();
timer.start(
TimerMode::Repeated,
Duration::from_millis(100),
move || {
// 移除已过期消息
data.borrow_mut()
.retain(|m| m.created_at.elapsed() < m.duration);
// 刷新 UI(更新进度条 + 移除过期项)
let updated: Vec<Message> =
data.borrow().iter().map(MessageData::to_slint).collect();
model.set_vec(updated);
},
);
// timer 需保持存活,泄漏到堆上使其与进程同生命周期
std::mem::forget(timer);
}

ui.run().expect("运行 UI 失败");
}

3、build.rs

fn main() {
// 编译 Slint UI
slint_build::compile("ui/main.slint").unwrap();
}

4、main.slint

import { Button, LineEdit, SpinBox, ComboBox, ScrollView } from "std-widgets.slint";

// ==================== 消息提示栏 ====================

// 消息类型枚举
export enum MessageType {
info, // 提示
warning, // 警告
error, // 故障
}

// 消息数据结构
export struct Message {
id: int,
text: string,
type: MessageType,
remaining: float, // 0.0 ~ 1.0 剩余进度
}

// 单条消息提示组件 – 左凹右凸的尖角箭头形状(首尾相连)
component MessageToast {
in property <string> message–text;
in property <MessageType> message–type;
in property <float> progress; // 0.0 ~ 1.0
callback close–clicked();

preferred–width: 160px;
preferred–height: 36px;
min–width: 160px;
min–height: 36px;

TouchArea {
width: 100%;
height: 100%;
clicked => { close–clicked(); }

// 箭头形状容器(比主体宽 12px,给右箭头留空间)
ribbon := Rectangle {
width: parent.width + 12px;
height: 100%;
clip: true;

// 形状:>__> 左右都是尖角 ">"
// 左边 "<" 凹入 12px,右边 ">" 凸出 12px
//
// (12,0)─────────(w-12,0)
// \\ \\
// \\ 消息内容 ▶ (w+12, h/2)
// / /
// (0,h/2) (w-12,h)
// \\
// \\
// (12,h)───────────
Path {
width: 100%;
height: 100%;
fill: message–type == MessageType.info
? #3b82f6
: message–type == MessageType.warning
? #f59e0b
: #ef4444;

commands: "M 12,0"
+ " L " + (ribbon.width / 1px – 12) + " 0"
+ " L " + (ribbon.width / 1px) + " " + (ribbon.height / 1px / 2)
+ " L " + (ribbon.width / 1px – 12) + " " + (ribbon.height / 1px)
+ " L 12 " + (ribbon.height / 1px)
+ " L 24 " + (ribbon.height / 1px / 2)
+ " Z";
}

// 半透明描边
Rectangle {
x: 0; y: 0;
width: ribbon.width;
height: ribbon.height;
border–width: 0px;
border–color: #ffffff30;
}

// 底部进度条
Rectangle {
x: 14px;
y: ribbon.height – self.height – 3px;
width: (ribbon.width – 45px) * progress;
height: 2px;
background: message–type == MessageType.info
? #bfdbfe
: message–type == MessageType.warning
? #fde68a
: #fecaca;
}
}

// 图标 + 文字叠加层
HorizontalLayout {
padding–left: 10px;
padding–right: 28px;
alignment: start;

Text {
text: message–type == MessageType.info
? "💡"
: message–type == MessageType.warning
? "⚡"
: "❌";
font–size: 14px;
vertical–alignment: center;
}

text–display := Text {
text: message–text;
color: #ffffff;
font–size: 12px;
vertical–alignment: center;
horizontal–alignment: left;
overflow: elide;
preferred–width: 100%;
horizontal–stretch: 1;
wrap: word–wrap;
}
}
}
}

// 消息栏容器 – 从左到右依次排列,消息多时可水平滚动
component MessageBar {
in property <[Message]> messages;
callback close–msg(int);

log–scroll := ScrollView {
vertical–scrollbar–policy: always–off;
horizontal–scrollbar–policy: always–off;

// 每条消息 160px,间距 -5px → 步长 155px,末尾 +5px 补偿
// 内容总宽 = messages.length * 155 + 5
viewport–x: –1*max(0px, (messages.length * 155 + 5) * 1px – self.width);

HorizontalLayout {
spacing: –5px;
alignment: start;

for msg[index] in messages : MessageToast {
message–text: msg.text;
message–type: msg.type;
progress: msg.remaining;
close–clicked => { close–msg(msg.id); }
}
}
}
}

// ==================== 主窗口 ====================
export component MainWindow inherits Window {
title: "消息提示栏";
preferred–width: 800px;
preferred–height: 600px;
background: #f1f5f9;
// 关闭消息回调
callback close–message(int);

// 添加消息回调(Rust 端处理)
callback add–info();
callback add–warning();
callback add–error();

// 消息列表(Rust 端设置,双向绑定到 MessageBar)
in property <[Message]> messages: [
{ id: 1, text: "这是一条信息提示", type: MessageType.info, remaining: 1.0 },
{ id: 2, text: "这是一条警告提示", type: MessageType.warning, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
{ id: 3, text: "这是一条故障提示", type: MessageType.error, remaining: 1.0 },
];

// 倒计时文本(Rust 端每秒更新)
property <int> countdown–sec: 0;
VerticalLayout {
Rectangle {
height: 50px;
}
// 顶部消息栏区域
Rectangle {
height: 60px;
background: transparent;

messages–bar := MessageBar {
x: 8px;
y: 8px;
width: parent.width – 16px;
height: parent.height – 16px;
messages: root.messages;
}
}

// 主内容区 – 演示按钮
Rectangle {
vertical–stretch: 1;
background: #f1f5f9;

VerticalLayout {
alignment: center;
spacing: 20px;

HorizontalLayout {
alignment: center;
spacing: 16px;

Button {
text: "💡 添加提示";
primary: true;
clicked => { add–info(); }
}

Button {
text: "⚡ 添加警告";
clicked => { add–warning(); }
}

Button {
text: "❌ 添加故障";
clicked => { add–error(); }
}
}

// 倒计时显示
Text {
text: countdown–sec > 0 ? "下一轮倒计时: " + countdown–sec + " 秒" : "";
font–size: 12px;
color: #94a3b8;
horizontal–alignment: center;
visible: countdown–sec > 0;
}
}
}
}

// 消息计数器(用于生成唯一 ID,Rust 端维护)
property <int> msg–counter: 0; // 保留供 Rust 端读写
}

5、Cargo.toml

[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"

[dependencies]
slint = "1.17.1"

[build–dependencies]
slint–build = "1.17.1"

三、实现原理

1、整体架构

整个消息通知栏采用 Rust 负责业务逻辑、Slint 负责界面渲染 的前后端分离架构。Rust 侧维护消息的完整生命周期(创建、计时、过期清理),Slint 侧只负责把消息列表渲染成可视化的尖角箭头样式,并通过回调与 Rust 交互。

#mermaid-svg-73EoDCyw5E3WTppb{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-73EoDCyw5E3WTppb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-73EoDCyw5E3WTppb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-73EoDCyw5E3WTppb .error-icon{fill:#552222;}#mermaid-svg-73EoDCyw5E3WTppb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-73EoDCyw5E3WTppb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-73EoDCyw5E3WTppb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-73EoDCyw5E3WTppb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-73EoDCyw5E3WTppb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-73EoDCyw5E3WTppb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-73EoDCyw5E3WTppb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-73EoDCyw5E3WTppb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-73EoDCyw5E3WTppb .marker.cross{stroke:#333333;}#mermaid-svg-73EoDCyw5E3WTppb svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-73EoDCyw5E3WTppb p{margin:0;}#mermaid-svg-73EoDCyw5E3WTppb .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-73EoDCyw5E3WTppb .cluster-label text{fill:#333;}#mermaid-svg-73EoDCyw5E3WTppb .cluster-label span{color:#333;}#mermaid-svg-73EoDCyw5E3WTppb .cluster-label span p{background-color:transparent;}#mermaid-svg-73EoDCyw5E3WTppb .label text,#mermaid-svg-73EoDCyw5E3WTppb span{fill:#333;color:#333;}#mermaid-svg-73EoDCyw5E3WTppb .node rect,#mermaid-svg-73EoDCyw5E3WTppb .node circle,#mermaid-svg-73EoDCyw5E3WTppb .node ellipse,#mermaid-svg-73EoDCyw5E3WTppb .node polygon,#mermaid-svg-73EoDCyw5E3WTppb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-73EoDCyw5E3WTppb .rough-node .label text,#mermaid-svg-73EoDCyw5E3WTppb .node .label text,#mermaid-svg-73EoDCyw5E3WTppb .image-shape .label,#mermaid-svg-73EoDCyw5E3WTppb .icon-shape .label{text-anchor:middle;}#mermaid-svg-73EoDCyw5E3WTppb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-73EoDCyw5E3WTppb .rough-node .label,#mermaid-svg-73EoDCyw5E3WTppb .node .label,#mermaid-svg-73EoDCyw5E3WTppb .image-shape .label,#mermaid-svg-73EoDCyw5E3WTppb .icon-shape .label{text-align:center;}#mermaid-svg-73EoDCyw5E3WTppb .node.clickable{cursor:pointer;}#mermaid-svg-73EoDCyw5E3WTppb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-73EoDCyw5E3WTppb .arrowheadPath{fill:#333333;}#mermaid-svg-73EoDCyw5E3WTppb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-73EoDCyw5E3WTppb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-73EoDCyw5E3WTppb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-73EoDCyw5E3WTppb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-73EoDCyw5E3WTppb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-73EoDCyw5E3WTppb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-73EoDCyw5E3WTppb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-73EoDCyw5E3WTppb .cluster text{fill:#333;}#mermaid-svg-73EoDCyw5E3WTppb .cluster span{color:#333;}#mermaid-svg-73EoDCyw5E3WTppb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-73EoDCyw5E3WTppb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-73EoDCyw5E3WTppb rect.text{fill:none;stroke-width:0;}#mermaid-svg-73EoDCyw5E3WTppb .icon-shape,#mermaid-svg-73EoDCyw5E3WTppb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-73EoDCyw5E3WTppb .icon-shape p,#mermaid-svg-73EoDCyw5E3WTppb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-73EoDCyw5E3WTppb .icon-shape .label rect,#mermaid-svg-73EoDCyw5E3WTppb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-73EoDCyw5E3WTppb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-73EoDCyw5E3WTppb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-73EoDCyw5E3WTppb :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

Rust 侧维护消息数据

同步到 Slint 的 ModelRc

MessageBar 渲染消息列表

用户点击消息关闭

回调 close-message 通知 Rust

Rust 移除消息并刷新模型

2、数据流设计

2.1 消息数据结构

Rust 侧定义了一个内部结构 MessageData,它比 Slint 可见的 Message 多保存了 created_at(创建时间)和 duration(显示时长)两个字段:

struct MessageData {
id: i32,
text: String,
msg_type: MessageType,
created_at: Instant, // 记录创建时刻,用于计算剩余进度
duration: Duration, // 该类型消息应显示的时长
}

而 Slint 侧的 Message 结构只暴露 UI 渲染所需的最小字段:id、text、type、remaining。remaining 是一个 0.0 ~ 1.0 的浮点数,表示消息剩余显示时间的比例,直接驱动进度条的宽度。

2.2 双向数据绑定

Rust 侧通过 VecModel<Message> 维护一个可被 Slint 观察的模型,再包装成 ModelRc 传给 UI:

let messages_model: Rc<VecModel<Message>> = Rc::new(VecModel::from(vec![]));
ui.set_messages(ModelRc::new(messages_model.clone()));

Slint 侧的 MessageBar 组件通过 for msg[index] in messages 语法遍历该模型,每一条消息渲染为一个 MessageToast 组件。当 Rust 侧调用 model.set_vec(updated) 时,Slint 会自动检测到模型变化并重新渲染,无需手动刷新界面。

3、消息生命周期管理

3.1 添加消息

点击按钮后,Rust 端通过回调创建一条新消息:

  • 从 next_id 计数器取一个自增 ID;
  • 根据消息类型(Info/Warning/Error)查表得到默认显示时长(5s/8s/12s);
  • 把 MessageData 压入 messages_data 向量;
  • 遍历向量,把每条数据通过 to_slint() 转换成 Message 并刷新模型。
  • fn duration_for(t: &MessageType) -> Duration {
    match t {
    MessageType::Info => Duration::from_secs(5),
    MessageType::Warning => Duration::from_secs(8),
    MessageType::Error => Duration::from_secs(12),
    }
    }

    3.2 进度条计算

    to_slint() 方法根据创建时间计算剩余进度:

    let elapsed = self.created_at.elapsed().as_secs_f32();
    let total = self.duration.as_secs_f32();
    let remaining = (1.0 – elapsed / total).clamp(0.0, 1.0);

    • elapsed:消息已存活的时间;
    • total:消息总显示时长;
    • remaining:剩余比例,clamp(0.0, 1.0) 保证不会出现负数。

    3.3 自动过期清理

    核心机制是一个每 100ms 触发一次的 Timer:

    timer.start(TimerMode::Repeated, Duration::from_millis(100), move || {
    // 移除已过期消息
    data.borrow_mut().retain(|m| m.created_at.elapsed() < m.duration);
    // 刷新 UI
    let updated: Vec<Message> = data.borrow().iter().map(MessageData::to_slint).collect();
    model.set_vec(updated);
    });

    这里有两个关键点:

  • retain 原地过滤:retain 会保留满足条件的元素,移除过期的消息,时间复杂度 O(n);
  • std::mem::forget(timer):定时器必须保持存活才能持续触发。如果直接让 timer 在闭包作用域结束时析构,定时器会立即停止。mem::forget 把定时器泄漏到堆上,使其与进程同生命周期。
  • 4、UI 渲染原理

    4.1 尖角箭头形状

    MessageToast 组件用 Slint 的 Path 元素绘制左右尖角箭头形状。核心是 commands 字符串,它用 SVG 路径语法描述多边形:

    M 12,0
    L (w-12) 0
    L w (h/2)
    L (w-12) h
    L 12 h
    L 24 (h/2)
    Z

    这个路径定义了 6 个顶点:左上角凹入 12px、右侧凸出 12px、底部再凹回 12px,形成 >__> 的箭头造型。ribbon.width / 1px 把 Slint 的 length 类型转换成纯数字参与字符串拼接。

    4.2 颜色与图标映射

    消息类型通过三元表达式映射到不同的颜色和图标:

    fill: message-type == MessageType.info
    ? #3b82f6 // 蓝色
    : message-type == MessageType.warning
    ? #f59e0b // 橙色
    : #ef4444; // 红色

    图标同理:Info 显示 💡、Warning 显示 ⚡、Error 显示 ❌。

    4.3 进度条

    进度条是一个宽度动态变化的 Rectangle:

    width: (ribbon.width – 45px) * progress;

    progress 就是 Rust 侧计算的 remaining 值。每 100ms 定时器刷新一次模型,进度条就会平滑缩短,直到消息过期被移除。

    5、关闭消息的回调链路

    用户点击某条消息时,事件从 Slint 层逐级向上传递:

    MessageToast.close-clicked()
    → MessageBar.close-msg(msg.id)
    → MainWindow.close-message(int)
    → Rust 端 on_close_message 回调

    Rust 端收到 ID 后,用 retain 移除对应消息并刷新模型:

    ui.on_close_message(move |id| {
    data.borrow_mut().retain(|m| m.id != id);
    let updated: Vec<Message> = data.borrow().iter().map(MessageData::to_slint).collect();
    model.set_vec(updated);
    });

    6、关键设计总结

    设计点实现方式作用
    数据与 UI 分离 Rust 维护 MessageData,Slint 只读 Message 职责清晰,便于扩展
    双向绑定 VecModel + ModelRc 数据变更自动刷新 UI
    自动过期 Timer 每 100ms 检查一次 无需手动管理消息生命周期
    进度条动画 remaining 驱动 Rectangle 宽度 平滑展示剩余时间
    定时器保活 std::mem::forget 防止定时器被提前析构
    尖角造型 Path + SVG 路径字符串 实现个性化箭头样式

    在这里插入图片描述

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Rust+Slint 实现个性化消息通知栏源码分享
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!