Slint Repeater 实战:基于 examples/repeater 深入解析 for 循环批量实例化与响应式模型
2026/9/13 4:50:21 网站建设 项目流程

Slint Repeater 实战:基于 examples/repeater 深入解析 for 循环批量实例化与响应式模型

【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint

导读

本文以 Slint 官方仓库中的 examples/repeater 示例为核心,完整拆解其 demo.slint 源码,带你掌握 Slint 中for-in语法驱动的批量元素实例化、响应式模型(Model)驱动的动态更新,以及如何用animation-tick()与三角函数实现自驱动的动画效果。读完本文,你将能够独立写出"数据驱动、随模型增删自动同步、运行时可控实例数量"的 Slint 界面,并理解其底层 Repeater 运行机制。

一、示例概览:用 Repeater 创建大量组件

该示例位于 examples/repeater,官方描述为:

Demonstrates how to use a repeater to create many components.

即演示如何利用 repeater(重复器)创建大量组件实例。它展示了 Slint 语言中for循环在 UI 声明层面的核心价值:数据放在模型(Model)里,界面只需声明"一行长什么样",框架负责按模型条目创建、更新与销毁实例

示例的视觉效果是:屏幕上飘动大量 Slint Logo 图片,每个 Logo 的大小、起始位置、波浪幅度、移动速度与周期各不相同,并且用户可以通过一个ComboBox下拉框,实时切换 Logo 的数量(10、50、100、200、500 个)——数量切换瞬间界面即完成重建,这正是 Repeater 响应模型变化的直接体现。

二、demo.slint 逐段剖析

2.1 引入标准控件

import { ComboBox } from "std-widgets.slint";

ComboBox(下拉框)来自 Slint 自带的标准控件库std-widgets.slint。该库的源码位于 internal/compiler/widgets 目录,而std-widgets.slint是聚合导出入口。这里引入它作为控制重复数量的交互控件。

2.2 WaveImage:被重复的单体组件

component WaveImage { in property <length> screen-width; in property <length> wave-period: 1000px; in property <length> wave-size: 50px; in property <length> start-x: 0px; in property <duration> random-duration: 0ms; in property <float> scale: 1; property <duration> baseDuration: 1s; property <duration> total-duration: (baseDuration + random-duration); property <length> total-distance: (screen-width + 150px) + start-x.abs(); ... }

WaveImage是一个自定义组件,定义了 6 个in property输入属性,供外层for循环为每个实例注入差异化参数:

属性类型默认值含义
screen-widthlength屏幕宽度,用于计算水平行程
wave-periodlength1000px正弦波的空间周期
wave-sizelength50px波浪的纵向振幅
start-xlength0px初始横向偏移
random-durationduration0ms随机附加的动画时长
scalefloat1图片缩放比例

内部另有三个私有属性(property而非in property,外部不可写):

  • baseDuration: 1s:基础周期;
  • total-duration: baseDuration + random-duration:总周期 = 基础周期 + 随机附加时长;
  • total-distance: (screen-width + 150px) + start-x.abs():水平总行程 = 屏幕宽度 + 150px + 起始偏移的绝对值,保证图片从屏幕外出发、完整横穿后还能继续移动。

2.3 水平循环滚动:animation-tick 与 mod

x: -(start-x + 90px) + (total-distance * (animation-tick() / total-duration)).mod(total-distance);

这是整个动画的核心一行,它同时演示了两个 Slint 特性:

  1. animation-tick()内置函数:返回自当前组件初始化以来经过的时长(类型为duration)。它由编译器以BuiltinFunction::AnimationTick注册,见 internal/compiler/lookup.rs。每次调用都会向求值引擎注册一个动画依赖,因此只要时间流逝,该绑定就会自动重算,从而驱动逐帧动画。
  2. .mod(...)取模运算mod是 Slint 内置宏之一,实现在 internal/compiler/builtin_macros.rs。(total-distance * (animation-tick() / total-duration))表示"当前进度 × 总行程",再对total-distance取模,就把无限增长的进度折叠进[0, total-distance)区间——Logo 从屏幕外左侧进入、向右运动,到达终点后立即回到起点,实现无缝循环。

表达式整体效果:-(start-x + 90px)让图片从屏幕外左侧起步,加上取模后的位移后,每个实例以不同的起始位置、不同周期横穿屏幕。

2.4 纵向波浪运动:sin 函数

Image { y: sin(360deg * (root.x / wave-period)) * wave-size; source: @image-url("../../logo/slint-logo-small-light.png"); width: self.source.width * scale * 1px; height: self.source.height * scale * 1px; colorize: #2479f4.mix(white, 1-(scale - 0.3)); }
  • 纵坐标ysin(360deg * (root.x / wave-period)) * wave-size决定:以当前横坐标root.x为自变量,每前进一个wave-period完成一个完整正弦周期,振幅为wave-size。于是 Logo 在水平移动的同时上下起伏,形成波浪。
  • @image-url("../../logo/slint-logo-small-light.png")加载仓库中的 logo/slint-logo-small-light.png(该路径相对于examples/repeater目录向上两级)。self.source.width/height读取图片原始像素尺寸。
  • colorize: #2479f4.mix(white, 1-(scale - 0.3)):利用颜色混合,让较大的 Logo 偏品牌蓝、较小的 Logo 偏向白色,进一步增强景深层次。mix是 Slint 内建的颜色混合方法,1-(scale - 0.3)作为混合比例因子。

2.5 主窗口:模型 + for 循环 + ComboBox

export component AppWindow inherits Window { property <[int]> logo-model: [10, 50, 100, 200, 500 ]; preferred-height: 600px; preferred-width: 1000px; // pseudo-random function function random(seed: int) -> float { return (115249 * (seed + 196) * seed).mod(25117) / 25117; } cb := ComboBox { x: 10px; y: 10px; model: [10, 50, 100, 200, 500]; current-index: 1; } // Set up each logo image with random properties for i in logo-model[cb.current-index] : WaveImage { y: root.height * random(i + 1); screen-width: root.width; random-duration: 5000ms + 10000ms * random(i + 20); wave-period: 800px + 200px * random(i); start-x: root.width * random(i); scale: 0.3 + 0.7 * random(i + 5); wave-size: 50px + 50px * random(i + 6); } }

逐点解读:

  1. 模型与动态数量logo-model是一个[int]数组属性,提供五个可选数量。for i in logo-model[cb.current-index]中,模型表达式是logo-model[cb.current-index]——即"用cb.current-index索引数组,取出的整数再作为循环次数"。这展示了 Slint 模型的强大之处:模型本身可以是任意表达式求值的结果。当用户在 ComboBox 中切换选项时,current-index变化 → 模型值变化 → Repeater 立即销毁旧实例、按新数量重建。

  2. for i in <整数>的语义:当模型是整数N时,元素被重复N次,循环变量i依次取0..N-1,类型为int。这在 docs/astro/src/content/docs/reference/language/repetition-and-conditional-elements.mdx 中有明确说明:模型可以是整数(重复 N 次)、数组/模型(每条目实例化一次)、或数组字面量。

  3. 伪随机函数:Slint 目前不提供内置随机数生成器,示例用function random(seed: int) -> float声明了一个纯函数,利用整数乘法的mod运算产生确定性伪随机数,返回值落在[0, 1)。以i(或i + k)为种子传入不同偏移,即可为每个实例产生互不相关的随机参数:

    • y: root.height * random(i + 1)—— 纵向起始位置散布在整个窗口高度;
    • random-duration: 5000ms + 10000ms * random(i + 20)—— 附加时长 5s ~ 15s;
    • wave-period: 800px + 200px * random(i)—— 波浪周期 800px ~ 1000px;
    • start-x: root.width * random(i)—— 起始偏移 0 ~ 屏宽;
    • scale: 0.3 + 0.7 * random(i + 5)—— 缩放 0.3 ~ 1.0;
    • wave-size: 50px + 50px * random(i + 6)—— 振幅 50px ~ 100px。

    由于是纯函数,同一i在多个属性中重复调用random(i)得到的是相同的确定值,因此各属性间可安全地共用种子;换用不同偏移(如i + 20i + 6)则得到互不相关的随机序列。这是 Slint 声明式语言中"无副作用求值"的典型应用。

三、for-in 语法:语言层面的完整规则

该示例只用到了for i in <int>这一种形式,但 Slint 的重复语法比这更通用。根据语言参考 repetition-and-conditional-elements.mdx 与入门指南 repetition-and-data-models.mdx,完整的语法为:

for name[index] in model : id := Element { }

各组成部分:

  • name(可选):绑定到当前条目的标识符,在元素内部及其子元素中作为只读伪属性使用;当模型为整数时,name即实例索引(int类型)。
  • [index](可选):零基索引,类型int。例如for my-color[index] in [ #e11, #1a2, #23d ]中,index可用于计算横向位移x: self.width * index
  • model:驱动重复的表达式,在for外层作用域求值(因此nameindex在模型表达式中不可见),可为整数、数组/模型或数组字面量。
  • id :=(可选):为实例命名,与普通元素一致。
  • Element { }:被实例化的单个元素,内部可以继续嵌套子元素、嵌套for/if

此外还有三条限制:

  1. 重复元素与条件元素只能作为某元素的子元素出现,不能作为组件根,也不能出现在globalinterface中,否则为编译错误;
  2. @children占位符不能出现在重复/条件元素内部,否则为编译错误;
  3. ListView只允许一个for元素作为其唯一子元素,出现第二个for、普通元素或if均属编译错误。

在编译器实现中,for会被解析为重复表达式,循环变量与索引通过Expression::RepeaterModelReference引用,相关处理逻辑集中在 internal/compiler/passes/resolving.rs 一带。

四、底层原理:Repeater 如何响应模型变化

界面上的"数量切换即重建"并非魔法,其背后是 Slint 核心库i-slint-core的 Repeater 运行时组件。仓库中的架构文档 docs/development/model-repeater-system.md 对此有系统描述,核心实现位于 internal/core/model/repeater.rs。

4.1 三个关键角色

  • Model(模型):提供数据行与变更通知的 trait。row_count()row_data()供数据,model_tracker()返回变更通知器。标准实现包括VecModel(可变数组)、SharedVectorModel与基本类型模型(usize模型产生 0..n 行、bool模型产生 0 或 1 行)。见 internal/core/model.rs。
  • Repeater:运行时的"重复器"。在源码中,Repeater<C>ModelChangeListenerContainer<RepeaterTracker<C>>的 newtype(repeater.rs),负责按模型数据实例化条目树。
  • RepeaterTracker:真正监听模型变更的对象,持有实例列表、模型属性、is_dirty(模型变脏标记)与instance_generation(实例增删代数)两个属性,见 repeater.rs。其中instance_generation供布局与遍历代码依赖,确保它们在实例真正增删后(而非模型刚变化时)重新求值。

4.2 变更通知与更新流程

当模型数据发生变化时(例如VecModel::push()set_row_data()),模型会调用ModelNotifyrow_changed()/row_added()/row_removed()/reset()通知监听者;RepeaterTracker收到通知后置is_dirty并更新实例状态。渲染期间调用Repeater::ensure_updated()(repeater.rs),根据is_dirty决定是否创建、更新或移除实例,完成后清除脏标记并递增instance_generation

VecModel.push(x) → ModelNotify.row_added() → RepeaterTracker 标记 is_dirty → 渲染时 ensure_updated() → 创建/更新/移除实例

在 demo 场景中,"ComboBox 切换数量"导致模型值(整数)变化,本质上等同于row_count改变,Repeater 据此整体重建指定数量的WaveImage实例。

4.3 性能延伸:ListView 虚拟化

对于超长列表,Repeater::ensure_updated_listview()(repeater.rs)实现了虚拟化:仅实例化可见区域内的行,RepeaterLayoutState记录首实例对应的模型行号、缓存的平均行高、上次滚动方向与偏移处条目位置。这也是为什么语言层面强制ListView只能有一个for子元素——它需要独占 Repeater 以进行虚拟化布局。若你的场景是"数千条数据",应优先选用ListView而非普通布局中的for

五、运行与调试

5.1 用 slint-viewer 直接预览

仓库自带通用查看器slint-viewer(定义于 tools/viewer/Cargo.toml),可直接加载.slint文件运行:

cargo run --release -p slint-viewer -- examples/repeater/demo.slint

运行后即可看到 Logo 波浪动画,并用下拉框实时切换 10 / 50 / 100 / 200 / 500 个实例,直观验证 Repeater 的批量实例化与动态重建能力。

5.2 在线编辑器

README 同时提供了 Slint 官方在线编辑器入口(examples/repeater/README.md 中的 Online Preview / Online code editor 链接),无需本地编译即可在浏览器中打开示例并即时修改代码、查看渲染结果,适合快速试验for循环的各种写法。

5.3 修改建议与验证技巧

想验证对 Repeater 的理解,可以尝试以下改动:

  1. 修改logo-model[3, 7, 15],观察下拉选项与实例数量同步变化;
  2. for循环体内把y改为i * 5px,验证i作为零基索引的使用;
  3. for i in logo-model[cb.current-index]改为for i in 20,确认整数模型的"重复 N 次"语义;
  4. 尝试在WaveImage内嵌套第二个for,验证重复元素支持任意层级嵌套。

此外,语言参考还指出:重复元素内部支持对模型值使用双向绑定的右侧引用,使实例内部状态与模型条目保持同步——这是构建可编辑列表的基础能力。

六、小结

通过examples/repeater这个看似简单的示例,我们完整走通了 Slint 数据驱动 UI 的核心链路:

  • 声明层for-in语法将"一个实例长什么样"与"有多少个实例"彻底解耦,模型可以是整数、数组或任意求值表达式;
  • 响应层:模型变更通过ModelNotifyRepeaterTrackerensure_updated()的机制自动同步到界面,无需手动增删元素;
  • 动画层animation-tick()modsin等内置函数组合,可在声明式中实现自驱动的复杂运动;
  • 工程层:面对超长列表时应使用ListView虚拟化,仅实例化可见行,控制资源开销。

如果你想深入源码,推荐从 internal/core/model/repeater.rs 的RepeaterTrackerensure_updated入手,配合架构文档 docs/development/model-repeater-system.md,即可完整理解 Slint 响应式重复机制的实现全貌。

【免费下载链接】slintSlint is an open-source declarative GUI toolkit to build native user interfaces for Rust, C++, JavaScript, or Python apps.项目地址: https://gitcode.com/GitHub_Trending/sl/slint

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询