插入2D点,在WPF中使用Bezier曲线
原文Interpolate 2D points, usign Bezier curves in WPF
Interpolate 2D points, usign Bezier curves in WPF
Introduction
Interpolating points sometimes is hard mathematical work, even more if the points are ordered. The solution is to create a function using the points, and using an extra parameter t that represents the time dimension. This often is called a parametric representation of the curve. This article shows a simple way of interpolating a set points using Bezier curves in WPF.
Background
The idea of this solution comes after asking this question inStack Overflow. The accepted answer makes references to a simple and interesting method proposed by Maxim Shemanarev, where the control points are calculated from the original points (called anchor points).
Here we create a WPF UserControl that draws the curve from any collection of points. This control can be used with the pattern MVVM. If any point's coordinate changes, the curve also will change automatically. For instance, it can be used for a draw application, where you can drag & drop the points for changing the drawing, or curve.
The Algorithm Behind
Due the original antigrain site is down, I'm going to explain what is the algorithm proposed by Maxim Shemanarev.
A Bezier curve has two anchor points (begin and end) and two control ones (CP) that determine its shape. Our anchor points are given, they are pair of vertices of the polygon. The question is, how to calculate the control points. It is obvious that the control points of two adjacent edges plus the vertex between them should form one straight line.
The solution found is a very simple method that does not require any complicated math. First, we take the polygon and calculate the middle points Ai of its edges.

Here we have line segments Ci that connect two points Ai of the adjacent segments. Then, we should calculate points Bi as shown in this picture.

The third step is final. We simply move the line segments Ci in such a way that their points Bi coincide with the respective vertices. That's it, we calculated the control points for our Bezier curve and the result looks good.

One little improvement. Since we have a straight line that determines the place of our control points, we can move them as we want, changing the shape of the resulting curve. I used a simple coefficient K that moves the points along the line relatively to the initial distance between vertices and control points. The closer the control points to the vertices are, the sharper figure will be obtained.

The method works quite well with self-intersecting polygons. The examples below show that the result is pretty interesting.
The Class for Calculation
Below it is exposed the class that makes the calculation of the spline segments, based in the algorithm, exposed above. This class is named InterpolationUtils , it has a static method (named InterpolatePointWithBeizerCurves) that returns a list of BeizerCurveSegment, that will be the solution of our problem.
The class <code>BeizerCurveSegment has the four properties that define a spline segment: StartPoint, EndPoint, FirstControlPoint, and the SecondControlPoint.
As the above algorithm is originally implemented for closed curves, and it is desired that it can be applied for open curves too, a little change is needed. For this reason, the InterpolatePointWithBeizerCurves method receive as second parameter, a boolean variable named isClosedCurve, that determines if the algorithm will return an open or closed curve. The change consists in: if isClosedCurve==true, then for building the first segment, the first point will be used two times, and the second point, and for the last segment will be used the last but one point, and the last point two times.
Copy Codeusing System;
using System.Collections.Generic;
using System.Linq;
using System.Windows; namespace BezierCurveSample.View.Utils
{
public class InterpolationUtils
{
public class BeizerCurveSegment
{
public Point StartPoint { get; set; }
public Point EndPoint { get; set; }
public Point FirstControlPoint { get; set; }
public Point SecondControlPoint { get; set; }
} public static List<BeizerCurveSegment> InterpolatePointWithBeizerCurves(List<Point> points, bool isClosedCurve)
{
if (points.Count < 3)
return null;
var toRet = new List<BeizerCurveSegment>(); //if is close curve then add the first point at the end
if (isClosedCurve)
points.Add(points.First()); for (int i = 0; i < points.Count - 1; i++) //iterate for points but the last one
{
// Assume we need to calculate the control
// points between (x1,y1) and (x2,y2).
// Then x0,y0 - the previous vertex,
// x3,y3 - the next one.
double x1 = points[i].X;
double y1 = points[i].Y; double x2 = points[i + 1].X;
double y2 = points[i + 1].Y; double x0;
double y0; if (i == 0) //if is first point
{
if (isClosedCurve)
{
var previousPoint = points[points.Count - 2]; //last Point, but one (due inserted the first at the end)
x0 = previousPoint.X;
y0 = previousPoint.Y;
}
else //Get some previouse point
{
var previousPoint = points[i]; //if is the first point the previous one will be it self
x0 = previousPoint.X;
y0 = previousPoint.Y;
}
}
else
{
x0 = points[i - 1].X; //Previous Point
y0 = points[i - 1].Y;
} double x3, y3; if (i == points.Count - 2) //if is the last point
{
if (isClosedCurve)
{
var nextPoint = points[1]; //second Point(due inserted the first at the end)
x3 = nextPoint.X;
y3 = nextPoint.Y;
}
else //Get some next point
{
var nextPoint = points[i + 1]; //if is the last point the next point will be the last one
x3 = nextPoint.X;
y3 = nextPoint.Y;
}
}
else
{
x3 = points[i + 2].X; //Next Point
y3 = points[i + 2].Y;
} double xc1 = (x0 + x1) / 2.0;
double yc1 = (y0 + y1) / 2.0;
double xc2 = (x1 + x2) / 2.0;
double yc2 = (y1 + y2) / 2.0;
double xc3 = (x2 + x3) / 2.0;
double yc3 = (y2 + y3) / 2.0; double len1 = Math.Sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0));
double len2 = Math.Sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
double len3 = Math.Sqrt((x3 - x2) * (x3 - x2) + (y3 - y2) * (y3 - y2)); double k1 = len1 / (len1 + len2);
double k2 = len2 / (len2 + len3); double xm1 = xc1 + (xc2 - xc1) * k1;
double ym1 = yc1 + (yc2 - yc1) * k1; double xm2 = xc2 + (xc3 - xc2) * k2;
double ym2 = yc2 + (yc3 - yc2) * k2; const double smoothValue = 0.8;
// Resulting control points. Here smooth_value is mentioned
// above coefficient K whose value should be in range [0...1].
double ctrl1_x = xm1 + (xc2 - xm1) * smoothValue + x1 - xm1;
double ctrl1_y = ym1 + (yc2 - ym1) * smoothValue + y1 - ym1; double ctrl2_x = xm2 + (xc2 - xm2) * smoothValue + x2 - xm2;
double ctrl2_y = ym2 + (yc2 - ym2) * smoothValue + y2 - ym2;
toRet.Add(new BeizerCurveSegment
{
StartPoint = new Point(x1, y1),
EndPoint = new Point(x2, y2),
FirstControlPoint = i == 0 && !isClosedCurve ? new Point(x1, y1) : new Point(ctrl1_x, ctrl1_y),
SecondControlPoint = i == points.Count - 2 && !isClosedCurve ? new Point(x2, y2) : new Point(ctrl2_x, ctrl2_y)
});
} return toRet;
}
}
}
The User Control
The user control that we propose is very simple to use, and it works with the MVVM pattern.
The LandMarkControl has only two dependency properties, one for the points, and other for the color of the curve. The most important property is the Points attached property. It is of IEnumerable type, and it assumes that each item, has an X and Y properties.
In case the collection of points implements the INotifyCollectionChanged interface, the control will register to the CollectionChanged event, and if each point implements the INotifyPropertyChanged interface, the control also will register to the PropertyChanged event. In this way, every time any point is added or removed, or any point's coordinates changed, the control will be refreshed.
This is the complete user control code behind:
Copy Codeusing System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using BezierCurveSample.View.Utils; namespace BezierCurveSample.View
{
/// <summary>
/// Interaction logic for LandmarkControl.xaml
/// </summary>
public partial class LandmarkControl : UserControl
{
#region Points public IEnumerable Points
{
get { return (IEnumerable)GetValue(PointsProperty); }
set { SetValue(PointsProperty, value); }
} // Using a DependencyProperty as the backing store for Points. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PointsProperty =
DependencyProperty.Register("Points", typeof(IEnumerable),
typeof(LandmarkControl), new PropertyMetadata(null, PropertyChangedCallback)); private static void PropertyChangedCallback(DependencyObject dependencyObject,
DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var landmarkControl = dependencyObject as LandmarkControl;
if (landmarkControl == null)
return; if (dependencyPropertyChangedEventArgs.NewValue is INotifyCollectionChanged)
{
(dependencyPropertyChangedEventArgs.NewValue as
INotifyCollectionChanged).CollectionChanged += landmarkControl.OnPointCollectionChanged;
landmarkControl.RegisterCollectionItemPropertyChanged
(dependencyPropertyChangedEventArgs.NewValue as IEnumerable);
} if (dependencyPropertyChangedEventArgs.OldValue is INotifyCollectionChanged)
{
(dependencyPropertyChangedEventArgs.OldValue as
INotifyCollectionChanged).CollectionChanged -= landmarkControl.OnPointCollectionChanged;
landmarkControl.UnRegisterCollectionItemPropertyChanged
(dependencyPropertyChangedEventArgs.OldValue as IEnumerable);
} if (dependencyPropertyChangedEventArgs.NewValue != null)
landmarkControl.SetPathData();
} #endregion #region PathColor public Brush PathColor
{
get { return (Brush)GetValue(PathColorProperty); }
set { SetValue(PathColorProperty, value); }
} // Using a DependencyProperty as the backing store for PathColor. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PathColorProperty =
DependencyProperty.Register("PathColor", typeof(Brush), typeof(LandmarkControl),
new PropertyMetadata(Brushes.Black)); #endregion #region IsClosedCurve public static readonly DependencyProperty IsClosedCurveProperty =
DependencyProperty.Register("IsClosedCurve", typeof (bool), typeof (LandmarkControl),
new PropertyMetadata(default(bool), OnIsClosedCurveChanged)); private static void OnIsClosedCurveChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
var landmarkControl = dependencyObject as LandmarkControl;
if (landmarkControl == null)
return;
landmarkControl.SetPathData();
} public bool IsClosedCurve
{
get { return (bool) GetValue(IsClosedCurveProperty); }
set { SetValue(IsClosedCurveProperty, value); }
} #endregion public LandmarkControl()
{
InitializeComponent();
} void SetPathData()
{
if (Points == null) return;
var points = new List<Point>(); foreach (var point in Points)
{
var pointProperties = point.GetType().GetProperties();
if (pointProperties.All(p => p.Name != "X") ||
pointProperties.All(p => p.Name != "Y"))
continue;
var x = (float)point.GetType().GetProperty("X").GetValue(point, new object[] { });
var y = (float)point.GetType().GetProperty("Y").GetValue(point, new object[] { });
points.Add(new Point(x, y));
} if (points.Count <= 1)
return; var myPathFigure = new PathFigure { StartPoint = points.FirstOrDefault() }; var myPathSegmentCollection = new PathSegmentCollection(); var beizerSegments = InterpolationUtils.InterpolatePointWithBeizerCurves(points, IsClosedCurve); if (beizerSegments == null || beizerSegments.Count < 1)
{
//Add a line segment <this is generic for more than one line>
foreach (var point in points.GetRange(1, points.Count - 1))
{ var myLineSegment = new LineSegment { Point = point };
myPathSegmentCollection.Add(myLineSegment);
}
}
else
{
foreach (var beizerCurveSegment in beizerSegments)
{
var segment = new BezierSegment
{
Point1 = beizerCurveSegment.FirstControlPoint,
Point2 = beizerCurveSegment.SecondControlPoint,
Point3 = beizerCurveSegment.EndPoint
};
myPathSegmentCollection.Add(segment);
}
} myPathFigure.Segments = myPathSegmentCollection; var myPathFigureCollection = new PathFigureCollection {myPathFigure} ; var myPathGeometry = new PathGeometry { Figures = myPathFigureCollection }; path.Data = myPathGeometry;
} private void RegisterCollectionItemPropertyChanged(IEnumerable collection)
{
if (collection == null)
return;
foreach (INotifyPropertyChanged point in collection)
point.PropertyChanged += OnPointPropertyChanged;
} private void UnRegisterCollectionItemPropertyChanged(IEnumerable collection)
{
if (collection == null)
return;
foreach (INotifyPropertyChanged point in collection)
point.PropertyChanged -= OnPointPropertyChanged;
} private void OnPointCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
RegisterCollectionItemPropertyChanged(e.NewItems); UnRegisterCollectionItemPropertyChanged(e.OldItems); SetPathData();
} private void OnPointPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "X" || e.PropertyName == "Y")
SetPathData();
}
}
}
And this is the XAML code:
<UserControl x:Class="BezierCurveSample.View.LandmarkControl"
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"
mc:Ignorable="d"
x:Name="UserControl"
d:DesignHeight="300" d:DesignWidth="300">
<Path x:Name="path" Stroke="{Binding PathColor, ElementName=UserControl}" StrokeThickness="1"/>
</UserControl>
Examples of Usage
Using the control for creating the data template for the LandMarkViewModel:
<DataTemplate DataType="{x:Type ViewModel:LandmarkViewModel}">
<PointInterpolation.View:LandmarkControl x:Name="control"
Points="{Binding LandmarkPoints}" Visibility="{Binding IsVisible,
Converter={StaticResource BoolToVisibilityConverter}}" ToolTip="{Binding Label}"/>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected}" Value="True">
<Setter Property="PathColor" TargetName="control" Value="Red"/>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
Now everywhere a LandMarkViewModel is displayed, this data template will show the item as a LandMarkControl. It needs be rendered on a Canvas:
<ListBox x:Name="landMarks" ItemsSource="{Binding Landmarks}">
<ListBox.Template>
<ControlTemplate>
<Canvas IsItemsHost="True"/>
</ControlTemplate>
</ListBox.Template>
</ListBox>
This is a final image example:

References
License
This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)
插入2D点,在WPF中使用Bezier曲线的更多相关文章
- WPF中使用AxisAngleRotation3D实现CAD的2D旋转功能
原文:WPF中使用AxisAngleRotation3D实现CAD的2D旋转功能 对于CAD图形来说,3D旋转比较常用,具体实现方法在上篇文章<WPF中3D旋转的实现 >中做了 ...
- 基于C#在WPF中使用斑马打印机进行打印【转】
原文链接:http://ju.outofmemory.cn/entry/132476 最近在项目中接手了一个比较有挑战性的模块——用斑马打印机将需要打印的内容打印出来.苦苦折腾了两天,总算有所收获,就 ...
- WPF中使用ReportViewer报表
本篇博客将介绍如何在WPF中使用ReportViewer控件. 1. 环境准备:下载安装最新版ReportViewer(PS:需要安装Microsoft SQL Server System CLR T ...
- WPF中图形表示语法详解(Path之Data属性语法)ZZ
大可山 [MSN:a3news(AT)hotmail.com] http://www.zpxp.com 萝卜鼠在线图形图像处理 ------------------------------------ ...
- WPF中图形表示语法详解(Path之Data属性语法)
原文 http://blog.csdn.net/johnsuna/article/details/1885597 老规矩,看图说话. 先看显示效果:(图1) XAML(代码A):<Page xm ...
- WPF中两条路径渐变的探讨
原文:WPF中两条路径渐变的探讨 我们在WPF中,偶尔也会涉及到两条路径作一些“路径渐变 ”.先看看比较简单的情形:如下图(关键点用红色圆点加以标识):(图1) 上面图1中的第1幅图可以说是最简单的路 ...
- WPF中展示HTML
业务需求:将具有表格信息的HTML片段在WPF中展示出来,并像网页端一样,可以进行input的填写,checkbox选择,最后以HTML的形式完成保存. 天真的以为直接引入WPF中的WebBrowse ...
- WPF中使用流文档
转载自:http://www.cnblogs.com/zlgcool/archive/2008/11/17/1335456.html WPF面向的是UI展现,而文本显示无疑是UI层中的重要功能之一.W ...
- 在WPF中自定义控件
一, 不一定需要自定义控件在使用WPF以前,动辄使用自定义控件几乎成了惯性思维,比如需要一个带图片的按钮,但在WPF中此类任务却不需要如此大费周章,因为控件可以嵌套使用以及可以为控件外观打造一套新的样 ...
随机推荐
- git 分支建立及合并
分支的新建与合并 让我们来看一个简单的分支新建与分支合并的例子,实际工作中你可能会用到类似的工作流. 你将经历如下步骤: 开发某个网站. 为实现某个新的需求,创建一个分支. 在这个分支上开展工作. 正 ...
- centos / Linux 服务环境下安装 Redis 5.0.3
原文:centos / Linux 服务环境下安装 Redis 5.0.3 1.首先进入你要安装的目录 cd /usr/local 2.下载目前最新稳定版本 Redis 5.0.3 wget http ...
- Delphi 7验证XML合法性(利用DTD、XSD)
拥有正确语法的XML被称为“形式良好”的XML.通过DTD验证的XML是“合法”的XML.DTD(文档类型定义)的作用是定义XML 文档的合法构建模块.它使用一系列的合法元素来定义文档结构.XML S ...
- iOS 利用FZEasyFile本地保存 和 常规保存
1.常规保存(较麻烦) NSFileManager *fileManager = [NSFileManager defaultManager]; //获取document路径,括号中属性为当前应用程序 ...
- html中DIV+CSS与TABLE布局方式的区别及HTML5新加入的结构标签(转)
DIV与TABLE布局的区别 div 和 table 的加载方式不同,div 的加载方式是即读即加载,遇到 <div> 没有遇到 </div> 的时候一样加载 div 中的内容 ...
- CentOS7 下的mysql安装与配置
之前虽然也安装过多次mysql,但每次都会遇到各种小问题,这次记录下来,以备后查. 首先是下载与安装 # wget http://dev.mysql.com/get/mysql-community-r ...
- Android应用《撕开美女衣服》的实现过程及源代码
现在很多Android市场中都能找到关于美女的应用,比如 撕开美女衣服.吹裙子等. 这些应用的下载量挺大的,作为Android的开发人员或者一名技术人员我们不能只局限在欣赏应用的层面,很多时候需要我们 ...
- ie height
苦恼我许久了,为啥在IE中设置div的height属性无效呢... 在网上查了,常用解决是设置line-height或者设置overflow:hidden,不过我这个div用来定位的,一是里面没文字, ...
- Gamma 函数及其应用
1. Γ(⋅) 函数定义 Γ(α)=∫∞0tα−1e−tdt 可知以下基本性质: Γ(α+1)=αΓ(α)(分部积分法) Γ(1)=1 ⇒ Γ(n+1)=n! Γ(12)=π√ 2. 常见变形 对于 ...
- wxWidgets初学者导引(3)——wxWidgets应用程序初体验
wxWidgets初学者导引全目录 PDF版及附件下载 1 前言2 下载.安装wxWidgets3 wxWidgets应用程序初体验4 wxWidgets学习资料及利用方法指导5 用wxSmith ...

