Autosize / Layout/zh CN

From Free Pascal wiki

English (en) русский (ru) 中文(中国大陆)‎ (zh_CN) 中文(台灣)‎ (zh_TW)

Contents

概述

LCL可以自动修改一个控件的大小和位置,以便它适应字体,主题和文本或者其它内容的更改。如果你想在一些平台上运行你的 程序,或者你的标题可以使用一些语言,你的控件需要适应它们的当前环境。LCL不仅允许你做一次快速设计(使用鼠标在一个窗体上到处移动控件) ,并且后期设置一些将使控件随后自动地适应更改控件的关键属性等等。

  • 固定设计:这是在设计器中放置一个控件的默认设置。控件的位置相对其父类控件是固定的。控件的大小和位置(左,顶)可由程序员完全调整。你可以使用鼠标四处移动控件,和自由重新调整其大小。
  • 对齐:对齐的控件将填充剩余父类控件空间顶部、底部、左侧和右侧,或者填充整个剩余空间。
  • :你可以锚定一个控件的边侧(左,顶,右,底)到它的父类组件或到另一个组件。锚定的意思:LCL将尝试维持与锚定点相同的距离。
  • 布局:控件可以在行和列中自动地对齐 (例如, TRadioGroup)
  • 通过OnResize自定义:你可以在代码中使用OnResize和OnChangeBounds事件来对齐你自己的控件。
  • 自定义控件:写你自己的控件,你可以重写(override)几乎任何你想的LCL行为。

优先级规则

  1. 约束式(Constraints)
  2. 对齐(Align)
  3. 锚(Anchors)
  4. 子控件大小.布局(ChildSizing.Layout)
  5. 自动大小(AutoSize)
  6. OnResize, OnChangeBounds - 但是,如果你设置与上述规则冲突的边界,你将创建一个死循环。

公共属性

可以更改一些重要的属性来配置自动大小:

  • Left, Top, Width, Height(左,顶,宽,高)
  • 自动大小(AutoSize):autosize通知LCL自动地重新调整一个控件的宽度和高度。
  • 锚(Anchors):让你创建依赖关系/附属关系,例如,来锚一个ComboBox到一个Label的右侧。
  • 对齐(Align)
  • 约束式(Constraints):让你为宽度和高度设置一个最小值和最大值。
  • 边界空格(BorderSpacing): 让你设置锚定控件之间的空格。
  • ChildSizing:让你设置子控件的布局和空格。

内部算法在这里解释:LCL自动大小(AutoSizing)

固定设计

固定设计是默认的。控件的属性设置为[akLeft,akTop],这意味着左侧和顶部的值没有被LCL更改。如果AutoSize的值是false,LCL不改变宽度或高度。如果AutoSize的值是true,宽度 和高度将被更改以适应内容。例如,TLabel.AutoSize默认值为true,因此将更改它的标题来将重新调整 label 大小以容纳更改的表达方式。TButton.AutoSize默认值是false,因此更改一个按钮的标题不会重新调整按钮大小。当你设置Button.AutoSize为true时,每次更改按钮的标题或它的字体(或主体),按钮将收缩或放大。注意,这些更改不总是立即发生的。例如,在FormCreate期间,所有的自动大小是挂起的。无论何时,你都可以更改"Left", "Top", "Width",或"Height"属性。

自动大小(AutoSize)

AutoSize是一个在很多类中可用找到的布尔值属性;它允许自动地调整一个控件的大小以适应顾及所含文本或图形的差异,也允许最有效地使用可用空间。这是创建跨平台窗体的关键机制。 通常地, AutoSize=true 对一个可视控件做两件事:

  • 如果有可能,它将重新调整控件的大小为首选大小。例如,一个TButton的宽度和高度被重新调整大小以适应标题,在一个TEdit仅在高度上重新调整大小的同时。一个TEdit的宽度不被自动大小。你可以更改一个TEdit的宽度。(查看GetPreferredSize)。
  • 它以相同的数量移动固定位置的子控件,以便最左侧的子控件移动到 Left=0 (取决于边界空格(BorderSpacing)),最顶部的子控件移动到 Top=0 。

Lazarus vs Delphi

  • Delphi的自动大小(AutoSize)仅当某些属性更改时发生,例如,当一个TLabel的字体更改时。LCL的自动大小(AutoSize)总是激活的。Delphi允许更改一个有AutoSize=true的Lable的大小, 而Lazarus控件不允许。
  • Delphi的隐藏的控件不会被自动大小。
  • 使用Delphi,在加载期间,更改一个控件的大小不会重新调整大小/移动锚定子控件,但是一个构造函数通常被用来制作预期效果。当使用akRight,akBottom锚时,设置AnchorSides(锚定边)和BorderSpacing(边界空格)以保持距离正确。

自动大小(AutoSize)和重新调整大小控件

使用AutoSize=false,按钮将给予一个默认的固定大小。

Autosize1.png

当为每个按钮设置 AutoSize=true 时,按钮将放大(或收缩)以适应文本和主题框架。

Autosize on.png

正如你在OK按钮中所见,AutoSize不会收缩到最小可能的尺寸大小。它使用控件的 GetPreferredSize方法,该方法调用CalculatePreferredSize方法。默认的TWinControl实施询问 widget集合,这可能有一个首选的宽度或高度。每个控件都可以重写(override) CalculatePreferredSize 方法。例如,TImage 重写(override)它,并返回图片的大小。如果没有可用的首选的宽度(高度),返回的值是0,并且LCL将保持宽度(高度) (除非ControlStyle设置标示csAutoSize0x0,当前仅TPanel使用)。

一个TWinControl计算其所有子控件的大小,并使用它来计算首选大小。

当一个控件被锚定到左侧和右侧,并且它的 宽度 是固定的。例如,使用 Align=alTop ,控件被锚定到左侧和右侧,并且沿袭父类控件的宽度。如果 Parent.AutoSize 为 true,那么 Parent 将使用控件的首选宽度来计算它自身的首选宽度,以此方式,控件将重新调整大小为它的首选宽度。查看 对齐和自动大小(AutoSize. 如果没有 可用的首选宽度,那么最后设置的边界将被使用(BaseBounds或ReadBounds)。 如果没有边界被设置,GetControlClassDefaultSize方法将被使用。Same for对于高度和锚定到顶部和底部是一样的。

约束式总是被应用,并且有优先权。

自动大小 (AutoSize)和移动子控件

当 AutoSize=false 时,你可以自由地放置和移动控件:

Autosize on.png

当 AutoSize=true 时,有固定位置的子控件将移动以适应位置。

Autosize panel on.png

在面板上是所有按钮都将通过相同的数量来移动到顶部和左侧,以便左侧和顶部没有剩余的空间。如果按钮可能有 BorderSpacing>0 或 Panel.ChildSizing.LeftRightSpacing>0 ,按钮可能被移动,以便使用定 义的空格量

仅具有下面值的子控件将被移动:

  • Anchors=[akLeft,akRight]
  • AnchorSide[akLeft].Control=nil
  • AnchorSide[akTop].Control=nil
  • Align=alNone

子控件的移动取决于属性ChildSizing.Layout。 该布局被应用于方法 TWinControl.AlignControls ,这将完全重写或部分重写 (overridden)例如,TToolBar 重写 (overridden) ControlsAligned 到所有使用 Align=alCustom 的控件的位置,并定义一个流布局,在流布局中不适合的控件将被推放到后续的行中。

控件可以通过设置ControlStyle标示csAutoSizeKeepChildLeft 和 csAutoSizeKeepChildTop (自0.9.29版本可用)来禁用子控件的移动。

自动 大小(AutoSize)和窗体

没有父类控件的窗体将被窗口管理器控制,因而不能自由地放置和重新调整大小。设置大小仅是一个建议,并且可能会被 窗口管理器忽略。例如,你可以设置一个窗体的宽度为1000, 而 widget集可能会重新调整大小为800作为回应。如果你在OnResize中设置宽度,你可能会创建一个死循环。这就是为什么当widget集重新设 置窗体大小时,LCL TForm 禁用自动大小(AutoSize)的原因。

这意味着当用户重新调整窗体大小或假设窗口管理器不喜欢边界时,窗体的自动大小(AutoSize)将停止。例 如,一些Linux窗口管理器有参照关联的其它窗口重新调整该窗体的磁性边功能。

强制一个窗体的自动大小

你可以启动/执行一次新的自动大小(AutoSize),通过这样做:

Form1.AutoSize := False;
Form1.AutoSize := True;

预先计算一个自动大小的窗体的大小

当放置一个自动大小的窗体时,你可能需要在显示该窗体前知道它的大小。自动大小需要句柄(handle)。你可以 在显示一个窗体前计算其大小,使用:

Form1.HandleNeeded;
Form1.GetPreferredSize(PreferredWidth,PreferredHeight);

注意: 窗口管理器和窗体事件可能会更改窗体大小。首先大小不包含窗体的窗口边界。这是一个计划中的功能。

锚定边

控件有四个边: akLeft, akTop, akRight 和 akBottom。每个边都能被锚定到父类控件或另一个同类控件(具有相同父类控件的一个控件)的边。锚意味着保 存距离。默认是 Anchors=[akLeft,akTop] 。垂直锚与水平锚互不干涉。像 Align 和 Parent.AutoSize 之类的一些的属性有较高的优先级,并可以更改行为。

锚定到父类控件或锚定到空值

锚定到空值(默认)与锚定到父类控件有几乎相同的效果。都尝试与父类控件客户端区域的一边保持距离。锚定到空值使 用最后的SetBounds调用距离,然而锚定到父类控件使用BorderSpacing值。

这里有akLeft,akRight (akTop,akBottom)的四种组合:

  • 有akLeft,没有akRight:控件的左边是固定的,不由 LCL更改。右边没有锚定,因此它跟随左边。这意味着,控件的长度也是保存不变的
  • 有akLeft,有akRight: 控件的左边是固定的。不由LCL更改。右边被锚定到父类控件的右边。这意味着,如果父类控件被放大 100pixel ,那么控件的宽度也被放大 100pixel 。
  • 有akRight,没有akLeft:控件的左边不是固定的,因此它将跟随它的右边。右边是 锚定的。这意味着,如果父类控件被放大 100pixel ,那么控件也将被向右移动 100pixel 。
  • 没有akLeft.没有akRight:两边都没有锚定。控件的中心的位置与父类控件一起缩 放。例如,如果控件在父类控件的中间,并且父类控件被放大 100 pixel ,那么控件将被向右移动 50pixel 。

更改一个锚定的控件的父类控件的大小

当更改一个父类控件的大小时,除非自动大小是禁用的,否则所有锚定的子控件也将被连立即移动和/或重新调整大小。 例如,在一个窗体的加载期间和在一个窗体的创建期间,自动大小是被禁用的。

一个组框带有一个按钮: Anchors1.png

当组框被放大时,被锚定边的距离将保持不变:

使用akLeft,不使用akRight: Anchors akLeft.png

使用akLeft和akRight: Anchors akLeft akRight.png

使用akRight,不使用akLeft: Anchors akRight no akLeft.png

三个按钮在一个组框中: Anchors no akLeft no akRight small.png

不使用akLeft和akRight,它们的中心被缩放: Anchors no akLeft no akRight big.png


注释:

  • 加载一个窗体像设置一大组边界。在加载期间,使用lfm的值设置属性。记住,因为原型和控件,这里可能有多 个lfm文件。在加载LCL启用自动大小后。锚定的控件使用在加载结束时边界。在中间的一些 步骤被忽略。
  • 对于自定义控件,通常最好设置锚定侧而不是仅锚定。

更改一个锚定控件的大小

当更改一个锚定控件的宽度时,例如,通过对象查看器或通过代码 Button1.Width:=3, 你可以看到锚定到父类控件 和锚定到空值的不同点。锚定到父类控件将重新调整大小,并将移动Button1,然而锚定到空值将仅重新调整大小。例如:

锚定到空值

Anchored right nil resize1.png 一个Button1锚定[akTop,akRight], AnchorSide[akRight].Control=nil

Anchored right nil resize2.png 设置宽度到一个较小的值将收缩按钮,保持按钮的左边, 距离,减少按钮右边的距离。

Anchored right nil resize3.png 当组框被重新设置大小时,按钮也将保持新的距离。

img12:设置宽度相当于调用SetBounds(SetBounds(左侧,顶部,新宽度,高度)。这就是为什么左侧距离被保持。这是与Delphi兼容的。

锚定到父类控件

Anchored right parent resize1.png 一个Button1锚定[akTop,akRight],AnchorSide[akRight].Control=Button1.Parent

Anchored right parent resize2.png 设置宽度到一个较小的值将收缩按钮,保持按钮的右边距离,减少按钮左边的距离。

锚定到同级控件

你可以锚定到相邻的控件,下面的示例显示:

  • 你可以锚定一个labe的左侧到一个按钮的左侧
  • 锚定一个 label 的顶部到一个按钮的底部
  • 锚定一个 label 的中心到一个按钮的中心


Anchorsides example1.png

Anchorside example2.png

更多详细信息和如何设置锚,查看: 锚定边

Anchoreditor2.png

边界空格

边界空格(BorderSpacing)属性控制一个控件周围的最小空格量。属性是:

  • Around(四周):这个量以像素为单位被添加到 左侧,顶部,右侧,底部。
  • Left(左侧):空格以像素为单位在控件的左侧
  • Top(顶部):空格以像素为单位在控件的顶部
  • Right(右侧):空格以像素为单位在控件的右侧l
  • Bottom(底部):空格以像素为单位在控件的底部
  • InnerBorder(内部边界): 这个量以像素为单位被两次添加到首先的宽度和高度。一些控件重写(override)计算并忽略这个属性。 TButton在这里就是一个工作的示例。使用InnerBorder,你可能使一个按钮比需要的大。
  • CellAlignHorizontal(单元格水平对齐):这被使用在表格中,像ChildSizing.Layout=cclLeftToRightThenTopToBottom。如果控件小于表格单元格,这 个属性定义控件对齐的位置:到左侧的ccaLeftTop,到右侧的ccaRightBottom,或子中间的 ccaCenter。
  • CellAlignVertical(单元格垂直对齐):类似于CellAlignHorizontal,但是由于垂直对齐。

边界空格规则

  • Around被添加到左侧,顶部,右侧,底部边界空格
  • 如果控件有不允许扩展的限制,则空格可能更大。

锚定到另一侧

例如,A的右侧到B的左侧。

A和B两者的边界空格都使用。

  • 在两控件(在父类控件上的LeftControl, RightControl)之间的最大水平空格是
    • LeftControl.BorderSpacing.Right + LeftControl.BorderSpacing.Around
    • RightControl.BorderSpacing.Left + RightControl.BorderSpacing.Around
    • Parent.ChildSizing.HorizontalSpacing
  • 垂直空格工作类似:在两控件(在父类控件上的TopControl, BottomControl)之间的最大空格是
    • TopControl.BorderSpacing.Bottom + TopControl.BorderSpacing.Around
    • BottomControl.BorderSpacing.Top + BottomControl.BorderSpacing.Around
    • Parent.ChildSizing.VerticalSpacing

例如,如果LeftControl.BorderSpacing.Right = 3 和 LeftControl.BorderSpacing.Around = 4,那么在两控件之间至少有7 pixel空格。如果RightControl.BorderSpacing.Left = 4 和 RightControl.BorderSpacing.Around = 4 ,那么空格将至少8。如果Parent.ChildSizing.HorizontalSpacing = 10,那么空格将至少是10。

锚定到同一侧

例如,A的右侧到B的右侧。

  • 仅使用A的边界空格。
  • Parent.ChildSizing.Horizontal/VerticalSpacing未被使用。

锚定到中心

例如,A的中心到B的中心。

未使用边界空格,未使用Parent.ChildSizing.Horizontal/VerticalSpacing 。

示例

一个常见示例是锚定一个Label到一个Edit。

BorderSpacing Anchors.png

Label中心被垂直锚定到Edit。Edit的左边被锚定到Label的右边。两者都有 BorderSpacing.Around=6。这在Lable和Edit中间产生6个像素空格,Label的左侧 6个像素空格,Edit的右侧6个像素空格。也在Edit的上面和下面有6个像素空格。

  • 在一个控件和它的父类控件(Label1在GroupBox1上)的左侧空格最大值
    • Label1.BorderSpacing.Left + Label1.BorderSpacing.Around
    • GroupBox1.ChildSizing.LeftTopSpacing
  • 在一个控件和它的父类控件(Edit1在GroupBox1上)的右侧空格最大值
    • Edit1.BorderSpacing.Right + Edit1.BorderSpacing.Around
    • GroupBox1.ChildSizing.RightBottomSpacing
  • 当一个控件的中心被锚定到另一个控件时,例如,上述的Label于Edit1垂直居中,那么所有空格都被忽略。
  • 当一个控件的左侧被锚定到另一个控件的左侧时(例如,它们使用左侧对齐),那么两者左侧之间的距离是
    • Control1.BorderSpacing.Left + Control1.BorderSpacing.Around


这里有一个更复杂的示例:

Borderspacing anchors2.png

lfm代码的重要部分:

  object GroupBox1: TGroupBox
    AutoSize = True
    Caption = 'GroupBox1'
    TabOrder = 0
    object Label1: TLabel
      AnchorSideLeft.Control = GroupBox1
      AnchorSideTop.Control = Edit1
      AnchorSideTop.Side = asrCenter
      BorderSpacing.Around = 6
      Caption = 'Label1'
    end
    object Edit1: TEdit
      AnchorSideLeft.Control = Label1
      AnchorSideLeft.Side = asrBottom
      AnchorSideTop.Control = GroupBox1
      AnchorSideRight.Control = GroupBox1
      AnchorSideRight.Side = asrBottom
      Anchors = [akTop, akLeft, akRight]
      BorderSpacing.Around = 6
      TabOrder = 0
      Text = 'Edit1'
    end
    object Label2: TLabel
      AnchorSideLeft.Control = GroupBox1
      AnchorSideTop.Control = Edit1
      AnchorSideTop.Side = asrBottom
      BorderSpacing.Around = 6
      Caption = 'Label2'
    end
    object ComboBox1: TComboBox
      AnchorSideLeft.Control = Label2
      AnchorSideTop.Control = Label2
      AnchorSideTop.Side = asrBottom
      AnchorSideRight.Control = GroupBox1
      AnchorSideRight.Side = asrBottom
      Anchors = [akTop, akLeft, akRight]
      BorderSpacing.Right = 6
      TabOrder = 1
      Text = 'ComboBox1'
    end
    object CheckBox1: TCheckBox
      AnchorSideLeft.Control = GroupBox1
      AnchorSideTop.Control = ComboBox1
      AnchorSideTop.Side = asrBottom
      BorderSpacing.Around = 6
      Caption = 'CheckBox1'
      TabOrder = 2
    end
    object Label3: TLabel
      AnchorSideLeft.Control = GroupBox1
      AnchorSideTop.Control = Edit2
      AnchorSideTop.Side = asrCenter
      BorderSpacing.Around = 6
      Caption = 'Label3'
    end
    object Edit2: TEdit
      AnchorSideLeft.Control = Label3
      AnchorSideLeft.Side = asrBottom
      AnchorSideTop.Control = CheckBox1
      AnchorSideTop.Side = asrBottom
      BorderSpacing.Around = 6
      TabOrder = 3
      Text = 'Edit2'
    end
  end

边界空格和对齐

边界空格与对齐一起使用。在下面的示例中,这里有一个Memo1带有Align=alTop,和一个Memo2带有Align=alCLient。

  • 正常情况下,两个Memo将填充整个组框。
  • 但是Memo1有BorderSpacing.Around=10, 所以在Memo1四周有10 pixel空间。
  • Memo2有BorderSpacing.Top=20。在Memo1和Memo2之间的空间是最大的,是来自Memo2的2pixel空间。
  • Memo2也有BorderSpacing.Right=50,所以在Memo2右侧有50pixel空间。
  • 组框可以通过ChildSizing.LeftRightSpacing/VerticalSpacing /HorizontalSpacing为其所有的子控件定义默认空间。在这个示例中,它没有定义(全部是0)。

Borderspacing align1.png

对齐

Align属性的工作与Delphi非常相似,并且可用于快速填充一块区域。例如,来使一个TListBox填充 它的父类控件的整个区域,设置ListBox1.Align=alClient。如果有可能的话,align值 alTop, alBottom, alLeft 和 alRight 将不部分重叠地放置控件。这意味着,如果有足够的空间,所有的控件都将以alLeft,alTop,alBottom,alRight对齐而不会重叠。

运算法则的工作如下:

  • 首先,所有使用alTop的控件被放到客户端区域的顶部。如果这里有使用alTop的控件,最后添加/移除 的的控件将被放到最上面。运算法则将尝试避免重叠和保持控件的高度,直到扩展宽度到最大值。忽略左侧,顶部和右 侧的锚定到侧。底部锚定到侧工作正常。Borderspacing和Parent.ChildSizing空间 被考虑,因此你可以定义每个对齐控件周围的空间。
  • 然后,所有使用slBottom的控件被放置到客户端区域的底部。除此以外,它的工作方式类似于alTop。
  • 然后,所有使用alLeft的控件被放置到在alTop和alBottom控件之间的客户端区域的左侧。除此以外,它的工作方式类似于alTop。
  • 然后,所有使用alRight的控件被放置到在alTop和alBottom控件之间的客户端区域的右侧。除此以外,它的工作方式类似于alTop。
  • 如果这里有一个使用alClient的控件,它将填充剩余的客户端。
  • 如果这里有多个使用alClient的控件,它们将被放置到相同的位置,相互重叠。使用 Visibility属性来定义显示哪一个。

Autosize align.png

对齐和边界空格

边界距离和父级控件的子控件大小的空间被应用于对齐控件。下面的memo有Align=alClient。

Autosize align borderspacing.png

对齐和锚

一个对齐控件的自由侧(例如,Align=alLeft的右侧)遵循锚定规则。如果未设置锚定,那么控件将保持它的宽度。如果设置锚定,那么宽度将更改。

对齐和自动大小(AutoSize)

一个使用alLeft或alRight对齐的控件垂直扩展,将使用它的设计宽度。如果AutoSize被 设置为true,那么Width将是首先宽度。下面的按钮有Align=alRightAutoSize=true

Autosize align autosize.png

对齐和父级控件自动大小(AutoSize)

一个使用alClient对齐的控件将填充剩余的空间。一个使用AutoSize=true 的父类控件将扩展或收缩到精确的围住它的子控件。如果你组合两种属性将会发生什么?假设你放置一个使用Align=alClient的按钮到一个 AutoSize=true的组框中?

Autosize nested1.png

LCL使 用按钮的首先大小,相应地扩展或收缩组框:

Autosize nested2.png

alCustom(自定 义)

这个对齐值存在于自定义自动大小(AutoSize)算法中,并且由LCL处理,差不多很像alNone。使用alCustom的控件不被LCL移动,而是由你的自定义控件移动。你必需重写(override) CalculatePreferredSize和DoAutoSize。

使用相同对齐方式控件的顺序

使用相同对齐方式控件按下面顺序放置。关于alLeft,使用最低的Left的控件获取,对于alTop,最低的 Top,对于alRight,最大的Left+Width,对于alBottom,最大的Top+Height。如 果两个控件有相同的坐标,最后移动的一个(调用SetBounds或SetParent)取胜。控件可以重写 (override ) CreateControlAlignList来更改顺序,或者重写(override) DoAlignChildControls来处理整个对齐方式。

注意:在0.9.29之前,在同一坐标下的控件按Z顺序排列。在一些情况下,这导致了重新排序和不得不修复。这里 有一个不兼容的地方:如果你添加一些使用alTop或alLeft的控件,而没有具体指明限制范围,控件将被放置在 0,0处,因此最后添加的控件取胜,在这里前面的第一个控件取胜。

Delphi用户注意: Delphi用户注意:与VCL相反,LCL可以并且经常延迟重新计算布局。Delphi在每次属性更改后都会进 行布局。在VCL中设置Align=alTop调用具体指明的AlignControls,在其中控件有一个具体指 明的意义。这就是为什么在VCL中设置属性的顺序很重要的原因。LCL布局算法与设置属性的时间顺序无关。

例如,在FormCreate中的所有更改都会被延迟,并且布局是根据最终的属性值计算的。在另一方 面,ButtonClick没有延迟(也就是说,你必需自己添加Disable/EnableAutoSize), 因此插入alTop控件可能有不同的结果。

如果你想要一个具体指定的顺序,使用Disable/EnableAutoSize,并设置"Top"属性。例 如,调用TForm.OnCreate时禁用AutoSize,所以你可以使用:

procedure TForm1.FormCreate(Sender: TObject);
begin
  // making sure Panel2 is above Panel1, even though Panel1 is set first:
  Panel1.Align:=alTop;
  Panel1.Top:=1;
  Panel2.Align:=alTop;
  Panel2.Top:=0;
end;

布局

行,列和线

你可以使用ChildSizing属性来对齐行和列中的子控件,例如:

  • ChildSizing.Layout=cclLeftToRightThenTopToBottom
  • ChildSizing.ControlsPerLine=3
  • AutoSize=false(组框不能重新调整大小来适应子控件)

Autosize layout lefttoright.png

Layout(布局)属性默认是cclNone。如果你设置Layout为另一个值,每一个有正常锚定的控件都将被对齐。正常锚定意味着:

  • Anchors=[akLeft,akTop]
  • AnchorSide[akLeft].Control=nil
  • AnchorSide[akTop].Control=nil
  • Align=alNone

cclLeftToRightThenTopToBottom将在顶部,左侧放置第一个子项,在其右侧放置第二个子项,以此类推。这是一条线。属性ControlsPerLine定义何时开启一行新行。在上面的示例中,每一条线(行)有3个控件。这里有12个控件,所以这里有4条线(行),每 行有3个控件(列)。如果ControlsPerLine是0,意味着每行控件数量不受限制 - 这里将仅有带有全部子控件的一行。

你可以看到这些行有不同的大小,每一行都有最大控件的大小,并且这些被重新调整大小到列宽。在行之间没有空格。在 图像中的空格来自使用的主题,而不是来自LCL

固定行和列之间的空间

你可以在这些属性之间添加空格:

  • ChildSizing.VerticalSpacing - 行间距
  • ChildSizing.HorizontalSpacing - 列间距
  • ChildSizing.LeftRightSpacing - 所有列的左右间距
  • ChildSizing.TopBottomSpacing - 所有列的上下间距

上面的示例使用 ChildSizing.VerticalSpacing=6, ChildSizing.HorizontalSpacing=15, ChildSizing.LeftRightSpacing=30, ChildSizing.TopBottomSpacing=10, AutoSize=true

Autosize layout parentspacing.png

另外,你可以使用BorderSpacing属性为每个控件设置单独的空间。

放大

上面的示例重新调整组框的大小到需要的空间。如果你的组框有一个固定的大小,或者如果它不能自由地重新调整大小, 例如,如果组框应该填充整个窗体的宽度,那么子控件应该放大。这里有一些模式。默认模式ChildSizing.EnlargeHorizontal=crsAnchorAligning是不能放大任何东西的。在右边的空间将不被使用。

  • crsAnchorAligning - 不使用额外的空间
  • crsScaleChilds - 使用相同的因数成倍增加宽度/高度
  • crsHomogeneousChildResize - 向每个宽度/高度添加相同的量
  • crsHomogeneousSpaceResize - 在子控件之间每个空间中添加相同的量

crsScaleChilds(成倍增加)

ChildSizing.EnlargeHorizontal=crsScaleChilds, ChildSizing.EnlargeVertical=crsScaleChilds, AutoSize=false

Autosize layout scalechilds.png

例如,如果客户端宽度(ClientWidth)是所需宽度的两倍,那么每个子控件的宽度都将是所需宽度的两倍。

crsHomogeneousChildResize(单倍增加)

ChildSizing.EnlargeHorizontal=crsHomogeneousChildResize, ChildSizing.EnlargeVertical=crsHomogeneousChildResize, AutoSize=false

Autosize layout homogeneouschildresize.png

例如,如果客户端宽度(ClientWidth)比所需要宽度大30像素,那么每个子控件都将宽10像素。

crsHomogeneousSpaceResize(定量增加)

ChildSizing.EnlargeHorizontal=crsHomogeneousSpaceResize, ChildSizing.EnlargeVertical=crsHomogeneousSpaceResize, AutoSize=false

Autosize layout homogeneousspaceresize.png

例如,如果客户端宽度(ClientWidth)比所需要宽度大40 pixel,在左,右和每个子控件之间都将有10 pixel空间。

收缩

收缩工作类似于放大。如果没有足够的空间供控件使用,你可以设置不同的模式。ShrinkHorizontal(水平收缩), ShrinkVertical(垂直收缩)

单个单元格(Individual cells)

在上面的示例中,所有的控件都被同样地重新调整大小,每个控件都填充整个单元格(cell)单元格(cell)是在具体的行和列中的空间。通常一个控件会填充整个单元格空间。可以使用属性BorderSpacing.CellAlignHorizontalBorderSpacing.CellAlignVertical更改。

例如,设置Fifth按钮的BorderSpacing.CellAlignHorizontalcaCenter,你将得到这样:

Autosize layout cellhorizontal cacenter.png

CellAlignHorizontal/CellAlignVertical有四个可能的值:

  • caFill:子控件填充单元格的整个宽度(高度)
  • caCenter:子控件在单元格中使用它的首选宽度(高度)并将居中
  • caLeftTop:子控件在单元格中使用它的首选宽度(高度)并将左对齐
  • caRightBottom:子控件在单元格中使用它的首选宽度(高度)并将右对齐

Autosize layout cellalign.png

使用 OnResize / OnChangeBounds 自定义布局

有时,LCL布局是不够用的。下面的示例显示一个GroupBox1带有一个ListBox1和一个Memo1。ListBox1应该填充1/3的空间,Memo1占 据剩余的空间。

Autosize onresize.png

无论何时重新调整GroupBox1大小,ListBox1.Width都应该是1/3。为达到这个目的,设置 GroupBox1的OnResize事件为:

procedure TForm1.GroupBox1Resize(Sender: TObject);
begin
  ListBox1.Width := GroupBox1.ClientWidth div 3;
end;

常见错误:OnResize事件错误

不要将你所有的调整大小代码放入到窗体的OnResize事件中。窗体OnResize事件仅当窗 体被重新调整大小时被调用。子控件(例如,GroupBox1)将会稍后重新调整大小,因此GroupBox1.ClientWidth在FormResize事件期间仍然有旧值。你必需使用GroupBox1.OnResize事件来响应GroupBox1.ClientWidth的更改。

常见错误:Width(宽度)代替ClientWidth(客户端宽度), AdjustClientRect(调整客户端矩形)

Width(宽度)是包含框架在内的尺寸大小。ClientWidth是不包含框架的内部宽度。有一些框架(像 TPanel)会绘制一个附加的框架。那么你必需使用AdjustClientRect。相同的示例,但是在一个 Panel1中是一个ListBox1,而不是一个GroupBox1:

procedure TForm1.Panel1Resize(Sender: TObject);
var 
  r: TRect;
begin
  r := Panel1.ClientRect;
  Panel1.AdjustClientRect(r);
  ListBox1.Width := (r.Right - r.Left) div 3;
end;

自定义控件

当你写你自己的控件时,你可以重写(override)和微调LCL自动大小的很多部分。

SetBounds, ChangeBounds, DoSetBounds

当属性 Left, Top, Width, Height, BoundsRect被设置时,或者用户直接调用时,SetBounds将被调用。 SetBounds更新BaseBounds和BaseParentClientSize,这就用于锚定以保持距离。例如,加载一个带有TMemo的窗 体,并且lfm包含TMemo的Left和Width,接下来TMemo将会调用两次SetBounds。当用户最 大化一个窗口时,SetBounds是被窗体调用的,而不是被TMemo调用的,以保持TMemo的 BaseBounds。如果TMemo被锚定到右侧,那么TMemo的宽度基于BaseBounds和 BaseParentClientSize更改。记住,给定的aLeft, aTop, aWidth, aHeight可能是无效的,并且在应用前通过LCL更改。Delphi更经常的调用SetBounds。SetBounds使用KeepBase=false调用ChangeBounds。

每当通过属性或LCL的布局器设置控件的位置或大小时,都会调用ChangeBounds。SetBounds 使用KeepBase=false内部调用ChangeBounds , 而LCL布局器使用KeepBase=true来调用它。重新这些代码可能会更改首先大小或重新调整其它控件的大小。记住,给定的aLeft, aTop, aWidth, aHeight可能是无效的,并在应用前通过LCL更 改。你可以调用这个函数。

DoSetBounds是一个低级函数,用以设置私有变量FLeft, FTop, FWidth, FHeight。不要调用这个函数,仅LCL调用它。它也相应地更新FClientWidth和FClientHeight。重写这个函数以更新控件的内容布局, 例如滚动条。一如往常:在这里不绘制,但会调用Invalidate并在OnPaint或重新绘制(override Paint)中绘制。

DoAdjustClientRectChange通过LCLLCL调用,当ClientRect更改时,宽度和高度保持不变。 WMSize是为实现Delphi/VCL的兼容性而存在的。通过LCL interface调用,并在每次边界更改时更改。

AdjustClientRect(调整客户端矩形)

方法 AdjustClientRect 可以被你自定义的控件所重写(override),并且影响 Align, ChildSizing.Layout 和AnchorSides。它不影响Left, Top的意思,也不影响正常的锚定(例如设置)。

当你想绘制你自己的框架时,那么子控件应该在这些框架中对齐。例如,TPanel绘制一个框架,并通过重写方法AdjustClientRect来减少客户端区域:

  TCustomPanel = class(TCustomControl)
  ...
  protected
    ...
    procedure AdjustClientRect(var aRect: TRect); override;
  ...

procedure TCustomPanel.AdjustClientRect(var aRect: TRect);
var
  BevelSize: Integer;
begin
  inherited AdjustClientRect(aRect);

  BevelSize := BorderWidth;
  if (BevelOuter <> bvNone) then
    inc(BevelSize, BevelWidth);
  if (BevelInner <> bvNone) then
    inc(BevelSize, BevelWidth);

  InflateRect(aRect, -BevelSize, -BevelSize);
end;

AdjustClientRect(调整客户端矩形)和Align(对齐)

AdjustClientRect可以用于减少所有自动调整大小操作所使用的客户端区域。例如TPanel使用 AdjustClientRect来减少客户端区域的borderwidth(边界宽度):

Adjustclientrect align.png

在截图中的Button1是使用Align=alClient创建的。ChildSizing.Layout也会受到影响。

当通过AnchorSides(锚定侧)锚定到一个父类对象时,AdjustClientRect也会被使用:

  Button1.AnchorParallel(akTop,0,Button1.Parent);

Button1的顶部被锚定到父类客户端区域的顶部。如果AdjustClientRect添加3到顶部,那么Button1.Top将会是3 (3加上0)。

Own AutoSize

AutoSize被设置为true时,控件应该被重新调整为首选的大小,如果有可能的话。

首选大小

新的大小经由LCL通过'GetPreferredSize获取,它调用CalculatePreferredSize, 它可以被重写(overridden)。例如,让我们写一个TQuadrat, 它是一个TShape,但是它的高度应该等于它的宽度:

  TQuadrat = class(TShape)
  protected
    procedure CalculatePreferredSize(var PreferredWidth,
        PreferredHeight: integer; WithThemeSpace: Boolean); override;
  end;
...
procedure TQuadrat.CalculatePreferredSize(var PreferredWidth,
  PreferredHeight: integer; WithThemeSpace: Boolean);
begin
  PreferredHeight:=Width;
end;

方法CalculatePreferredSize获取两个变量参数:PreferredWidthPreferredHeight 。它们默认为0,这意味着:这里没有首选大小,因此LCL将 不会更改大小。上面的函数设置PreferredHeight为当前的Width。布尔参数WithThemeSpace已经被弃用,并且始终是false。

重要: CalculatePreferredSize禁止更改一个控件的边界或任意其它值,因为 这样做将会创建一个循环。

计算PreferredWidth(首选宽度)/Height(高度)可能会代价很高。因此LCL会缓存结果,直到控件调用 InvalidatePreferredSize 。在我们的示例中,PreferredHeight(首选高度)依赖于Width,因此当Width更改时,我们必需使其无效:

  TQuadrat = class(TShape)
  protected
    ...
    procedure DoSetBounds(ALeft, ATop, AWidth, AHeight: integer); override;
  end;
...
procedure TQuadrat.DoSetBounds(ALeft, ATop, AWidth, AHeight: integer);
begin
  inherited DoSetBounds(ALeft, ATop, AWidth, AHeight);
  InvalidatePreferredSize;
  // Note: The AdjustSize can be omitted here, because the [[LCL]] does that after calling DoSetBounds.
end;

当边界更改时,LCL将自动地触发自动调整大小,因此示例是完整。

CalculatePreferredSize的默认T实施询问widget集,可能会返回 一个首选的宽度和/或高度。每一个控件都可以重写 (override)CalculatePreferredSize方法。例如,TImage将重写(override)它,并将返回图像的大小。如果没有可用的首选宽度(高度),返回值是0,LCL将保持当前是宽度(高度)。如果0是你的控件的有效大小,你必须设置ControlStyle标志 csAutoSize0x0 (ControlStyle:=ControlStyle+[csAutoSize0x0];)。LCL 控件面板是一个示例。

AdjustSize(调整大小)

当首选大小依赖于一个新的属性时,那么每次属性更改时,自动大小都必须被触发。例如:

procedure TQuadrat.SetSubTitle(const AValue: string);
begin
  if FSubTitle = AValue then exit;
  FSubTitle := AValue;
  InvalidatePreferredSize;
  AdjustSize;
end;

使用禁用自动大小(DisableAutoSizing),启用自动大小(EnableAutoSizing)来减少资源管理开销(overhead)

自从Lazarus 0.9.29 以来,lazarus有了一个新的自动调整大小的算法,它减少了资源管理开销并运行深度嵌套依赖关系。一直到Lazarus 0.9.28,DisableAlign/EnableAlign和Disable /EnableAutoSizing仅对一个控件和它的直系子控件有效。

你每次更改一个属性时,LCL都会触发一次布局的重新计算:

Label1.Caption := 'A';  // first recompute
Label2.Caption := 'B';  // second recompute

重新计算将会触发事件和发生信息。为减少资源管理开销,你可以暂停(suspend)自动大小:

DisableAutoSizing;
try
  Label1.Caption := 'A';  // no recompute
  Label2.Caption := 'B';  // no recompute
finally
  EnableAutoSizing; // one recompute
end;

Y你必需平衡调用禁用/启用自动大小(Disable/EnableAutoSizing)。仅在一次相关的 (早期) 禁用自动大小(DisableAutoSizing)后调用启用自动大小(EnableAutoSizing) 时,才会启动启用自动大小(EnableAutoSizing)

自0.9.29以来,禁用/启用自动大小Disable/EnableAutoSizing)适用于整个窗体。这意味着每一次调用DisableAutoSizing都会终止这个窗体上的所有控件自动调整大小。如果你编写你自己的控件,你现在可以使用下面的:

procedure TMyRadioGroup.DoSomething;
begin
  DisableAutoSizing;  // disables not only TMyRadioGroup, but the whole form's autosizing
  try
    // delete items ...
    // add, reorder items ...
    // change item captions ...
  finally
    EnableAutoSizing; // recompute
  end;
end;

这基本上意味着:你不必在意。只需要调用Disable/EnableAutoSizing.

注意:这是错误的:

Button1.DisableAutoSizing;
Label1.EnableAutoSizing; // wrong: every control has its own autosizing reference counter

禁用自动大小(DisableAutoSizing)和窗体边界

例如在Linux系统中,在异步窗口管理器下,DisableAutoSizing有另外一个有用的效果。每次重新调整窗体大小或者移动边界都会发送非widget集(DoSendBoundsToInterface)。即使窗体未显示,处理程序也会重新调整大小。窗口管理器常常仅将这些边界作为建议处理。窗口管理器有它自己的逻辑,并且通常仅使用发送的第一步的边界。第二步、第三步或更进一步的移动将会被忽略。使用DisableAutoSizing,你可以确保将最终边界发送到widget集,从而使窗体边界更可靠。

示例:一个按钮面板

这个示例结合一些LCL布局机制来创建一个包含三个按钮的面板:一个Help按钮到左侧,OkCancel按钮到右侧。我们希望面板在窗体的底部填充整个宽度。按钮和面板都会自动调整大小来适合使用的字体和主题。

步骤 1: 创建面板并设置它的Align属性为alBottom。添加三个TBitBtns。

Autosize example buttonpanel1.png

设置BitBtns的Kind属性来显示符号(glyphs)。你可能需要设置GlyphShowMode为gsmAlways来在你的平台上查看它们。设置BitBtn的AutoSize属性为True,这将收缩/放大按钮以完美地适合符号(glyphs)和文本。取决于你的主题和平台,你可能会注意到按钮有不同的高度。

Autosize example buttonpanel2.png

设置help按钮的Align属性为alLeft,并设置其它两个按钮的Align属性为alRight 。这将垂直放大按钮并将移动它们到最左侧/右侧。alLeft/alRight对宽度没有影响,因此按钮使用它们的首选宽度。

Autosize example buttonpanel3.png

面板高度仍然是不变的。现在设置面板的AutoSize属性为True。面板现在垂直收缩来适合最高的按钮。

Autosize example buttonpanel4.png

Ok按钮有一个简短的标题(caption),因此按钮是非常短的。设置按钮的Constraints.MinWidth为75。按钮现在将稍微放大一些。

Autosize example buttonpanel5.png

现在在按钮的周围添加一些空格。设置面板的ChildSizing.LeftTopSpacing/RightBottomSpacing/HorizontalSpacing6.

Autosize example buttonpanel6.png

最后清除面板的标题(Caption),并设置它的BevelOuterbvNone

Autosize example buttonpanel7.png

滚动(Scrolling)

一些LCL控件(像TScrollBox, TForm,和TFrame)显示滚动条,如果其子控件太大,而无法适应滚动框,窗体或框架的话。它们从其祖先TScrollingWinControl继承这种行为。

一个滚动控件的逻辑客户区域(logical client area)可以大于可见客户区域(visible client area)可见客户区域(visible client area)ClientRect它总是从0,0开始,并且它的宽度和高度是内部区域。例如,在一个TGroupBox中,在框架内部中大小。因此下面总是true:

ClientWidth <= Width
ClientHeight <= Height

逻辑客户区域(logical client area)是通过方法GetLogicalClientRect.定义的。默认情况下,它与ClientRect相同。当一个子控件锚定到右侧时,它使用逻辑客户区域(logical client area)TScrollingWinControl 重写这个方法并返回滚动条的范围(Range),如果它们大于ClientRect的话。 范围(Range)可以使用AutoScroll=true来手动设置或自动设置。一个关于AutoScroll=true的示例:

Autoscroll all fit1.png

上面的按钮有一个固定是宽度200。下面的按钮被锚定到面板的右侧。因此,子控件有一个首先的宽度200。因为面 板是大于200的,逻辑客户区域(logical client area)也是更大的,因此下面的按钮扩展。

现在面板是收缩的,因此ClientWidth变得小于200

Autoscroll not fit1.png

首先的宽度仍然是200,因此逻辑客户区域(logical client area)现在是200,并大于可见客户区域(visible client area)。下面的按钮现在有一个200的宽度,面板显示一个水平滚动条。

滚动(Scroll)位置

更改一个滚动条的位置并不会方法任何子控件的左侧或顶部,也不会更改逻辑客户区域(logical client area),也不会影响自动大小。子控件仅会模拟移动。

滚动(Scrolling)和自动大小(AutoSize)

当AutoSize=true时,LCL将扩展控件以容纳其所有的子控件,并且不需要滚动条。如果控件不能被扩展,那么(仅)执行 AutoSize的第二个动作:移动子控件。

停靠(Docking)

停靠(Docking)使用这页中描述的方法和属性,查看 停靠.

分隔符

查看 分隔符.

TLabel.WordWrap

TLabel.WordWrap更改标签(label)的首选大小的行为。WordWrap=true 要求标签(label)的宽度是固定的,例如,通过锚定标签(label)的左、右侧的大小。标签(label)的 首选高度将通过打断标题(Caption)成多行的方式来重新计算。

DPI自动调整和绝对布局的自动调整

在历史上,LCL主要用于设计绝对布局,尽管Lazarus为灵活布局提供大量的选项,像:对齐(Align),锚定(Anchors),等等,像在这篇文章的其它部分所 描述的那样。除此之外,在历史上,它忽略了目标的DPI,而使用像素值来测量控件的左侧(left),顶部(top),宽度(width)和高度(height)属性。对于桌面平台和Windows CE来说,这已经工作地相当不错了,但是随着LCL支持Android的到来,这已经不能被忽视。从Lazarus 0.9.31开始,LCL可以将LCL的绝对布局以像素的形式重新解释为一个灵活的网格。这里有多种模式可供 选择,并且它们将允许重新解释像素值来作为绝对值,或者作为DPI调整,或者只是作为窗体尺寸的一部分考虑。

值得注意的是,这些新的规则仅影响没有对齐(Align)位置并且仅使用最标准锚定(Anchor)的控件.

在这种情况下,这里的值仅针对DPI调整,这里有一个新的属 性:TCustomForm.DesignTimeDPI,这将存储被设计窗体系统的DPI值。当目标的DPI大于 设计时的DPI,定位的值将被扩大,否则将会被减少。桌面DPI的常见值是96,并且这是给定的默认值。

属性DesignTimeDPI:整数读取FDesignTimeDPI然后写入FDesignTimeDPI;

这种布局方法使用属性TApplication.LayoutAdjustmentPolicy进行控制调整

  TLayoutAdjustmentPolicy = (
    lapDefault,     // widget set dependent
    lapFixedLayout, // A fixed absolute layout in all platforms
    lapAutoAdjustWithoutHorizontalScrolling, // Smartphone platforms use this one,
                                             // the x axis is stretched to fill the screen and
                                             // the y is scaled to fit the DPI
    lapAutoAdjustForDPI // For desktops using High DPI, scale x and y to fit the DPI
  );

并且,在下面的TControl的新方法允许强制进行一个特定控件和其所有子控件的一次布局自动调整,如何控制TControl特定衍生物,对此这样回应:

TControl = class
public
...
    procedure AutoAdjustLayout(AMode: TLayoutAdjustmentPolicy;
      const AFromDPI, AToDPI, AOldFormWidth, ANewFormWidth: Integer); virtual;
    function ShouldAutoAdjustLeftAndTop: Boolean; virtual;
    function ShouldAutoAdjustWidthAndHeight: Boolean; virtual;

例如,当屏幕旋转时,LCL-CustomDrawn-Android将调用AutoAdjustLayout 。

更多细节

很多控件重写(override) TControl.DoAutoSize执行实际地自动大小。

重要: 很多Delphi控件重写(override)这个方法,很多控件在设置一些属性后会直接调用这个方法。

在处理程序(handle)创建过程中,并非所有的接口都可以创建完整的设备上下文,这些上下文需要计算一些像文 本大小的事情。

这就是为什么你应该总是调用AdjustSize而不是DoAutoSize的原因。

TControl.AdjustSize以一种智能流行的方式调用DoAutoSize

在加载和处理程序(handle)创建过程中,会延迟调用。

这种方法的最初的作用与TWinControl.DoAutoSize相同。但是,由于DoAutoSize 通常被衍生组件所重写,它并不是对执行所有的测试都有用,它会导致太多的资源开销。为减少这种情况,LCL调用AdjustSize来作为替换品。

当设置AutoSize = true时,LCL 在宽度和高度上自动大小控件。这是LCL最复杂的一部分,因为最终的结果取决于近一百个属性。让我们简单地开始:

LCL仅能自动调整宽度(Width) (高度(Height)),如果它可以自由重新调整大小的话。换句话说-宽度不会自动调整大小,如果:

  • 左侧和右侧是被锚定的。你可以使用Anchors属性锚定一侧,或者通过设置Align属性为alTop,alBottom或alClient。
  • 宽度(Width)Constraints(约束式)属性所约束。约束式也可以通过widget集重写。例如,winapi并不允许重新调整一个组合框的高度。并且gtk widget集也不允许重新调整一个垂直滚动条的宽度。

高度(Height)宽度(Width)相同。

新的大小是通过受保护(protected)的方法TControl.CalculatePreferredSize所计算得出来的。这个方法会要求widget集的一个适当的宽度和高度。例如,一个TButton有首先的宽度和高 度。一个TComboBox仅有一个首选的高度。首选宽度被作为0返回,因此LCL并不会自动大小宽度- 它保持宽度不变。一个TMemo没有首先的宽度或高度。因此,自动大小对一个TMemo没有影响。

一些控件重写(override)这个方法。例如,TGraphicControlyan衍生物,像TLabel没有窗口处理程序,因此不能询问其widget。它们必需计算它们自己的首选宽度和高度。

widget集必需为每个具有首选大小(宽度或高度或两者都有)的widget类重写(override)方法GetPreferredSize。

Parent.AutoSize(父类.自动大小)

上面描述了简单的解释。真正的算法提供了很多的可能性因而变得更复杂。

Properties / Methods(属性/方法)

  • Left
  • Top

如果Parent<>nil,那么Left, Top是到父类客户端区域(非滚动的)的左侧像素,右侧像素的像素距离。记住客户端区域总是不包含父类的框 架和滚动条的。对于 Delphi用户来说:一些VCL控件,像TGroupbox,定义客户端区域为整个控件,包含框架,而另外一些控件则不是这样-LCL相比Delphi来说更加一致,因此其与Delphi不兼容。Left和Top可以是负数或者是大于客户端区域的数。一些widget集会定义一个最小值/最大值 ,在一些地方约有10.000或更多。

当客户端区域是滚动的时,Left和Top将保持不变。

在重新调整大小/移动期间,Left和Top并不总是与处理程序对的坐标同步。

如果Parent=nil ,那么Left, Top取决于widget集和窗口管理器。直到Lazarus 0.9.25 ,这通常是窗体的客户端区域的left,top的屏幕坐标。这是与Delphi不兼容的。计划将其更改为窗口的Left, Top 。


提示: 你每次更改Left和Top时,LCL会立即移动并重新计算整个布局。如果你想更改Left和Top,请改用:

with Button1 do
  SetBounds(NewLeft, NewTop, Width, Height);
  • Width
  • Height

以像素形式的大小禁止为负数,并且大多数widget集不允许Width=0和/或Height=0 。在一些平台上的一些控件以Constraints.MinInterfaceWidth/Height的形式定义 一个较大的最小值约束。而不是重新大小一个控件为Width=0和/或Height=0 ,设置Visible=false或Parent=nil。在重新调整大小/移动期间,Width和Height并不总是与处理程序对的坐标同步。


  • BoundsRect

等同于Bounds(Left, Top, Width, Height).

新手常见错误:

BoundsRect.Left := 3; // WRONG: common newbie mistake

这没有效果, 因为读取BoundsRect的是一个函数。它在堆上创建一个临时的TRect 。上述等同于:

var
  r: TRect;
begin
  r := BoundsRect; // fetch the bounds
  r.Left := 3;  // change a value on the stack
end;  // no change
  • ClientRect

Left和Top总是0,0 。宽度和高度是客户端区域的可见像素大小。记住客户端区域不包含框架和滚动条。在一个可滚动的客户端区域中,逻辑客户端区域可能大于可见的客户端区域。

  • ClientOrigin

返回客户端区域的左侧,顶部坐标0,0的屏幕坐标。注意,这个值是存储在接口(interface)中的位置。并不总是与LCL同步。当移动一个控件时,LCL设置边界到所需位置,并发送一条移动信息到接口(interface)。这取决于接口(interface)立即处理移动或排队处理移动。

  • LCLIntf.GetClientBounds

返回一个控件的客户端边界。像ClientRect,但是Left和Top是到控件左侧,顶部的像素距离。例如在 一个TGroupBox上,Left, Top是左侧和右侧框架边界的宽度和高度。滚动在GetClientBounds上没有影响。

  • LCLIntf.GetWindowRect

在调用后,ARect将是屏幕坐标中控件区域。这意味着,Left和Top将是处理程序对象的左侧和顶部像素的屏 幕坐标,Right和Bottom将是右侧和底部像素的屏幕坐标。

  • FBaseBoundsLock: integer

通过LockBaseBounds/UnlockBaseBounds来增加/减小。在SetBounds调用期 间,用于保持FBaseBounds。

  • FBaseParentClientSize: TPoint

Parent.ClientRect大小对FBaseBounds有效。FBaseBounds和 FBaseParentClientSize通常用于计算akRight (akBottom)的距离。当重新调整父类大小时,LCL知 道要保持什么距离。


  • FBoundsRectForNewParent: TRect

当更改一个控件的父类时,会重新创建处理重新并可能会发送很多事情。尤其是停靠窗体过程是太不可靠了。因此将会保 存BoundsRect。VCL使用一种类似的机制。


  • fLastAlignedBounds: TRect

查看TControl.SetAlignedBounds说明解释。简而言之:它停止在接口(interface)和LCL自动大小之间的一些循环。

  • FLastChangebounds: TRect

用于停止调用具有相同坐标的ChangeBounds。这种情况经常发生。

  • FLastDoChangeBounds: TRect

用于避免调用具有相同坐标的OnChangeBounds。这将减少用户定义的自动大小。


  • FLastResizeClientHeight: integer
  • FLastResizeClientWidth: integer
  • FLastResizeHeight: integer
  • FLastResizeWidth: integer

用于避免调用具有相同坐标的OnResize。这将减少用户定义的自动大小。

  • FLoadedClientSize: TPoint

在加载期间,很多东西被迟延,很多东西被设置。并且最糟糕的是:以一种错误的顺序。这就是为什么在再次加载时,存 储和设置SetClientWidth/SetClientHeight调用的原因。使用这种方法,LCL可以恢复设计期间使用的距离(例如,akRight)。

  • FReadBounds: TRect

类似于FLoadedClientSize,但是适用于 SetLeft,SetTop,SetWidth,SetHeight。

  • procedure SetBoundsRectForNewParent(const AValue: TRect);

用于设置FBoundsRectForNewParent。查看上文。

  • procedure SetAlignedBounds(aLeft, aTop, aWidth, aHeight: integer); virtual;

像SetBounds,但是不更改默认大小。

  • procedure SetInitialBounds(aLeft, aTop, aWidth, aHeight: integer); virtual;

一个SetBounds的智能版本, r在创建和加载过程中减少资源开销(overhead)。

  • procedure UpdateBaseBounds(StoreBounds, StoreParentClientSize, UseLoadedValues: boolean); virtual;

提交当前的边界为基础边界。

  • procedure SetClientHeight(Value: Integer);
  • procedure SetClientSize(Value: TPoint);
  • procedure SetClientWidth(Value: Integer);

也是用于Delphi兼容。重新调整控件的大小,来获取想要的ClientRect大小。

  • procedure ChangeBounds(ALeft, ATop, AWidth, AHeight: integer); virtual;

这是内部的SetBounds。应用于约束式,更新基础边界,调用OnChangeBound, OnResize,锁定边界。

  • procedure DoSetBounds(ALeft, ATop, AWidth, AHeight: integer); virtual;

这个实际上设置FLeft, FTop, FWidth, FHeight私有(private)变量。

  • procedure SetBounds(aLeft, aTop, aWidth, aHeight: integer); virtual;

这是很多Delphi控件的标准的过程(procedure)重写(overriden)方法。 TWinControl也会重写它。

    • 当边界被锁定时忽略调用
    • 锁定FBoundsRealized来避免在自动调整大小期间的资源开销。

ChangeBounds未锁定这种方法。


  • Function GetClientOrigin: TPoint; virtual;

客户端区域的Left, Top的屏幕左侧。

  • Function GetClientRect: TRect; virtual;

客户端区域的大小。(总是Left=0, Top=0)。

  • Function GetScrolledClientRect: TRect; virtual;

在ClientRect中的可见客户端区域。

  • function GetChildsRect(Scrolled: boolean): TRect; virtual;

返回相对于控件的左侧,顶部的客户端矩形。如果Scrolled是true,矩形将通过当前滚动值移动(查看示例 TScrollingWincontrol)。

  • function GetClientScrollOffset: TPoint; virtual;

返回客户端区域的滚动偏移量。

  • function GetControlOrigin: TPoint; virtual;

返回控件区域的左侧,顶部坐标0,0的屏幕坐标。(在屏幕上的控件的左侧,顶部像素)。注意,这个值是存储在接口 (interface)中的位置。并不总是与LCL同步。当移动一个控件时,LCL设置边界到所需位置,并发送一条移动信息到接口(interface)。这屈居于接口(interface)立即处理移动或排队处理移动。

常见问题解答

为什么自动大小(AutoSize)在设计器中不能正确的工作?

在设计器中,控件可以被拖来拖去,属性几乎可以按照任何顺序设置。为了接受自动大小这一点,并避免可能的冲突,在 设计时,自动大小并不会在每次更改时都会更新。

在一些东西更改时,为什么TForm.AutoSize不工作?

查看自动大小和窗体

当创建很多控件时,我需要调用Application.ProcessMessages吗?

在每一条信息后(例如,在每一次OnClick事件后),Application.ProcessMessages将被LCL自动调用。如果更改需要立即变得可看见的话,你需要自己调用它。例如:

procedure TFrom.Button1Click(Sender: TObject);
begin
  // change width of a control
  Button1.Width := Button1.Width + 10;
  // apply any needed changes and repaint the button
  Application.ProcessMessages;
  // do a lot of things that takes a long time
  ...
  // after leaving the OnClick the LCL automatically processes messages
end;

在启用锚定的情况下,在运行时,控件重新调整大小,而不是使用当前值。为什么?

akBottom意味着:与父类底部侧保持一定的距离。保持距离是通过基本边界定义。它们在设计时设置,或者在运 行时调用SetBounds或UpdateBaseBounds。

例如: 一个TListBox (Anchors=[akLeft,aTop])在设计时有一个100像素的底部距离。并且通过一个按钮来启用/禁用TListBox的akBottom。现在启动应用程序并按下按钮来启用akBottom,将激活100像素的距离,因为这是程序员最 后定义的TListBox的基本边界。所有其它的重新调整大小动作都将会由LCL完成。无关紧要的大小,程序员会基于边界规则。你可以重新调整窗体的大小,并保持100像素的距离。为了将当前边界作为基本边界使用:

ListBox1.UpdateBaseBounds(true, true, false);
ListBox1.Anchors := ListBox1.Anchors + [akBottom];

设置锚不会自动调用UpdateBaseBounds,因为这将会破坏独立更改属性的能力。

在窗体中重新调整字符串网格列大小的OnResize事件中不工作

当窗体的宽度( Width),高度(Height),客户端宽度(ClientWidth)或客户端高度(ClientHeight) 更改时,窗体的OnResize将会被触发。这本质上不依赖于TStringGrid。当然,它经常在调整窗体和 TStringGrid时发生。这意味着使用窗体的OnResize将经常会工作,但并不是总是这样。一个明显会失 败的示例,当主题更改时,在你的一个TForm中的一个TGroupBox中有一个TStringGrid。当主题 更改时,窗体大小保持不变,因此将不会触发Forms的OnResize。但是TGroupBox已更改,因此 TStringGrid也会重新调整大小。

解决方案:使用TStringGrid的OnResize。

为什么TForm.Width等同于TForm.ClientWidth?

Mattias的注释:

"这里有历史原因和技术原因。

对于没有父类的窗体,Clientwidth等于宽度,因为真实的宽度包含框架在10年前的Linux上是不可用 的(至少在各种窗口管理器上是不可靠的)。我没有测试过,但是我听说现在使用gtk2是有这种可能的。主要问题是自 动大小,因为在窗体映射到屏幕前需要框架的大小。它可能仅在一个事件后才可用,这意味着你必须等待它,这对 ShowModal来说就意味着麻烦。但是,更改这项变化会破坏到很多使用现有LCL代 码的兼容,为此,我们需要在lfm文件中添加LCLVersion。

在Lazarus trunk 1.7 中有一个新的编译器定义LCLRealFormBounds,它启用一个窗体的真实大小。为使用它,只需要使用LCLRealFormBounds ON来编译你的应用程序。到目前为止,仅支持win32的widget集。

对于所有其它的控件来说,规则是ClientWidth<=Width。Width是 ClientWidth加上widget框架值。问题是滚动条是否属于框架。我会说是,在不久前,它已经这样实施了。显然,这已经更改了。查看关于synedit的当前光标问题。"

我得到一个无效循环/如何调试自动调整大小?

这里有一些笔记,其它用户做错了什么,和如何发现:

与Delphi的不同

对于Delphi用户:请阅读:Lazarus_vs_Delphi

重写(Overriding)一个LCL方法,并触发一次重新计算

你重写(override)一个方法,并告诉LCL来重新计算,即使什么都没有更改。检查AdjustSize,Realign,AlignControls, InvalidatePreferredSize。例如:

procedure TMainForm.SetBounds(ALeft, ATop, AWidth, AHeight: integer);
begin
  // This will create an endless loop
  InvalidatePreferredSize;

  // This will create an endless loop too:
  OtherComponent.Left:=10;
  OtherComponent.Left:=20;

  inherited SetBounds(ALeft, ATop, AWidth, AHeight);
end;

说明:SetBounds方法经常被调用,即使没有任何更改。例如,一个"Left:=30;"也将执行此操作。

改进措施:检查更改:

procedure TMainForm.SetBounds(ALeft, ATop, AWidth, AHeight: integer);
begin
  if (Left <> ALeft) or (Top <> ATop) or (Width <> AWidth) or (Height <> AHeight) then
    InvalidatePreferredSize;
  inherited SetBounds(ALeft, ATop, AWidth, AHeight);
end;

TWinControl.AlignControls

procedure AlignControls(aControl: TControl; var aRect: TRect); override;

AlignControls移动并改变所有子控件的大小。LCL使用Align=alCustom来实施(implementation) 忽略控件。

参数aControl: TControl为保持与VCL的兼容性。LCL总是传递(passes) nil 。当应用Align属性时,TControl将给予aControl优先级。例如,当你有两个使用Align=alLeft的控件A,B时,那么在较下侧的左侧控件将被放到最左侧 。如果两者都有相同的左侧位置,那么按照创建顺序。现在假设你想在设计器中切换两个控件A,B。你拖拽B到左侧。这将导致B.Left设置为0。现在 AlignControls启动并找到A.Left=0和B.Left=0。通常情况下,A将获胜。为让B获胜,VCL调用AlignControls(B,r)。因此aControl是最后被移动的。与VCL截然不同,LCL 保持跟踪在TWinControl.fAlignControls中最后移动的控件,并应用在 TWinControl.CreateControlAlignList中的顺序。aControl参数始终是nil 。

查看TWinControl.CreateControlAlignList。

OnResize/OnChangeBounds与LCL属性冲突

你设置边界咬入(bite)LCL属性。例如,TLabel.AutoSize默认是true。如果你在 一个OnResiz事件中设置Label1.Width,LCL将 重新计算,调整Label1大小,并再次调用OnResize。在调试器中启动你的应用程序,使再次发生错误。当它进入循环时,如果你看到你的一个事件或你的方法在那里开始搜索,暂停应用程序并查看调用堆栈。例如:

procedure TMainForm.FormResize(Sender: TObject);
begin
  // if Button1 is anchored or AutoSize=true then the following might create an endless loop:
  Button1.Width:=100;
end;

LCL接口(interface)错误,自定义widget

有时,LCL接口(interface)或你的自定义控件有一个错误和取消LCL 边界。使用参数-dVerboseIntfSizing编译LCL 。这将写入LCLLCL接口(interface)之间的通讯。如果一个widget不运行自由重新调整大小,它必须通过约束条件来告诉LCL 。在各种LCL接口(interface)和TWSControlClass.ConstraintWidth/Height 中搜索SetInterfaceConstraints。

alCustom

你使用alCustom 。在早期的Lazarus版本中这只是实现了一半,但是一些聪明的程序员使用它来创建一些特殊的效果。现在它已经实现了,并且你的程序将不在工作。请查看 alCustom做什么: alCustom.

参考

贡献者和更改

  • 简体中文版本由 robsean 于 2020-02-18 创建(2021-03-10完成全部翻译)。