UE5 从画布面板看插槽、锚点的使用

CanvasPanel作为UMG里自由度相对较高的面板控件,适用于各种复杂的页面布局,也是UMG中少数可以直接在面板中拖拽子控件的面板控件

  • 绝对定位:通过 Anchors(锚点)/Offsets(偏移) 精准控制位置
  • 适用场景
    • 复杂UI布局(角色属性面板)
    • 需要响应式设计的界面

高自由度同时也带来了高难度,很多新手朋友并不了解如何使用画布面板的锚点来对面板布局进行精确的控制,只会笨笨地拖拽子控件来调整其位置,在遇到界面缩放时常常会遇到不期望的结果。因此本文会从插槽的概念讲起,详细讲解如何使用画布面板对布局进行精确的控制。

插槽

在 Unreal Engine 的 UMG 中,插槽(Slot) 是控件布局系统的核心机制,它定义了子控件如何在其父容器内定位和调整尺寸。理解插槽机制对创建自适应 UI 至关重要。以下是插槽的解析:

插槽的本质

每个添加到容器控件(如 Canvas PanelVertical Box 等)的子控件,都会被包裹在一个 Slot 对象 中。这个 Slot 负责:

  1. 存储布局数据:位置、尺寸、对齐方式等
  2. 执行布局计算:根据容器规则调整子控件几何形状
  3. 传递事件:处理点击测试、渲染裁剪等

以添加按钮到 Canvas Panel 为例:

image-20250702154142288

当用户往画布面板中添加控件时,画布面板会自动创建一个插槽(UCanvasPanelSlot),将新控件包裹在插槽中,添加到其Slots成员变量中

/** The slots in the widget holding the child widgets of this panel. */
UPROPERTY(Instanced)
TArray<TObjectPtr<UPanelSlot>> Slots;

同时,在UMG蓝图界面中,新添加子控件的细节面板会出现一块叫做“插槽(画布面板槽)”的细节项:

image-20250702154814429

这些细节项就是这个子控件所属插槽的详细信息,而画布面板槽是插槽的类型,由于我们是在画布面板里添加子控件,所以他是画布面板槽。同理,如果我们是在覆层(Overlay)中添加子控件,那么这个插槽的类型就是覆层槽。

之所以强调这一点,是因为新手常常会误以为这些插槽项是子控件自己的属性,其实并不是,插槽定义的是子控件如何在其父容器内定位和调整尺寸,其记录的是子控件在父控件中的布局信息,因此其与父控件也就是画布面板是强相关的,不同的容器控件具有不同的插槽,因此这里显示的细节并不会是固定的。

布局信息(LayoutData)

大小到内容ZOrder暂且不谈, 从上图的画布面板槽中可以看到,画布面板槽具有三个布局相关的重要属性,分别为

  1. 锚点(Anchors)
  2. 偏移(Offsets)
  3. 对齐(Alignment)

有的读者可能纳闷怎么数都数不出三个来,别急,后面一一讲解。这三个布局数据在源码中被封装为FAnchorData

/** The anchoring information for the slot */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Getter="GetLayout", Setter="SetLayout", BlueprintGetter ="GetLayout", BlueprintSetter="SetLayout", Category = "Layout|Canvas Slot")
	FAnchorData LayoutData;

内部有三个成员,这三个成员共同定义了插槽在画布面板控件中的位置和布局信息

	/** Offset. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=AnchorData)
	FMargin Offsets;
	
	/** Anchors. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=AnchorData)
	FAnchors Anchors;

	/**
	 * Alignment is the pivot point of the widget.  Starting in the upper left at (0,0),
	 * ending in the lower right at (1,1).  Moving the alignment point allows you to move
	 * the origin of the widget.
	 */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=AnchorData)
	FVector2D Alignment = FVector2D::ZeroVector;

接下来一一讲解

锚点(Anchors)和偏移(Offsets)

锚点

锚点是画布面板的灵魂,在自适应大小中起到了关键的作用

  • 作用:锚点是控件相对于父容器(Canvas Panel)的定位参考点,定义了控件如何随着屏幕尺寸变化而调整自身位置和大小。
  • 工作原理
    • 锚点由父容器上的 起点(Min)终点(Max) 坐标构成(取值范围 [0,1]),即定义锚点在父容器上的相对位置
    • 示例
      • (0,0)(0,0):左上角(固定位置)。
      • (0,0)(1,1):铺满全屏(随父容器伸缩)。
      • (0.5,0.5)(0.5,0.5):中心点(固定中心)。
    • 当父容器尺寸变化时,控件会根据锚点范围自动调整位置和大小。
  • 用途:实现响应式布局(适配不同屏幕分辨率)。
struct FAnchors
{
	/** Holds the minimum anchors, left + top. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	FVector2D Minimum;

	/** Holds the maximum anchors, right + bottom. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	FVector2D Maximum;
}

如图所示,红框内就是该插槽对应的锚点,由于其作用比较抽象,UE的注释和网上的解释大多也晦涩难懂,因此需要自己上手实验才能弄懂其作用。
image-20250702161130880

上手调几下锚点可知,锚点根据其上下左右被分为了四个部分,其中,最小(Min)和最大(Max)定义了锚点四个部分在父控件上的相对位置。其中,Min.X代表了左半部分,Min.Y代表上半部分,Max.X代表右半部分,Max.Y代表下半部分。

如图所示,左上部分在父控件的左上角,也就是(0,0位置),右下部分在父控件X方向上97%的位置,因此Max.X值为0.97,
image-20250702170028054

为了更深入地理解锚点是如何发挥作用的,还需了解偏移在这之中起的作用。

Offsets(偏移量)

  • 作用:在锚点基础上添加固定像素偏移,精确微调控件的位置或尺寸。
  • 组成
    • Left / Right / Top / Bottom:控件边缘到锚点对应边的距离(像素)。
    • 是否生效取决于锚点类型
      • 若锚点范围水平拉伸(如 (0,0)-(1,0)),则 LeftRight 同时生效(定义左右边距)。
      • 若锚点水平固定(如 (0.5,0)-(0.5,0)),只需设置 Left(控件中心偏移)。
struct FMargin
{
	/** Holds the margin to the left. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	float Left;

	/** Holds the margin to the top. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	float Top;
	
	/** Holds the margin to the right. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	float Right;
	
	/** Holds the margin to the bottom. */
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Appearance)
	float Bottom;
}

他在细节面板中对应红框中四个属性,由于其名字会根据锚点的变化而变化,我用黄字标注出其实际变量名(bottom打错了,不要在意 )。如图所示,当锚点的Min==Max时,锚点此时为一个点,而偏移的四个变量其名分别为位置X,位置Y,尺寸X,尺寸Y。

其中,位置X和位置Y代表子控件相对于锚点的偏移量,尺寸就是子控件的实际尺寸,此时不会跟随画布面板的缩放而变化。
image-20250702165017791

当锚点Min ≠ Max时,其名也会发生对应的变化,如图所示,Min.X ≠ Max.X,可以看见锚点裂成了左右两部分, 因此其Left和Right项分别变为了偏移左侧和偏移右侧,代表了 该子控件左侧距离锚点左半部分的固定偏移,以及控件右侧距离锚点右半部分的固定偏移。当父容器X方向上的尺寸发生变化时,锚点的位置也随之变化,但由于偏移左侧和偏移右侧的值是固定的,为了保持其值固定,子控件的尺寸也会随之变化

image-20250702164118689

同理,当Min.Y≠Max.Y时,Top和Bottom分别变成了偏移顶部和偏移底部

image-20250702165123101

锚点的工作原理

通过Offset的介绍,我们大致可以猜到锚点和偏移是如何协同进行工作的了,下面是更详细的工作原理

可以将锚点的模式粗略的分为两类,一类是固定点模式(Min == Max),一类是拉伸模式(Min≠Max)

固定点模式(Min == Max)

在该模式下,锚点仅为一个固定点,此时子控件的与锚点的相对位置固定,子控件的 尺寸大小也是固定的,不会随着父容器的变化而变化。

点击锚点下拉框,可以看到UE为我们预设了一系列锚点类型,红框内的就是锚点的固定点模式

image-20250702171441336

我们选择最中间的那个,可以看到锚点被移动到了父容器的重间,其Min和Max都被设置为了0.5,表示其X和Y方向上都在父容器的中间。此时,位置X代表子控件在X轴方向上相对于锚点的偏移,位置Y代表子控件在Y方向上的偏移。这个偏移量是固定的,不会随着父控件的缩放而缩放。

image-20250702171520573

控件在Canvas Panel中的位置由以下公式决定:

实际位置 = 父容器位置 
         + (锚点位置 * 父容器尺寸) 
         + 偏移量(Offset)

拉伸模式(Min ≠ Max)

在该模式下,锚点会被分为两个或者四个部分,其偏移属性名也会发生相应的变化。

此时,子控件的尺寸将不再固定,由锚点Min和Max之间的差值决定(这个公式也可应用于固定点模式):

控件宽度 = (Max.X - Min.X) * 父容器宽度 + (Right - Left)
控件高度 = (Max.Y - Min.Y) * 父容器高度 + (Bottom - Top)

举例,当我将锚点的Max.Y设置为1时,锚点被分为了上下两个部分,其下半部分在父控件Y方向上的最大值处。此时,子控件的位置(在对齐为(0,0)时,就是控件左上角的坐标)由位置X(Left)和偏移顶部(Top)确定,控件的高度由偏移底部(Bottom)-偏移顶部(Top)确定。

当父容器的高度发生变化时,由于Min、Max、Bottom和Top都不会发生变化,因此控件的实际高度与父容器高度成正比,以此实现自适应大小。

image-20250702172122883

对齐(Alignment)

上文提到,子控件的位置由锚点和偏移量共同决定,那有人肯定就要问了,子控件这么大一个控件,覆盖了这么多个像素点,所谓子控件的位置又应该取哪一个像素呢?这里就是对齐发挥作用了。

对齐的核心定义

对齐定义了控件自身的哪个点(Pivot) 会吸附到锚点确定的位置或区域上。这是一个归一化坐标系

  • (0,0) = 控件左上角
  • (1,1) = 控件右下角
  • (0.5,0.5) = 控件中心

对齐的计算公式:

 实际位置 = 锚点位置 + (对齐值 * 控件尺寸) - (锚点值 * 父容器尺寸)

简单的理解为定义子控件的原点即可。

当对齐为(0,0)时,子控件的原点在左上角
image-20250702174543277

当对齐为(1,1)时,子控件的原点在右上角
image-20250702174620720

当锚点不为固定点模式时,对齐功能似乎就失去了作用,毕竟偏移量是直接以控件的边缘开始进行计算的。

动态设置布局

实际应用中,我们经常需要根据实际需求动态设置控件在父容器中的位置,这里顺手贴一个设置控件布局信息的代码,在蓝图中使用也是同理的

void UMyObject::SetUIPositionByAnchorsAndMargin(UUserWidget* InWidget, float AnchorsMinX, float AnchorsMinY, float AnchorsMaxX, float AnchorsMaxY, float OffsetLeft, float OffsetTop, float OffsetRight, float OffsetBottom, float AlignmentX, float AlignmentY)
{
	if (InWidget)
	{
		if (UCanvasPanelSlot* pSlot = Cast<UCanvasPanelSlot>(InWidget->Slot))
		{
			FAnchors Anchors(AnchorsMinX, AnchorsMinY, AnchorsMaxX, AnchorsMaxY);
			pSlot->SetAnchors(Anchors);

			FMargin Margin(OffsetLeft, OffsetTop, OffsetRight, OffsetBottom);
			pSlot->SetOffsets(Margin);

			FVector2D Alignment(AlignmentX, AlignmentY);
			pSlot->SetAlignment(Alignment);
		}
	}
}

直接传入布局参数似乎太笨了?更通常的做法是,画布面板中提前布置了一个slot,在实际运行时我们就可以拷贝该slot的信息,设置控件的位置。

void UZCMainUI::SetUIPositionByCanvasSlot(UUserWidget* InWidget, UNamedSlot* InNewWidget)
{
	if (InWidget && InNewWidget)
	{
		if (UCanvasPanelSlot* pSlot = Cast<UCanvasPanelSlot>(InWidget->Slot))
		{
			if (UCanvasPanelSlot* pNewSlot = Cast<UCanvasPanelSlot>(InNewWidget->Slot))
			{
				FAnchors CurAnchors = pSlot->GetAnchors();
				FMargin CurOffset = pSlot->GetOffsets();
				FVector2D CurAlignment = pSlot->GetAlignment();
				FMargin TargetOffset = pNewSlot->GetOffsets();

				pSlot->SetAnchors(pNewSlot->GetAnchors());
				pSlot->SetOffsets(TargetOffset);
				pSlot->SetAlignment(pNewSlot->GetAlignment());
			}
		}
	}
}

顺带提一下 大小到内容(Auto Size)

前面提到,插槽的布局信息会给子控件预留空间,通过调整Offset可以设置子控件的尺寸。

但实际设计UI时我们通常各种各样不同的需求,插槽包裹了形态大小各异的子控件,此时若在每个插槽下手动设置尺寸的话效率非常低,因此UMG为我们提供了大小到内容,在UE5中叫做Auto Size,以前的某些版本中叫做Size To Content,其作用如下:

  • 控件忽略预设尺寸
  • 根据内容需求自动计算最佳尺寸
  • 在布局系统中优先于固定尺寸

也就是说,启用了这个选项后,子控件会忽略插槽中预设的尺寸大小,而是以递归的方式,自底向上计算子控件所需的布局空间(Desire Size),作为最终展现的尺寸。

例如,在当前设置下,我勾选了大小到尺寸,此时插槽中设置了极大的尺寸X和尺寸Y都没有生效,而是透过尺寸框(尺寸框里没有重载高度和宽度),计算了文本框的大小,最终作为最终显示的尺寸。
image-20250703155928582

当然,这只是最基础的应用,配合不同的子控件还有很多看起来很坑的规则。

例如,当我尺寸框重载了高度和宽度之后,那么计算下来的所需空间将会是这个尺寸框重载后的该高度和宽度。

还有一种情况,当画布面板插槽的锚点并不是固定点模式的话,控件的尺寸将会固定为偏移量所计算的尺寸,此时无论是否勾选了大小到内容,其尺寸都由插槽的布局信息决定。

所以这个大小到内容能起到什么作用,还得具体情况具体分析,踩过几次坑大概就能明白了。

posted @ 2025-07-03 16:34  仇白  阅读(638)  评论(0)    收藏  举报