目录
- 实现思路
- 实现效果
- 实现代码
实现思路
收集路径点集。
平均采样路径点集。
将路径点集转为 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; | |
} | |
/// <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); | |
} | |
/// <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; | |
} | |
} | |
/// <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); | |
} | |
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); | |
} | |
} | |
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(); | |
} | |
} | |
} |