每个 .NET 控件都有许多方法,每个方法都以前缀 On
命名,例如 OnInitialized
或 OnApplyTemplate
。 这些是受保护的方法,可以在任何扩展 .NET 控件的自定义类中重写。 它们在控件生命周期中的某些点被调用,使我们能够更改每个控件的默认行为。
它们使我们能够做一些简单的事情,例如在控件初始化后立即启动进程,或者在应用后从自定义 ControlTemplate
访问命名控件。 但它们也可用于完全改变默认行为,或控件的外观。 在本章中,我们将研究这些方法,并举例说明如何利用它们来发挥我们的优势。
然后,我们将进一步研究自定义内置控件的方法,方法是调整它们的默认 ControlTemplate
并利用它们的新用途,同时维护或扩展它们现有的功能。 在本章中,我们将内置控件视为满足我们需求的起点,并学习如何在它们的基础上进行构建,保留我们需要的东西并改变我们不需要的东西。
检查受保护的方法
每个 .NET 控件都有几种方法,使开发人员能够扩展该控件以与其交互或更改其功能。请注意,这些不是事件,而是受保护的方法,它们在控件的整个生命周期中的特定时间点被调用。 正如我们在第 5 章中已经看到的,为作业使用正确的控件,每个 .NET 控件都扩展了许多基类,每个基类都提供某些附加功能。
以类似的方式,每个基类也提供了许多这些受保护的方法,使我们能够在内部与控件进行交互。 在本章中,我们还将展示如何创建自己的方法,使扩展我们自己的控件类的开发人员能够适应或扩展其功能。
我们先来看看Window
类的protected
方法:
protected override Size ArrangeOverride(Size arrangeBounds);
protected override Size MeasureOverride(Size availableSize);
protected virtual void OnActivated(EventArgs e);
protected virtual void OnClosed(EventArgs e);
protected virtual void OnClosing(CancelEventArgs e);
protected override void OnContentChanged(object oldContent, object newContent);
protected virtual void OnContentRendered(EventArgs e);
protected override AutomationPeer OnCreateAutomationPeer();
protected virtual void OnDeactivated(EventArgs e);
protected virtual void OnLocationChanged(EventArgs e);
protected override void
OnManipulationBoundaryFeedback(ManipulationBoundaryFeedbackEventArgs. e);
protected virtual void OnSourceInitialized(EventArgs e);
protected virtual void OnStateChanged(EventArgs e);
protected internal sealed override void OnVisualParentChanged(DependencyObject oldParent);
您可能会注意到它们都标有 virtual
或 override
关键字,表明它们可以在扩展类中被覆盖。 除了我们在第 5 章中发现的 ArrangeOverride
和 MeasureOverride
方法之外,您应该看到它们的名称都以前缀 On
开头。 这意味着他们被要求采取一些行动。
例如,当 Window 成为计算机上的活动窗口时调用 OnActivated
方法,而当 Window 失去焦点时调用 OnDeactivated
方法。 这些方法通常一起用于暂停和恢复动画或其他进程,而 Window 不在焦点上。
正如预期的那样,在 Window 关闭时调用 OnClosed
方法,让我们有机会在关闭应用程序之前释放任何资源,或保存用户首选项。 相反,在窗口关闭之前调用 OnClosing
方法,让我们有机会取消关闭操作。
因此, OnClosing
方法将是显示对话框的好方法,要求用户确认关闭操作。 让我们快速看一下如何在扩展 Window 类的类中实现这一点:
using System.ComponentModel;
using System.Windows;
...
protected override void OnClosing(CancelEventArgs e)
{
base.OnClosing(e);
MessageBoxResult result = MessageBox.Show("Are you sure you want to close?",
"Close Confirmation",
MessageBoxButton.OKCancel,
MessageBoxImage.Question);
e.Cancel = result == MessageBoxResult.Cancel;
}
在这个简单的示例中,我们重写了 OnClosing
方法,在其中,我们首先调用基类方法,以确保任何基类例程都按预期运行。 然后我们向用户显示一个消息框,要求他们确认他们的关闭操作。
使用通过消息框按钮从用户获得的结果值,我们设置传递给方法的 CancelEventArgs
对象的 Cancel
属性。 如果返回值为 Cancel
,则将 Cancel
属性设置为 true
并取消关闭操作,否则设置为 false
并关闭应用程序。
现在回到 Window
类,我们看到 OnLocationChanged
方法,每当 Window
以移动其左上角的方式移动或调整大小时都会调用该方法。 我们可以使用此方法保存 Window
的最后位置,以便下次用户打开他们的应用程序时可以将其返回。 然而,此操作更典型地在用户关闭应用程序时执行。
OnSourceInitialized
方法在创建窗口源之后调用,但在显示之前调用,并且在 WindowState
属性更改时调用 OnStateChanged
方法。 所以你看,这些方法为我们提供了在每个控件的生命周期中的特定点执行操作的机会。
每个基类都添加了自己的这些受保护方法的集合以供我们利用,并且在扩展类中会覆盖感兴趣的方法。 查看 Window
类声明,我们看到它扩展了 ContentControl
类。 请注意,它的 OnContentChanged
方法标有 override
关键字。
这是因为实际上在 ContentControl
类中声明的这个方法已经在 Window
类中被覆盖,以便它可以在执行基类功能后添加自己的代码。 让我们从 Window
类看一下这个方法的源代码。 为简洁起见,已删除源代码中的注释:
protected override void OnContentChanged(object oldContent, object newContent)
{
base.OnContentChanged(oldContent, newContent);
SetIWindowService();
if (IsLoaded == true)
{
PostContentRendered();
}
else
{
if (_postContentRenderedFromLoadedHandler == false)
{
this.Loaded += new RoutedEventHandler(LoadedHandler);
_postContentRenderedFromLoadedHandler = true;
}
}
}
该方法首先调用该方法的基类版本,这始终是一个好习惯,除非我们想要停止执行现有功能。 接下来,它调用 SetIWindowService
方法,该方法只是将 Window
对象设置为 IWindowServiceProperty
依赖属性,然后检查 Window
是否已通过加载阶段。
如果有,则调用 PostContentRendered
方法,该方法基本上使用 Dispatcher
对象调用 OnContentRendered
方法。 否则,如果 _postContentRenderedFromLoadedHandler
变量为 false
,它会将事件处理程序附加到 Loaded
事件并将变量设置为 true
,以确保它不会被附加多次。
现在回到我们的调查,我们看到 Window
类添加了与 Window
相关的受保护方法,而 ContentControl
类添加了与控件内容相关的受保护方法。 现在让我们看看 ContentControl
类的受保护方法:
protected virtual void AddChild(object value);
protected virtual void AddText(string text);
protected virtual void OnContentChanged(object oldContent, object newContent);
protected virtual void OnContentStringFormatChanged(
string oldContentStringFormat,
string newContentStringFormat);
protected virtual void OnContentTemplateChanged(
DataTemplate oldContentTemplate,
DataTemplate newContentTemplate);
protected virtual void OnContentTemplateSelectorChanged(
DataTemplateSelector oldContentTemplateSelector,
DataTemplateSelector newContentTemplateSelector);
除了前两个方法可用于将指定的对象或文本字符串添加到 ContentControl
元素之外,其余四个方法都是为了响应内容或控件内容格式的变化而调用的。
现在继续,ContentControl
类扩展了 Control
类,它引入了 ControlTemplate
的概念。 因此,它提供了一个受保护的 OnTemplateChanged
方法,该方法在 ControlTemplate
值更改时调用
protected override Size ArrangeOverride(Size arrangeBounds);
protected override Size MeasureOverride(Size constraint);
protected virtual void OnMouseDoubleClick(MouseButtonEventArgs e);
protected virtual void OnPreviewMouseDoubleClick(MouseButtonEventArgs e);
protected virtual void OnTemplateChanged(ControlTemplate oldTemplate,
ControlTemplate newTemplate);
Control
类扩展了 FrameworkElement
类,后者提供了框架级别的方法和事件。 其中包括鼠标、键盘、触控笔、触摸和与焦点相关的受保护方法,以及其他几种方法:
protected virtual Size ArrangeOverride(Size finalSize);
protected override Geometry GetLayoutClip(Size layoutSlotSize);
protected override Visual GetVisualChild(int index);
protected virtual Size MeasureOverride(Size availableSize);
protected virtual void OnContextMenuClosing(ContextMenuEventArgs e);
protected virtual void OnContextMenuOpening(ContextMenuEventArgs e);
protected override void OnGotFocus(RoutedEventArgs e);
protected virtual void OnInitialized(EventArgs e);
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e);
protected virtual void OnToolTipClosing(ToolTipEventArgs e);
protected virtual void OnToolTipOpening(ToolTipEventArgs e);
也许到现在为止,您会注意到其中许多方法名称与每个类引发的事件名称密切相关。 事实上,有一个 .NET Framework 编程指南可以让受保护的虚拟方法引发事件,以允许派生类覆盖事件调用行为,我们将在本章后面看到一个示例。
因此,当覆盖这些方法时,我们需要调用基类方法以引发相应的事件。 如有疑问,通常最好调用该方法的基类版本,以确保不会丢失默认功能。 但是,最好在 www.referencesource.microsoft.com 网站上查看基类方法源代码,以检查是否需要调用它。
您可能想知道处理事件和覆盖相关受保护方法之间的区别是什么,对此有一些答案,具体取决于所讨论的方法。 首先要指出的是,为了覆盖一个受保护的方法,我们需要声明一个声明该方法的类的子类。
那么,假设我们已经有一个扩展基类的类,有什么区别呢? 对于一些方法,比如我们探索的 OnClosing
方法,差别不大。 我们可以在附加到 Closing
事件的事件处理程序中实现相同的功能,尽管不需要调用基类方法。 事实上,这是唯一真正的区别。
当重写 OnClosing
方法时,我们可以控制何时或是否调用基类方法。 在处理事件时,我们无法控制它。 因此,如果我们需要在执行基类例程之前执行一些操作,或者如果我们想阻止它执行,那么我们将需要重写 OnClosing
方法。
所以,OnClosing
方法的出现,纯粹是为了方便,让我们能够改变 Closing
事件的默认行为。 然而,其他方法,例如 OnContextMenuClosing
方法,为我们引入了一种对相关事件执行类范围处理的方法。
但有时,我们别无选择,只能覆盖这些受保护的方法。通常,这些类型的方法不以前缀 On
开头,也不与任何事件相关。 有时,为了执行特定的操作,我们可能需要扩展一个类,以便我们可以为这些方法之一提供新的实现。
让我们看一个使用刚刚看到的 FrameworkElement
类中的 GetLayoutClip
方法的示例。
Clipping the layout
默认情况下,TextBlock
类在其边界矩形处剪切其文本内容,以便文本不会泄漏。 剪辑是切断控件的一部分可见输出的过程。 但是如果我们想让文本扩展它的边界呢?
有一个名为 Clip 的属性,我们通常使用它来调整控件的可见部分。 但是,这只能减少已经可见的内容。 它不能增加控件可用的渲染空间。 在继续我们的示例之前,让我们绕道而行来研究这个属性.
在 UIElement
类中定义的 Clip
属性将 Geometry
对象作为其值。 我们传递给它的对象可以从任何扩展 Geometry
类的类创建,包括 CombinedGeometry
类。 因此,被剪裁的对象可以做成任何形状。 我们来看一个简单的例子:
<Rectangle Fill="Salmon" Width="150" Height="100" RadiusX="25"
RadiusY="50">
<Rectangle.Clip>
<EllipseGeometry Center="150,50" RadiusX="150" RadiusY="50" />
</Rectangle.Clip>
</Rectangle>
在这里,我们使用 EllipseGeometry
对象使 Rectangle
元素显示为小子弹形状。 它的工作原理是显示位于 EllipseGeometry
对象的椭圆边界内的 Rectangle
元素中的所有图像像素,并隐藏所有位于边界外的像素。 让我们看一下这段代码的视觉输出:
回到我们之前的示例,TextBlock
类也以类似的方式剪辑其内容,但使用控件大小的矩形,而不是偏离中心的椭圆形。 而不是使用 Clip
属性,它为用户提供了剪辑控件的相同能力 正如其他控件提供的那样,它使用受保护的方法来请求几何对象在剪切过程中使用。
我们确实可以从该方法返回任何几何形状,但它不会具有与将形状传递给 Clip
属性相同的视觉效果。 对于我们的示例,我们不想限制控件的可见大小,而是删除控件边界处的剪切区域。
如果我们确切地知道我们想要设置剪裁范围的大小,我们可以从 GetLayoutClip
方法返回一个该大小的 Geometry
对象。 但是,出于我们的目的,并且为了使我们的任何自定义 TextBlock
对象能够将无穷无尽的文本泄漏到其边界之外,我们可以简单地从此方法返回 null
。 让我们看看两者之间的区别。
首先,我们通过扩展 TextBlock
类来创建我们的 BoundlessTextBlock
类。 可能,在 Visual Studio 中执行此操作的最简单方法之一是将 WPF
用户控件对象添加到我们的控件文件夹中,然后在 XAML 文件及其关联的代码隐藏文件中将单词 UserControl
替换为单词 TextBlock
。 两者都更改失败将导致设计时错误,抱怨“BoundlessTextBlock
”的部分声明不得指定不同的基类:
<TextBlock
x:Class="CompanyName.ApplicationName.Views.Controls.BoundlessTextBlock"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" />
从这个例子中可以看出,我们的 XAML
文件可以非常空,并且为了我们的需求,我们只需要覆盖文件隐藏代码中的单个 GetLayoutClip
方法。 在第一个示例中,我们将返回一个与将在用户界面中使用的文本块大小相同的 EllipseGeometry
对象:
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace CompanyName.ApplicationName.Views.Controls
{
public partial class BoundlessTextBlock : TextBlock
{
public BoundlessTextBlock()
{
InitializeComponent();
}
protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
return new EllipseGeometry(new Rect(new Size(150, 22)));
}
}
}
让我们看看如何使用我们的新类。 首先,我们需要定义一个映射到我们保存类的 CLR
命名空间的 XAML
命名空间。 接下来,出于演示目的,我们将 BoundlessTextBlock
对象包装在 Border 对象中,以便我们可以看到它的自然边界:
xmlns:Controls="clr-namespace:CompanyName.ApplicationName.Views.Controls"
...
<Border BorderBrush="Black" BorderThickness="1"
HorizontalAlignment="Center"
VerticalAlignment="Center" SnapsToDevicePixels="True">
<Controls:BoundlessTextBlock Text="Can you see what has happened?"
Background="Aqua" FontSize="14" Width="150" Height="22" />
</Border>
让我们看一下这个例子的视觉输出:
如您所见,BoundlessTextBlock
对象的视觉输出已被限制为仅显示位于从 GetLayoutClip
方法返回的 EllipseGeometry
对象内的像素。 但是如果我们返回一个大于我们自定义文本块的 EllipseGeometry
对象会发生什么? 让我们通过返回这个对象来找出答案:
return new EllipseGeometry(new Rect(new Size(205, 22)));
现在,查看 BoundlessTextBlock
对象的视觉输出,我们可以看到自定义文本块的内容现在超出了边界,这要归功于 Border
对象和蓝色背景:
因此,我们可以看到,使用从 GetLayoutClip
方法返回的 Geometry
对象应用的剪辑不仅不受控件自然边界的影响,而且实际上可以直接改变它们。 回到我们在这个主题上的最初想法,如果我们愿意的话 要完全移除控件边界处的剪裁,我们可以简单地从此方法返回 null
:
protected override Geometry GetLayoutClip(Size layoutSlotSize)
{
return