WPF+Canvas实现平滑笔迹的示例代码

.NET
330
0
0
2023-02-04
目录
  • 实现思路
  • 实现效果
  • 实现代码

实现思路

收集路径点集。

平均采样路径点集。

将路径点集转为 LineB

把 LineB 数据传给 Path

实现效果

实现代码

1)Vector2D.cs 代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{
    public class Vector2D
    {
        public double X { getset; } = 0;
        public double Y { getset; } = 0;
       
        /// <summary>
        /// 向量的模
        /// </summary>
        public double Mold
        {
            get
            {
                //自身各分量平方运算.
                double X = this.X * this.X;
                double Y = this.Y * this.Y;
                return Math.Sqrt(X + Y);//开根号,最终返回向量的长度/模/大小.
            }
        }
        /// <summary>
        /// 单位向量
        /// </summary>
        public Vector2D UnitVector
        {
            get
            {
                double sumSquares = (X * X) + (Y * Y);
                return new Vector2D(X / Math.Sqrt(sumSquares), Y / Math.Sqrt(sumSquares));
            }
        }

        public Vector2D()
        {

        }
        public Vector2D(double x, double y)
        {
            X = x;
            Y = y;
        }
        public Vector2D(System.Windows.Point point)
        {
            X = point.X;
            Y = point.Y;
        }

        public void Offset(double angle, double distance, AngleType angleType = AngleType.Radian)
        {
            var vector2D = Vector2D.CalculateVectorOffset(this, angle, distance, angleType);
            X = vector2D.X;
            Y = vector2D.Y;
        }
        public void Rotate(double angle, Vector2D vectorCenter = null, AngleType angleType = AngleType.Radian)
        {
            vectorCenter = vectorCenter == null ? this : vectorCenter;
            var vector2D = Vector2D.CalculateVectorRotation(this, vectorCenter, angle, angleType);
            X = vector2D.X;
            Y = vector2D.Y;
        }

        #region 静态方法
        /// <summary>
        /// 计算两个向量之间的距离
        /// </summary>
        public static double CalculateVectorDistance(Vector2D vector2DA, Vector2D vector2DB)
        {
            Vector2D vector2D = vector2DA - vector2DB;
            return vector2D.Mold;
        }

        /// <summary>
        /// 计算两点夹角,右侧X轴线为0度,向下为正,向上为负
        /// </summary>
        public static double IncludedAngleXAxis(Vector2D vector2DA, Vector2D vector2DB, AngleType angleType = AngleType.Radian)
        {
            double radian = Math.Atan2(vector2DB.Y - vector2DA.Y, vector2DB.X - vector2DA.X); //弧度:1.1071487177940904
            return angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);
        }

        /// <summary>
        /// 计算两点夹角,下侧Y轴线为0度,向右为正,向左为负
        /// </summary>
        public static double IncludedAngleYAxis(Vector2D vector2DA, Vector2D vector2DB, AngleType angleType = AngleType.Radian)
        {
            double radian = Math.Atan2(vector2DB.X - vector2DA.X, vector2DB.Y - vector2DA.Y); //弧度:0.46364760900080609
            return angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);
        }

        /// <summary>
        /// 偏移向量到指定角度,指定距离
        /// </summary>
        public static Vector2D CalculateVectorOffset(Vector2D vector2D, double angle, double distance, AngleType angleType = AngleType.Radian)
        {
            Vector2D pointVector2D = new Vector2D();
            if (angleType == AngleType.Angle)
            {
                angle = angle / (180 / Math.PI);//角度转弧度
            }
            double width = Math.Cos(Math.Abs(angle)) * distance;
            double height = Math.Sin(Math.Abs(angle)) * distance;
            if(angle <= Math.PI && angle >= 0)
            //if (angle is <= Math.PI and >= 0)
            {
                pointVector2D.X = vector2D.X - width;
                pointVector2D.Y = vector2D.Y - height;
            }
            if (angle >= (-Math.PI) && angle <= 0)
            //if (angle is >= (-Math.PI) and <= 0)
            {
                pointVector2D.X = vector2D.X - width;
                pointVector2D.Y = vector2D.Y + height;
            }
            return pointVector2D;
        }

        /// <summary>
        /// 围绕一个中心点,旋转一个向量,相对旋转
        /// </summary>
        public static Vector2D CalculateVectorRotation(Vector2D vector2D, Vector2D vectorCenter, double radian, AngleType angleType = AngleType.Radian)
        {
            radian = angleType == AngleType.Radian ? radian : ComputingHelper.RadianToAngle(radian);
            double x1 = (vector2D.X - vectorCenter.X) * Math.Sin(radian) + (vector2D.Y - vectorCenter.Y) * Math.Cos(radian) + vectorCenter.X;
            double y1 = -(vector2D.X - vectorCenter.X) * Math.Cos(radian) + (vector2D.Y - vectorCenter.Y) * Math.Sin(radian) + vectorCenter.Y;
            return new Vector2D(x1, y1);
        }
        public static Vector2D CalculateVectorCenter(Vector2D vector2DA, Vector2D vector2DB)
        {
            return new Vector2D((vector2DA.X + vector2DB.X) / 2, (vector2DA.Y + vector2DB.Y) / 2);
        }

        /// <summary>
        /// 判断坐标点是否在多边形区域内,射线法
        /// </summary>
        public static bool IsPointPolygonalArea(Vector2D vector2D, List<Vector2D> aolygonaArrayList)
        {
            var N = aolygonaArrayList.Count;
            var boundOrVertex = true//如果点位于多边形的顶点或边上,也算做点在多边形内,直接返回true
            var crossNumber = 0//x的交叉点计数
            var precision = 2e-10//浮点类型计算时候与0比较时候的容差
            Vector2D p1, p2; //neighbour bound vertices
            var p = vector2D; //测试点
            p1 = aolygonaArrayList[0]; //left vertex        
            for (var i = 1; i <= N; ++i)
            {
                //check all rays            
                if (p.X.Equals(p1.X) && p.Y.Equals(p1.Y))
                {
                    return boundOrVertex; //p is an vertex
                }

                p2 = aolygonaArrayList[i % N]; //right vertex            
                if (p.X < Math.Min(p1.X, p2.X) || p.X > Math.Max(p1.X, p2.X))
                {
                    //ray is outside of our interests                
                    p1 = p2;
                    continue//next ray left point
                }

                if (p.X > Math.Min(p1.X, p2.X) && p.X < Math.Max(p1.X, p2.X))
                {
                    //ray is crossing over by the algorithm (common part of)
                    if (p.Y <= Math.Max(p1.Y, p2.Y))
                    {
                        //x is before of ray                    
                        if (p1.X == p2.X && p.Y >= Math.Min(p1.Y, p2.Y))
                        {
                            //overlies on a horizontal ray
                            return boundOrVertex;
                        }

                        if (p1.Y == p2.Y)
                        {
                            //ray is vertical                        
                            if (p1.Y == p.Y)
                            {
                                //overlies on a vertical ray
                                return boundOrVertex;
                            }
                            else
                            {
                                //before ray
                                ++crossNumber;
                            }
                        }
                        else
                        {
                            //cross point on the left side                        
                            var xinters =
                                (p.X - p1.X) * (p2.Y - p1.Y) / (p2.X - p1.X) +
                                p1.Y; //cross point of Y                        
                            if (Math.Abs(p.Y - xinters) < precision)
                            {
                                //overlies on a ray
                                return boundOrVertex;
                            }

                            if (p.Y < xinters)
                            {
                                //before ray
                                ++crossNumber;
                            }
                        }
                    }
                }
                else
                {
                    //special case when ray is crossing through the vertex                
                    if (p.X == p2.X && p.Y <= p2.Y)
                    {
                        //p crossing over p2                    
                        var p3 = aolygonaArrayList[(i + 1) % N]; //next vertex                    
                        if (p.X >= Math.Min(p1.X, p3.X) && p.X <= Math.Max(p1.X, p3.X))
                        {
                            //p.X lies between p1.X & p3.X
                            ++crossNumber;
                        }
                        else
                        {
                            crossNumber += 2;
                        }
                    }
                }
                p1 = p2; //next ray left point
            }
            if (crossNumber % 2 == 0)
            {
                //偶数在多边形外
                return false;
            }
            else
            {
                //奇数在多边形内
                return true;
            }
        }

        /// <summary>
        /// 判断一个点是否在一条边内
        /// </summary>
      
        public static bool IsPointEdge(Vector2D point, Vector2D startPoint, Vector2D endPoint)
        {
            return (point.X - startPoint.X) * (endPoint.Y - startPoint.Y) == (endPoint.X - startPoint.X) * (point.Y - startPoint.Y)
                && Math.Min(startPoint.X, endPoint.X) <= point.X && point.X <= Math.Max(startPoint.X, endPoint.X)
                && Math.Min(startPoint.Y, endPoint.Y) <= point.Y && point.Y <= Math.Max(startPoint.Y, endPoint.Y);
        }
        #endregion 静态方法

        #region 运算符重载
        /// <summary>
        /// 重载运算符,和运算,可以用来计算两向量距离
        /// </summary>
        public static Vector2D operator +(Vector2D vector2DA, Vector2D vector2DB)
        {
            Vector2D vector2D = new Vector2D();
            vector2D.X = vector2DA.X + vector2DB.X;
            vector2D.Y = vector2DA.Y + vector2DB.Y;
            return vector2D;
        }

        /// <summary>
        /// 重载运算符,差运算,可以用来计算两向量距离
        /// </summary>
        public static Vector2D operator -(Vector2D vector2DA, Vector2D vector2DB)
        {
            Vector2D vector2D = new Vector2D();
            vector2D.X = vector2DA.X - vector2DB.X;
            vector2D.Y = vector2DA.Y - vector2DB.Y;
            return vector2D;
        }

        /// <summary>
        /// 重载运算符,差运算,可以用来计算两向量距离
        /// </summary>
        public static Vector2D operator -(Vector2D vector2D, double _float)
        {
            return new Vector2D(vector2D.X - _float, vector2D.Y - _float);
        }

        /// <summary>
        /// 重载运算符,点积运算,可以用来计算两向量夹角
        /// </summary>
        public static double operator *(Vector2D vector2DA, Vector2D vector2DB)
        {
            return (vector2DA.X * vector2DB.X) + (vector2DA.Y * vector2DB.Y);
        }
        public static double operator *(Vector2D vector2D, double _float)
        {
            return (vector2D.X * _float) + (vector2D.Y * _float);
        }

        /// <summary>
        /// 重载运算符,点积运算,可以用来计算两向量夹角
        /// </summary>
        public static double operator /(Vector2D vector2D, double para)
        {
            return (vector2D.X / para) + (vector2D.Y / para);
        }

        /// <summary>
        /// 重载运算符
        /// </summary>
        
        public static bool operator >=(Vector2D vector2D, double para)
        {
            if (vector2D.Mold >= para)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool operator <=(Vector2D vector2D, double para)
        {
            if (vector2D.Mold <= para)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool operator >(Vector2D vector2D, double para)
        {
            if (vector2D.Mold > para)
            {
                return true;
            }
            else
            {
                return false;
            }
        }

        public static bool operator <(Vector2D vector2D, double para)
        {
            if (vector2D.Mold < para)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        #endregion 运算符重载

        #region 隐式转换
        /// <summary>
        /// 重载隐式转换,可以直接使用Point
        /// </summary>
        /// <param name="v"></param>
        public static implicit operator Vector2D(System.Windows.Point v)//隐式转换
        {
            return new Vector2D(v.X, v.Y);
        }
        /// <summary>
        /// 重载隐式转换,可以直接使用Point
        /// </summary>
        /// <param name="v"></param>
        public static implicit operator System.Windows.Point(Vector2D v)//隐式转换
        {
            return new System.Windows.Point(v.X, v.Y);
        }
        /// <summary>
        /// 重载隐式转换,可以直接使用double
        /// </summary>
        /// <param name="v"></param>
        public static implicit operator Vector2D(double v)//隐式转换
        {
            return new Vector2D(v, v);
        }
        #endregion 隐式转换

        #region ToString
        public override string ToString()
        {
            return X.ToString() + "," + Y.ToString();
        }
        public string ToString(string symbol)
        {
            return X.ToString() + symbol + Y.ToString();
        }
        public string ToString(string sender, string symbol)
        {
            return X.ToString(sender) + symbol + Y.ToString(sender);
        }
        #endregion
    }
    public enum AngleType 
    {
        Angle,
        Radian
    }
}

2)ComputingHelper.cs 代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{
    public static class ComputingHelper
    {
        public static double AngleToRadian(double angle)
        {
            return angle * (Math.PI / 180);
        }
        public static double RadianToAngle(double radian)
        {
            return radian * (180 / Math.PI);
        }

        /// <summary>
        /// 将一个值从一个范围映射到另一个范围
        /// </summary>
        public static double RangeMapping(double inputValue, double enterLowerLimit, double enterUpperLimit, double outputLowerLimit, double OutputUpperLimit, CurveType curveType = CurveType.None)
        {
            var percentage = (enterUpperLimit - inputValue) / (enterUpperLimit - enterLowerLimit);
            switch (curveType)
            {
                case CurveType.Sine:
                    percentage = Math.Sin(percentage);
                    break;
                case CurveType.CoSine:
                    percentage = Math.Cos(percentage);
                    break;
                case CurveType.Tangent:
                    percentage = Math.Tan(percentage);
                    break;
                case CurveType.Cotangent:
                    percentage = Math.Atan(percentage);
                    break;
                default:
                    break;
            }
            double outputValue = OutputUpperLimit - ((OutputUpperLimit - outputLowerLimit) * percentage);

            return outputValue;
        }

        public static string ByteToKB(double _byte)
        {
            List<string> unit = new List<string>() { "B""KB""MB""GB""TB""P""PB" };
            int i = 0;
            while (_byte > 1024)
            {
                _byte /= 1024;
                i++;
            }
            _byte = Math.Round(_byte, 3);//保留三位小数
            return _byte + unit[i];
        }

        /// <summary>
        /// 缩短一个数组,对其进行平均采样
        /// </summary>
        
        public static double[] AverageSampling(double[] sourceArray, int number)
        {
            if (sourceArray.Length <= number)
            {
                return sourceArray;
                //throw new Exception("新的数组必须比原有的要小!");
            }
            double[] arrayList = new double[number];
            double stride = (double)sourceArray.Length / number;
            for (int i = 0, jIndex = 0; i < number; i++, jIndex++)
            {
                double strideIncrement = i * stride;
                strideIncrement = Math.Round(strideIncrement, 6);
                double sum = 0;
                int firstIndex = (int)(strideIncrement);
                double firstDecimal = strideIncrement - firstIndex;

                int tailIndex = (int)(strideIncrement + stride);
                double tailDecimal = (strideIncrement + stride) - tailIndex;

                if (firstDecimal != 0)
                    sum += sourceArray[firstIndex] * (1 - firstDecimal);

                if (tailDecimal != 0 && tailIndex != sourceArray.Length)
                    sum += sourceArray[tailIndex] * (tailDecimal);

                int startIndex = firstDecimal == 0 ? firstIndex : firstIndex + 1;
                int endIndex = tailIndex;

                for (int j = startIndex; j < endIndex; j++)
                    sum += sourceArray[j];

                arrayList[jIndex] = sum / stride;
            }
            return arrayList;
        }
        public static List<Vector2D> AverageSampling(List<Vector2D> sourceArray, int number)
        {
            if (sourceArray.Count <= number - 2)
            {
                return sourceArray;
            }
            double[] x = new double[sourceArray.Count];
            double[] y = new double[sourceArray.Count];
            for (int i = 0; i < sourceArray.Count; i++)
            {
                x[i] = sourceArray[i].X;
                y[i] = sourceArray[i].Y;
            }

            double[] X = AverageSampling(x, number - 2);
            double[] Y = AverageSampling(y, number - 2);

            List<Vector2D> arrayList = new List<Vector2D>();
            for (int i = 0; i < number - 2; i++)
            {
                arrayList.Add(new Vector2D(X[i], Y[i]));
            }

            arrayList.Insert(0, sourceArray[0]);//添加首
            arrayList.Add(sourceArray[sourceArray.Count - 1]);//添加尾

            return arrayList;
        }
    }
    public enum CurveType 
    {
        Sine,
        CoSine,
        Tangent,
        Cotangent,
        None
    }
}

3)LineB.cs 代码如下

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media;

namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{
    public class LineB
    {
        private List<Vector2D> _vector2DList = new List<Vector2D>();
        public List<Vector2D> Vector2DList
        {
            get { return _vector2DList; }
            set
            {
                _vector2DList = value;
            }
        }

        private List<BezierCurve> _bezierCurveList = new List<BezierCurve>();
        public List<BezierCurve> BezierCurveList
        {
            get { return _bezierCurveList; }
            private set { _bezierCurveList = value; }
        }


        private double _tension = 0.618;
        public double Tension
        {
            get { return _tension; }
            set
            {
                _tension = value;
                if (_tension > 10)
                    _tension = 10;
                if (_tension < 0)
                    _tension = 0;
            }
        }

        private bool _isClosedCurve = true;
        public bool IsClosedCurve
        {
            get { return _isClosedCurve; }
            set { _isClosedCurve = value; }
        }

        private string _pathData = string.Empty;
        public string PathData
        {
            get
            {
                if (_pathData == string.Empty)
                {
                    _pathData = Vector2DToBezierCurve();
                }
                return _pathData;
            }
        }

        private string Vector2DToBezierCurve() 
        {
            if (Vector2DList.Count < 3)
                return string.Empty;

            BezierCurveList.Clear();
            for (int i = 0; i < Vector2DList.Count; i++)
            {
                int pointTwoIndex = i + 1 < Vector2DList.Count ? i + 1 : 0;
                int pointThreeIndex = i + 2 < Vector2DList.Count ? i + 2 : i + 2 - Vector2DList.Count;
                Vector2D vector2D1 = Vector2DList[i];
                Vector2D vector2D2 = Vector2DList[pointTwoIndex];
                Vector2D vector2D3 = Vector2DList[pointThreeIndex];

                Vector2D startVector2D = Vector2D.CalculateVectorCenter(vector2D1, vector2D2);
                double startAngle = Vector2D.IncludedAngleXAxis(vector2D1, vector2D2);
                double startDistance = Vector2D.CalculateVectorDistance(startVector2D, vector2D2) * (1 - Tension);
                Vector2D startControlPoint = Vector2D.CalculateVectorOffset(vector2D2, startAngle, startDistance);

                Vector2D endVector2D = Vector2D.CalculateVectorCenter(vector2D2, vector2D3);
                double endAngle = Vector2D.IncludedAngleXAxis(endVector2D, vector2D2);
                double endDistance = Vector2D.CalculateVectorDistance(endVector2D, vector2D2) * (1 - Tension);
                Vector2D endControlPoint = Vector2D.CalculateVectorOffset(endVector2D, endAngle, endDistance);

                BezierCurve bezierCurve = new BezierCurve();
                bezierCurve.StartVector2D = startVector2D;
                bezierCurve.StartControlPoint = startControlPoint;
                bezierCurve.EndVector2D = endVector2D;
                bezierCurve.EndControlPoint = endControlPoint;
                BezierCurveList.Add(bezierCurve);
            }
            if (!IsClosedCurve)
            {
                BezierCurveList[0].StartVector2D = Vector2DList[0];
                BezierCurveList.RemoveAt(BezierCurveList.Count - 1);
                BezierCurveList[BezierCurveList.Count - 1].EndVector2D = Vector2DList[Vector2DList.Count - 1];
                BezierCurveList[BezierCurveList.Count - 1].EndControlPoint = BezierCurveList[BezierCurveList.Count - 1].EndVector2D;
            }
            string path = $"M {BezierCurveList[0].StartVector2D.ToString()} ";
            foreach (var item in BezierCurveList)
            {
                path += $"C {item.StartControlPoint.ToString(" ")},{item.EndControlPoint.ToString(" ")},{item.EndVector2D.ToString(" ")} ";
            }
            return path;
        }

        public LineB()
        {
        }
        public LineB(List<Vector2D> verVector2DList, bool isClosedCurve = true)
        {
            this.Vector2DList = verVector2DList;
            this.IsClosedCurve = isClosedCurve;
        }

        /// <summary>
        /// 重载隐式转换,可以直接使用Point
        /// </summary>
        /// <param name="v"></param>
        public static implicit operator Geometry(LineB lineB)//隐式转换
        {
            return Geometry.Parse(lineB.PathData);
        }
    }
    public class BezierCurve
    {
        private Vector2D _startVector2D = new Vector2D(00);
        public Vector2D StartVector2D
        {
            get { return _startVector2D; }
            set { _startVector2D = value; }
        }

        private Vector2D _startControlPoint = new Vector2D(0100);
        public Vector2D StartControlPoint
        {
            get { return _startControlPoint; }
            set { _startControlPoint = value; }
        }

        private Vector2D _endControlPoint = new Vector2D(1000);
        public Vector2D EndControlPoint
        {
            get { return _endControlPoint; }
            set { _endControlPoint = value; }
        }

        private Vector2D _endVector2D = new Vector2D(100100);
        public Vector2D EndVector2D
        {
            get { return _endVector2D; }
            set { _endVector2D = value; }
        }


    }
}

4)CanvasHandWritingExample.xaml 代码如下

<UserControl x:Class="WPFDevelopers.Samples.ExampleViews.CanvasHandWriting.CanvasHandWritingExample"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:local="clr-namespace:WPFDevelopers.Samples.ExampleViews.CanvasHandWriting"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <Style TargetType="{x:Type TextBlock}">
            <Setter Property="Foreground" Value="{StaticResource PrimaryTextSolidColorBrush}" />
        </Style>
    </UserControl.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition/>
        </Grid.RowDefinitions>
        <StackPanel Orientation="Horizontal" Margin="4">
            <TextBlock Text="张力:" VerticalAlignment="Center"/>
            <TextBox Text="{Binding Tension,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"/>
            <Slider Width="100" SmallChange="0.01" 
                    Value="{Binding Tension,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}" Maximum="1" 
                    VerticalAlignment="Center" 
                    Margin="5,0"/>
            <TextBlock  Text="平滑采样:" VerticalAlignment="Center"/>
            <TextBox Text="{Binding SmoothSampling,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"
                     Margin="5,0"/>
            <Slider Value="{Binding SmoothSampling,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"
                    Width="100" 
                    VerticalAlignment="Center" 
                    SmallChange="0.01" Maximum="1" 
                    TickFrequency="0.1"/>
            <CheckBox Content="橡皮擦" 
                      VerticalAlignment="Center"
                      Margin="5,0"
                      IsChecked="{Binding IsEraser,RelativeSource={RelativeSource AncestorType=local:CanvasHandWritingExample}}"/>
            <Button Content="清空画布" Click="btnClertCanvas_Click"/>
        </StackPanel>
        <Canvas x:Name="drawingCanvas" 
                Grid.Row="1" Background="Black" 
                PreviewMouseLeftButtonDown="DrawingCanvas_PreviewMouseLeftButtonDown"
                PreviewMouseMove="DrawingCanvas_PreviewMouseMove"
                PreviewMouseLeftButtonUp="DrawingCanvas_PreviewMouseLeftButtonUp"/>
          
    </Grid>
</UserControl>

5)CanvasHandWritingExample.xaml.cs 代码如下

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;

namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriting
{
    /// <summary>
    ///     CanvasHandWritingExample.xaml 的交互逻辑
    /// </summary>
    public partial class CanvasHandWritingExample : UserControl
    {
        public static readonly DependencyProperty TensionProperty =
            DependencyProperty.Register("Tension"typeof(double), typeof(CanvasHandWritingExample),
                new PropertyMetadata(0.618));

        public static readonly DependencyProperty SmoothSamplingProperty =
            DependencyProperty.Register("SmoothSampling"typeof(double), typeof(CanvasHandWritingExample),
                new UIPropertyMetadata(OnSmoothSamplingChanged));

        public static readonly DependencyProperty IsEraserProperty =
            DependencyProperty.Register("IsEraser"typeof(bool), typeof(CanvasHandWritingExample),
                new PropertyMetadata(false));

        private readonly Dictionary<Path, List<Vector2D>> _PathVector2DDictionary ;

        volatile bool _IsStart = false;
        Path _DrawingPath = default;

        private static void OnSmoothSamplingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var mWindow = (CanvasHandWritingExample)d;
            foreach (var item in mWindow._PathVector2DDictionary.Keys)
            {
                mWindow.DrawLine(item);
            }
        }
        public CanvasHandWritingExample()
        {
            InitializeComponent();
            _PathVector2DDictionary = new Dictionary<Path, List<Vector2D>>();
            SmoothSampling = 0.8;
        }

        private void DrawingCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            _IsStart = true;
            _DrawingPath = new Path()
            {
                StrokeDashCap = PenLineCap.Round,
                StrokeStartLineCap = PenLineCap.Round,
                StrokeEndLineCap = PenLineCap.Round,
                StrokeLineJoin = PenLineJoin.Round,
            };

            if (IsEraser)
            {
                _DrawingPath.Stroke = new SolidColorBrush(Colors.Black);
                _DrawingPath.StrokeThickness = 40;
            }
            else
            {
                var random = new Random();
                var strokeBrush = new SolidColorBrush(Color.FromRgb((byte)random.Next(200255), (byte)random.Next(0255), (byte)random.Next(0255)));
                _DrawingPath.Stroke = strokeBrush;
                _DrawingPath.StrokeThickness = 10;
            }
            _PathVector2DDictionary.Add(_DrawingPath, new List<Vector2D>());
            drawingCanvas.Children.Add(_DrawingPath);
        }

        private void DrawingCanvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            _IsStart = false;
            _DrawingPath = default;
        }

        private void DrawingCanvas_PreviewMouseMove(object sender, MouseEventArgs e)
        {
            if (!_IsStart)
                return;

            if (_DrawingPath is null)
                return;

            Vector2D currenPoint = e.GetPosition(drawingCanvas);
            if (currenPoint.X < 0 || currenPoint.Y < 0)
                return;

            if (currenPoint.X > drawingCanvas.ActualWidth || currenPoint.Y > drawingCanvas.ActualHeight)
                return;

            if (_PathVector2DDictionary[_DrawingPath].Count > 0)
            {
                if (Vector2D.CalculateVectorDistance(currenPoint, _PathVector2DDictionary[_DrawingPath][_PathVector2DDictionary[_DrawingPath].Count - 1]) > 1)
                    _PathVector2DDictionary[_DrawingPath].Add(e.GetPosition(drawingCanvas));
            }
            else
                _PathVector2DDictionary[_DrawingPath].Add(e.GetPosition(drawingCanvas));

            DrawLine(_DrawingPath);
        }


        public double Tension
        {
            get => (double)GetValue(TensionProperty);
            set => SetValue(TensionProperty, value);
        }

        public double SmoothSampling
        {
            get => (double)GetValue(SmoothSamplingProperty);
            set => SetValue(SmoothSamplingProperty, value);
        }

        public bool IsEraser
        {
            get => (bool)GetValue(IsEraserProperty);
            set => SetValue(IsEraserProperty, value);
        }

       

        private void DrawLine(Path path)
        {
            if (_PathVector2DDictionary[path].Count > 2)
            {
                var pathVector2Ds = _PathVector2DDictionary[path];
                var smoothNum = (int)(_PathVector2DDictionary[path].Count * SmoothSampling);
                if (smoothNum > 1)
                    pathVector2Ds = ComputingHelper.AverageSampling(_PathVector2DDictionary[path], smoothNum);
                var lineB = new LineB(pathVector2Ds, false);
                lineB.Tension = Tension;

                path.Data = lineB;
            }
        }

        private void btnClertCanvas_Click(object sender, RoutedEventArgs e)
        {
            drawingCanvas.Children.Clear();
            _PathVector2DDictionary.Clear();
        }
    }
}