Slint UI 基础:Slint 组件的定位和布局(Positioning and Layout)

2,928 阅读12分钟

Slint 的原则就是,用户界面 UI 使用 Slint 语言编写,逻辑部分交给其他语言处理,比如 Rust,Javascript,C++等,做到 UI 和逻辑分离,有利于编写逻辑清晰的代码。

​用户界面 UI 由.slint文件或者slint!宏 构建,每个.slint文件(或 slint!宏)定义一个或多个组件。这些组件声明了一个元素树。组件构成了 Slint 中组合的基础。使用它们构建自己的可重复使用的 UI 控件集,高效的开发流畅的图形用户界面的应用程序。每个声明的组件可以作为另一个组件中的元素。

以下是组件和元素的示例:

component MyButton inherits Text {
    color: black;
    // ...
}

export component MyApp inherits Window {
    preferred-width: 200px; // 首选宽度
    preferred-height: 100px; // 首选高度
    Rectangle {
        width: 200px;
        height: 100px;
        background: green;
    }
    MyButton {
        x:0;
        y:0;
        text: "hello";
    }
    MyButton {
        y:0;
        x:50px;
        text: "world";
    }
}

MyButton都是MyApp组件。Window并且Rectangle是 . 使用的内置元素MyAppMyApp也将组件重新用作MyButton两个单独的元素。

元素具有属性,可以为其分配值。在这里,我们将字符串常量“hello”分配给 firstMyButtontext属性。您还可以分配整个表达式。当表达式所依赖的任何属性发生变化时,Slint 将重新评估表达式,这使得用户界面具有反应性。

可以使用以下语法命名元素:=

component MyButton inherits Text {
    // ...
}

export component MyApp inherits Window {
    preferred-width: 200px;
    preferred-height: 100px;

    // 使用`:=`为元素命名
    hello := MyButton {
        x:0;
        y:0;
        text: "hello";
    }
    world := MyButton {
        y:0;
        x: 50px;
        text: "world";
    }
}

名称必须是有效的标识符

一些元素也可以在预定义的名称下访问:

  • root指组件的最外层元素。
  • self引用当前元素。
  • parent引用当前元素的父元素。

这些名称是唯一保留的,不能重新定义它们。

元素的定位和布局

Slint 是单窗口模式,也就是所有视觉元素都显示在一个窗口(Window)中。每一个组件都有定位和布局属性,比如:

属性xy:表示元素相对于其父元素的位置。

属性width``height:表示可视元素的大小。

参考:github.com/slint-ui/sl…

component Empty {
    in property <length> x;
    in property <length> y;
    in property <length> width;
    in property <length> height;
    //-is_internal
}

可以通过以两种方式来定位和布局已创建的图形用户界面:

  • 显式 - 通过设置xywidthheight属性。
  • 自动 - 通过使用布局元素。

显式放置非常适合元素很少的静态场景,自动布局适用于复杂的用户界面,有助于创建可扩展的用户界面。

使用显式放置

下面的示例将两个矩形放入一个窗口中,一个是蓝色的,一个是绿色的。绿色矩形是蓝色的子矩形:

// Explicit positioning
export component Example inherits Window {
    width: 200px;
    height: 200px;
    Rectangle {
        x: 100px;
        y: 70px;
        width: parent.width - self.x;
        height: parent.height - self.y;
        background: blue;
        Rectangle {
            x: 10px;
            y: 5px;
            width: 50px;
            height: 30px;
            background: green;
        }
    }
}

两个矩形的位置和内部绿色矩形的大小都是固定的。外部蓝色矩形的大小是使用widthheight属性的绑定表达式自动计算的。计算结果是左下角与窗口的角对齐——只要窗口大小发生变化,它就会更新。

为任何几何属性指定显式值时,Slint 要求将单位附加到数字。可以选择两种不同的单位:

  • 逻辑像素,使用px单位后缀。这是推荐单位。
  • 物理像素,使用phx单位后缀

逻辑像素会根据系统配置的设备像素比自动缩放。例如,在现代高 DPI 显示器上,设备像素比可以为 2,因此每个逻辑像素占用 2 个物理像素。在旧屏幕上,用户界面无需任何调整即可缩放。

此外,widthheight属性也可以指定为%百分比单位,它相对于父元素的百分比。例如width: 50%表示父组件width的50%,xy属性的默认值使得元素在其父元素中居中。

widthheight的默认值取决于元素的类型。某些元素会根据其内容自动调整大小,例如ImageText和大多数小部件。以下元素没有内容,当它们没有子元素时默认填充它们的父元素的:

  • Rectangle
  • TouchArea
  • FocusScope
  • Flickable

布局也默认填充父组件的大小,无论它们自己的首选大小如何。

其他元素(包括没有基础的自定义元素)默认使用它们的首选大小。

首选大小

可以使用preferred-widthpreferred-height属性指定元素的首选大小。

如果未明确设置,则首选大小取决于子项,并且是具有较大首选大小的子项的首选大小,其xy属性未设置。因此,首选大小是从子项到父项计算的,就像其他约束(最大和最小大小)一样,除非明确覆盖。

一种特殊情况是将首选大小设置为使用父级的大小,即100%。例如,此组件将默认使用父级的大小:

component MyComponent {
    preferred-width: 100%;
    preferred-height: 100%;
    // ...
}

使用自动布局放置

Slint 有些布局组件,可以自动计算其子元素的位置和大小:

  • VerticalLayoutHorizontalLayout:子项沿垂直或水平轴放置。
  • GridLayout:子元素被放置在一个由列和行组成的网格中。

您还可以嵌套布局以创建复杂的用户界面。

您可以使用不同的约束来调整自动放置,以适应用户界面设计。每个元素都有最小大小、最大大小和首选大小。使用以下属性显式设置这些:

  • min-width
  • min-height
  • max-width
  • max-height
  • preferred-width
  • preferred-height

在布局中具有指定width和固定大小的任何元素。height

当布局中有额外空间时,元素可以沿布局轴伸展拉伸。可以使用以下属性控制元素及其兄弟元素之间的拉伸因子:

  • horizontal-stretch:水平方向拉伸因子
  • vertical-stretch:垂直方向拉伸因子

0表示该元素根本不会拉伸。如果所有元素的拉伸因子都为0 ,则所有元素均等拉伸0

这些约束属性的默认值可能取决于元素的内容。如果元素的xy未设置,这些约束也会自动应用于父元素。

布局元素的通用属性

所有布局元素都具有以下共同属性:

  • spacing:这控制子项之间的间距(即间隔
  • padding:这指定布局内的填充(即内边距

对于更细粒度的控制,padding可以将属性拆分为布局每一侧的属性:

  • padding-left
  • padding-right
  • padding-top
  • padding-bottom

VerticalLayoutHorizontalLayout

VerticalLayout元素HorizontalLayout将它们的子元素放在一列或一行中。默认情况下,它们会拉伸或收缩以占据整个空间。您可以根据需要调整元素的对齐方式。

以下示例将蓝色和黄色矩形放置在一行中,并均匀拉伸到 的 200 个逻辑像素width

// Stretch by default
export component Example inherits Window {
    width: 200px;
    height: 200px;
    HorizontalLayout {
        Rectangle { background: blue; min-width: 20px; }
        Rectangle { background: yellow; min-width: 30px; }
    }
}
image.png

另一方面,下面的示例指定矩形应与布局的开始对齐(视觉左侧)。这不会导致拉伸,而是矩形会保留其指定的最小宽度:

// Unless an alignment is specified
export component Example inherits Window {
    width: 200px;
    height: 200px;
    HorizontalLayout {
        alignment: start; // 指定对其方式:左侧对齐
        Rectangle { background: blue; min-width: 20px; }
        Rectangle { background: yellow; min-width: 30px; }
    }
}
image.png

下面的示例为更复杂的场景嵌套了两个布局:

export component Example inherits Window {
    width: 200px;
    height: 200px;
    HorizontalLayout {
        // Side panel
        Rectangle { background: green; width: 10px; }

        VerticalLayout {
            padding: 0px;
            //toolbar
            Rectangle { background: blue; height: 7px; }

            Rectangle {
                border-color: red; border-width: 2px;
                HorizontalLayout {
                    Rectangle { border-color: blue; border-width: 2px; }
                    Rectangle { border-color: green; border-width: 2px; }
                }
            }
            Rectangle {
                border-color: orange; border-width: 2px;
                HorizontalLayout {
                    Rectangle { border-color: black; border-width: 2px; }
                    Rectangle { border-color: pink; border-width: 2px; }
                }
            }
        }
    }
}
image.png

对齐

每个元素都根据它们的大小widthheight指定大小,否则将其设置为使用 min-width 或 min-height 属性设置的最小大小,或者内部布局的最小大小,以较大者为准。

元素根据对齐方式放置alignment仅当布局属性为LayoutAlignment.stretch(默认)时,元素的大小才大于最小大小

此示例显示了不同的对齐可能性

export component Example inherits Window {
    width: 300px;
    height: 200px;
    VerticalLayout {
        HorizontalLayout {
            alignment: stretch;
            Text { text: "stretch (default)"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: start;
            Text { text: "start"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: end;
            Text { text: "end"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: start;
            Text { text: "start"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: center;
            Text { text: "center"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: space-between;
            Text { text: "space-between"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
        HorizontalLayout {
            alignment: space-around;
            Text { text: "space-around"; }
            Rectangle { background: blue; min-width: 20px; }
            Rectangle { background: yellow; min-width: 30px; }
        }
    }
}
image.png

拉伸算法

alignment设置为拉伸(默认)时,元素的大小将调整为它们的最小尺寸,然后在元素之间共享额外的空间,剩下空间大小根据拉伸因子 horizontal-stretchvertical-stretch的值按比例计算分派。拉伸尺寸不会超过最大尺寸。拉伸因子是一个浮点数。具有默认内容大小的元素通常默认为 0,而默认为其父元素大小的元素默认为 1。拉伸因子为 0 的元素将保持其最小大小,除非所有其他元素也具有拉伸因子0 或达到其最大尺寸。

例子:

export component Example inherits Window {
    width: 300px;
    height: 200px;
    VerticalLayout {
        // Same stretch factor (1 by default): the size is divided equally
        HorizontalLayout {
            Rectangle { background: blue; }
            Rectangle { background: yellow;}
            Rectangle { background: green;}
        }
        // Elements with a bigger min-width are given a bigger size before they expand
        HorizontalLayout {
            Rectangle { background: cyan; min-width: 100px;}
            Rectangle { background: magenta; min-width: 50px;}
            Rectangle { background: gold;}
        }
        // Stretch factor twice as big:  grows twice as much
        HorizontalLayout {
            Rectangle { background: navy; horizontal-stretch: 2;}
            Rectangle { background: gray; }
        }
        // All elements not having a maximum width have a stretch factor of 0 so they grow
        HorizontalLayout {
            Rectangle { background: red; max-width: 20px; }
            Rectangle { background: orange; horizontal-stretch: 0; }
            Rectangle { background: pink; horizontal-stretch: 0; }
        }
    }
}
image.png

for表达式

VerticalLayout 和 Horizo​​ntal 布局也可以包含fororif表达式:

export component Example inherits Window {
    width: 200px;
    height: 50px;
    HorizontalLayout {
        Rectangle { background: green; }
        for t in [ "Hello", "World", "!" ] : Text {
            text: t;
        }
        Rectangle { background: blue; }
    }
}
image.png

网格布局

GridLayout 将元素放置在网格中。每个元素都获得属性rowcolrowspancolspan。可以使用子Row元素,也可以row显式设置属性。这些属性必须在编译时静态已知,因此不可能使用算术或依赖属性。在网格布局 GridLayout 中不允许使用foror if表达式。

  1. 使用Row元素的例子:
export component Foo inherits Window {
    width: 200px;
    height: 200px;
    GridLayout {
        spacing: 5px;
        Row {
            Rectangle { background: red; }
            Rectangle { background: blue; }
        }
        Row {
            Rectangle { background: yellow; }
            Rectangle { background: green; }
        }
    }
}
image.png
  1. 使用colandrow属性的例子
export component Foo inherits Window {
    width: 200px;
    height: 150px;
    GridLayout {
        spacing: 0px;
        Rectangle { background: red; }
        Rectangle { background: blue; }
        Rectangle { background: yellow; row: 1; }
        Rectangle { background: green; }
        Rectangle { background: black; col: 2; row: 0; }
    }
}
image.png

容器组件

创建组件时,有时影响子元素在使用时的放置位置很有用。例如,假设一个组件在用户放置在其中的任何元素上方绘制一个标签:这时可以使用BoxWithLabel布局来实现这样的功能。先定义一个Text元素成为默认直接子元素,然后通过@children定义其他子元素在组件层次结构中的位置:

component BoxWithLabel inherits GridLayout {
    Row {
        Text { text: "label text here"; }
    }
    Row {
        @children
    }
}

export component MyApp inherits Window {
    preferred-height: 100px;
    BoxWithLabel {
        Rectangle { background: blue; }
        Rectangle { background: yellow; }
    }
}
image.png

字体处理

诸如TextTextInput之类的元素可以呈现文本并允许通过不同的属性自定义文本的外观。font-family会影响用于呈现到屏幕font-sizefont-weight 字体的选择。

为渲染选择的字体会自动从系统中获取。也可以使用导入的自定义字体。自定义字体必须是 TrueType 字体 ( .ttf) 或 TrueType 字体集 ( .ttc)。可以使用import:导入自定义字体在 .slint 文件中,表示使用导入字体,并全局可用于font-family属性。import "./my_custom_font.ttf"

例如:

import "./NotoSans-Regular.ttf";

export component Example inherits Window {
    // default-font-size: 10px;  // 设置窗口内默认的文字大小
    // default-font-weight: 900;  // 设置窗口默认的字体粗细 100~900
    default-font-family: "Noto Sans"; // 设置窗口使用的字体系列

    Text {
        text: "Hello World";
    }
}

焦点处理

某些元素例如TextInput不仅接受来自鼠标/手指的输入,还接受来自(虚拟)键盘的键事件。为了让项目接收这些事件,它必须有焦点。这可以通过has-focus(out) 属性判断。

  1. 通过调用手动激活元素上的焦点focus()
import { Button } from "std-widgets.slint";

export component App inherits Window {
    VerticalLayout {
        alignment: start;
        Button {
            text: "press me";
            clicked => { input.focus();  // 点击激活TextInput焦点}
        }
        // 某些元素例如TextInput不仅接受来自鼠标/手指的输入,还接受来自(虚拟)键盘的键事件。
        // 为了让项目接收这些事件,它必须有焦点。这可以通过has-focus(out) 属性看到。
        input := TextInput {
            text: "I am a text input field";
        }
    }
}
  1. 如果TextInput包装在一个组件中,可以使用forward-focus属性转发焦点激活:
import { Button } from "std-widgets.slint";

component LabeledInput inherits GridLayout {
    forward-focus: input; // 转发input的焦点
    Row {
        Text {
            text: "Input Label:";
        }
        // 申明一个input
        input := TextInput {}
    }
}

export component App inherits Window {
    GridLayout {
        Button {
            text: "press me";
            clicked => { label.focus(); // 点击激活LabeledInput焦点}
        }
        // 申明一个label,内部包装一个input
        label := LabeledInput {
        }
    }
}

如果您forward-focusWindow上使用该属性,则指定的元素将在窗口第一次接收到焦点时接收到焦点 - 它成为初始焦点元素。