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>

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

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

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

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

我们使用自定义装饰元素。通过修改上面的例子来完成。我们新建一个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> 

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

posted @ 2021-05-20 22:38  杜文龙  阅读(4207)  评论(1编辑  收藏  举报