Visual->UIElement->FrameworkElement,带来更多功能的同时也带来了更多的限制

本文深入探讨WPF中UIElement与FrameworkElement的区别,解析布局系统、屏幕交互、样式和模板等核心概念,揭示Width和Height属性的真实作用及如何避免元素被切掉。

版权声明:本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:http://blog.youkuaiyun.com/wpwalter/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系(walter.lv@qq.com)。 https://blog.youkuaiyun.com/WPwalter/article/details/78619688

在 WPF 或 UWP 中,我们平时开发所遇到的那些 UI 控件或组件,都直接或间接继承自 Framework。例如:GridStackPanelCanvasBorderImageButtonSlider。我们总会自然而然地认为这些控件都是有大小的,它们会在合适的位置显示自己,通常不会超出去。但是,FrameworkElement 甚至是 Control 用得久了,都开始忘记 VisualUIElement 带给我们的那些自由。

阅读本文将了解我们熟知的那些功能以及限制的由来,让我们站在限制之外再来审视 WPF 的可视化树,再来看清 WPF 各种控件属性的本质。


宽度和高度

如果问 Width/Height 属性来自谁,只要在 WPF 和 UWP 里混了一点儿时间都会知道——FrameworkElement。随着 FrameworkElement 的宽高属性一起带来的还有 ActualWidthActualHeightMinWidthMinHeightMaxWidthMaxHeight。正是这些属性的存在,让我们可以直观地给元素指定尺寸——想设置多少就设置多少。

然而……当你把宽或高设置得比父容器允许的最大宽高还要大的时候呢?我们会发现,控件被“切掉”了。


▲ 被切掉的椭圆

然而,因布局被“切掉”这一特性也是来自于 FrameworkElement

UIElement 布局时即便空间不够也不会故意去将超出边界的部分切掉,这一点从其源码就能得到证明:

/// <summary>
/// This method supplies an additional (to the <seealso cref="Clip"/> property) clip geometry
/// that is used to intersect Clip in case if <seealso cref="ClipToBounds"/> property is set to "true".
/// Typcally, this is a size of layout space given to the UIElement.
/// </summary>
/// <returns>Geometry to use as additional clip if ClipToBounds=true</returns>
protected virtual Geometry GetLayoutClip(Size layoutSlotSize)
{
    if(ClipToBounds)
    {
        RectangleGeometry rect = new RectangleGeometry(new Rect(RenderSize));
        rect.Freeze();
        return rect;
    }
    else
        return null;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

只会在 ClipToBounds 设置为 true 的时候进行矩形切割。

然而 FrameworkElement 的切掉逻辑就复杂多了,鉴于有上百行,就只贴出链接 FrameworkElement.GetLayoutClip。其处理了各种布局、变换过程中的情况。

由于 FrameworkElement 的出现是为了让我们编程中像对待一个有固定尺寸的物体一样,所以也在切除上模拟了这样的空间有限的效果。

如果希望不被切掉,有两种方法修正:

  1. 确保布局的时候所需尺寸不大于可用尺寸(一点也不能大于,就算是 double 精度问题导致的细微偏大都不行)
    • MeasureOverride 返回的尺寸不大于参数传入的尺寸
    • ArrangeOverride 返回的尺寸不大于参数传入的尺寸
  2. 重写 GetLayoutClip 方法,并返回 null(或者写成 UIElement 那样)

布局系统

提及 MeasureOverrideArrangeOverride,大家都会认为这是 WPF 布局系统给我们提供的两个可供重写的方法。然而,这两个方法其实也是 FrameworkElement 才提供的。

真正布局的方法是 MeasureArrange,而可供重写的方法是 MeasureCoreArrangeCore。这两组方法均来自于 UIElement,而布局系统其实是 UIElement 引入的。

那么 FrameworkElement 做了什么呢?它密封了 MeasureCoreArrangeCore 这两个布局的重写方法,以便能够处理 WidthHeightMinWidthMinHeightMaxWidthMaxHeightMargin 这些属性对布局的影响。

你觉得 WidthHeight 属性是元素的最终宽高吗?我们在 宽度和高度 一节中已经说了不是,前面一段也说了不是——它们真的只是布局属性!然而,这真的很容易形成误解!Width``Height 属性其实和 MinWidth``MinHeightMaxWidth``MaxHeight 是完全一样的用途,只是在布局过程中为计算最终尺寸提供的布局限制而已。只不过 MinWidth``MinHeightMaxWidth``MaxHeight 用大于和小于进行尺寸的限制,而 Width``Height 用等于进行尺寸的限制。最终的尺寸依然是 ActualWidth``ActualHeight,而这个值跟 RenderSize 其实是一个意思,因为内部获取的就是 RenderSize

值得注意的是,ActualWidth``ActualHeightRenderSize 一样,是布局结束后才会更新的,开发中需要如果修改了属性立即获取这些值其实必然是旧的,拿这些值进行计算会造成错误的尺寸数据。

顺便吐槽一下:其实微软是喜欢用 Core 来作为子类重写方法的后缀的,比如 FreezableEasingFunction 都是用 Core 后缀来处理重写。Override 后缀纯属是因为 UIElement 把这个名字用了而已。

屏幕交互

UIElement 中存在着布局计算,FrameworkElement 中存在着带限制的布局计算,这很容易让人以为屏幕相关的坐标计算会存在于 UIElement 或者 FrameworkElement 中。

然而其实 UIElement 或者 FrameworkElement 只涉及到控件之间的坐标计算(TranslatePoint),真正涉及到屏幕坐标的转换是位于 Visual 中的,典型的是这几个:

  • TransformToAncestor
  • TransformToDescendant
  • TransformToVisual
  • PointFromScreen
  • PointToScreen

所以其实如果希望做出非常轻量级的高性能 UI,继承自 Visual 也是一个大胆的选择。当然,真正遇到瓶颈的时候,继承自 Visual 也解决不了多少问题。

样式和模板

FrameworkElement 开始有了样式(Style),Control 开始有了模板(Template)。而模板极大地方便了样式定制的同时,也造成了强大的性能开销,因为本来的一个 Visual 瞬间变成了几个、几十个。一般情况下这根本不会是性能瓶颈,然而当这种控件会一次性产生几十个甚至数百个(例如表格)的时候,这种瓶颈就会非常明显。

总结容易出现理解偏差的几个点

  1. WidthHeight 属性其实只是为布局过程中的计算进行限制而已,跟 MinWidthMinHeightMaxWidthMaxHeight 没有区别,并不直接决定实际尺寸。
  2. 如果发现元素布局中被切掉了,这并不是不可避免的问题;因为切掉是 FrameworkElement 为我们引入的特性,不喜欢可以随时关掉。
  3. 微软对于子类重写核心逻辑的方法喜欢使用 Core 后缀,布局中用了 Override 只是因为名字被占用了。
  4. Visual 就可以计算与屏幕坐标之间的转换。
  5. 模板(Template)会额外产生很多个 Visual,有可能会成为性能瓶颈。

参考资料

--------------------- 本文来自 walter lv 的优快云 博客 ,全文地址请点击:https://blog.youkuaiyun.com/WPwalter/article/details/78619688?utm_source=copy

看起来你提供的图片内容是一些编程错误信息,主要与 Windows Presentation Foundation (WPF) 项目中的 XAML 编码相关。以下是这些错误的简要解释: 1. 属性 'TargetType' 的值无效: "materialDesign:Card" 错误原因:materialDesign 命名空间未定义。 解决方法:确保你已经正确引入了 MaterialDesign 的命名空间。可以在 XAML 文件的顶部添加如下行: xml xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" 2. 属性 'Property' 的值无效: 'Margin', 'Padding' 错误原因:这些属性的使用可能存在问题,或者所在的元素不支持这些属性。 解决方法:检查这些属性是否被正确地应用在支持它们的元素上。例如,Margin 和 Padding 通常用于 FrameworkElement 和 UIElement 的派生类。 3. 命名空间前缀 'local' 尚未定义 错误原因:local 命名空间未定义。 解决方法:在 XAML 文件的顶部定义 local 命名空间,例如: xml xmlns:local="clr-namespace:YourNamespace" 4. 未找到类型 'local:ScreenSizeConverter' 错误原因:ScreenSizeConverter 类型未找到。 解决方法:确保 ScreenSizeConverter 类已经定义并且在正确的命名空间中。同时,检查是否正确引用了包含该类的程序集。 5. 未找到类型 'system:Double' 错误原因:system:Double 类型未找到。 解决方法:确保正确引用了 System 命名空间。可以在 XAML 文件的顶部添加如下行: xml xmlns:sys="clr-namespace:System;assembly=mscorlib" 如果你有具体的 XAML 代码或更详细的需求,请提供进一步的信息,以便我能给出更精确的解答。 <!-- 自适应卡片容器样式 --> <Style x:Key="AdaptiveCardStyle" TargetType="materialDesign:Card"> <Setter Property="Margin" Value="10"/> <Setter Property="Padding" Value="15"/> <Style.Triggers> <!-- 小屏幕 --> <DataTrigger Binding="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}}" Value="1024"> <Setter Property="Margin" Value="5"/> <Setter Property="Padding" Value="10"/> </DataTrigger> <!-- 大屏幕 --> <DataTrigger Binding="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}}" Value="1920"> <Setter Property="Margin" Value="15"/> <Setter Property="Padding" Value="20"/> </DataTrigger> </Style.Triggers> </Style> <!-- 响应式布局转换器 --> <local:ScreenSizeConverter x:Key="ScreenSizeConverter"/> <!-- 动态边距 --> <Thickness x:Key="AdaptiveMargin" Left="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}, Converter={StaticResource ScreenSizeConverter}, ConverterParameter='Left'}" Top="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}, Converter={StaticResource ScreenSizeConverter}, ConverterParameter='Top'}" Right="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}, Converter={StaticResource ScreenSizeConverter}, ConverterParameter='Right'}" Bottom="{Binding Source={x:Static SystemParameters.PrimaryScreenWidth}, Converter={StaticResource ScreenSizeConverter}, ConverterParameter='Bottom'}"/> <!-- 动态字体大小 --> <system:Double x:Key="AdaptiveFontSize"> <Binding Source="{x:Static SystemParameters.PrimaryScreenWidth}" Converter="{StaticResource ScreenSizeConverter}" ConverterParameter="FontSize"/> </system:Double> 修复
最新发布
11-14
<Style x:Key="SliderThumbStyle" TargetType="{x:Type Thumb}"> <Setter Property="UIElement.SnapsToDevicePixels" Value="true"/> <Setter Property="FrameworkElement.OverridesDefaultStyle" Value="true"/> <Setter Property="FrameworkElement.Height" Value="10"/> <Setter Property="FrameworkElement.Width" Value="10"/> <Setter Property="Control.Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type Thumb}"> <Ellipse x:Name="Ellipse" StrokeThickness="1"> <Shape.Stroke> <LinearGradientBrush StartPoint="0,0" EndPoint="0,1"> <GradientBrush.GradientStops> <GradientStopCollection> <GradientStop Color="{DynamicResource BorderLightColor}" Offset="0.0"/> <GradientStop Color="{DynamicResource BorderDarkColor}" Offset="1.0"/> </GradientStopCollection> </GradientBrush.GradientStops> </LinearGradientBrush> </Shape.Stroke> <Shape.Fill> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="{DynamicResource ControlMediumColor}" Offset="1"/> <GradientStop Color="{DynamicResource ControlLightColor}"/> </LinearGradientBrush> </Shape.Fill> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill). (GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="Ellipse"> <EasingColorKeyFrame KeyTime="0" Value="{StaticResource ControlMouseOverColor}"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Pressed"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill). (GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="Ellipse"> <EasingColorKeyFrame KeyTime="0" Value="{StaticResource ControlPressedColor}"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> <VisualState x:Name="Disabled"> <Storyboard> <ColorAnimationUsingKeyFrames Storyboard.TargetProperty="(Shape.Fill). (GradientBrush.GradientStops)[0].(GradientStop.Color)" Storyboard.TargetName="Ellipse"> <EasingColorKeyFrame KeyTime="0" Value="{StaticResource DisabledControlDarkColor}"/> </ColorAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> </Ellipse> </ControlTemplate> </Setter.Value> </Setter> </Style>给我来个完整的转化 不要写一部分 我要全部的转为avalonia格式的
09-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值