欢迎光临
我们一直在努力

Rust+Slint 实现动态消息提示框源码分享

在这里插入图片描述

Rust+Slint 实现动态消息提示框源码分享

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、main.rs
    • 3、build.rs
    • 4、main.slint
    • 5、Cargo.toml
  • 三、实现原理
    • 1、数据模型与类型绑定
    • 2、消息的添加与回调机制
    • 3、进度条更新机制
    • 4、消息的自动移除与出场动画
    • 5、界面渲染与动画实现
    • 6、整体流程

一、效果展示

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

二、源码分享

1、工程结构

在这里插入图片描述

2、main.rs

slint::include_modules!();

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

/// 消息条目(Rust 端记录)
struct MessageEntry {
create_time: Instant,
/// 总显示时长(毫秒)
duration_ms: u64,
}

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

// ── 消息模型 ──
let model: Rc<VecModel<MessageItem>> = Rc::new(VecModel::from(vec![]));
ui.set_messages(ModelRc::new(model.clone()));

// ── 内部状态 ──
let entries: Rc<RefCell<Vec<MessageEntry>>> = Rc::new(RefCell::new(Vec::new()));
let counter = Rc::new(std::cell::Cell::new(0_i32));

// ── 注册 add-message 回调 ──
{
let model = model.clone();
let entries = entries.clone();
let counter = counter.clone();

ui.on_add_message(move |msg_type, title, content| {
let id = counter.get();
counter.set(id + 1);

let item = MessageItem {
id,
r#type: msg_type,
title,
content,
progress: 0.0,
removing: false,
};

model.push(item);
entries.borrow_mut().push(MessageEntry {
create_time: Instant::now(),
duration_ms: 5000,
});
});
}

// ── 注册 dismiss-message 回调(手动关闭) ──
{
let model = model.clone();
let entries = entries.clone();

ui.on_dismiss_message(move |id| {
// 先标记为 removing(触发出场动画)
let entries_ref = entries.borrow();
if let Some(idx) = model.iter().position(|m| m.id == id) {
let mut item = model.row_data(idx).unwrap();
item.removing = true;
model.set_row_data(idx, item);
}
drop(entries_ref);

// 300ms 后真正移除(等动画完成)
let model_clone = model.clone();
let entries_clone = entries.clone();
slint::Timer::single_shot(Duration::from_millis(350), move || {
if let Some(idx) = model_clone.iter().position(|m| m.id == id) {
model_clone.remove(idx);
entries_clone.borrow_mut().remove(idx);
}
});
});
}

// ── 进度更新定时器:每 50ms 更新一次 ──
{
let model = model.clone();
let entries = entries.clone();

let progress_timer = Timer::default();
progress_timer.start(
TimerMode::Repeated,
Duration::from_millis(50),
move || {
let entries_ref = entries.borrow();
let now = Instant::now();

for (idx, entry) in entries_ref.iter().enumerate() {
let elapsed = now.duration_since(entry.create_time).as_millis() as f64;
let progress = (elapsed / entry.duration_ms as f64).min(1.0);

if let Some(mut item) = model.row_data(idx) {
if (item.progress progress as f32).abs() > 0.01 {
item.progress = progress as f32;
model.set_row_data(idx, item);
}
}
}
},
);
std::mem::forget(progress_timer);
}

// ── 自动移除定时器:每秒检查 ──
{
let model = model.clone();
let entries = entries.clone();

let cleanup_timer = Timer::default();
cleanup_timer.start(
TimerMode::Repeated,
Duration::from_millis(100),
move || {
let entries_ref = entries.borrow();
let now = Instant::now();

// 找出已过期的消息
let expired: Vec<(i32, usize)> = entries_ref
.iter()
.enumerate()
.filter(|(_, entry)| {
now.duration_since(entry.create_time).as_millis() as u64 >= entry.duration_ms
})
.map(|(idx, entry)| (entry.create_time.elapsed().as_millis() as i32, idx))
.collect();

drop(entries_ref);

// 标记为 removing(触发出场动画)
for (_, idx) in &expired {
if let Some(mut item) = model.row_data(*idx) {
if !item.removing {
item.removing = true;
model.set_row_data(*idx, item);
}
}
}

// 延迟移除已过期的(等动画)
if !expired.is_empty() {
let model_clone = model.clone();
let entries_clone = entries.clone();
let indices: Vec<usize> = expired.iter().map(|(_, idx)| *idx).collect();

slint::Timer::single_shot(Duration::from_millis(350), move || {
let mut entries_mut = entries_clone.borrow_mut();
// 从高到低删除,避免索引偏移
for &idx in indices.iter().rev() {
if idx < model_clone.row_count() {
model_clone.remove(idx);
if idx < entries_mut.len() {
entries_mut.remove(idx);
}
}
}
});
}
},
);
std::mem::forget(cleanup_timer);
}

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

3、build.rs

fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}

4、main.slint

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

// 消息类型枚举
export enum MessageType {
success,
error,
warning,
info
}

// 消息数据结构
export struct MessageItem {
id: int,
type: MessageType,
title: string,
content: string,
// 进度值 0.0 ~ 1.0
progress: float,
// 是否正在移除(触发退出动画)
removing: bool,
}

// 颜色常量
global MessageTheme {
// 成功 – 绿色
out property <color> successcolor: #10b981;
out property <color> successbg: #ecfdf5;

// 错误 – 红色
out property <color> errorcolor: #ef4444;
out property <color> errorbg: #fef2f2;

// 警告 – 橙色
out property <color> warningcolor: #f59e0b;
out property <color> warningbg: #fffbeb;

// 信息 – 蓝色
out property <color> infocolor: #3b82f6;
out property <color> infobg: #eff6ff;
}

// 可复用按钮组件
component MsgButton inherits Rectangle {
in property <image> iconsrc;
in property <string> label;
in property <color> accent;
callback clicked();

height: 50px;
width: 130px;
borderradius: 25px;
borderwidth: 2px;
bordercolor: accent;
background: accent;
opacity: area.hashover ? 0.8 : 1.0;

HorizontalBox {
padding: 12px;
alignment: center;
spacing: 8px;

Rectangle {
Image {
width: 16px;
height: 16px;
verticalstretch: 0;
source: iconsrc;
colorize: white;
}
}

Text {
text: label;
color: white;
fontsize: 16px;
fontweight: 600;
verticalstretch: 0;
verticalalignment: center;
}
}

area := TouchArea {
width: 100%;
height: 100%;
clicked => { root.clicked(); }
mousecursor: pointer;
}
}

// 单条消息组件
component MessageCard inherits Rectangle {
in property <MessageItem> message;
callback dismiss(int);

width: 380px;
height: 80px;
borderradius: 12px;

// 根据消息类型获取主题色
property <color> accent: message.type == MessageType.success ? MessageTheme.successcolor :
message.type == MessageType.error ? MessageTheme.errorcolor :
message.type == MessageType.warning ? MessageTheme.warningcolor :
MessageTheme.infocolor;

property <color> bg: message.type == MessageType.success ? MessageTheme.successbg :
message.type == MessageType.error ? MessageTheme.errorbg :
message.type == MessageType.warning ? MessageTheme.warningbg :
MessageTheme.infobg;

// 白色圆角背景(问题2修复:使用更深的边框颜色,确保圆角可见)
background: bg;
borderwidth: 2px;
bordercolor: accent;

// 入场/出场动画
animate x { duration: 400ms; easing: cubicbezier(0.34, 1.56, 0.64, 1); }
animate opacity { duration: 300ms; easing: easeinout; }
opacity: message.removing ? 0.0 : 1.0;

// 图标区域
Rectangle {
x: 12px;
y: 16px;
width: 48px;
height: 48px;
borderradius: 24px;
background: accent;

// 使用 SVG 图标(问题1修复:用图片替代文字)
Image {
x: 12px;
y: 12px;
width: 24px;
height: 24px;
source: message.type == MessageType.success ? @imageurl("../images/icon-success.svg") :
message.type == MessageType.error ? @imageurl("../images/icon-error.svg") :
message.type == MessageType.warning ? @imageurl("../images/icon-warning.svg") :
@imageurl("../images/icon-info.svg");
}
}

// 文字内容区域
VerticalBox {
x: 72px;
y: 12px;
width: parent.width 108px;
height: 56px;

Text {
text: message.title;
fontsize: 15px;
fontweight: 600;
color: #1f2937;
overflow: elide;
}

Text {
text: message.content;
fontsize: 13px;
color: #6b7280;
overflow: elide;
}
}

// 关闭按钮
Rectangle {
x: parent.width 32px;
y: 8px;
width: 24px;
height: 24px;
borderradius: 12px;
property <bool> touched: closebtn.hashover;
background: touched ? #00000015 : transparent;

Text {
text: "x";
fontsize: 14px;
fontweight: 700;
color: touched ? #1f2937 : #9ca3af;
horizontalalignment: center;
verticalalignment: center;
}

closebtn := TouchArea {
width: 100%;
height: 100%;
clicked => { dismiss(message.id); }
mousecursor: pointer;
}
}

// ======== 进度条 ========
Rectangle {
x: 12px;
y: root.height 6px;
width: (root.width 24px) * (1.0 message.progress);
height: 3px;
borderradius: 1.5px;
background: accent;
animate width { duration: 100ms; easing: linear; }
}
}

export component MainWindow inherits Window {
title: "高级消息提示框";
preferredwidth: 1000px;
preferredheight: 700px;

// 消息列表
in property <[MessageItem]> messages:[
{ id: 0, type: MessageType.success, title: "操作成功", content: "您的更改已保存并同步到云端。", progress: 0.0, removing: false },
{ id: 1, type: MessageType.error, title: "操作失败", content: "由于网络问题,您的更改未能保存。", progress: 0.0, removing: false },
{ id: 2, type: MessageType.warning, title: "警告", content: "您的操作可能存在风险,请谨慎处理。", progress: 0.0, removing: false },
];

// 回调
callback addmessage(MessageType, string, string);
callback dismissmessage(int);

// 渐变背景
background: @lineargradient(135deg, #667eea 0%, #764ba2 100%);

// 主布局
VerticalBox {
padding: 40px;

// 标题
Text {
// text: "高级消息提示框演示";
fontsize: 32px;
fontweight: 700;
color: white;
horizontalalignment: center;
}

Rectangle { height: 20px; }

// 控制区域
Rectangle {
height: 120px;
borderradius: 16px;
background: #ffffff1a;
clip: true;

// 毛玻璃效果模拟
borderwidth: 1px;
bordercolor: #ffffff33;

HorizontalBox {
padding: 20px;
alignment: spacearound;

MsgButton {
iconsrc: @imageurl("../images/btn-success.svg");
label: "成功";
accent: #10b981;
clicked => { addmessage(MessageType.success, "操作成功", "您的更改已保存并同步到云端。"); }
}

MsgButton {
iconsrc: @imageurl("../images/btn-error.svg");
label: "错误";
accent: #ef4444;
clicked => { addmessage(MessageType.error, "发生错误", "无法连接到服务器,请检查网络。"); }
}

MsgButton {
iconsrc: @imageurl("../images/btn-warning.svg");
label: "警告";
accent: #f59e0b;
clicked => { addmessage(MessageType.warning, "注意", "存储空间即将用尽,请及时清理。"); }
}

MsgButton {
iconsrc: @imageurl("../images/btn-info.svg");
label: "信息";
accent: #3b82f6;
clicked => { addmessage(MessageType.info, "系统提示", "新版本 v2.0 已发布,点击查看详情。"); }
}
}
}

Rectangle { height: 30px; }

}

// 消息提示框容器(右上角,带滚动)
sv := ScrollView {
x: parent.width 400px;
y: 20px;
width: 380px;
height: parent.height 40px;

// 自动滚动到底部
viewporty: sv.visibleheight sv.viewportheight;
verticalscrollbarpolicy: alwaysoff;

VerticalLayout {
spacing: 12px;
alignment: start;

for msg in messages : MessageCard {
message: msg;
dismiss(id) => { dismissmessage(id); }
}
}
}
}

5、Cargo.toml

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

[dependencies]
slint = "1.17.1"

[builddependencies]
slintbuild = "1.17.1"

三、实现原理

本项目基于 Rust + Slint 构建,核心思路是用 Rust 管理业务逻辑与数据模型,用 Slint 负责界面渲染与动画,两者通过 Slint 自动生成的类型安全接口进行通信。整体实现可分为以下几个关键部分:

1、数据模型与类型绑定

在 main.slint 中定义了 MessageItem 结构体,包含 id、type、title、content、progress、removing 六个字段,分别用于标识消息的唯一编号、类型、标题、内容、进度值以及是否正在移除。同时通过 export enum MessageType 定义了 success、error、warning、info 四种消息类型。

Rust 端通过 slint::include_modules!() 宏将 Slint 编译生成的代码引入,从而可以直接使用 MainWindow、MessageItem 等类型。借助 VecModel<MessageItem> 作为消息列表的数据模型,并通过 ui.set_messages(ModelRc::new(model.clone())) 将模型绑定到界面,实现 Rust 与 Slint 之间的数据共享。

2、消息的添加与回调机制

界面上的四个按钮(成功、错误、警告、信息)通过 clicked 信号触发 add-message 回调。Rust 端通过 ui.on_add_message(…) 注册回调闭包,接收消息类型、标题和内容三个参数:

  • 使用 counter 自增生成消息的唯一 id;
  • 构造 MessageItem 并 push 到 VecModel 中;
  • 同时在 entries 向量中记录该消息的创建时间 Instant::now() 和显示时长 duration_ms(这里设为 5000ms)。

由于 VecModel 是响应式的,push 操作会立即触发 Slint 界面的 for msg in messages 循环重新求值,从而自动渲染出新的消息卡片。

3、进度条更新机制

进度条用于展示消息的剩余显示时间。Rust 端创建了一个 Timer,以 50ms 为周期重复触发:

  • 遍历 entries 中所有消息,计算当前时间与创建时间的差值 elapsed;
  • 用 elapsed / duration_ms 得到 0.0 ~ 1.0 的进度值 progress;
  • 当进度变化超过 0.01 时,通过 model.set_row_data(idx, item) 更新对应消息的 progress 字段。

Slint 端进度条通过 width: (root.width – 24px) * (1.0 – message.progress) 绑定进度值,并配合 animate width { duration: 100ms; } 实现平滑的进度条收缩动画。

4、消息的自动移除与出场动画

当消息的存活时间达到 duration_ms 后,自动移除定时器(每 100ms 检查一次)会将其标记为过期:

  • 先找出所有已过期的消息索引;
  • 将对应消息的 removing 字段设为 true,触发 Slint 端的出场动画(opacity 渐隐 + x 位移动画);
  • 等待 350ms(动画时长)后,通过 Timer::single_shot 延迟移除该消息,从高到低删除索引以避免偏移。
  • 手动关闭的逻辑类似:用户点击消息卡片的关闭按钮时,触发 dismiss-message 回调,同样先标记 removing 再延迟移除,保证出场动画完整播放。

    5、界面渲染与动画实现

    Slint 端通过声明式语法完成界面布局与动画:

    • 消息卡片 MessageCard 根据 message.type 动态选择主题色(成功绿、错误红、警告橙、信息蓝);
    • 入场动画:卡片默认从右侧滑入,通过 animate x 和 animate opacity 实现弹性缓动效果;
    • 出场动画:当 removing 为 true 时,卡片透明度渐变为 0 并向右滑出;
    • 滚动容器:消息列表放在 ScrollView 中,并通过 viewport-y: sv.visible-height – sv.viewport-height 实现自动滚动到底部,保证新消息始终可见。

    6、整体流程

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

    用户点击按钮

    触发 add-message 回调

    Rust 构造 MessageItem 并 push 到 VecModel

    Slint 自动渲染消息卡片

    进度定时器每 50ms 更新 progress

    进度条宽度实时收缩

    存活时间 >= 5000ms?

    标记 removing = true

    触发出场动画(渐隐 + 位移)

    350ms 后从模型中移除

    通过以上机制,Rust 负责数据与定时逻辑,Slint 负责渲染与动画,两者各司其职、通过类型安全的绑定高效协作,最终实现了流畅的动态消息提示效果。

    在这里插入图片描述

    赞(0)
    未经允许不得转载:171主机测评 » Rust+Slint 实现动态消息提示框源码分享
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址