WPF教程十四:了解元素的渲染OnRender()如何使用

时间:2023-03-10 00:32:14
WPF教程十四:了解元素的渲染OnRender()如何使用

上一篇分析了WPF元素中布局系统的MeasureOverride()和ArrangeOverride()方法。本节将进一步深入分析和研究元素如何渲染它们自身。

大多数WPF元素通过组合方式创建可视化外观。元素通过其他更基础的元素进行构建。比如,使用标记定义用户控件的组合元素,处理标记方式与自定义窗口中的XAML相同。使用控件模板为自定义控件提供可视化树。并且当创建自定义面板时,根本不必定义任何可视化细节。组合元素由控件使用者提供,并添加到Children集合中。

​ 接下来就是绘制内容,在WPF中,一些累需要负责绘制内容。在WPF中,这些类位于元素树的底层。在典型窗口中,是通过单独的文本、形状以及位图执行渲染的,而不是通过高级元素。

OnRender()方法

为了执行自定义渲染,元素必须重写OnRender()方法,该方法继承自UIElement基类。一些控件使用OnRender()方法绘制可视化细节并使用组合在其上叠加其他元素。Border和Panel类是两个例子,Border类在OnRender()方法中绘制边框,Panel类在OnRender()方法中绘制背景。Border和Panel类都支持子内容,并且这些子内容在自定义的绘图细节之上进行渲染。

OnRender()方法接收一个DrawingCntext对象,该对象为绘制内容提供了一套很有用的方法。在OnRender()方法中执行绘图的主要区别是不能显式的创建和关闭DrawingContext对象。这是因为几个不同的OnRender()方法可能使用相同的DrawingContext对象。例如派生的元素可以执行一些自定义绘图操作并调用基类中的OnRender()方法来绘制其他内容。这种方法是可行的,因为当开始这一过程时,WPF会自动创建DrawingContext对象,并且当不再需要时关闭该对象。

OnRender()方法实际上没有将内容绘制到屏幕上,而是绘制到DrawingContext对象上,然后WPF缓存这些信息。WPF决定元素何时需要重新绘制并绘制使用DrawingContext对象创建的内容。这是WPF保留模式图形系统的本质--由开发人员定义内容,WPF无缝的管理绘制和刷新过程。

关于WPF渲染,大多数类是通过其他更简单的类构建的,并且对于典型的控件,为了找到实际重写OnRender()方法的类,需要进入到控件元素树种非常深的层次。下面是一些重写了OnRender()方法的类:

  • TextBlock类 无论在何处放置文本,都会有TextBlock对象使用使用OnRender()方法绘制文本。
  • Image类。Image类重写OnRender()方法,使用DrawingContext.DrawImage()方法绘制图形内容。
  • MediaElement类。如果正在使用该类播放视频文件,该类会重写OnRender()方法以绘制视频帧。
  • 各种形状类。Shape基类重写了OnRender()方法,通过使用DrawingContext.DrawGeometry()方法,绘制在其内部存储的Geometry对象。根据Shape类的特定派生类,Geometry对象可以表示椭圆、矩形、或更复杂的由直线和曲线构成的路径。许多元素使用形状绘制小的可视化细节。
  • 各种修饰类。比如ButtonChrome和ListBoxChrome绘制通用控件的外侧外观,并在具体指定的内部放置内容。其他许多继承自Decorator的类,如Border类,都重写了OnRender()方法。
  • 各种面板类。尽管面板的内容是由其子元素提供的,但是OnRender()方法绘制具有背景色(假设设置了Background属性)的矩形。

重写OnRender()方法不是渲染内容并且将其添加到用户界面的唯一方法。也可以创建DrawingVisual对象,并使用AddVisualChild()方法为UIElement对象添加该可视化对象,然后调用DrawingVisual.RenderOpen()方法为DrawingVisual对象检索DrawingContext对象,并使用返回的DrawingContext对象渲染DrawingVisual对象的内容。

在WPF种,一些元素使用这种策略在其他元素内容之上现实一些图形细节。例如在拖放指示器、错误提示器以及焦点框种可以看到这种情况。在所有这些情况种,DrawingVisual类允许元素在其他内容之上绘制内容,而不是在其他内容之下绘制内容。但对于大部分情况,是在专门的OnRender()方法种进行渲染。

写了这么多是不是不好理解?多看几遍,这里我除了比较啰嗦的引跑题的内容,其他的基本上原封不动的抄了过来,或者等看完下面的内容,在回来上面从新读一遍,上面的内容主要是讲应用场景,我自认为我总结的没有他的好《编程宝典》,就全拿过来了。

请注意,可能看到这里就发现这些东西也不常用,为啥要放到这个入门的系列里。因为在某些场景下,这种OnRender()更适用。因为前段时间熬了半个月的夜,写一个通过Stylus写字时字体美化的效果,主要逻辑就是OnRender()这些相关的内容,所以我觉得在客户端开发中,会遇到这种使用OnRender()能更好更快速解决问题的场景,现在开始本章的学习。

什么场合合适使用较低级的OnRender()方法。

大多数自定义元素不需要自定义渲染。但是当属性发生变化或执行特定操作时,需要渲染复杂的变化又特别大的可视化外观,此时使用自定义的渲染方法可能更加简单并且更便捷。

我们通过一段代码来演示一个简单的效果。我们在用户移动鼠标时,显示一个跟随鼠标的光圈。

我们创建名为CustomDrawnElement.cs的类,继承自FrameworkElement类,该类只提供一个可以设置的属性渐变的背景色(前景色被硬编码为白色)。

使用Propdp=>2次tab创建依赖项属性BackgroundColor。注意这里的Metadata被修改为FrameworkPropertyMetadata,并且设置了AffectsRender,F12跳转过去,提示更改此依赖属性的值会影响呈现或布局组合的某一方面(不是测量或排列过程)。因此,无论何时改变了背景色,WPF都会自动调用OnRender()方法。当鼠标移动时,也需要确保调用了OnRender()方法。通过在合适的位置使用InvalidateVisual()方法来实现。

  public class CustomDrawnElement : FrameworkElement
{
public Color BackgroundColor
{
get { return (Color)GetValue(BackgroundColorProperty); }
set { SetValue(BackgroundColorProperty, value); }
} // Using a DependencyProperty as the backing store for BackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElement), new FrameworkPropertyMetadata(Colors.Yellow, FrameworkPropertyMetadataOptions.AffectsRender));
protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
this.InvalidateVisual();
} protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
this.InvalidateVisual();
}
}

当这些都做完时,剩下就是我们需要重写的OnRender()方法了。我们通过这个方法绘制元素背景。ActualWidth和ActualHeight属性指示控件最终的渲染尺寸。为了保证能在当前鼠标正确的位置来渲染,我们需要一个方法来计算当前鼠标位置和渲染的中心点。

  protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight);
drawingContext.DrawRectangle(GetForegroundBrush(), null, bounds); } private Brush GetForegroundBrush()
{
if (!IsMouseOver)
{
return new SolidColorBrush(Color.FromRgb(0x7D, 0x7D, 0xFF));
}
else
{
RadialGradientBrush brush = new RadialGradientBrush(Color.FromRgb(0xE0, 0xE0,0xE0), Color.FromRgb(0x7D, 0x7D, 0xFF));
brush.RadiusX = 0.9;
brush.RadiusY = 0.9;
Point absoluteGradientOrigin = Mouse.GetPosition(this);
Point relativeGradientOrigin = new Point(absoluteGradientOrigin.X / base.ActualWidth, absoluteGradientOrigin.Y / base.ActualHeight);
brush.GradientOrigin = relativeGradientOrigin;
brush.Center = relativeGradientOrigin;
return brush;
}
}

在主窗体中添加对该元素的使用:

<Window x:Class="CustomOnRender.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CustomOnRender"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<local:CustomDrawnElement Width="400" Height="300"/>
<StackPanel Margin="100">
<TextBlock Text="测试TextBlock" Width="100" />
<Button Width="120" Content="fffff"/>
</StackPanel>
</Grid>
</Window>

WPF教程十四:了解元素的渲染OnRender()如何使用

但是如果这么实现的话,就会出现一个和之前学习内容矛盾的问题,如果在控件中使用自定义绘图的话,我们硬编码了绘图逻辑,控件的可视化外观就不能通过模板进行定制了。

更好的办法是设计单独的绘制自定义内容的元素,然后再控件的默认模板内部使用自定义元素。

自定义绘图元素通常扮演两个角色:

  • 它们绘制一些小的图形细节,(滚动按钮上的箭头)。
  • 它们再另一个元素周围提供更加详细的背景或边框。

我们使用自定义装饰元素。通过修改上面的例子来完成。我们新建一个CustomDrawnDecorator类继承自Decorator类;

重新修改代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media; namespace CustomOnRender
{
public class CustomDrawnElementDecorator : Decorator
{
public Color BackgroundColor
{
get { return (Color)GetValue(BackgroundColorProperty); }
set { SetValue(BackgroundColorProperty, value); }
} // Using a DependencyProperty as the backing store for BackgroundColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty BackgroundColorProperty = DependencyProperty.Register("BackgroundColor", typeof(Color), typeof(CustomDrawnElementDecorator), new FrameworkPropertyMetadata(Colors.Yellow, FrameworkPropertyMetadataOptions.AffectsRender)); protected override void OnMouseMove(MouseEventArgs e)
{
base.OnMouseMove(e);
this.InvalidateVisual();
} protected override void OnMouseLeave(MouseEventArgs e)
{
base.OnMouseLeave(e);
this.InvalidateVisual();
} protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
Rect bounds = new Rect(0, 0, base.ActualWidth, base.ActualHeight);
drawingContext.DrawRectangle(GetForegroundBrush(), null, bounds); } private Brush GetForegroundBrush()
{
if (!IsMouseOver)
{
return new SolidColorBrush(Color.FromRgb(0x7D, 0x7D, 0xFF));
}
else
{
RadialGradientBrush brush = new RadialGradientBrush(Color.FromRgb(0xE0, 0xE0, 0xE0), Color.FromRgb(0x7D, 0x7D, 0xFF));
brush.RadiusX = 0.9;
brush.RadiusY = 0.9;
Point absoluteGradientOrigin = Mouse.GetPosition(this);
Point relativeGradientOrigin = new Point(absoluteGradientOrigin.X / base.ActualWidth, absoluteGradientOrigin.Y / base.ActualHeight);
brush.GradientOrigin = relativeGradientOrigin;
brush.Center = relativeGradientOrigin;
return brush;
}
}
protected override Size MeasureOverride(Size constraint)
{
//return base.MeasureOverride(constraint);
UIElement child = this.Child;
if (child != null)
{
child.Measure(constraint);
return child.DesiredSize;
}
else
{
return new Size();
}
}
}
}
<Window x:Class="CustomOnRender.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CustomOnRender"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.Resources>
<ControlTemplate x:Key="WithCustomChrome" >
<local:CustomDrawnElementDecorator BackgroundColor="LightGray">
<ContentPresenter Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
Content="{TemplateBinding ContentControl.Content}" RecognizesAccessKey="True"/>
</local:CustomDrawnElementDecorator>
</ControlTemplate>
</Window.Resources> <Page Template="{StaticResource WithCustomChrome}">
<StackPanel Margin="100">
<TextBlock Text="测试TextBlock" Width="100" />
<Button Width="120" Content="fffff"/>
</StackPanel>
</Page>
<!-- <local:CustomDrawnElement Width="400" Height="300"/>-->
</Window>

WPF教程十四:了解元素的渲染OnRender()如何使用

这篇主要内容就是如何使用OnRender()方法进行重绘。目前就这么多拉。