Rust+Slint 实现动态轮播图源码分享,支持动态删除、添加
2026/9/2 8:16:13 网站建设 项目流程

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

  • 一、效果展示
  • 二、源码分享
    • 1、工程结构
    • 2、main.rs
    • 3、build.rs
    • 4、main.slint
    • 5、Cargo.toml
    • 6、完整工程下载
  • 三、实现原理
    • 1、数据模型与状态管理
    • 2、 视觉布局与动画
      • 位置与层级计算
      • 视觉变换
    • 3、用户交互
    • 4、自动轮播机制
    • 5、 组件化与数据绑定
    • 7、总结

一、效果展示


二、源码分享

1、工程结构

2、main.rs

slint::include_modules!();useslint::{ModelRc,SharedString,Timer,TimerMode,VecModel,Weak,Image};usestd::cell::RefCell;usestd::rc::Rc;usestd::time::Duration;fnmake_timer_callback(weak_win:Weak<MainWindow>)->implFn(){move||{letSome(win)=weak_win.upgrade()else{return};ifwin.get_hover_detected(){return;}letcurrent=win.get_current_index();letcount=win.get_item_count();ifcount>0{letnext=if(current.round()asi32)+1<count{current+1.0}else{0.0};win.set_current_index(next);}}}fnmain()->Result<(),slint::PlatformError>{letmain_window=MainWindow::new()?;// Create initial error items matching ShowItem { number: int, note: string, image: image }letinitial_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(),},];letmodel=Rc::new(VecModel::from(initial_items));main_window.set_error_items(ModelRc::from(model.clone()));// Set up the remove-item callbackletmodel_for_remove=model.clone();main_window.on_remove_item(move|index:i32|{model_for_remove.remove(indexasusize);});// 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)letweak_window=main_window.as_weak();lettimer=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 scratchlettimer_for_reset=timer.clone();letweak_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 applicationstd::mem::forget(timer);main_window.run()}

3、build.rs

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

4、main.slint

structShowItem{number:int,note:string,image:image,}// Individual carousel card componentcomponentCarouselCardinheritsRectangle{inproperty<int>number;inproperty<string>note;inproperty<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 contentVerticalLayout{horizontal-stretch:1;// Error imageImage{source:image;horizontal-alignment:center;vertical-stretch:2;}// Note textText{//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 buttonRectangle{//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 componentMainWindowinheritsWindow{width:800px;height:400px;title:"Dynamic Carousel";background:#1a1a2e;inproperty<[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 dimensionsproperty<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 areatouch-area:=TouchArea{width:root.width;height:root.height;}// Carousel cards: single for loop, all cards always renderedforitem[idx]inerror-items:CarouselCard{// Integer offset from current centerproperty<int>ci:round(current-index);property<int>raw:idx-ci;// Normalization range: [-(N-1)/2, N/2] - ensures single item stays centeredproperty<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 falloffopacity: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 animationanimate 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 modelnumber: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 dotsHorizontalLayout{y:root.height-35px;width:root.width;height:24px;alignment:center;spacing:6px;foritem[idx]inerror-items:Rectangle{width:10px;height:10px;border-radius:5px;background:idx==round(current-index)?#6070ddff:#30406080;animate background{duration:300ms;}}}// Left navigation arrowleft-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();ifcurrent-index>0{current-index=current-index-1;}elseifitem-count>0{current-index=item-count-1;}}}animate background{duration:150ms;}}// Right navigation arrowright-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();ifround(current-index)+1<item-count{current-index=current-index+1;}else{current-index=0;}}}animate background{duration:150ms;}}}

5、Cargo.toml

[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组件来实现,而非传统的只渲染可见项。其核心算法如下:

位置与层级计算

  1. 计算原始偏移 (raw):idx - ci,其中cicurrent-index的整数近似值。这表示每张卡片相对于当前中心的整数位置差。
  2. 归一化范围 (norm): 为了使轮播在视觉上呈现“循环”效果,并确保在项目数量较少时仍能正确居中,算法定义了一个归一化范围[low, high]。任何超出此范围的raw值会被加上或减去item-count,将其“折叠”回该范围内。这保证了无论current-index如何变化,视觉上离中心最近的卡片其norm值总是最小的。
  3. 绝对距离 (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函数):

  1. 创建一个每 3 秒触发一次的重复计时器。
  2. 计时器回调函数会检查hover-detected状态,如果为真(用户正在交互),则跳过本次轮播。
  3. 否则,计算下一个索引(current-index + 1,到达末尾后归零),并更新current-index属性。
  4. 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这一单一状态源,驱动整个视觉系统的计算与更新,并通过计时器和用户输入来改变这一状态,从而实现自动与手动轮播。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询