Rust+Slint 实现动态轮播图源码分享,支持动态删除、添加
一、效果展示



二、源码分享
1、工程结构

2、main.rs
rust
slint::include_modules!();
use slint::{ModelRc, SharedString, Timer, TimerMode, VecModel, Weak,Image};
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Duration;
fn make_timer_callback(weak_win: Weak<MainWindow>) -> impl Fn() {
move || {
let Some(win) = weak_win.upgrade() else { return };
if win.get_hover_detected() {
return;
}
let current = win.get_current_index();
let count = win.get_item_count();
if count > 0 {
let next = if (current.round() as i32) + 1 < count {
current + 1.0
} else {
0.0
};
win.set_current_index(next);
}
}
}
fn main() -> Result<(), slint::PlatformError> {
let main_window = MainWindow::new()?;
// Create initial error items matching ShowItem { number: int, note: string, image: image }
let initial_items = vec![
ShowItem {
number: 1,
note: SharedString::from("苹果"),
image: Image::load_from_path(std::path::Path::new("images/apple.svg")).unwrap(),
},
ShowItem {
number: 2,
note: SharedString::from("香蕉"),
image: Image::load_from_path(std::path::Path::new("images/banana.svg")).unwrap(),
},
ShowItem {
number: 3,
note: SharedString::from("西瓜"),
image: Image::load_from_path(std::path::Path::new("images/watermelon.svg")).unwrap(),
},
ShowItem {
number: 4,
note: SharedString::from("桃子"),
image: Image::load_from_path(std::path::Path::new("images/peach.svg")).unwrap(),
},
ShowItem {
number: 5,
note: SharedString::from("芒果"),
image: Image::load_from_path(std::path::Path::new("images/mango.svg")).unwrap(),
},
ShowItem {
number: 6,
note: SharedString::from("草莓"),
image: Image::load_from_path(std::path::Path::new("images/strawberry.svg")).unwrap(),
},
];
let model = Rc::new(VecModel::from(initial_items));
main_window.set_error_items(ModelRc::from(model.clone()));
// Set up the remove-item callback
let model_for_remove = model.clone();
main_window.on_remove_item(move |index: i32| {
model_for_remove.remove(index as usize);
});
// Set up the clear-error callback (receives int number now)
main_window.on_clear_error(move |number: i32| {
println!("Clear error: {}", number);
});
// Auto-rotation timer (3 seconds)
let weak_window = main_window.as_weak();
let timer = Rc::new(RefCell::new(Timer::default()));
timer.borrow().start(
TimerMode::Repeated,
Duration::from_secs(3),
make_timer_callback(weak_window.clone()),
);
// Reset-timer callback: restart the 3-second timer from scratch
let timer_for_reset = timer.clone();
let weak_window_for_reset = weak_window.clone();
main_window.on_reset_timer(move || {
timer_for_reset.borrow().start(
TimerMode::Repeated,
Duration::from_secs(3),
make_timer_callback(weak_window_for_reset.clone()),
);
});
// Keep timer alive for the lifetime of the application
std::mem::forget(timer);
main_window.run()
}
3、build.rs
rust
fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}
4、main.slint
rust
struct ShowItem {
number: int,
note: string,
image: image,
}
// Individual carousel card component
component CarouselCard inherits Rectangle {
in property <int> number;
in property <string> note;
in property <image> image;
callback clear-clicked();
border-radius: 15px;
background: #09b1db79;
border-width: 2px;
border-color: #3040aaff;
HorizontalLayout {
padding-left: 10px;
padding-right: 10px;
padding-top: 10px;
padding-bottom: 10px;
// Left side: image + text content
VerticalLayout {
horizontal-stretch: 1;
// Error image
Image {
source: image;
horizontal-alignment: center;
vertical-stretch: 2;
}
// Note text
Text {
//width: parent.width;
height: 50px;
text: note;
font-size: 15px;
color: #ccccccff;
horizontal-alignment: center;
vertical-alignment: center;
vertical-stretch: 1;
}
// Right side: clear/delete button
Rectangle {
//width: parent.width;
height: 30px;
border-radius: 10px;
background: btn-touch.pressed ? #80ff4444 : #40ff6666;
Text {
text: "清除";
color: white;
font-size: 20px;
horizontal-alignment: center;
vertical-alignment: center;
}
btn-touch := TouchArea {
clicked => {
clear-clicked();
}
}
animate background { duration: 150ms; }
}
}
}
}
export component MainWindow inherits Window {
width: 800px;
height: 400px;
title: "Dynamic Carousel";
background: #1a1a2e;
in property <[ShowItem]> error-items;
in-out property <float> current-index: 0;
in-out property <int> item-count: error-items.length;
in-out property <bool> hover-detected: touch-area.has-hover;
callback remove-item(int);
callback clear-error(int);
callback reset-timer();
// Card dimensions
property <length> card-w: root.width * 0.40;
property <length> card-h: root.height * 0.70;
property <length> side-offset: root.width * 0.34;
// Hover detection area
touch-area := TouchArea {
width: root.width;
height: root.height;
}
// Carousel cards: single for loop, all cards always rendered
for item[idx] in error-items : CarouselCard {
// Integer offset from current center
property <int> ci: round(current-index);
property <int> raw: idx - ci;
// Normalization range: [-(N-1)/2, N/2] - ensures single item stays centered
property <int> low: 0 - (item-count - 1) / 2;
property <int> high: item-count / 2;
property <int> norm: raw < low ? raw + item-count :
raw > high ? raw - item-count : raw;
property <float> abs-n: norm < 0 ? (0 - norm) * 1.0 : norm * 1.0;
// Computed size (independent property, avoids self.width feedback loop)
property <float> sf: 1.0 - abs-n * 0.25;
property <length> cw: card-w * sf;
property <length> ch: card-h * sf;
// Smooth opacity falloff
opacity: abs-n <= 0.0 ? 1.0 :
abs-n <= 1.0 ? 0.85 - (abs-n - 0.0) * 0.15 :
abs-n <= 2.0 ? 0.7 - (abs-n - 1.0) * 0.5 :
abs-n <= 3.0 ? 0.2 - (abs-n - 2.0) * 0.2 :
0.0;
width: cw;
height: ch;
x: root.width / 2 + norm * side-offset - cw / 2;
y: root.height / 2 - ch / 2;
// Smooth slide/fade animation
animate x { duration: 400ms; easing: ease-in-out; }
animate y { duration: 400ms; easing: ease-in-out; }
animate width { duration: 400ms; easing: ease-in-out; }
animate height { duration: 400ms; easing: ease-in-out; }
animate opacity { duration: 400ms; }
// Content from model
number: error-items[idx].number;
note: error-items[idx].note;
image:error-items[idx].image;
clear-clicked => {
reset-timer();
remove-item(idx);
clear-error(error-items[idx].number);
}
}
// Navigation dots
HorizontalLayout {
y: root.height - 35px;
width: root.width;
height: 24px;
alignment: center;
spacing: 6px;
for item[idx] in error-items : Rectangle {
width: 10px;
height: 10px;
border-radius: 5px;
background: idx == round(current-index) ? #6070ddff : #30406080;
animate background { duration: 300ms; }
}
}
// Left navigation arrow
left-arrow := Rectangle {
x: 10px;
y: root.height / 2 - 20px;
width: 40px;
height: 40px;
border-radius: 20px;
background: touch-left.pressed ? #80ffffff : #40ffffff;
Text {
text: "◀";
color: white;
font-size: 18px;
horizontal-alignment: center;
vertical-alignment: center;
}
touch-left := TouchArea {
clicked => {
reset-timer();
if current-index > 0 {
current-index = current-index - 1;
} else if item-count > 0 {
current-index = item-count - 1;
}
}
}
animate background { duration: 150ms; }
}
// Right navigation arrow
right-arrow := Rectangle {
x: root.width - 50px;
y: root.height / 2 - 20px;
width: 40px;
height: 40px;
border-radius: 20px;
background: touch-right.pressed ? #80ffffff : #40ffffff;
Text {
text: "▶";
color: white;
font-size: 18px;
horizontal-alignment: center;
vertical-alignment: center;
}
touch-right := TouchArea {
clicked => {
reset-timer();
if round(current-index) + 1 < item-count {
current-index = current-index + 1;
} else {
current-index = 0;
}
}
}
animate background { duration: 150ms; }
}
}
5、Cargo.toml
rust
[package]
name = "chart"
version = "0.1.0"
edition = "2024"
[dependencies]
slint = { version = "1.16.1", features = ["renderer-winit-femtovg"] }
[build-dependencies]
slint-build = "1.16.1"
6、完整工程下载
https://download.csdn.net/download/qq_15181569/93274167
三、实现原理
本项目的核心是一个基于 Slint GUI 框架实现的动态、可交互的轮播组件。其实现原理可以分解为以下几个关键部分:
1、数据模型与状态管理
轮播的数据源是一个 ShowItem 结构体数组,每个元素包含编号 (number)、说明 (note) 和图片 (image)。在 Rust 后端 (main.rs) 中,这个数组被包装进 VecModel,再转换为 Slint 的 ModelRc 类型,从而建立起一个可被前端 UI (main.slint) 观察和绑定的响应式数据模型。
current-index: 一个浮点数属性,表示当前"中心"卡片的索引。它驱动着所有卡片的位置、大小和透明度计算。item-count: 绑定到数据模型长度,用于各种边界计算。hover-detected: 布尔属性,用于检测用户悬停,以暂停自动轮播。
2、 视觉布局与动画
轮播的视觉效果通过 main.slint 中一个 for 循环动态渲染所有 CarouselCard 组件来实现,而非传统的只渲染可见项。其核心算法如下:
位置与层级计算
- 计算原始偏移 (
raw) :idx - ci,其中ci是current-index的整数近似值。这表示每张卡片相对于当前中心的整数位置差。 - 归一化范围 (
norm) : 为了使轮播在视觉上呈现"循环"效果,并确保在项目数量较少时仍能正确居中,算法定义了一个归一化范围[low, high]。任何超出此范围的raw值会被加上或减去item-count,将其"折叠"回该范围内。这保证了无论current-index如何变化,视觉上离中心最近的卡片其norm值总是最小的。 - 绝对距离 (
abs-n) :norm的绝对值,用于计算缩放和透明度。
视觉变换
- 缩放 (
sf) :1.0 - abs-n * 0.25。距离中心越远的卡片,缩放比例越小,营造出景深效果。 - 透明度 (
opacity) : 根据abs-n分段设置,距离中心越远,透明度越低,直至完全消失。 - 位置 (
x,y) : 卡片水平位置由root.width / 2 + norm * side-offset - cw / 2计算得出,使其沿水平线均匀分布。side-offset是控制卡片间间距的关键参数。
所有视觉属性(x, y, width, height, opacity)都应用了 400ms 的缓动动画,使轮播切换过程平滑流畅。
3、用户交互
- 手动导航 : 通过左右箭头按钮(
left-arrow,right-arrow)的TouchArea组件捕获点击事件,直接修改current-index属性。 - 项目删除 : 每个
CarouselCard上的"清除"按钮绑定了clear-clicked回调。点击后会触发reset-timer()(重置自动轮播计时器)、remove-item(idx)(从数据模型中移除该项)以及clear-error(number)(执行后端业务逻辑)。 - 悬停检测 : 整个窗口覆盖了一个透明的
TouchArea(touch-area),其has-hover状态被绑定到hover-detected属性。当检测到悬停时,自动轮播会暂停。
4、自动轮播机制
自动轮播由 Rust 后端的 Timer 驱动 (main.rs 中的 make_timer_callback 函数):
- 创建一个每 3 秒触发一次的重复计时器。
- 计时器回调函数会检查
hover-detected状态,如果为真(用户正在交互),则跳过本次轮播。 - 否则,计算下一个索引(
current-index + 1,到达末尾后归零),并更新current-index属性。 - Slint 的属性绑定系统会检测到
current-index的变化,自动触发前端所有依赖此属性的计算和动画,从而完成一次轮播过渡。
5、 组件化与数据绑定
项目采用了清晰的组件化架构:
CarouselCard组件 : 封装了单个卡片的视觉和交互逻辑,通过in property接收数据,通过callback向上传递事件。MainWindow根组件 : 管理数据模型 (error-items)、轮播状态 (current-index) 和全局交互。它通过for循环将数据模型实例化为多个CarouselCard,并处理它们发出的事件(如remove-item)。- 数据绑定: Slint 的声明式语法使得 UI 属性(如卡片位置、导航点颜色)能够直接绑定到 Rust 后端的数据和状态上,实现了数据与 UI 的自动同步。
7、总结
该轮播实现巧妙地结合了 响应式数据绑定 、声明式 UI 布局 和 基于物理的动画计算 ,在 Slint 框架下构建了一个高性能、流畅且交互丰富的动态组件。其核心在于利用 current-index 这一单一状态源,驱动整个视觉系统的计算与更新,并通过计时器和用户输入来改变这一状态,从而实现自动与手动轮播。
