WPF绘图(二):绘制图形

发布时间 2023-10-19 18:46:45作者: 卓尔不设凡

WPF绘制图形有三种方式:

  • 使用FrameworkElement的派生类
  • 使用图元转换器绘制几何图形
  • 使用DrawingContext绘制

1. 使用FrameworkElement派生类

FrameworkElement类继承自UIElement类,意味它的派生类,都是UI元素,可以直接显示在界面上中。例如Shape的子类,Control的子类等。这是最简单的方式,就不再赘述。

2. 使用图元转换器绘制图形

上一篇文章已经介绍过Geometry了。但Geometry创建的仅仅是“理论上”的几何图形,不能直接显示出来。如果还要显示出来,还需要借助以下两个类:

  • XXDrawing类(Drawing在这里不是动词,而是名词“图画”的意思,XX代表不同的内容)。
  • DrawingXX类(“转换器”,XX代表不同的内容)

  2.1 图元包装器:Drawing

    Drawing类用于包装2D图元(图元可以是Geometry,也可以是GlyphRun、Image、Video),并为其添加一些额外的信息,比如Pen,Brush等属性。

    它是一个抽象类,可以使用它的派生类来包装不同的图形:

 

类名 功能
GeometryDrawing 包装几何图形
GlyphRunDrawing 包装字符
ImageDrawing 包装图片
VideoDrawing 包装视频
DrawingGroup 将多个Drawing对象组合到一个绘图器中,进行统一包装

  2.2 图元转换器:DrawingXX

    使用Drawing类的子类包装之后,它依旧不能直接显示出来。因为它还只是“理论上”的图元,还需要借助“图元转换器”来显示。图元转换器有两种:

类名 功能
DrawingBrush 将Drawing对象当成画刷。类似与Photoshop中的画刷功能
DrawingImage 将Drawing对象当成ImageSource。

    2.2.1 DrawingBrush

      DrawingBrush的作用是将Drawing对象转换为Brush。在任何需要用到Brush的地方,就可以使用它。

// 创建几何图形
EllipseGeometry cicle = new(new Point(5, 5), 5, 5);
EllipseGeometry cicle2 = new(new Rect(new Size(20, 16)));
GeometryGroup geoGroup = new() { Children = { cicle, cicle2 }, FillRule= FillRule.EvenOdd };

// 创建图画
GeometryDrawing drawing = new(Brushes.Red, new Pen(Brushes.Blue, 2), geoGroup);

// 创建DrawingBrush
DrawingBrush brushes = new DrawingBrush(drawing);

border.BorderThickness = new Thickness(50);
border.Background = brushes;

    2.2.2. DrawingImage

      DrawingImage的作用是把Drawing对象转换为ImageSource。在任何需要ImageSource的地方,就可以使用它。

// 创建几何图形
EllipseGeometry cicle = new(new Point(5, 5), 5, 5);
EllipseGeometry cicle2 = new(new Rect(new Size(20, 16)));
GeometryGroup geoGroup = new() { Children = { cicle, cicle2 }, FillRule= FillRule.EvenOdd };

// 创建图画
GeometryDrawing drawing = new(Brushes.Red, new Pen(Brushes.Blue, 2), geoGroup);

// 创建DrawingBrush
DrawingBrush brushes = new DrawingBrush(drawing);

DrawingImage image = new(drawing);
img.Source = image;

3. 使用DrawingContext绘制图形

DrawingContext是真正的“绘图”的类。它提供了一系列的方法来绘制图元。但是它是一个抽象类,不能直接创建类的实例。而且也不能创建它的子类(因为它没有公开的构造函数)。

  3.1 创建DrawingContext对象

    要创建DrawingContext对象,有两种方式:

    • 使用类的方法
    • 继承自UIElement类

    3.1.1 使用类的方法:

      某些类提供一个方法以创建DrawingContext对象,例如DrawingVisual类的RendOpen()方法、DrawingGroup类Open()方法。

// DrawingVisual.RendOpen()方法
DrawingVisual dv = new DrawingVisual();
using DrawingContext context = dv.RenderOpen();
context.DrawXX(...);     // 绘制图形

// DrawingGroup.Open()方法
DrawingGroup dg = new DrawingGroup();
using DrawingContext dc = dg.Open();
dc.DrawXX(...);           // 绘制图形

    3.1.2 继承自UIElement类

    public class visualElement:UIElement
    {
        protected override void OnRender(DrawingContext drawingContext)
        {
            base.OnRender(drawingContext);
        }
    }

  注释:

    • UIElement类提供了OnRender()方法,但在工作中,不一定要直接继承UIElement,可以从Framework,或者Control类继承。
    • OnRender()方法中的DrawingContext对象,是系统传递过来的,而不是自己的代码创建的。因此可以直接使用。

  3.2 显示DrawingContext对象

    创建完DrawingContext对象,并用其绘制了图元后的,发现它依旧无法显示出来。为什么呢?这是因为DrawingContext不是一个Visual对象,而仅仅只是一个绘图环境,绘制的图形仅保存在系统中。如果要显示出来,就必须将其转换为Visual对象。

    3.2.1 使用DrawingVisual对象

      在3.1.1的代码示例中,使用DrawingVisual类的RendOpen()方法创建了DrawinContext对象。而DrawingVisual就是一个Visual的对象,那么要如何才能显示它呢?

      根据微软官方的解释,需要为DrawingVisual对象提供一个容器,它的约束条件如下:

      • 容器必须继承自FrameworkElement;
      • 必须重写GetVisualChild()方法;
      • 必须重写VisualChildrenCount属性。
      • 在界面元素中添加该容器的实例。

      示例代码如下:

      (1)创建可视化对象的内容

    public class VisualElement : FrameworkElement
    {

        readonly DrawingVisual dv = new();

        // 重写VisualChildrenCount属性
        protected override int VisualChildrenCount => dv.Children.Count;


        // 重写GetVisualChild()方法
        protected override Visual GetVisualChild(int index)
        {
            return dv.Children[index];
        }


        public VisualElement()
        {
            // 确定dv在VisualTree的父级。否则无法支持鼠标事件
            this.AddVisualChild(dv);

            // 确定dv在逻辑树上的父级。
            this.AddLogicalChild(dv);
        }

        // 添加视觉对象
        public virtual void AddVisual(Visual visual)
        {
            if (visual == null) return;
            dv.Children.Add(visual);
        }

        public void DrawLine(Pen pen, Point start, Point end)
        {
            DrawingVisual visual = new();
            using DrawingContext dc = visual.RenderOpen();
            dc.DrawLine(pen, start, end);

            AddVisual(visual);
        }
    }

      (2)显示容器中的内容

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        // 创建容器对象,并绘制图像
        VisualElement element = new();
        element.DrawingLine(new Pen(Brushes.Yellow,2),new Point(0,0),new Point(200,300));
        
        // 因为容器是一个FrameworkElement,所有可以加入到Grid的子集中。
        grid.Children.Add(element);
    }
}

      注释:

      • Visual类有一个AddVisualChildren()的方法。该方法不是为了显示“可视化”对象的,而是为了确定可视化对象的父子关系。WPF不是一个实时绘图程序,只有在需要的时候才绘制界面。所以该方法不能用来“显示”。
      • DrawingVisual类,表示一个可视化的对象。它继承自ContainerVisual,进而继承自Visual,因此它是一个可以显示在界面上的东西。
      • DrawingContext类,表示一个绘图环境,在这个环境中绘图完毕之后,需要将其关闭。一旦绘图环境关闭,就不能在打开并修改了。

    3.2.2 使用DrawingXX对象

      在3.1.1示例代码中,使用DrawingGroup类的Open()方法创建了DrawinContext对象。

      但DrawingGroup是一个Drawing对象,而不是Visual对象,无法直接显示。那要怎么显示呢?

      只能利用第2节提到的图元转换器:DrawingBrush、DrawingImage类。详细参见第2节,这里不再一一赘述。

    3.2.3 继承自UIElement类

      在3.1.2的示例代码中,创建了一个继承自的UIElement的类,并重写了OnRender()方法。

      因为子类是继承自UIElement的,因此子类本身就是一个可以现在在界面上的元素。那么在OnRender()方法中绘制的图元,可以直接显示出来。这里就不再赘述。