
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> success–color: #10b981;
out property <color> success–bg: #ecfdf5;
// 错误 – 红色
out property <color> error–color: #ef4444;
out property <color> error–bg: #fef2f2;
// 警告 – 橙色
out property <color> warning–color: #f59e0b;
out property <color> warning–bg: #fffbeb;
// 信息 – 蓝色
out property <color> info–color: #3b82f6;
out property <color> info–bg: #eff6ff;
}
// 可复用按钮组件
component MsgButton inherits Rectangle {
in property <image> icon–src;
in property <string> label;
in property <color> accent;
callback clicked();
height: 50px;
width: 130px;
border–radius: 25px;
border–width: 2px;
border–color: accent;
background: accent;
opacity: area.has–hover ? 0.8 : 1.0;
HorizontalBox {
padding: 12px;
alignment: center;
spacing: 8px;
Rectangle {
Image {
width: 16px;
height: 16px;
vertical–stretch: 0;
source: icon–src;
colorize: white;
}
}
Text {
text: label;
color: white;
font–size: 16px;
font–weight: 600;
vertical–stretch: 0;
vertical–alignment: center;
}
}
area := TouchArea {
width: 100%;
height: 100%;
clicked => { root.clicked(); }
mouse–cursor: pointer;
}
}
// 单条消息组件
component MessageCard inherits Rectangle {
in property <MessageItem> message;
callback dismiss(int);
width: 380px;
height: 80px;
border–radius: 12px;
// 根据消息类型获取主题色
property <color> accent: message.type == MessageType.success ? MessageTheme.success–color :
message.type == MessageType.error ? MessageTheme.error–color :
message.type == MessageType.warning ? MessageTheme.warning–color :
MessageTheme.info–color;
property <color> bg: message.type == MessageType.success ? MessageTheme.success–bg :
message.type == MessageType.error ? MessageTheme.error–bg :
message.type == MessageType.warning ? MessageTheme.warning–bg :
MessageTheme.info–bg;
// 白色圆角背景(问题2修复:使用更深的边框颜色,确保圆角可见)
background: bg;
border–width: 2px;
border–color: accent;
// 入场/出场动画
animate x { duration: 400ms; easing: cubic–bezier(0.34, 1.56, 0.64, 1); }
animate opacity { duration: 300ms; easing: ease–in–out; }
opacity: message.removing ? 0.0 : 1.0;
// 图标区域
Rectangle {
x: 12px;
y: 16px;
width: 48px;
height: 48px;
border–radius: 24px;
background: accent;
// 使用 SVG 图标(问题1修复:用图片替代文字)
Image {
x: 12px;
y: 12px;
width: 24px;
height: 24px;
source: message.type == MessageType.success ? @image–url("../images/icon-success.svg") :
message.type == MessageType.error ? @image–url("../images/icon-error.svg") :
message.type == MessageType.warning ? @image–url("../images/icon-warning.svg") :
@image–url("../images/icon-info.svg");
}
}
// 文字内容区域
VerticalBox {
x: 72px;
y: 12px;
width: parent.width – 108px;
height: 56px;
Text {
text: message.title;
font–size: 15px;
font–weight: 600;
color: #1f2937;
overflow: elide;
}
Text {
text: message.content;
font–size: 13px;
color: #6b7280;
overflow: elide;
}
}
// 关闭按钮
Rectangle {
x: parent.width – 32px;
y: 8px;
width: 24px;
height: 24px;
border–radius: 12px;
property <bool> touched: close–btn.has–hover;
background: touched ? #00000015 : transparent;
Text {
text: "x";
font–size: 14px;
font–weight: 700;
color: touched ? #1f2937 : #9ca3af;
horizontal–alignment: center;
vertical–alignment: center;
}
close–btn := TouchArea {
width: 100%;
height: 100%;
clicked => { dismiss(message.id); }
mouse–cursor: pointer;
}
}
// ======== 进度条 ========
Rectangle {
x: 12px;
y: root.height – 6px;
width: (root.width – 24px) * (1.0 – message.progress);
height: 3px;
border–radius: 1.5px;
background: accent;
animate width { duration: 100ms; easing: linear; }
}
}
export component MainWindow inherits Window {
title: "高级消息提示框";
preferred–width: 1000px;
preferred–height: 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 add–message(MessageType, string, string);
callback dismiss–message(int);
// 渐变背景
background: @linear–gradient(135deg, #667eea 0%, #764ba2 100%);
// 主布局
VerticalBox {
padding: 40px;
// 标题
Text {
// text: "高级消息提示框演示";
font–size: 32px;
font–weight: 700;
color: white;
horizontal–alignment: center;
}
Rectangle { height: 20px; }
// 控制区域
Rectangle {
height: 120px;
border–radius: 16px;
background: #ffffff1a;
clip: true;
// 毛玻璃效果模拟
border–width: 1px;
border–color: #ffffff33;
HorizontalBox {
padding: 20px;
alignment: space–around;
MsgButton {
icon–src: @image–url("../images/btn-success.svg");
label: "成功";
accent: #10b981;
clicked => { add–message(MessageType.success, "操作成功", "您的更改已保存并同步到云端。"); }
}
MsgButton {
icon–src: @image–url("../images/btn-error.svg");
label: "错误";
accent: #ef4444;
clicked => { add–message(MessageType.error, "发生错误", "无法连接到服务器,请检查网络。"); }
}
MsgButton {
icon–src: @image–url("../images/btn-warning.svg");
label: "警告";
accent: #f59e0b;
clicked => { add–message(MessageType.warning, "注意", "存储空间即将用尽,请及时清理。"); }
}
MsgButton {
icon–src: @image–url("../images/btn-info.svg");
label: "信息";
accent: #3b82f6;
clicked => { add–message(MessageType.info, "系统提示", "新版本 v2.0 已发布,点击查看详情。"); }
}
}
}
Rectangle { height: 30px; }
}
// 消息提示框容器(右上角,带滚动)
sv := ScrollView {
x: parent.width – 400px;
y: 20px;
width: 380px;
height: parent.height – 40px;
// 自动滚动到底部
viewport–y: sv.visible–height – sv.viewport–height;
vertical–scrollbar–policy: always–off;
VerticalLayout {
spacing: 12px;
alignment: start;
for msg in messages : MessageCard {
message: msg;
dismiss(id) => { dismiss–message(id); }
}
}
}
}
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"
三、实现原理
本项目基于 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 检查一次)会将其标记为过期:
手动关闭的逻辑类似:用户点击消息卡片的关闭按钮时,触发 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 负责渲染与动画,两者各司其职、通过类型安全的绑定高效协作,最终实现了流畅的动态消息提示效果。



