WPF之布局流程

文章目录

    • [1. 概述](#1. 概述)
    • [2. 布局元素的边界框](#2. 布局元素的边界框)
    • [3. 布局系统原理](#3. 布局系统原理)
      • [3.1 布局流程时序图](#3.1 布局流程时序图)
    • [4. 测量阶段(Measure Phase)](#4. 测量阶段(Measure Phase))
      • [4.1 测量过程](#4.1 测量过程)
      • [4.2 MeasureOverride方法](#4.2 MeasureOverride方法)
    • [5. 排列阶段(Arrange Phase)](#5. 排列阶段(Arrange Phase))
      • [5.1 排列过程](#5.1 排列过程)
      • [5.2 ArrangeOverride方法](#5.2 ArrangeOverride方法)
    • [6. 渲染阶段(Render Phase)](#6. 渲染阶段(Render Phase))
    • [7. 布局事件](#7. 布局事件)
      • [7.1 主要布局事件](#7.1 主要布局事件)
      • [7.2 布局事件示例](#7.2 布局事件示例)
    • [8. 自定义面板示例](#8. 自定义面板示例)
    • [9. 布局性能优化](#9. 布局性能优化)
      • [9.1 选择合适的面板](#9.1 选择合适的面板)
      • [9.2 使用RenderTransform而非LayoutTransform](#9.2 使用RenderTransform而非LayoutTransform)
      • [9.3 避免不必要的UpdateLayout调用](#9.3 避免不必要的UpdateLayout调用)
      • [9.4 使用虚拟化](#9.4 使用虚拟化)
      • [9.5 使用布局舍入](#9.5 使用布局舍入)
    • [10. 总结](#10. 总结)
    • 参考链接

1. 概述

Windows Presentation Foundation (WPF) 的布局系统是WPF应用程序中的核心部分,它负责计算界面中每个元素的大小和位置,并最终呈现到屏幕上。理解WPF的布局流程对于创建高性能、响应迅速的用户界面至关重要。

WPF布局系统是一个递归系统,它会自顶向下地处理视觉树中的每个元素。布局过程主要包括三个阶段:测量(Measure)、排列(Arrange)和渲染(Render)。除此之外,布局事件在整个流程中也扮演着重要角色。

2. 布局元素的边界框

在讨论WPF布局之前,我们需要了解元素边界框的概念。在WPF中,每个元素都被定义在一个表示其边界的矩形内,这个矩形称为"布局槽(Layout Slot)"。布局槽的实际大小由布局系统在运行时根据屏幕大小、父属性和元素本身的属性(如边框、宽度、高度、边距和内边距)计算得出。

当计算元素的布局属性后,元素的最终可见区域称为"布局剪辑(Layout Clip)"。可以使用LayoutInformation类来获取元素的布局槽和布局剪辑信息。

csharp 复制代码
// 获取元素的布局槽
Rect layoutSlot = LayoutInformation.GetLayoutSlot(myElement);

// 获取元素的布局剪辑
Geometry clipGeometry = LayoutInformation.GetLayoutClip(myElement);

3. 布局系统原理

WPF布局系统的核心思想是"两段式布局"。在两段式布局中,父容器和子元素通过协商来确定每个元素的最终尺寸和位置。这个过程涉及到三种尺寸:

  1. 可用尺寸(Available Size): 父元素愿意给子元素的最大空间值。
  2. 期望尺寸(Desired Size): 子元素希望获得的尺寸。
  3. 实际尺寸(Actual Size): 最终分配给子元素的尺寸。

这三个尺寸通常符合以下不等式:

复制代码
期望尺寸(Desired Size) ≤ 实际尺寸(Actual Size) ≤ 可用尺寸(Available Size)

3.1 布局流程时序图

父元素 子元素 开始布局过程 调用Measure(availableSize) MeasureCore处理 MeasureOverride计算期望尺寸 返回DesiredSize 调用Arrange(finalRect) ArrangeCore处理 ArrangeOverride安排内容 返回最终尺寸 继续处理其他子元素 父元素 子元素

4. 测量阶段(Measure Phase)

测量阶段是布局流程的第一步,主要目的是确定每个元素希望获得的大小。在这个阶段,父元素会询问每个子元素它需要多大的空间,子元素会计算并返回它的期望尺寸(DesiredSize)。

4.1 测量过程

测量过程从调用UIElement.Measure方法开始,这个方法会在父面板元素的实现中被调用,通常不需要显式调用它。测量过程的大致步骤如下:

  1. 首先计算UIElement的基本属性,如ClipVisibility,生成一个名为constraintSize的值并传递给MeasureCore
  2. 处理FrameworkElement上定义的框架属性,如HeightWidthMarginStyle,这些属性会影响constraintSize的值。
  3. 调用MeasureOverride方法,传入constraintSize作为参数。
  4. 子元素确定自己的DesiredSize,并存储以供排列阶段使用。

4.2 MeasureOverride方法

MeasureOverride方法是FrameworkElement类的重要方法,当创建自定义控件或面板时,通常需要重写这个方法来提供自定义的测量逻辑。

csharp 复制代码
/// <summary>
/// 重写MeasureOverride方法以自定义测量逻辑
/// </summary>
/// <param name="availableSize">父容器提供的可用尺寸</param>
/// <returns>元素期望的尺寸</returns>
protected override Size MeasureOverride(Size availableSize)
{
    // 定义期望的尺寸
    Size desiredSize = new Size();
    
    // 遍历所有子元素进行测量
    foreach (UIElement child in this.Children)
    {
        // 测量子元素
        child.Measure(availableSize);
        
        // 根据子元素的期望尺寸更新自身的期望尺寸
        // 这里的逻辑取决于面板的布局策略
        desiredSize.Width = Math.Max(desiredSize.Width, child.DesiredSize.Width);
        desiredSize.Height += child.DesiredSize.Height;
    }
    
    // 返回计算得到的期望尺寸
    return desiredSize;
}

5. 排列阶段(Arrange Phase)

在测量阶段完成后,每个元素都知道了自己期望的大小,接下来就进入了排列阶段。排列阶段的主要目的是确定每个元素的最终位置和大小。

5.1 排列过程

排列过程从调用UIElement.Arrange方法开始。在排列过程中,父面板元素会生成一个表示子元素边界的矩形,这个值会传递给ArrangeCore方法处理。排列过程的大致步骤如下:

  1. ArrangeCore方法评估子元素的DesiredSize以及可能影响元素渲染大小的任何其他边距。
  2. ArrangeCore生成一个arrangeSize,并作为参数传递给面板的ArrangeOverride方法。
  3. ArrangeOverride生成子元素的最终大小finalSize
  4. ArrangeCore方法执行偏移属性(如边距和对齐)的最终计算,并将子元素放在其布局槽内。

5.2 ArrangeOverride方法

ArrangeOverride方法是FrameworkElement类的另一个重要方法,用于自定义排列逻辑。

csharp 复制代码
/// <summary>
/// 重写ArrangeOverride方法以自定义排列逻辑
/// </summary>
/// <param name="finalSize">最终分配给元素的尺寸</param>
/// <returns>实际使用的尺寸</returns>
protected override Size ArrangeOverride(Size finalSize)
{
    // 初始位置
    double yPos = 0;
    
    // 遍历所有子元素进行排列
    foreach (UIElement child in this.Children)
    {
        // 计算子元素的位置和大小
        // 这里以垂直堆叠为例
        Rect rect = new Rect(0, yPos, finalSize.Width, child.DesiredSize.Height);
        
        // 排列子元素
        child.Arrange(rect);
        
        // 更新下一个子元素的垂直位置
        yPos += child.DesiredSize.Height;
    }
    
    // 返回最终使用的尺寸
    return finalSize;
}

6. 渲染阶段(Render Phase)

渲染阶段是布局流程的最后一步,它负责将测量和排列后的元素绘制到屏幕上。渲染过程由WPF的渲染引擎负责,通常开发者不需要直接干预这个过程。

渲染阶段的主要特点:

  1. 异步执行: 渲染过程通常是异步的,与UI线程分离,以提高性能。
  2. 按需渲染: WPF只会渲染需要更新的部分,以减少不必要的计算。
  3. 硬件加速: WPF利用DirectX进行硬件加速渲染,提高图形性能。

虽然开发者通常不需要直接操作渲染过程,但可以通过重写OnRender方法来自定义控件的渲染。

csharp 复制代码
/// <summary>
/// 重写OnRender方法以自定义渲染逻辑
/// </summary>
/// <param name="drawingContext">绘图上下文</param>
protected override void OnRender(DrawingContext drawingContext)
{
    // 调用基类的渲染方法
    base.OnRender(drawingContext);
    
    // 自定义绘制逻辑
    // 例如绘制一个矩形
    Rect rect = new Rect(0, 0, ActualWidth, ActualHeight);
    drawingContext.DrawRectangle(Brushes.LightBlue, new Pen(Brushes.Blue, 1), rect);
}

7. 布局事件

在布局过程中,WPF会触发一系列事件,这些事件可以帮助开发者了解布局的过程并在适当的时机执行自定义逻辑。

7.1 主要布局事件

  1. LayoutUpdated: 当布局系统完成更新时触发。
  2. SizeChanged: 当元素的实际大小改变时触发。
  3. Loaded: 当元素被加载到视觉树中并完成布局时触发。

7.2 布局事件示例

csharp 复制代码
public class CustomControl : Control
{
    public CustomControl()
    {
        // 订阅布局事件
        this.Loaded += CustomControl_Loaded;
        this.SizeChanged += CustomControl_SizeChanged;
        this.LayoutUpdated += CustomControl_LayoutUpdated;
    }

    private void CustomControl_Loaded(object sender, RoutedEventArgs e)
    {
        // 当控件加载完成时执行的逻辑
        Console.WriteLine("控件已加载完成");
    }

    private void CustomControl_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        // 当控件大小改变时执行的逻辑
        Console.WriteLine($"控件大小已改变: 旧尺寸={e.PreviousSize}, 新尺寸={e.NewSize}");
    }

    private void CustomControl_LayoutUpdated(object sender, EventArgs e)
    {
        // 当布局更新时执行的逻辑
        Console.WriteLine("布局已更新");
    }
}

8. 自定义面板示例

下面是一个自定义面板的完整示例,它实现了一个简单的"V"形布局,将子元素排列成一个"V"字形。

csharp 复制代码
/// <summary>
/// 自定义V形布局面板
/// </summary>
public class VShapePanel : Panel
{
    /// <summary>
    /// 重写测量方法,计算面板所需尺寸
    /// </summary>
    protected override Size MeasureOverride(Size availableSize)
    {
        Size desiredSize = new Size();
        
        // 测量所有子元素
        foreach (UIElement child in this.InternalChildren)
        {
            // 让子元素自行测量所需大小
            child.Measure(availableSize);
            
            // 更新面板所需的宽度和高度
            desiredSize.Width = Math.Max(desiredSize.Width, child.DesiredSize.Width * 2);
            desiredSize.Height += child.DesiredSize.Height / 2;
        }
        
        // 确保V形底部有足够空间
        if (this.InternalChildren.Count > 0)
        {
            var lastChild = this.InternalChildren[this.InternalChildren.Count - 1];
            desiredSize.Height += lastChild.DesiredSize.Height / 2;
        }
        
        return desiredSize;
    }
    
    /// <summary>
    /// 重写排列方法,将子元素排列成V形
    /// </summary>
    protected override Size ArrangeOverride(Size finalSize)
    {
        if (this.InternalChildren.Count == 0)
            return finalSize;
            
        int middleIndex = this.InternalChildren.Count / 2;
        double centerX = finalSize.Width / 2;
        double currentY = 0;
        
        // 排列V形左侧的元素
        for (int i = 0; i <= middleIndex; i++)
        {
            UIElement child = this.InternalChildren[i];
            double offsetX = centerX - (middleIndex - i) * (child.DesiredSize.Width * 0.75);
            
            // 排列子元素
            child.Arrange(new Rect(
                offsetX, 
                currentY, 
                child.DesiredSize.Width, 
                child.DesiredSize.Height));
                
            currentY += child.DesiredSize.Height / 2;
        }
        
        // 排列V形右侧的元素
        currentY = 0;
        for (int i = 0; i < middleIndex; i++)
        {
            UIElement child = this.InternalChildren[i];
            UIElement symmetricChild = this.InternalChildren[this.InternalChildren.Count - 1 - i];
            
            double offsetX = centerX + (middleIndex - i) * (symmetricChild.DesiredSize.Width * 0.75);
            
            // 排列对称元素
            symmetricChild.Arrange(new Rect(
                offsetX, 
                currentY, 
                symmetricChild.DesiredSize.Width, 
                symmetricChild.DesiredSize.Height));
                
            currentY += child.DesiredSize.Height / 2;
        }
        
        return finalSize;
    }
}

使用示例:

xml 复制代码
<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApp"
        Title="VShape Layout Demo" Height="450" Width="800">
    <local:VShapePanel>
        <Button Content="按钮1" Width="100" Height="40"/>
        <Button Content="按钮2" Width="100" Height="40"/>
        <Button Content="按钮3" Width="100" Height="40"/>
        <Button Content="按钮4" Width="100" Height="40"/>
        <Button Content="按钮5" Width="100" Height="40"/>
    </local:VShapePanel>
</Window>

9. 布局性能优化

布局是一个递归过程,每次调用布局系统时,都会处理子元素集合中的每个子元素。因此,应该避免不必要地触发布局系统。以下是一些性能优化建议:

9.1 选择合适的面板

不同类型的面板有不同的布局复杂度。例如,Canvas的布局算法非常简单,而Grid则复杂得多。如果不需要Grid提供的功能,应该使用性能开销较小的替代方案,如Canvas或自定义面板。

9.2 使用RenderTransform而非LayoutTransform

LayoutTransform会影响布局系统,而RenderTransform不会。如果变换不需要影响其他元素的位置,最好使用RenderTransform,因为它不会调用布局系统。

9.3 避免不必要的UpdateLayout调用

UpdateLayout方法会强制执行递归布局更新,通常是不必要的。除非确定需要完整更新,否则应该依赖布局系统自动调用此方法。

9.4 使用虚拟化

处理大型集合时,考虑使用VirtualizingStackPanel代替常规的StackPanel。通过虚拟化子集合,VirtualizingStackPanel只在内存中保留当前位于父级视区内的对象,从而显著提高性能。

xml 复制代码
<ListBox VirtualizingPanel.IsVirtualizing="True"
         VirtualizingPanel.VirtualizationMode="Recycling"
         ScrollViewer.IsDeferredScrollingEnabled="True">
    <ListBox.ItemsPanel>
        <ItemsPanelTemplate>
            <VirtualizingStackPanel/>
        </ItemsPanelTemplate>
    </ListBox.ItemsPanel>
    <!-- 列表项 -->
</ListBox>

9.5 使用布局舍入

WPF图形系统使用设备无关单位来实现分辨率和设备独立性。每个设备独立像素会根据系统的DPI设置自动缩放。但这种DPI独立性可能会因为抗锯齿而导致不规则的边缘渲染。

布局舍入是WPF提供的一种解决方案,它会在布局过程中将非整数像素值舍入为整数。默认情况下,布局舍入是禁用的,可以通过设置UseLayoutRounding属性为true来启用它。

xml 复制代码
<!-- 为整个UI启用布局舍入 -->
<Window x:Class="WpfApp.MainWindow"
        UseLayoutRounding="True"
        ...>
    <!-- 窗口内容 -->
</Window>

10. 总结

WPF的布局系统是一个复杂而强大的机制,它通过测量、排列和渲染三个阶段来确定UI元素的大小和位置。理解这个过程对于创建高效、响应迅速的WPF应用程序至关重要。

通过重写MeasureOverrideArrangeOverride方法,开发者可以创建自定义布局行为,满足特定的UI需求。同时,了解并应用布局性能优化技巧,可以避免不必要的布局计算,提高应用程序的整体性能。

参考链接

  1. WPF Layout System - Microsoft Docs
  2. Optimizing Performance: Layout and Design - Microsoft Docs
  3. Understanding WPF Layout - CodeProject
  4. WPF 布局原理 - 博客园
相关推荐
RPA中国4 小时前
OpenAI大变革!继续与微软等,以非营利模式冲击AGI
microsoft·agi
可喜~可乐4 小时前
SQLite数据类型
数据库·sql·sqlite·c#
冰茶_8 小时前
WPF之面板特性
microsoft·微软·c#·wpf·布局系统
狗屁不会还不知上进的咸鱼10 小时前
C#中不能通过new关键字创建实例的情况
c#
VB.Net11 小时前
C# 综合示例 库存管理系统20 操作员管理(FormAdmin)
开发语言·数据库·c#
Kai-爱记录13 小时前
C#中读取文件夹(包含固定字样文件名)
开发语言·c#
bicijinlian15 小时前
多语言笔记系列:Polyglot Notebooks 中使用扩展库
jupyter·c#·polyglot·notebooks·c# jupyter·.net jupyter·.net notebook
red-fly15 小时前
c#OdbcDataReader的数据读取
开发语言·sqlserver·c#