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

目录
  • 实现思路
  • 实现效果
  • 实现代码

实现思路

收集路径点集。

平均采样路径点集。

将路径点集转为 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 { get; set; } = 0;
        public double Y { get; set; } = 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(0, 0);
        public Vector2D StartVector2D
        {
            get { return _startVector2D; }
            set { _startVector2D = value; }
        }

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

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

        private Vector2D _endVector2D = new Vector2D(100, 100);
        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(200, 255), (byte)random.Next(0, 255), (byte)random.Next(0, 255)));
                _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();
        }
    }
}

到此这篇关于WPF+Canvas实现平滑笔迹的示例代码的文章就介绍到这了,更多相关WPF Canvas平滑笔迹内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • WPF使用Canvas画布面板布局

    Canvas:画布面板 画布,用于完全控制每个元素的精确位置.他是布局控件中最为简单的一种,直接将元素放到指定位置,主要来布置图面.使用Canvas,必须指定一个子元素的位置(相对于画布),否则所有元素都将出现在画布的左上角.调整位置用Left.Right.Top和Bottom四个附加属性.如果Canvas是窗口主元素(即最外层的布局面板是Canvas),用户改变窗口大小时,Canvas也会随之变化,子元素的位置也会随之移动,以保证相对于Canvas的位置属性不变. Canvas允许子元素的部分

  • WPF实现好看的Loading动画的示例代码

    实现思路 框架使用大于等于.NET40: Visual Studio 2022; 项目使用 MIT 开源许可协议: 老板觉得公司系统等待动画转圈太简单,所以需要做一个稍微好看点的,就有这篇等待RingLoading动画 最外层使用Viewbox为父控件内部嵌套创建三组 Grid -> Ellipse . Border分别给它们指定不同的Angle从左侧开始 -135 225 54,做永久 Angle 动画: PART_Ring1.RotateTransform.Angle从From -135 到

  • WPF使用DrawingContext实现二维绘图

    DrawingContext比较类似WinForm中的Graphics 类,是基础的绘图对象,用于绘制各种图形,它主要API有如下几种: 绘图API 绘图API一般形为DrawingXXX系列,常用的基础的绘图API有: DrawEllipse DrawGeometry DrawGlyphRun DrawImage DrawRectangle DrawRoundedRectangle 这些和GDI的API是非常相似的,WPF的API中另外还都有一个带动画的版本,不过一般很少用. 另外还有两个相对

  • C# wpf Canvas中实现控件拖动调整大小的示例

    目录 前言 一.功能说明 二.如何实现? 1.继承Adorner 2.使用Thumb 3.实现拖动逻辑 三.完整代码 四.使用示例 总结 前言 我们做图片编辑工具.视频编辑工具.或者画板有时需要实现控件缩放功能,比如图片或图形可以拉伸放大或缩小,实现这种功能通常需要8个点,对应4条边和4个角,在wpf中通常可以使用装饰器实现. 一.功能说明 8个点方放置在控件的8个方位上,通过拖动这些点对控件进行拉伸或缩小,示意图如下: 二.如何实现? 1.继承Adorner 通过装饰器的方式添加8个点在控件上

  • WPF使用Geometry绘制几何图形

    在WPF的DrawingContext对象中,提供了基本的绘制椭圆和矩形的API:DrawEllipse和DrawRectangle.但是,这些是远远不够用的,我们在日常应用中,更多的是使用DrawGeometry函数,它可以绘制更多复杂的几何图形,并且提供了许多强大而易用的函数,在大多数场景下,甚至可以取代DrawEllipse和DrawRectangle函数. 在WPF图形体系中,Geometry类表示几何图形的基类,使用的时候是实例化它的一些子类,具体的有: 基本几何图形 线段:LineG

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

    目录 实现思路 实现效果 实现代码 实现思路 收集路径点集. 平均采样路径点集. 将路径点集转为 LineB. 把 LineB 数据传给 Path. 实现效果 实现代码 1)Vector2D.cs 代码如下 using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace WPFDevelopers.Samples.ExampleViews.CanvasHandWriti

  • 基于WPF实现用户头像选择器的示例代码

    目录 实现思路 核心代码 参考资料 实现思路 制作一个用户头像选择器仿 WeGame 制作一个用户头像选择Canvas为父控件所实现,展示图片使用Image,Path当作上方的蒙版; Canvas:主要用途方便移动Image,设置ClipToBounds="True"裁剪为一个正方形200x200做为主要展示区域; Image:展示需要裁剪的图片: Path:CombinedGeometry[1]绘制蒙版大小200x200效果如下: 当选择一个本地图片的时候判断宽与高谁更大,谁小就将它

  • WPF实现绘制扇形统计图的示例代码

    扇形统计图 绘制一个扇形原理也是基于Canvas进行绘制; ArcSegment[1]绘制弧形; 绘制指示线: 绘制文本: 鼠标移入动画: 显示详情Popup: 源码Github[2] Gitee[3] 示例代码 1)SectorChart.cs代码如下; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Win

  • WPF实现抽屉菜单效果的示例代码

    WPF 实现抽屉菜单 框架使用大于等于.NET40: Visual Studio 2022; 项目使用 MIT 开源许可协议: 更多效果可以通过GitHub[1]|码云[2]下载代码; 由于在WPF中没有现成的类似UWP的抽屉菜单,所以我们自己实现一个. 1) DrawerMenu.cs 代码如下. using System.Collections.Generic; using System.Windows; using System.Windows.Controls; using System

  • WPF实现窗体亚克力效果的示例代码

    WPF 窗体设置亚克力效果 框架使用大于等于.NET40. Visual Studio 2022. 项目使用 MIT 开源许可协议. WindowAcrylicBlur 设置亚克力颜色. Opacity 设置透明度. 实现代码 1) 准备WindowAcrylicBlur.cs如下: using System; using System.Runtime.InteropServices; using System.Windows; using System.Windows.Interop; usi

  • WPF实现控件拖动的示例代码

    实现控件拖动的基本原理是对鼠标位置的捕获,同时根据鼠标按键的按下.释放确定控件移动的幅度和时机. 简单示例: 在Grid中有一个Button,通过鼠标事件改编Button的Margin属性,从而改变Button在Grid中的相对位置. <Grid Name="gd"> <Button Width=90 Height=30 Name="btn">button</Button> </Grid> 为Button控件绑定三个事

  • WPF实现环(圆)形菜单的示例代码

    目录 前言 实现代码 1.CircularMenuItemCustomControl.cs 2.CircularMenuItemCustomControlStyle.xaml 3.MainWindow.xaml 4.MainWindow.xaml.cs 前言 需要实现环(圆)形菜单. 效果预览(更多效果请下载源码体验): 实现代码 1.CircularMenuItemCustomControl.cs using System; using System.Collections.Generic;

  • WPF模拟实现Gitee泡泡菜单的示例代码

    WPF实现 Gitee泡泡菜单 框架使用大于等于.NET40: Visual Studio 2022; 项目使用 MIT 开源许可协议: 需要实现泡泡菜单需要使用Canvas画布进行添加内容: 保证颜色随机,位置不重叠: 点击泡泡获得当前泡泡的值: 实现代码 1) BubblleCanvas.cs 代码如下: using System.Windows; using System.Windows.Controls; using WPFDevelopers.Helpers; using WPFDev

  • 基于WPF实现弹幕效果的示例代码

    WPF 实现弹幕效果 框架使用大于等于.NET40: Visual Studio 2022; 项目使用 MIT 开源许可协议: 此篇代码目的只是为了分享思路 实现基础弹幕一定是要使用Canvas比较简单,只需实现Left动画从右到左. 弹幕消息使用Border做弹幕背景. 内容使用TextBlock做消息文本展示. 当动画执行完成默认移除Canvas中的弹幕控件. 使用这种方式去加载弹幕GPU会占较高. 1) 准备BarrageExample.xaml如下: <UserControl x:Cla

随机推荐