Slint 中的元素定位 (Positioning) 和布局 (Layout)

发布时间 2023-11-05 17:38:03作者: 那阵东风

基本逻辑

Slint 当中进行元素定位的基本逻辑是这样的:所有的可见元素都需要放置在窗口 (window) 中,每个元素都有 xy 属性,这两个属性表示当前元素 相对父元素的位置偏移。Slint 计算某个元素在整个窗口中的位置时,会按照层级关系,一级一级将这个 xy 的值进行累加,最终得到元素相对顶层窗口的位置。

相应的,widthheight 两个属性决定了可见元素的宽度和高度。结合使用这两对属性,我们有如下两种定位元素的基本方式:

  • 显式指定:直接设置每个元素的 xywidthheight 属性;
  • 自动定位:通过使用布局元素 (layout element)。

前者适合简单的或者静态的布局,而后者适合复杂、可缩放的用户界面。布局描述了元素之间的位置关系。

显式指定

我们从一个案例[1]来理解元素的显式定位:

export component Example inherits Window {
	preferred-width: 200px;
	preferred-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;
		}
	}
}

在这个例子中,两个 Rectangle 的位置和大小都是固定[2]的,其中内层 Rectangle 的大小和位置都是固定的值(相对于窗口的左上角是不变的),外层的位置固定,但大小由于与 parent 的宽高绑定,所以是动态适应的。

单位

在使用显式的值定义元素的位置和大小时,Slint 需要我们给出具体使用的数值单位。具体有以下两种:

  • 逻辑像素:使用 px,也是推荐使用的单位;
  • 物理像素:使用 phx

之所以更推荐逻辑像素 px,是因为它能够自适应高分辨率屏幕的需要,可以避免我们自行对界面执行缩放,是更简便、通用的方法。

此外,宽度和高度还可以写成百分比的形式,表示 元素相对父元素的宽度/高度值

默认值

  • 如果没有特别给出 xy 属性的值,那么元素默认会被放置在父元素居中的位置。
  • 如果没有特别给出 widthheight 属性的值,那么元素会按照:
    • 对于 ImageText 和大多数微件 (widget),这些值会按照具体内容(不是子元素)的大小进行设定;
    • 对于下列几个内置组件,它们不需要填充内容,且当没有子元素时,会默认填满父元素:RectangleTouchAreaFocusScopeFlickable
    • 对于布局元素,不管设定了怎样的尺寸倾向 (preferred size),默认都会填满父元素。
    • 对于其他元素(包括自定义组件),默认会按照尺寸倾向的设定来设定尺寸。

尺寸倾向

通过设定 preferred-widthpreferred-height 属性可以定义元素的尺寸倾向。

没有内容 的情况下,尺寸倾向取决于子元素,并且是所有子元素中,尺寸倾向值最大,且 xy 属性没有设置的那个。所以,尺寸倾向属性默认值的计算方式是:从子元素向父元素逐级进行,除非被显式设定

唯一一种特殊的情况就是,当我们把 preferred-widthpreferred-height 设置为 100% 时,当前组件的尺寸默认会使用父元素的 尺寸(非尺寸倾向)。

自动布局

我们可以在 Slint 中通过不同的布局元素来自动计算子元素的位置和尺寸:

  • VerticalLayout / HorizontalLayout 会将子元素沿着垂直或水平轴向排布;
  • GridLayout 会将子元素按照网格的行、列排布。

布局元素是可以进行嵌套的。我们也可以使用一些限定属性来进一步调整布局。比如每个元素都有最大最小尺寸和尺寸倾向属性,可以通过以下属性值来调整:

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

当一个元素被显式指定了 widthheight 时,它在布局元素中就有了固定的尺寸。

我们可以通过 horizontal-stretchvertical-stretch 属性调整元素及其相邻元素对布局中轴向剩余空间的利用方式。比如 0 值表示即使有多余空间,也不应该缩放元素。当所有元素的这个属性都为 0 时,所有元素会均匀利用剩余空间。

所有限定属性值也具有默认值,它们取决于元素的 内容。如果元素的 xy 都没有显式指定,那么这些限定属性也会自动被应用到其父元素上。

布局元素的公共属性

所有的布局元素都有这样两个公共属性:

  • spacing:表示布局元素的子元素之间的留白,通常会被应用到轴向分布的场景中;
  • padding:表示布局内部元素与布局边界之间的留白。

如果需要更细致得调节布局内部元素与四个方向边界间的留白,可以使用这四个额外的属性:

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

VerticalLayoutHorizontalLayout

VerticalLayout 和 HorizontalLayout 分别将子元素按行和列轴向分布。默认情况下,所有子元素会被适当拉伸或挤压以适应布局元素的大小。我们以可以通过额外属性调节它们的对齐方式 (alignment)。对其方式会影响子元素的拉伸情况

在下面的例子中,两个矩形 (Rectangle) 放置在同一个水平布局 (HorizontalLayout) 中,两个矩形都设置了尺寸限定属性 min-width,它们在父组件中的排布是这样的:

export component Example inherits Window {
  width: 256px;
  height: 256px;

  HorizontalLayout {
    Rectangle {
      background: gray;
      min-width: 64px;
    }
    Rectangle {
      background: yellow;
      min-width: 64px;
    }
  }
 }

|256

假如我们为水平分布增加对齐限定属性 (alignment constraint) alignment: start,那么 Slint 在进行水平布局时会按照此限定条件,取消原本的默认拉伸行为,转而使所有元素尽可能小,即按照子元素 Rectangle 的限定属性 min-width 进行布局:

export component Example inherits Window {
  width: 256px;
  height: 256px;

  HorizontalLayout {
    alignment: start;
    Rectangle {
      background: gray;
      min-width: 64px;
    }
    Rectangle {
      background: yellow;
      min-width: 64px;
    }
  }
 }

|256

甚至如果我们再极端一点,去掉两个 Rectanglemin-width 属性,那么当水平布局的对齐方式限定为 alignment: start 时,整个界面中将不存在这两个矩形!

|256

对齐方式 (alignment)

在水平和垂直布局中,如果我们为子元素设置了 widthheight 属性,本应该在布局时尊重这些属性的值。但假如布局的 alignment 设置为 stretch(同时也是默认值),它们在实际布局中的尺寸就取决于:

  • 这些子元素本身的 min-widthmin-height,或
  • 这些子元素内部的布局、元素尺寸经过计算后得到的最小尺寸

而最终的结果取决于上述两者中更大的那个。

在 Slint 中,水平和垂直布局可以采用的 alignment 对齐属性值包括:

  • stretch(默认值)
  • start
  • end
  • center
  • space-between:布局剩余空间等量分布各子元素之间
  • space-around:布局剩余空间等量分布在最外侧的子元素与边界之间

拉伸算法

当我们将布局的 alignment 设置为 stretch,布局中所有子元素都会首先按照它们的最小尺寸计算实际尺寸。然后如果剩余空间将会按照各个子元素的 horizontal-stretchvertical-stretch 属性分配给各个子元素,得到更新后的子元素尺寸。但各元素更新后的尺寸不能超过其自身的最大尺寸值。

horizontal-stretchvertical-stretch 属性的值被称为拉伸因子 (stretch factor),它是个浮点数。默认情况下,按照其内容大小计算尺寸元素的元素,其拉伸因子的默认值是 0;而按照其父元素尺寸计算尺寸的元素,其拉伸因子的默认值是 1。在还有剩余空间的情况下,一个拉伸因子为 0 的元素,只要不满足以下任何一个条件,它的尺寸就会维持最小尺寸限定值:

  • 其他所有元素的拉伸因子也都是 0
  • 其他所有元素都已经拉伸到了它们的最大尺寸限定值。

比如下面的例子:

export component Example inherits Window {
  width: 256px;
  height: 256px;

  VerticalLayout {
    HorizontalLayout {
      alignment: start;
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: 0;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: 0;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: 0;
        }
      }
    }
    HorizontalLayout {
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 0.5;
        padding-left: 16px;

        Text {
          text: 0.5;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        horizontal-stretch: 0.5;
        padding-left: 16px;

        Text {
          text: 0.5;
        }
      }
    }
    HorizontalLayout {
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: 0;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
    }
    HorizontalLayout {
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: 0;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        max-width: 128px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
    }
    HorizontalLayout {
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
    }
    HorizontalLayout {
      Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: yellow;
        min-width: 64px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
      Rectangle {
        background: green;
        min-width: 128px;
        max-width: 128px;
        horizontal-stretch: 1;
        padding-left: 16px;

        Text {
          text: 1;
        }
      }
    }
  }
 }

例子很长,我们先看结果:

|256

第一行是为水平布局设置了 alignment: start 的结果,剩余各行则都是用了默认的拉伸行为,即 alignment: stretch。每个色块中的文字表示这个 Rectanglehorizontal-stretch 的值。

  1. 第一行:任何子元素都不拉伸;
  2. 第二行:剩余空间 (64px) 被三个元素按照 \(1:2:1\) 的比例分配,即 16px32px16px
  3. 第三行:剩余空间 (64px) 被第二、三个元素按照 \(1:1\) 均分,即 32px32px
  4. 第四行:原本和第三行一样,应该按照 \(1:1\) 均分,即 32px32px 分配,但是第三个元素设置了 max-width: 128px,因此它不应该继续拉伸,所以只有第二个元素独占额外的 64px
  5. 第五行:剩余空间被三个元素按照 \(1:1:1\) 分配,即 21.33px21.33px21.33px
  6. 第五行:原本和第五行一样,应该按照 \(1:1:1\) 均分,即 21.33px21.33px21.33px 分配,但是第三个元素设置了 max-width: 128px,因此它不应该继续拉伸,所以前两个元素均分额外的 64px,即 32px32px

for 循环表达式

在水平和垂直布局中,我们可以使用 forif 表达式:

export component Example inherits Window {
  width: 256px;
  height: 256px;

  VerticalLayout {
    HorizontalLayout {
      spacing: 16px;
    
      for t in ["Hello", "Beautiful World","from", "Slint!"] : Rectangle {
        background: gray;
        min-width: 32px;
        horizontal-stretch: 0;
        padding-left: 16px;

        Text {
          text: t;
        }
      }
    }
  }
}

|256

网格布局 (GridLayout)

网格布局用于将子元素按行和列依次放入网格中。每个元素都有 rowcolrowspancolspan 几个属性[3]

子元素在网格中的排布方式是这样的:

  1. rowcol 计数器都从 0 开始分配;
  2. 如果没有指定 Row 作为子元素的容器:
    1. 下一个子元素将会保持 row 值,递增 col 值;
    2. 显式指定 row 值 (N) 后,从当前元素开始,将会重新将元素放置在 row=Ncol=0 的位置(除非显式指定 col 值改变当前 col 计数器);
  3. 如果指定 Row 作为子元素的容器:
    1. 当子元素没有指定 row 属性时,同一个 Row 内的子元素会按照 row 不变,col 递增的逻辑依次排布;
    2. 每变化一个 Row,会在 当前 row 计数器的基础上增加 1
    3. 在同一个 Row 中,如果显式指定了 row 的值,会重置 row 计数器,并且同一个 Row 中后续的元素都会在新的 row 中依次递增 col 排布。
  4. 如果因为显式指定计数器 row 和/或 col 导致当前元素的位置与已经放置的元素重叠,那么该元素位置会被覆盖。

值得注意的是,rowcolrowspancolspan 几个属性都必须在编译时确定,也就是无法通过算术表达式和属性依赖的方式指定。另外,当前版本的 Slint 尚不支持在网格布局中使用 forif 表达式。


  1. 案例来自 Slint 官方文档,稍作修改。 ↩︎

  2. 指父级元素不变的情况下。这里的歧义主要来自于外部 Rectangle 的大小实际取决于顶层 Window 的大小。 ↩︎

  3. rowcol 都是从 0 开始的索引值,除非显式指定,它们的值都是由计数器进行的。 ↩︎