
Rust+Slint 实现动态流光边框源码分享
- 一、效果展示
- 二、源码分享
- 三、实现原理
-
- 1、整体思路
- 2、圆角裁剪(clip)
- 3、底层旋转渐变矩形
- [4、Timer 驱动动画](#4、Timer 驱动动画)
- 5、内层遮盖,露出边框
- [6、与 C++ 实现的对应关系](#6、与 C++ 实现的对应关系)
- 7、小结
一、效果展示



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

2、main.rs
rust
slint::include_modules!();
fn main() {
let ui = MainWindow::new().expect("无法创建主窗口");
ui.run().expect("运行 UI 失败");
}
3、build.rs
rust
fn main(){
slint_build::compile("ui/main.slint")
.unwrap()
}
4、main.slint
rust
import { VerticalBox, HorizontalBox } from "std-widgets.slint";
// ==================== 流光边框组件 ====================
component FlowingBorder inherits Rectangle {
in property <string> title;
in property <color> color1: #e78f0c;
in property <color> color2: #00ffff;
in property <length> corner-radius: 12px;
in property <length> border-size: 2px;
width: 200px;
height: 80px;
// 父容器:圆角 + 裁剪(模拟 QPainterPath clip)
border-radius: corner-radius;
clip: true;
background: #0d0d1a;
// 底层:两个矩形旋转(模拟 C++ 的 QPainter 旋转矩形)
Rectangle {
width: 100%;
height: 100%;
clip: true; // 裁剪到父容器大小
// 旋转角度(一直增加,不取模)
property <int> angle: 0;
flow-timer := Timer {
interval: 20ms;
running: true;
triggered() => {
angle += 5;
}
}
// 矩形1:颜色1,绕中心旋转
Rectangle {
width: max(root.width,root.height)*1.2;
height: max(root.width,root.height)*1.2;
background: @linear-gradient(0deg, color1 0%, color2 100%);
transform-rotation: parent.angle * 1deg;
transform-origin: {x: self.width / 2, y: self.height / 2};
}
}
// 顶层:内层遮盖,露出边框间隙
inner-bg := Rectangle {
x: border-size;
y: border-size;
width: root.width - border-size * 2;
height: root.height - border-size * 2;
border-radius: corner-radius - border-size;
background: #0d0d1a;
VerticalBox {
padding: 8px;
alignment: center;
spacing: 4px;
Text {
text: title;
color: #ffffff;
font-size: 16px;
font-weight: 600;
horizontal-alignment: center;
vertical-alignment: center;
}
}
}
}
// ==================== 主窗口 ====================
export component MainWindow inherits Window {
title: "流光边框背景";
width: 900px;
height: 600px;
background: #0a0a14;
VerticalBox {
padding: 30px;
spacing: 25px;
alignment: center;
Text {
text: "流光边框背景控件";
color: #ffffff;
font-size: 24px;
font-weight: 700;
horizontal-alignment: center;
vertical-stretch: 0;
}
// 第一行:不同颜色组合
HorizontalBox {
spacing: 20px;
alignment: center;
vertical-stretch: 0;
FlowingBorder {
title: "青色流光";
color1: #f76809;
color2: #0088ff;
}
FlowingBorder {
title: "红色流光";
color1: #ff4444;
color2: #ff8800;
}
FlowingBorder {
title: "绿色流光";
color1: #ff00d4;
color2: #88ff44;
}
FlowingBorder {
title: "紫色流光";
color1: #00ff37;
color2: #ff44aa;
}
}
// 第二行:不同尺寸
HorizontalBox {
spacing: 20px;
alignment: center;
vertical-stretch: 0;
FlowingBorder {
title: "小";
width: 120px;
height: 50px;
corner-radius: 8px;
color1: #ffaa00;
color2: #ff4488;
}
FlowingBorder {
title: "中等";
width: 200px;
height: 80px;
corner-radius: 12px;
color1: #dff70b;
color2: #aa00ff;
}
FlowingBorder {
title: "大号控件";
width: 280px;
height: 100px;
corner-radius: 16px;
color1: #ff0088;
color2: #00ff88;
}
}
// 第三行:不同圆角
HorizontalBox {
spacing: 20px;
alignment: center;
vertical-stretch: 0;
FlowingBorder {
title: "小圆角";
corner-radius: 4px;
color1: #ffff00;
color2: #ff8800;
}
FlowingBorder {
title: "大圆角";
corner-radius: 30px;
color1: #ffee00;
color2: #ff0000;
}
FlowingBorder {
title: "胶囊";
corner-radius: 40px;
width: 180px;
height: 60px;
color1: #ff00ff;
color2: #00ff22;
}
}
}
}
5、Cargo.toml
rust
[package]
name = "modbus-debug-assistant"
version = "0.1.0"
edition = "2024"
[dependencies]
slint = "1.17.1"
[build-dependencies]
slint-build = "1.17.1"
三、实现原理
1、整体思路
流光边框的核心思路是:用旋转的渐变矩形作为底层背景,再用一个内层矩形遮盖中间区域,只露出边缘一圈,从而形成"光在边框上流动"的视觉效果。
整个实现可以拆成四层来看:
#mermaid-svg-4aqaXOKbDNpaN4vX{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-4aqaXOKbDNpaN4vX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-4aqaXOKbDNpaN4vX .error-icon{fill:#552222;}#mermaid-svg-4aqaXOKbDNpaN4vX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-4aqaXOKbDNpaN4vX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-4aqaXOKbDNpaN4vX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-4aqaXOKbDNpaN4vX .marker.cross{stroke:#333333;}#mermaid-svg-4aqaXOKbDNpaN4vX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-4aqaXOKbDNpaN4vX p{margin:0;}#mermaid-svg-4aqaXOKbDNpaN4vX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster-label text{fill:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster-label span{color:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster-label span p{background-color:transparent;}#mermaid-svg-4aqaXOKbDNpaN4vX .label text,#mermaid-svg-4aqaXOKbDNpaN4vX span{fill:#333;color:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX .node rect,#mermaid-svg-4aqaXOKbDNpaN4vX .node circle,#mermaid-svg-4aqaXOKbDNpaN4vX .node ellipse,#mermaid-svg-4aqaXOKbDNpaN4vX .node polygon,#mermaid-svg-4aqaXOKbDNpaN4vX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-4aqaXOKbDNpaN4vX .rough-node .label text,#mermaid-svg-4aqaXOKbDNpaN4vX .node .label text,#mermaid-svg-4aqaXOKbDNpaN4vX .image-shape .label,#mermaid-svg-4aqaXOKbDNpaN4vX .icon-shape .label{text-anchor:middle;}#mermaid-svg-4aqaXOKbDNpaN4vX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-4aqaXOKbDNpaN4vX .rough-node .label,#mermaid-svg-4aqaXOKbDNpaN4vX .node .label,#mermaid-svg-4aqaXOKbDNpaN4vX .image-shape .label,#mermaid-svg-4aqaXOKbDNpaN4vX .icon-shape .label{text-align:center;}#mermaid-svg-4aqaXOKbDNpaN4vX .node.clickable{cursor:pointer;}#mermaid-svg-4aqaXOKbDNpaN4vX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-4aqaXOKbDNpaN4vX .arrowheadPath{fill:#333333;}#mermaid-svg-4aqaXOKbDNpaN4vX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-4aqaXOKbDNpaN4vX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-4aqaXOKbDNpaN4vX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4aqaXOKbDNpaN4vX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-4aqaXOKbDNpaN4vX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4aqaXOKbDNpaN4vX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster text{fill:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX .cluster span{color:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX 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-4aqaXOKbDNpaN4vX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-4aqaXOKbDNpaN4vX rect.text{fill:none;stroke-width:0;}#mermaid-svg-4aqaXOKbDNpaN4vX .icon-shape,#mermaid-svg-4aqaXOKbDNpaN4vX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-4aqaXOKbDNpaN4vX .icon-shape p,#mermaid-svg-4aqaXOKbDNpaN4vX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-4aqaXOKbDNpaN4vX .icon-shape .label rect,#mermaid-svg-4aqaXOKbDNpaN4vX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-4aqaXOKbDNpaN4vX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-4aqaXOKbDNpaN4vX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-4aqaXOKbDNpaN4vX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 父容器 Rectangle
clip: true 裁剪到圆角矩形
底层旋转渐变矩形
Timer 驱动角度递增
内层遮盖矩形
露出边框间隙 = 流光效果
- 父容器 :负责圆角裁剪,相当于 C++ 里的
QPainterPath裁剪区域; - 底层:一个超大的渐变矩形,绕中心持续旋转;
- Timer:每 20ms 让角度 +5°,驱动旋转动画;
- 内层遮盖 :比父容器小一圈(缩进
border-size),把中间盖住,只留边框一圈可见。
2、圆角裁剪(clip)
slint
border-radius: corner-radius;
clip: true;
background: #0d0d1a;
border-radius设置圆角半径;clip: true表示所有子元素超出父容器圆角范围的部分都会被裁剪掉;- 这样底层旋转矩形即使转出边界,也不会"溢出"到圆角外面,保证边框始终是圆角矩形。
这一步对应 C++ 实现里的
painter.setClipPath(roundedRectPath)。
3、底层旋转渐变矩形
slint
Rectangle {
width: max(root.width,root.height)*1.2;
height: max(root.width,root.height)*1.2;
background: @linear-gradient(0deg, color1 0%, color2 100%);
transform-rotation: parent.angle * 1deg;
transform-origin: {x: self.width / 2, y: self.height / 2};
}
关键点:
- 尺寸放大到 1.2 倍:保证矩形旋转时四个角始终能覆盖到父容器的边缘,不会露出空白;
- 线性渐变 :
color1 → color2的渐变让"光"有方向感,旋转时看起来像光在流动; - transform-rotation :以自身中心为原点旋转,角度由父级
angle属性驱动; - transform-origin:旋转中心设为矩形正中心,保证绕中心转而不是绕左上角转。
4、Timer 驱动动画
slint
property <int> angle: 0;
flow-timer := Timer {
interval: 20ms;
running: true;
triggered() => {
angle += 5;
}
}
interval: 20ms:每 20ms 触发一次,即每秒 50 帧;- 每次触发
angle += 5,即每秒旋转50 × 5 = 250°,约 1.4 秒转一圈; - 角度不取模,一直累加,Slint 内部会自动处理大角度旋转,无需手动归零。
角度累加不取模的好处是:避免取模运算带来的性能开销,同时 Slint 的旋转计算对大角度天然支持。
5、内层遮盖,露出边框
slint
inner-bg := Rectangle {
x: border-size;
y: border-size;
width: root.width - border-size * 2;
height: root.height - border-size * 2;
border-radius: corner-radius - border-size;
background: #0d0d1a;
}
- 内层矩形比父容器四周各缩进
border-size(默认 2px); - 背景色与父容器一致(
#0d0d1a),把中间区域完全盖住; - 于是只有边框一圈能透出底层旋转的渐变,形成"流光边框";
- 圆角也相应缩小
border-size,保证内层圆角与外层同心。
6、与 C++ 实现的对应关系
| C++(QPainter) | Slint 实现 | 作用 |
|---|---|---|
setClipPath |
clip: true + border-radius |
圆角裁剪 |
QPainter::rotate |
transform-rotation |
旋转渐变矩形 |
QTimer |
Timer |
驱动动画 |
| 内层填充矩形 | inner-bg |
遮盖中间,露出边框 |
QLinearGradient |
@linear-gradient |
渐变颜色 |
7、小结
整个流光边框的核心就一句话:旋转的渐变 + 内层遮盖 = 边框流光 。通过调整 color1、color2、border-size、corner-radius 和 Timer 的间隔/步长,可以轻松改变光效的颜色、粗细、圆角和速度,从而适配不同的 UI 风格。
