WPF的2D绘图介绍

WPF的2D绘图

纲要:

  1. Shape类介绍及使用
  2. DrawingVisual类介绍及使用
  3. WritableBitmap类双缓存介绍及使用
  4. 图形变换

Shape的特点

	1、Shape与其它的WPF控件一样,也继承FrameworkElement,属于UI 元素,所以具有大多数控件通用的属性和事件,如果创建图元规模较小的程序,采用Shape应该是比较好的选择。
	2、shape对象派生自FrameworkElement类,因此使用这些对象会使应用程序的内存消耗显著增加。
	3、如果对于图形内容确实不需要FrameworkElement功能,请考虑使用更轻量的Drawing对象。

Shape的继承层次:
在这里插入图片描述
Rectangle:矩形
Ellipse:椭圆
Line:直线
Polyline:多段线
Polygon:封闭多段线
Path:路径

♦ 上面图形相对比较简单,下面介绍Path路径:
WPF提供两个类来描述路径数据:
■ StreamGeometry
■ PathFigureCollection

使用场景:
	StreamGeometry:当你建立路径后,不再需要修改时,
	PathFigureCollection:如果还需要对路径数值进行修改
<Canvas>
        <!--PathFigureCollection的表示方法-->
        <Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
            <Path.Data>
                <PathGeometry Figures="M 10,100 L 100,100 100,50"/>
            </Path.Data>
        </Path>

        <!--StreamGeometry的表示方法-->
        <Path Stroke="Black" Data="M 10,100 L 100,100 100,50" Canvas.Top="100"/>
</Canvas>

运行效果如下:
在这里插入图片描运行述
♦ StreamGeometry指令说明
1. 移动指令 Move Command(M):M 起始点/m 起始点
例如:M 100,50或m 100,50
M:表示绝对值;
m:表示相对于前一点的值,如果前一点没有指定,则使用(0,0)。

2. 绘制指令(Draw Command):
	(1) 直线:Line(L)
	(2) 水平直线: Horizontal line(H)
	(3) 垂直直线: Vertical line(V)
	(4) 三次方程式贝塞尔曲线: Cubic Bezier curve(C)
	(5) 二次方程式贝塞尔曲线: Quadratic Bezier curve(Q)
	(6) 平滑三次方程式贝塞尔曲线: Smooth cubic Bezier curve(S)
	(7) 平滑二次方程式贝塞尔曲线: smooth quadratic Bezier curve(T)
	(8) 椭圆圆弧: elliptical Arc(A)

3. 关闭指令 Close Command:Z / z	

直线举例:

<Path Stroke="Red" Data="M 100,100 L200,200"/>
<Path Stroke="Red" Data="M 100,100 L200,200 200 300"/>

运行效果:
在这里插入图片描述
♦ PathFigureCollection几何图形
Data的类型是Geomery(几何图形),是一个抽象类,不能直接使用
在这里插入图片描述
下面针对各图形举例说明:
1、Path路径直线几何图形

<Path Stroke="Red" StrokeThickness="3" >
            <Path.Data>
                <LineGeometry StartPoint="20, 20" EndPoint="120, 120"/>
            </Path.Data>
</Path>

在这里插入图片描述

<Path Stroke="Red" StrokeThickness="3" Data="M 10,100 L 100,100 100,50" />

在这里插入图片描述
2、矩形图形(RectangleGeometry)

<Path Stroke="Red" Fill="Yellow" StrokeThickness="2">
      <Path.Data>
           <RectangleGeometry Rect="20,20,120,120" RadiusX="10" RadiusY="10"/>
       </Path.Data>
</Path>

在这里插入图片描述
3、椭圆几何图形(EllipseGeometry)

<Path Stroke="Red" Fill="LawnGreen">
      <Path.Data>
           <EllipseGeometry Center="80,80" RadiusX="60" RadiusY="60"/>
      </Path.Data>
</Path>

在这里插入图片描述
4、PathGeometry
PathGeometry 之所以如此重要,是因为 Path的 Figures属性可以容纳 PathFigure对象,而 PathFigure 的 Segments属性又可以容纳各种线段结合成的复杂图形。
代码轮廓:

<Path>
    <Path.Data>
      <PathGeometry>
        <PathFigure>
	<!--各种线段-->  
        </PathFigure>
      </PathGeometry>
    </Path.Data>
</Path>

4.1、Segments线段
注意:这些线段是没有起点,起点就是前一个线段的终点
在这里插入图片描述

4.1.1、圆弧线段(ArcSegment)

<Path Stroke="Red">
            <Path.Data>
                <PathGeometry>
                    <PathFigure IsClosed="False" StartPoint="50,50">
                        <ArcSegment Point="100,100" Size="50,50" SweepDirection="Clockwise" 
                                    IsLargeArc="False" RotationAngle="45"/>
                    </PathFigure>
                </PathGeometry>
            </Path.Data>
</Path>

在这里插入图片描述

<Path Stroke="Red" StrokeThickness="2" Data="M50 50 A50 50 45 0 1 100 100"/>

这两段代码实现的图形是相同的

ArcSegment设置许多属性,它们分别是:
Point:指明圆弧连接的终点
Size:指明完整椭圆的横轴半径和纵轴半径
IsLargeArc:指明是否使用大弧去连接 . . .
SweepDirection :指明圆弧是顺时针方向还是逆时针方向
RotationAngle:指明圆弧椭圆的旋转角度

4.1.2、贝塞尔曲线(BezierSegment)

3次方贝塞尔曲线由 4 个点决定:
(1)起点:前一线段的终点 或者 PathFigure 的 StartPoint
(2)终点:Point3 属性,即曲线的终点位置
(3)两个控制点:Point1 和 Point2 属性
<Path Stroke="Black" StrokeThickness="2" Margin="30">
            <Path.Data>
                <PathGeometry>
                    <PathFigure StartPoint="0,0">
                        <BezierSegment Point1="250,0" Point2="50,200" 
                        	Point3="300,200"/>
                    </PathFigure>
                </PathGeometry>
            </Path.Data>
</Path>

在这里插入图片描述
贝塞尔曲线原理链接:
https://blog.csdn.net/danshiming/article/details/125821526

几何图形代码示例:

public MainWindow()
        {
            InitializeComponent();

            Path myPath = new Path();
            myPath.Stroke = Brushes.Black;
            myPath.StrokeThickness = 1;

            StreamGeometry geometry = new StreamGeometry();
            using (StreamGeometryContext ctx = geometry.Open())
            {
                ctx.BeginFigure(new Point(10, 100), true /* is filled */, true /* is closed */);
                ctx.LineTo(new Point(100, 100), true, false);
                ctx.LineTo(new Point(100, 50), true, false);
            }

            geometry.Freeze();
            myPath.Data = geometry;

            StackPanel mainPanel = new StackPanel();
            mainPanel.Children.Add(myPath);
            this.Content = mainPanel;
}

在这里插入图片描述

DrawingVisual类

DrawingVisual类是WPF中最轻量级的绘图类,继承自ContainerVisual、ContainerVisual又继承自Visual(Visual有以下能力:输出显示、坐标变换、区域剪裁、命中测试、边框计算。)

DrawingVisual无法单独存在,必须放在一个容器中(需要有布局系统)呈现.
DrawingVisual继承自ContainerVisual,该类有一个RenderOpen方法,返回一个可用于定义可视化内容的DrawingContext对象,绘制完毕后需要调用DrawingContext.Close()方法来结束绘图。
DrawingContext中有各种绘图的方法:
	DrawText
	DrawLine
	DrawRectangle
	DrawRoundedRectangle
	DrawEllipse
	DrawGeometry
	DrawImage
	         等等

DrawingVisual示例1:

private List<DrawingVisual> _drawingVisuals = new List<DrawingVisual>();
        public MainWindow()
        {
            DrawingVisual _drawingVisual = new DrawingVisual();
            _drawingVisuals.Add(_drawingVisual);
            DrawingContext dc = _drawingVisual.RenderOpen();
            dc.DrawGeometry(Brushes.White, new Pen(Brushes.White, 1), new PathGeometry());
            dc.Close();
        }

        //必须重载这两个方法,不然是画不出来的
        // 重载自己的VisualTree的孩子的个数
        protected override int VisualChildrenCount
        {
            get { return _drawingVisuals.Count; }
        }

        // 重载当WPF框架向自己要孩子的时候,返回返回DrawingVisual
        protected override Visual GetVisualChild(int index)
        {
            return _drawingVisuals[index];
        }

DrawingVisual示例2:

public partial class Window5 : Window
{
    public Window5()
    {
        InitializeComponent();
        this.Content = new RectangleElement();
    }
}
 
public class RectangleElement : UIElement
{
    protected override void OnRender(DrawingContext drawingContext)
    {
         drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 20));
         base.OnRender(drawingContext);
    }
}

注意:OnRender渲染界面时、通过将drawingContext实现绘制,绘制完后**不能调用Close()**方法关闭。

Bitmap类和WritableBitmap

Bitmap类和WritableBitmap的区别:
	1、可写性:
	       WriteableBitmap 可以在运行时对其进行修改和绘制,
	       Bitmap 只能读取位图数据。
	2、存储方式:
	       WriteableBitmap 是基于内存中的位图数组存储的,
	       Bitmap 是基于磁盘文件存储的。
	3、可用性:
	       WriteableBitmap 可以在 WPF 应用程序中使用,
	       Bitmap 可以在 Windows 应用程序中使用。
	4、性能:
	       WriteableBitmap 的读写性能比 Bitmap 更高,因为它不需要进行磁盘读写操作。
	5、方便性:
	       WriteableBitmap 提供了简单的 API 用于在运行时修改和绘制位图,
	       Bitmap 需要使用 GDI+ 进行图形操作。

在图元或数据量很大时,可考虑WritableBitmap可写位图实现绘图,以提高性能,以下时WritableBitmap的一些特点:

1、WriteableBitmap类使用两个缓冲区。后台缓冲区在系统内存中分配并累积当前未显示的内容。前台缓冲区分配在系统内存中,包含当前显示的内容。渲染系统将前端缓冲区复制到显存中进行显示。

2、两个线程使用这些缓冲区。用户界面 (UI) 线程生成 UI 但不将其呈现到屏幕上。UI 线程响应用户输入、计时器和其他事件。一个应用程序可以有多个 UI 线程。呈现线程组合并呈现来自 UI 线程的更改。每个应用程序只有一个渲染线程。

3、UI 线程将内容写入后台缓冲区。渲染线程从前端缓冲区读取内容并将其复制到显存。使用更改的矩形区域跟踪对后台缓冲区的更改。

4、WriteableBitmap在需要编辑图片的时候,是非常有效的手段,通过它我们可以做到在内存中直接修改图片的字节流,并自动通知到界面自动刷新,且效率较高,因为不需要每次重新加载图像

WritableBitmap调用流程
为了更好地控制更新,以及对后台缓冲区的多线程访问,请使用以下工作流程。
1、调用Lock方法为更新保留后台缓冲区。
2、通过访问BackBuffer属性获取指向后台缓冲区的指针。
3、将更改写入后台缓冲区。当WriteableBitmap被锁定时,其他线程可能会将更改写入后台缓冲区。
4、调用AddDirtyRect方法来指示已更改的区域。
5、调用Unlock方法释放后台缓冲区并允许在屏幕上显示。
6、当更新被发送到渲染线程时,渲染线程将更改后的矩形从后台缓冲区复制到前台缓冲区。渲染系统控制这种交换以避免死锁和重绘伪像

using CommonLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using drawing = System.Drawing;

namespace Controls.AxisSeries
{
    /// <summary>
    /// 高性能曲线图
    /// </summary>
    public class PolyLineAsync : UserControl
    {
        #region 字段
        /// <summary>
        /// 有效高度
        /// </summary>
        private double height;
        #endregion

        #region 属性
        /// <summary>
        /// 横向比例系数
        /// </summary>
        public double Kx { get; private set; }
        /// <summary>
        /// 纵向比例系数
        /// </summary>
        public double Ky { get; private set; }
        /// <summary>
        /// 面板
        /// </summary>
        public Canvas Root { get; private set; }
        /// <summary>
        /// 横轴的数据范围
        /// </summary>
        public Range HorizontalRange
        {
            get { return (Range)GetValue(HorizontalRangeProperty); }
            set { SetValue(HorizontalRangeProperty, value); }
        }
        /// <summary>
        /// 纵轴的数据范围
        /// </summary>
        public Range VerticalRange
        {
            get { return (Range)GetValue(VerticalRangeProperty); }
            set { SetValue(VerticalRangeProperty, value); }
        }
        /// <summary>
        /// 数据源
        /// </summary>
        public DataSeries DataSeries
        {
            get { return (DataSeries)GetValue(DataSeriesProperty); }
            set { SetValue(DataSeriesProperty, value); }
        }
        #endregion

        #region 依赖属性
        /// <summary>
        /// 横轴的数据范围
        /// </summary>
        public static readonly DependencyProperty HorizontalRangeProperty = DependencyProperty.Register(nameof(HorizontalRange),
            typeof(Range), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));

        /// <summary>
        /// 纵轴的数据范围
        /// </summary>
        public static readonly DependencyProperty VerticalRangeProperty = DependencyProperty.Register("VerticalRange",
            typeof(Range), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));

        /// <summary>
        /// 数据源
        /// </summary>
        public static readonly DependencyProperty DataSeriesProperty = DependencyProperty.Register("DataSeries",
            typeof(DataSeries), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));
        #endregion

        #region 构造函数
        public PolyLineAsync()
        {
            Root = new Canvas();
            Content = Root;

            DataSeries = new DataSeries();
            Random r = new Random();
            for (int i = 0; i < 50; i++)
            {
                DataSeries.Add(new drawing.Point
                    (
                        r.Next(0, 300),
                        r.Next(-100, 100)
                    ));
            }

            SizeChanged += (s, e) => { ResetScale(); Refresh(); };
        }
        #endregion

        #region 内部方法
        /// <summary>
        /// 刷新
        /// </summary>
        private void Refresh()
        {
            Root.Children.Clear();
            if (Kx == 0 || Ky == 0) return;

            if (DataSeries.Any())
            {
                var points = new List<drawing.PointF>();
                DataSeries.ToList().ForEach(p => {
                    points.Add(Normalize(p));
                });

                if (points.Count <= 1) return;

                WriteableBitmap bitmap = new WriteableBitmap((int)this.RenderSize.Width,(int)this.RenderSize.Height,96, 96,PixelFormats.Bgra32,null);
                Image image = new Image { Source = bitmap};
                bitmap.Lock();

                using (drawing.Bitmap buff = new drawing.Bitmap((int)bitmap.Width, (int)bitmap.Height, bitmap.BackBufferStride, System.Drawing.Imaging.PixelFormat.Format32bppArgb, bitmap.BackBuffer))
                {
                    using (drawing.Graphics g = drawing.Graphics.FromImage(buff))
                    {
                        Color color = (Foreground as SolidColorBrush).Color;
                        var brush = drawing.Color.FromArgb(color.A, color.R, color.G, color.A);
                        var pen = new drawing.Pen(brush, 1); // 颜色和线条宽度
                        g.DrawLines(pen, points.ToArray());
                        g.Flush();
                    }
                }

                bitmap.AddDirtyRect(new Int32Rect(0, 0, (int)bitmap.Width, (int)bitmap.Height));
                bitmap.Unlock();
                Root.Children.Add(image);
            }
        }

        /// <summary>
        /// 重置大小
        /// </summary>
        /// <param name="polyLineFigure"></param>
        private void ResetScale()
        {
            this.Kx = 0;
            this.Ky = 0;

            if (this.HorizontalRange == null || this.VerticalRange == null) return;
            if (this.HorizontalRange.Distance == 0 || this.VerticalRange.Distance == 0) return;

            double width = double.IsNaN(this.Width) ? this.RenderSize.Width : this.Width;
            this.Kx = width / this.HorizontalRange.Distance;

            this.height = double.IsNaN(this.Height) ? this.RenderSize.Height : this.Height;
            this.Ky = this.height / this.VerticalRange.Distance;
        }

        /// <summary>
        /// 属性变更回调函数
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnParamterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            (d as PolyLineAsync)?.ResetScale();
        }

        /// <summary>
        /// 数据点投影到视图点
        /// </summary>
        /// <param name="v"></param>
        /// <returns></returns>
        private drawing.PointF Normalize(drawing.Point p) => new drawing.PointF
            (
                (float)((p.X - HorizontalRange.Min) * Kx),
                (float)(height - (p.Y - VerticalRange.Min) * Ky)
            );
        #endregion
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值