自定义控件
自定义控件简介
Section titled “自定义控件简介”可点击的按钮
Section titled “可点击的按钮”import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { in-out property <int> counter: 0; VerticalBox { button := Button { text: "Button, pressed " + root.counter + " times"; clicked => { root.counter += 1; } } }}在第一个示例中,你会看到 Slint 语言的基础知识:
- 我们使用
import语句从标准库导入VerticalBox布局和Button控件。 该语句可以导入控件,也可以导入你在其他文件中声明的自定义组件。 你无需导入Window或Rectangle这样的内置元素。 - 我们使用
component关键字声明Recipe组件。Recipe继承自Window, 并包含元素:一个带单个按钮的布局(VerticalBox)。 - 通过元素名称后跟一对花括号(内容可选)来实例化元素。
你可以用
:=为特定元素指定名称。 - 元素具有属性。使用
:设置属性值。这里我们为Button的text属性 赋了一个绑定,它通过拼接若干字符串字面量来计算出字符串,同时还赋了counter属性。 - 你可以用
property <...>为任意元素声明自定义属性。属性需要有类型, 可以有默认值和访问修饰符。像private、in、out或in-out这样的 访问修饰符定义了外部元素如何与该属性交互。Private是默认值, 会阻止任何外部元素访问该属性。本例中的counter属性就是自定义属性。 - 元素也可以有回调。这里我们用
=> { ... }为button的clicked回调赋了一个回调处理器。 - 如果绑定所依赖的任何属性发生变化,属性绑定会自动重新求值。每当
counter变化时,按钮的text绑定都会自动重新计算。
在原生代码中响应按钮点击
Section titled “在原生代码中响应按钮点击”这个示例使用原生代码递增 counter:
import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { in-out property <int> counter: 0; callback button-pressed <=> button.clicked; VerticalBox { button := Button { text: "Button, pressed " + root.counter + " times"; } }}<=> 语法将两个回调绑定在一起。这里新的 button-pressed 回调
绑定到 button.clicked。
主组件的根元素将所有非 private 的属性和回调暴露给原生代码。
在 Slint 中,所有标识符里的 - 和 _ 等价且可互换。
原生代码则不同:大多数编程语言禁止在标识符中使用 -,
因此 - 会被替换为 _。
出于技术原因,这个示例在 slint! 宏中使用了 export {Recipe}。
在实际代码中,你可以把整个 Slint 代码放进 slint! 宏,或者配合构建脚本
使用外部的 .slint 文件。
slint::slint!(export { Recipe } from "button_native.slint";);
fn main() { let recipe = Recipe::new().unwrap(); let recipe_weak = recipe.as_weak(); recipe.on_button_pressed(move || { let recipe = recipe_weak.upgrade().unwrap(); let mut value = recipe.get_counter(); value = value + 1; recipe.set_counter(value); }); recipe.run().unwrap();}Slint 编译器会为 Recipe 组件根元素的每个可访问属性生成一个 struct Recipe,
其中包含 getter(get_counter)和 setter(set_counter)。它还会为每个可访问的
回调生成一个函数,比如这里的 on_button_pressed。
Recipe 结构体实现了 slint::ComponentHandle trait。组件
管理着一个强引用计数和一个弱引用计数,类似于 Rc。
我们调用 as_weak 函数来获取组件的弱句柄,这样就能把它移入回调中。
这里不能使用强句柄,因为那样会形成循环:组件句柄拥有回调的所有权, 而回调又拥有闭包所捕获变量的所有权。
在 C++ 中你可以这样写
#include "button_native.h"
int main(int argc, char **argv){ auto recipe = Recipe::create(); recipe->on_button_pressed([&]() { auto value = recipe->get_counter(); value += 1; recipe->set_counter(value); }); recipe->run();}CMake 集成会按需处理 Slint 编译器的调用,
它会解析 .slint 文件并生成 button_native.h 头文件。
这个头文件包含一个 Recipe 类,为每个可访问属性提供 getter 和 setter,
并为 Recipe 中的每个可访问回调提供设置回调的函数。在本例中,我们会有
用于访问 counter 属性的 get_counter、set_counter,以及用于设置回调的
on_button_pressed。
在 Python 中,你可以这样写:
import slint
class App(slint.loader.recipe.Recipe): @slint.callback def button_pressed(self): value = self.counter value = value + 1 self.counter = value
app = App()app.run()Slint 自动加载器提供了一个源自 recipe.slint 的 Recipe 类,并对其进行了子类化。
Recipe 类提供 counter 属性,而 @slint.callback 装饰器将
button_pressed 方法与 button-pressed 回调连接起来。
使用属性绑定同步控件
Section titled “使用属性绑定同步控件”import { VerticalBox, Slider } from "std-widgets.slint";export component Recipe inherits Window { VerticalBox { slider := Slider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}这个示例介绍了 Slider 控件。
它还介绍了字符串字面量中的插值:使用 \{...} 将花括号之间代码的结果
渲染为字符串。
为元素位置添加动画
Section titled “为元素位置添加动画”import { CheckBox } from "std-widgets.slint";export component Recipe inherits Window { width: 200px; height: 100px;
rect := Rectangle { x:0; y: 5px; width: 40px; height: 40px; background: blue; animate x { duration: 500ms; easing: ease-in-out; } }
CheckBox { y: 25px; text: "Align rect to the right"; toggled => { if (self.checked) { rect.x = parent.width - rect.width; } else { rect.x = 0px; } } }}布局会自动为元素定位。在这个示例中,我们改为使用 x、y、width、height
属性手动为元素定位。
注意 animate x 块指定了一个动画。每当属性变化时它都会运行:
要么是因为某个回调设置了该属性,要么是因为它的绑定值发生了变化。
import { CheckBox } from "std-widgets.slint";export component Recipe inherits Window { width: 200px; height: 100px;
rect := Rectangle { x:0; y: 5px; width: 40px; height: 40px; background: blue; animate x { duration: 500ms; easing: ease-in-out; } animate y { duration: 250ms; delay: 500ms; easing: ease-in; } }
CheckBox { y: 25px; text: "Align rect bottom right"; toggled => { if (self.checked) { rect.x = parent.width - rect.width; rect.y = parent.height - rect.height; } else { rect.x = 0px; rect.y = 0px; } } }}这个示例使用 delay 属性让一个动画在另一个动画之后运行。
将属性值与状态关联
Section titled “将属性值与状态关联”import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";
component Circle inherits Rectangle { width: 30px; height: 30px; border-radius: root.width / 2; animate x { duration: 250ms; easing: ease-in; } animate y { duration: 250ms; easing: ease-in-out; } animate background { duration: 250ms; }}
export component Recipe inherits Window { states [ left-aligned when b1.pressed: { circle1.x: 0px; circle1.y: 40px; circle1.background: green; circle2.x: 0px; circle2.y: 0px; circle2.background: blue; } right-aligned when b2.pressed: { circle1.x: 170px; circle1.y: 70px; circle1.background: green; circle2.x: 170px; circle2.y: 00px; circle2.background: blue; } ]
VerticalBox { HorizontalBox { max-height: self.min-height; b1 := Button { text: "State 1"; } b2 := Button { text: "State 2"; } } Rectangle { background: root.background.darker(20%); width: 200px; height: 100px;
circle1 := Circle { y:0; background: green; x: 85px; } circle2 := Circle { background: green; x: 85px; y: 40px; } } }}import { HorizontalBox, VerticalBox, Button } from "std-widgets.slint";
component Circle inherits Rectangle { width: 30px; height: 30px; border-radius: root.width / 2;}
export component Recipe inherits Window { states [ left-aligned when b1.pressed: { circle1.x: 0px; circle1.y: 40px; circle2.x: 0px; circle2.y: 0px; in { animate circle1.x, circle2.x { duration: 250ms; } } out { animate circle1.x, circle2.x { duration: 500ms; } } } right-aligned when !b1.pressed: { circle1.x: 170px; circle1.y: 70px; circle2.x: 170px; circle2.y: 00px; } ]
VerticalBox { HorizontalBox { max-height: self.min-height; b1 := Button { text: "Press and hold to change state"; } } Rectangle { background: root.background.darker(20%); width: 250px; height: 100px;
circle1 := Circle { y:0; background: green; x: 85px; } circle2 := Circle { background: blue; x: 85px; y: 40px; } } }}import { VerticalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { VerticalBox { Button { text: "First"; } Button { text: "Second"; } Button { text: "Third"; } }}import { HorizontalBox, Button } from "std-widgets.slint";export component Recipe inherits Window { HorizontalBox { Button { text: "First"; } Button { text: "Second"; } Button { text: "Third"; } }}import { GridBox, Button, Slider } from "std-widgets.slint";export component Recipe inherits Window { GridBox { Row { Button { text: "First"; } Button { text: "Second"; } } Row { Button { text: "Third"; } Button { text: "Fourth"; } } Row { Slider { colspan: 2; } } }}从 Slint 调用全局注册的原生回调
Section titled “从 Slint 调用全局注册的原生回调”这个示例使用全局单例在原生代码中实现通用逻辑。 这个单例还可以存储原生代码可访问的属性。
注意:预览只可视化 Slint 代码,并未连接到原生代码。
import { HorizontalBox, VerticalBox, LineEdit } from "std-widgets.slint";
export global Logic { pure callback to-upper-case(string) -> string; // You can collect other global properties here}
export component Recipe inherits Window { VerticalBox { input := LineEdit { text: "Text to be transformed"; } HorizontalBox { Text { text: "Transformed:"; } // Callback invoked in binding expression Text { text: { Logic.to-upper-case(input.text); } } } }}在 Rust 中你可以这样设置回调:
fn main() { let recipe = Recipe::new().unwrap(); recipe.global::<Logic>().on_to_upper_case(|string| { string.as_str().to_uppercase().into() }); // ...}在 C++ 中你可以这样设置回调:
int main(int argc, char **argv){ auto recipe = Recipe::create(); recipe->global<Logic>().on_to_upper_case([](slint::SharedString str) -> slint::SharedString { std::string arg(str); std::transform(arg.begin(), arg.end(), arg.begin(), toupper); return slint::SharedString(arg); }); // ...}在 JavaScript 中你可以这样设置回调:
import * as slint from 'slint-ui';let file = slint.loadFile(new URL('recipe.slint', import.meta.url));let recipe = new file.Recipe();recipe.Logic.to_upper_case = (str) => { return str.toUpperCase();};// ...在 Python 中,回调通过 @slint.callback 装饰器的 global_name 参数关联:
import slint
class App(slint.loader.recipe.Recipe): @slint.callback(global_name="Logic") def to_upper_case(self, value: str) -> str: return value.upper()
# ...component Button inherits Rectangle { in-out property text <=> txt.text; callback clicked <=> touch.clicked; border-radius: root.height / 2; border-width: 1px; border-color: root.background.darker(25%); background: touch.pressed ? #6b8282 : touch.has-hover ? #6c616c : #456; height: txt.preferred-height * 1.33; min-width: txt.preferred-width + 20px; txt := Text { x: (parent.width - self.width)/2 + (touch.pressed ? 2px : 0); y: (parent.height - self.height)/2 + (touch.pressed ? 1px : 0); color: touch.pressed ? #fff : #eee; } touch := TouchArea { }}
export component Recipe inherits Window { VerticalLayout { alignment: start; Button { text: "Button"; } }}ToggleSwitch
Section titled “ToggleSwitch”export component ToggleSwitch inherits Rectangle { callback toggled; in-out property <string> text; in-out property <bool> checked; in-out property<bool> enabled <=> touch-area.enabled; height: 20px; horizontal-stretch: 0; vertical-stretch: 0;
HorizontalLayout { spacing: 8px; indicator := Rectangle { width: 40px; border-width: 1px; border-radius: root.height / 2; border-color: self.background.darker(25%); background: root.enabled ? (root.checked ? blue: white) : white; animate background { duration: 100ms; }
bubble := Rectangle { width: root.height - 8px; height: bubble.width; border-radius: bubble.height / 2; y: 4px; x: 4px + self.a * (indicator.width - bubble.width - 8px); property <float> a: root.checked ? 1 : 0; background: root.checked ? white : (root.enabled ? blue : gray); animate a, background { duration: 200ms; easing: ease;} } }
Text { min-width: max(100px, self.preferred-width); text: root.text; vertical-alignment: center; color: root.enabled ? black : gray; }
}
touch-area := TouchArea { width: root.width; height: root.height; clicked => { if (root.enabled) { root.checked = !root.checked; root.toggled(); } } }}
export component Recipe inherits Window { VerticalLayout { alignment: start; ToggleSwitch { text: "Toggle me"; } ToggleSwitch { text: "Disabled"; enabled: false; } }}TouchArea 覆盖整个控件,因此你可以从滑块内部的任意位置拖动它。
import { VerticalBox } from "std-widgets.slint";
export component MySlider inherits Rectangle { in-out property<float> maximum: 100; in-out property<float> minimum: 0; in-out property<float> value;
min-height: 24px; min-width: 100px; horizontal-stretch: 1; vertical-stretch: 0;
border-radius: root.height/2; background: touch.pressed ? #eee: #ddd; border-width: 1px; border-color: root.background.darker(25%);
handle := Rectangle { width: self.height; height: parent.height; border-width: 3px; border-radius: self.height / 2; background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff; border-color: self.background.darker(15%); x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum); } touch := TouchArea { property <float> pressed-value; pointer-event(event) => { if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { self.pressed-value = root.value; } } moved => { if (self.enabled && self.pressed) { root.value = max(root.minimum, min(root.maximum, self.pressed-value + (touch.mouse-x - touch.pressed-x) * (root.maximum - root.minimum) / (root.width - handle.width)));
} } }}
export component Recipe inherits Window { VerticalBox { alignment: start; slider := MySlider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}这个示例展示了另一种实现,它有一个可拖动的滑块手柄: 只有当我们点击手柄时手柄才会移动。 TouchArea 位于手柄内部,并随手柄一起移动。
import { VerticalBox } from "std-widgets.slint";
export component MySlider inherits Rectangle { in-out property<float> maximum: 100; in-out property<float> minimum: 0; in-out property<float> value;
min-height: 24px; min-width: 100px; horizontal-stretch: 1; vertical-stretch: 0;
border-radius: root.height/2; background: touch.pressed ? #eee: #ddd; border-width: 1px; border-color: root.background.darker(25%);
handle := Rectangle { width: self.height; height: parent.height; border-width: 3px; border-radius: self.height / 2; background: touch.pressed ? #f8f: touch.has-hover ? #66f : #0000ff; border-color: self.background.darker(15%); x: (root.width - handle.width) * (root.value - root.minimum)/(root.maximum - root.minimum);
touch := TouchArea { moved => { if (self.enabled && self.pressed) { root.value = max(root.minimum, min(root.maximum, root.value + (self.mouse-x - self.pressed-x) * (root.maximum - root.minimum) / root.width)); } } } }}
export component Recipe inherits Window { VerticalBox { alignment: start; slider := MySlider { maximum: 100; } Text { text: "Value: \{round(slider.value)}"; } }}自定义标签页
Section titled “自定义标签页”当你想要创建自己的自定义标签页控件时,可以将这个配方作为基础。
import { Button } from "std-widgets.slint";
export component Recipe inherits Window { preferred-height: 200px; in-out property <int> active-tab; VerticalLayout { tab_bar := HorizontalLayout { spacing: 3px; Button { text: "Red"; clicked => { root.active-tab = 0; } } Button { text: "Blue"; clicked => { root.active-tab = 1; } } Button { text: "Green"; clicked => { root.active-tab = 2; } } } Rectangle { clip: true; Rectangle { background: red; x: root.active-tab == 0 ? 0 : root.active-tab < 0 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } Rectangle { background: blue; x: root.active-tab == 1 ? 0 : root.active-tab < 1 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } Rectangle { background: green; x: root.active-tab == 2 ? 0 : root.active-tab < 2 ? - self.width - 1px : parent.width + 1px; animate x { duration: 125ms; easing: ease; } } } }}自定义表格视图
Section titled “自定义表格视图”Slint 提供了表格控件,但你也可以基于 ListView 实现自定义方案。
import { VerticalBox, ListView } from "std-widgets.slint";
component TableView inherits Rectangle { in property <[string]> columns; in property <[[string]]> values;
private property <length> e: self.width / root.columns.length; private property <[length]> column_sizes: [ root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, root.e, ];
VerticalBox { padding: 5px; HorizontalLayout { padding: 5px; spacing: 5px; vertical-stretch: 0; for title[idx] in root.columns : HorizontalLayout { width: root.column_sizes[idx]; Text { overflow: elide; text: title; } Rectangle { width: 1px; background: gray; TouchArea { width: 10px; x: (parent.width - self.width) / 2; property <length> cached; pointer-event(event) => { if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { self.cached = root.column_sizes[idx]; } } moved => { if (self.pressed) { root.column_sizes[idx] += (self.mouse-x - self.pressed-x); if (root.column_sizes[idx] < 0) { root.column_sizes[idx] = 0; } } } mouse-cursor: ew-resize; } } } } ListView { for r in root.values : HorizontalLayout { padding: 5px; spacing: 5px; for t[idx] in r : HorizontalLayout { width: root.column_sizes[idx]; Text { overflow: elide; text: t; } } } } }}
export component Example inherits Window { TableView { columns: ["Device", "Mount Point", "Total", "Free"]; values: [ ["/dev/sda1", "/", "255GB", "82.2GB"] , ["/dev/sda2", "/tmp", "60.5GB", "44.5GB"] , ["/dev/sdb1", "/home", "255GB", "32.2GB"] , ]; }}响应式用户界面的断点
Section titled “响应式用户界面的断点”这个配方实现了一个响应式的 SideBar,当父级宽度小于给定的断点时它会折叠。 点击 Button 时,SideBar 会再次展开。使用蓝色的 Splitter 来调整容器大小, 以测试响应式行为。
import { Button, Palette } from "std-widgets.slint";
export component SideBar inherits Rectangle { private property <bool> collapsed: root.reference-width < root.break-point;
/// Defines the reference width to check `break-point`. in-out property <length> reference-width;
/// If `reference-width` is less `break-point` the `SideBar` collapses. in-out property <length> break-point: 600px;
/// Set the text of the expand button. in-out property <string> expand-button-text;
width: 160px;
container := Rectangle { private property <bool> expanded;
width: parent.width; background: Palette.background.darker(0.2);
VerticalLayout { padding: 2px; alignment: start;
HorizontalLayout { alignment: start;
if (root.collapsed) : Button { checked: container.expanded; text: root.expand-button-text;
clicked => { container.expanded = !container.expanded; } } }
@children }
states [ expanded when container.expanded && root.collapsed : { width: 160px;
in { animate width { duration: 200ms; } } out { animate width { duration: 200ms; } } in { animate width { duration: 200ms; } } out { animate width { duration: 200ms; } } } ] }
states [ collapsed when root.collapsed : { width: 62px; } ]}
component Splitter inherits TouchArea { width: 4px; mouse-cursor: ew-resize;
Rectangle { width: 100%; height: 100%; background: blue; }}
export component SideBarTest inherits Window { preferred-width: 700px; min-height: 400px; background: gray;
GridLayout { x: 0; width: splitter.x;
Rectangle { height: 100%; col: 1; background: white;
HorizontalLayout { padding: 8px;
Text { color: black; text: "Content"; } } } SideBar { col: 0; reference-width: parent.width; expand-button-text: "E"; } }
splitter := Splitter { x: root.width - self.width; height: 100%;
moved => { self.x = min(root.width - self.width, max(400px, self.x + self.mouse-x - self.pressed-x)); } }}© 2026 SixtyFPS GmbH