本文属于 WPF 自定义控件入门系列博客。本文整理在 WPF 里面,自定义控件,非用户控件时,可以重写基类的许多方法和属性,这些方法和属性的作用和含义。方便让大家了解到自定义控件时,有哪些方法或属性可以被重写,重写时的正确实现以及其影响是什么

这是有伙伴问我,他在自定义控件时,发现了自己的自定义控件里面的子控件的 Loaded 事件不触发,命中测试不进入,以及测量布局方法没有被调用等问题。我开始无法快速帮助他定位到问题所在,于是在解决完问题之后,我就准备记录下来这篇博客,期望能够让大家有更好的思路去解决自定义控件时,所遇到的问题

在开始之前,期望大家对以下知识点有一个大概的了解,至少是需要听过:逻辑树,可视化树(又被我称为视觉树),控件,布局,元素,依赖属性,附加属性

本文将使用直接继承 FrameworkElement 的自定义控件类型为例子,由于在 WPF 里面有着新手比较友好的设计,在自己定义的一层(视觉树概念上的层级)控件上,各个事件或方法基本都能被符合预期正常触发。更底层的原因是在 WPF 里面,一个控件元素的布局或框架相关的事件和方法时由控件的父级控件所决定的,一个自定义的控件如果加入的是原生 WPF 自带的容器控件上,自然由于原生 WPF 自带的容器控件是正确实现了各个机制,于是自定义的控件的事件或方法都能正常被执行

换句话说就是,一个自定义的控件,加入到 WPF 自带的容器控件,如 Grid 等这些上面时。由于 WPF 自带的容器控件,如 Grid 等,是正确实现了机制,于是自定义的控件就抱了 WPF 自带的容器控件大腿,啥都不用干,各个事件和方法都是符合预期触发的

比如说自己定义一个名为 F1 的继承 FrameworkElement 的控件,代码如下

class F1 : FrameworkElement
{
public F1()
{
Width = 500;
Height = 500; Loaded += F1_Loaded;
} protected override Size MeasureOverride(Size availableSize)
{
Debug.WriteLine("F1 MeasureOverride");
return base.MeasureOverride(availableSize);
} private void F1_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine(nameof(F1_Loaded));
}
}

将以上的 F1 加入到 Grid 里面,代码如下

    <Grid>
<local:F1></local:F1>
</Grid>

运行时,将会发现 F1 的 MeasureOverrideF1_Loaded 都会被触发。证明了 Loaded 事件符合预期被触发,且重写的 MeasureOverride 方法也符合预期被调用

F1 MeasureOverride
F1_Loaded

这就给了许多新手开发者一个误导,误以为自己定义的控件写对了。这里值得一提的是,如果只是单纯一层控件只是用来展示的话,那真的就是啥机制都不需要实现,就可以了。但是如果自定义的控件需要有复杂的交互或布局,比如包含子控件等,那就有一些机制需要正确实现

为了更好的说明,这里我需要用到放入到 F1 这个自定义控件里面的 F2 子控件来进一步和大家说明。这里的 F2 子控件,也是一个继承 FrameworkElement 的类型,代码定义如下

class F2 : FrameworkElement
{
public F2()
{
Width = 500;
Height = 500; Loaded += F2_Loaded;
} protected override Size MeasureOverride(Size availableSize)
{
Debug.WriteLine("F2 MeasureOverride");
return base.MeasureOverride(availableSize);
} private void F2_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine(nameof(F2_Loaded));
}
}

可以看到几乎和 F1 一摸一样。这个 F2 子控件是从界面层级关系上,作为 F1 的子控件,也就是 F2 被包含在 F1 里面。以下是 F1 里面包含 F2 的代码

class F1 : FrameworkElement
{
public F1()
{
Width = 500;
Height = 500; F2 = new F2(); Loaded += F1_Loaded;
} private F2 F2 { get; } protected override Size MeasureOverride(Size availableSize)
{
Debug.WriteLine("F1 MeasureOverride");
return base.MeasureOverride(availableSize);
} private void F1_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine(nameof(F1_Loaded));
}
}

可以看到,此时 F1 仅仅只是将 F2 作为一个属性而已,没有附加任何机制。相信此时大家也能猜到 F2 的 Loaded 事件和 MeasureOverride 方法,肯定是不能符合预期的被调用

以上代码放在githubgitee 欢迎访问

可以通过如下方式获取本文以上的源代码,先创建一个名为 KearkemnerwhayneqiChaywibelfo 的空文件夹,接着使用命令行 cd 命令进入此空文件夹,在命令行里面输入以下代码,即可获取到本文的代码

git init
git remote add origin https://gitee.com/lindexi/lindexi_gd.git
git pull origin e24633ab99ebc5a1def7204d4d4595bc582c7d1d

以上使用的是 gitee 的源,如果 gitee 不能访问,请替换为 github 的源。请在命令行继续输入以下代码

git remote remove origin
git remote add origin https://github.com/lindexi/lindexi_gd.git
git pull origin e24633ab99ebc5a1def7204d4d4595bc582c7d1d

获取代码之后,进入 KearkemnerwhayneqiChaywibelfo 文件夹

回顾一下,正常开发 WPF 应用的时候,如果一个控件元素将会包含多个子控件,大部分情况下这个控件元素会被咱写为一个继承自 Panel 的类型,表示这是一个容器控件。但有些情况,例如这个控件元素仅仅只包含一个子级,一个子控件且是固定的类型,而且从业务逻辑上也不是一个容器的概念。这个时候咱依然可以继承 FrameworkElement 来进行自己编写。本文也着重告诉大家这个方法,而不是采用比较上层封装的 Panel 容器类型,从而让大家能够了解更多的细节

十分符合预期的 F2 类型如果只是作为 F1 的一个 CLR 属性,是不能让 F2 加入到 WPF 机制里面的,无法让 F2 的事件和重写的方法被符合预期的调用

接下来咱来修改一下 F1 类型,重写 VisualChildrenCount 属性和 GetVisualChild 方法

修改 F1 的代码如下

class F1 : FrameworkElement
{
... // 忽略其他代码 private F2 F2 { get; } protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => F2;
}

修改之后的代码放在githubgitee 欢迎访问

获取这一步骤的代码,可以在上文获取源代码的基础上,在 KearkemnerwhayneqiChaywibelfo 文件夹里面继续输入以下代码进行获取

git pull origin ad3fe86708a297ea8058b44bb576d51a858349b7

运行代码,可以看到输出如下

F1 MeasureOverride
F1_Loaded
F2_Loaded

也就是说 F2 的 Loaded 事件被触发了。但也仅仅只是 Loaded 事件被触发,而 MeasureOverride 方法没有被调用

通过以上的例子即可说明,想要让子自定义控件的 Loaded 事件能够正常被触发,是需要在 GetVisualChild 里返回子自定义控件

接下来继续测试其他的重写方法,比如命中测试和 OnRender 方法。先在以上代码的基础上,添加 HitTestCore 和 OnRender 方法,同时为了展现效果,也在 OnRender 里面绘制一个圆形,代码如下

class F2 : FrameworkElement
{
... // 忽略其他代码 protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawEllipse(Brushes.Red, null, new Point(30, 30), 20, 20);
} protected override HitTestResult HitTestCore(PointHitTestParameters hitTestParameters)
{
Debug.WriteLine($"F2 HitTestCore");
return base.HitTestCore(hitTestParameters);
}
}

加上以上代码之后,继续运行程序。可以看到无论鼠标怎么晃,都不会进入 F2 的 HitTestCore 命中测试方法。同时 F2 绘制的圆形也无法在界面上看到。也就是说仅仅只有 重写 VisualChildrenCount 属性和 GetVisualChild 方法对此需求来说还是不够。在有需要将子自定义控件的 OnRender 方法的内容打到界面上以及让子自定义控件参与命中测试时,还需要加上更多的代码

先分析一下为什么 F2 的 OnRender 方法没有在界面打出来绘制的圆形。在 OnRender 方法上打断点,运行代码,可以看到断点没有进来

根据 dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标 博客,可以了解到,在 WPF 里面是会在 Arrange 方法里面调用 OnRender 方法的。换句话说就是,想要 OnRender 方法被调用,那就需要调用 Arrange 方法

了解了这个问题之后,解决方法也就自然知道了,既然没有调用 Arrange 方法,那就不妨调用一下。修改之后的代码如下

    public F1()
{
Width = 500;
Height = 500; F2 = new F2(); F2.Arrange(new Rect(new Point(), new Size(100, 100)));
}

修改完成之后,运行代码,即可看到 F2 的内容可以打到界面上了

以上代码是在 F1 里面调用 F2.Arrange 方法,那直接在 F2 里面自己调用自己呢?其实也是可以的,尽管这样不太符合设计。因为 WPF 框架设计上 Arrange 就是专门给上一级控件在布局时调用的。尽管不符合设计,但是也是能解决问题

    public F2()
{
Width = 500;
Height = 500; Loaded += F2_Loaded; Arrange(new Rect(new Point(), new Size(100, 100)));
}

这里还存在另一个问题,那就是布局裁剪问题。默认 WPF 在 FrameworkElement 将会自动裁剪超过布局传入尺寸的画面。比如 Arrange 方法的 Size 参数是 100x100 时,实际渲染的 RenderSize 却是 200x200 尺寸,默认行为下,只有 100x100 的界面内容可见

可以通过重写 GetLayoutClip 方法重新设置布局裁剪,如此即可方便让渲染内容超过实际画布大小。对于继承 FrameworkElement 元素的控件来说,默认 WPF 将会自动裁剪超过布局传入尺寸的画面,除非重写 GetLayoutClip 修改行为。对于继承 UIElement 元素的控件来说,取决于 ClipToBounds 属性,默认此 ClipToBounds 属性是 false 值,意味着不会自动裁剪,如果设置 true 的值,将会返回裁剪大小为 RenderSize 尺寸。以下是 UIElement 的源代码

public class UIElement
{
... // 忽略其他代码
protected virtual Geometry GetLayoutClip(Size layoutSlotSize)
{
if(ClipToBounds)
{
RectangleGeometry rect = new RectangleGeometry(new Rect(RenderSize));
rect.Freeze();
return rect;
}
else
return null;
}
}

无论如何,重写 GetLayoutClip 都可以实现绘制界面超过布局尺寸,重写 GetLayoutClip 方法可以返回一个几何裁剪,如果无需任何裁剪,则返回 null 值,如以下代码

class F2 : FrameworkElement
{
public F2()
{
Width = 500;
Height = 500; Loaded += F2_Loaded; Arrange(new Rect(new Point(), new Size(1, 1)));
} protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
return null;
} ... // 忽略其他代码
}

尽管 Arrange 传入是 1x1 尺寸,但是通过重写 GetLayoutClip 返回 null 从而让 F2 绘制的内容可以绘制到界面

命中测试也是依存于布局的功能,命中测试需要在元素具备布局尺寸才会被调用。同时可参与命中测试的元素也要求是在视觉树上的元素,为了让一个元素能够参与命中测试,也就是让控件的 HitTestCore 方法被触发,就需要让控件加入到视觉树上。可以通过调用 AddVisualChild 方法让控件加入到视觉树上,代码如下

class F1 : FrameworkElement
{
public F1()
{
Width = 500;
Height = 500; F2 = new F2(); AddVisualChild(F2); F2.Arrange(new Rect(new Point(), new Size(100, 100)));
} ... // 忽略其他代码
}

修改之后的代码放在githubgitee 欢迎访问

获取这一步骤的代码,可以在上文获取源代码的基础上,在 KearkemnerwhayneqiChaywibelfo 文件夹里面继续输入以下代码进行获取

git pull origin 383ccb0c09f41ab676feae36fe5085898255b347

运行代码,然后晃动鼠标,在 F2 的 HitTestCore 方法上打断点,可以看到进入断点,证明 F2 的 HitTestCore 被调用

如果发现自己自定义的控件里面,子自定义控件的 HitTestCore 命中测试没有被触发,除了看 IsHitTestVisible 属性之外,还需要关注一下控件元素是否已经被布局了,且布局尺寸符合预期,同时控件元素也加入到视觉树上

以上就是通过简单的代码告诉大家 WPF 自定义控件的多个可重写方法的用法和意义

更多博客,请参阅我的 博客导航

WPF 自定义控件入门 可重写的各个方法或属性的意义的更多相关文章

  1. 第8.30节 重写Python __setattr__方法实现属性修改捕获

    一. 引言 在<第8.26节 重写Python类中的__getattribute__方法实现实例属性访问捕获>章节介绍了__getattribute__方法,可以通过重写该方法,截获所有通 ...

  2. c#反射入门篇(Reflection)——MethodInfo 发现方法的属性

    网站:https://www.jianshu.com/p/52dc85668d00 也算记录自己的学习篇=.= 适合入门看 这里简单介绍下MethodInfo和他基本的几个方法 简介 MethodIn ...

  3. WPF自定义控件(二)の重写原生控件样式模板

    话外篇: 要写一个圆形控件,用Clip,重写模板,去除样式引用圆形图片可以有这三种方式. 开发过程中,我们有时候用WPF原生的控件就能实现自己的需求,但是样式.风格并不能满足我们的需求,那么我们该怎么 ...

  4. [WPF自定义控件]从ContentControl开始入门自定义控件

    1. 前言 我去年写过一个在UWP自定义控件的系列博客,大部分的经验都可以用在WPF中(只有一点小区别).这篇文章的目的是快速入门自定义控件的开发,所以尽量精简了篇幅,更深入的概念在以后介绍各控件的文 ...

  5. WPF快速入门系列(1)——WPF布局概览

    一.引言 关于WPF早在一年前就已经看过<深入浅出WPF>这本书,当时看完之后由于没有做笔记,以至于我现在又重新捡起来并记录下学习的过程,本系列将是一个WPF快速入门系列,主要介绍WPF中 ...

  6. WPF自定义控件与样式(15)-终结篇 & 系列文章索引 & 源码共享

    系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与样式(3)-TextBox & Ric ...

  7. WPF自定义控件与样式(12)-缩略图ThumbnailImage /gif动画图/图片列表

    一.前言 申明:WPF自定义控件与样式是一个系列文章,前后是有些关联的,但大多是按照由简到繁的顺序逐步发布的等,若有不明白的地方可以参考本系列前面的文章,文末附有部分文章链接. 本文主要针对WPF项目 ...

  8. WPF自定义控件与样式(15)-终结篇

    原文:WPF自定义控件与样式(15)-终结篇 系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与 ...

  9. WPF自定义控件(四)の自定义控件

    在实际工作中,WPF提供的控件并不能完全满足不同的设计需求.这时,需要我们设计自定义控件. 这里LZ总结一些自己的思路,特性如下: Coupling UITemplate Behaviour Func ...

  10. WPF自定义控件(三)の扩展控件

    扩展控件,顾名思义就是对已有的控件进行扩展,一般继承于已有的原生控件,不排除继承于自定义的控件,不过这样做意义不大,因为既然都自定义了,为什么不一步到位呢,有些不同的需求也可以通过此来完成,不过类似于 ...

随机推荐

  1. vue项目,关闭eslint语法检测

    vue.config.js文件中 module.exports = { lintOnSave:false //关闭语法检查 } 然后重启项目生效!

  2. ResNet-RS:谷歌领衔调优ResNet,性能全面超越EfficientNet系列 | 2021 arxiv

    论文重新审视了ResNet的结构.训练方法以及缩放策略,提出了性能全面超越EfficientNet的ResNet-RS系列.从实验效果来看性能提升挺高的,值得参考   来源:晓飞的算法工程笔记 公众号 ...

  3. KingbaseES V8R6 中syssql_tmp目录说明

    前言 不久前有前端人员咨询过一个问题,为什么syssql_tmp目录下会产生如此多的大文件. 针对这个目录的解释是:临时文件(用于排序超出内存容量的数据等操作)是在$KINGBASE_DATA/bas ...

  4. SQL日期操作函数(CONCAT、DATE_FORMAT、LAST_DAY)

    获取某月底日期:SELECT LAST_DAY('2021-07-01') AS month_end_date; 拼接年月格式: CONCAT(DATE_FORMAT(hp.planned_payme ...

  5. 下载标准国民经济行业分类与代码GB/T 4754-2011,存入mysql数据库

    戳链接下载:https://download.csdn.net/download/weixin_45556024/34913490 或关注公众号[靠谱杨阅读人生]回复[行业]获取. 整理不易,资源fu ...

  6. 【已解决】idea编译器插入数据到数据库乱码以及jsp页面乱码的解决方法

    1.jsp页面需要设置编码格式为utf-8 1 <%@ page contentType="text/html;charset=UTF-8" language="j ...

  7. C++设计模式 - 解析器模式(Interpreter)

    领域规则模式 在特定领域中,某些变化虽然频繁,但可以抽象为某种规则.这时候,结合特定领域,将问题抽象为语法规则,从而给出在该领域下的一般性解决方案. 典型模式 Interpreter Interpre ...

  8. Java 编程实例:相加数字、计算单词数、字符串反转、元素求和、矩形面积及奇偶判断

    Java如何相加两个数字 相加两个数字 示例 int x = 5; int y = 6; int sum = x + y; System.out.println(sum); // 打印 x + y 的 ...

  9. HMS Core分析服务智能运营,“智能时机”上线,轻松提升Push点击

    对于运营者来说,消息推送一直是提升用户活跃与转化的重要工具,如何在提升转化的情况下,同时不降低用户的接受程度,这一直是运营不断追求的目标. 好的推送不只在于优质的推送内容,还需要把握合适的时机.在合适 ...

  10. Linux 编译 libjpeg-9e

    jpeg的库有两个:一个是官方的 libjpeg  还有一个是 libjpeg-turbo JPEG库(libjpeg-turbo):https://libjpeg-turbo.org/ Libjpe ...