GDI图形系统已经形成了很多年。它提供了2D图形和文本功能,以及受限的图像处理功能,在传统的Windows Form 编程中,我们经常使用Graphics图形对象的DrawCurve方法绘制平滑的曲线。
该方法定义如下:
public void DrawCurve(Pen pen, Point[] points, float tension)
其中tension参数是弯曲强度(张力),用来确定样条的形状及平滑点直接的连线,该值范围为0.0f ~1.0f,默认0.5,超出此范围会产生异常,当弯曲强度为零时,两点直接的连线就成了直线。
WPF绘图编程与传统GDI编程有显著不同,WPF中已经提供很多更强大灵活的方法进行绘制,可以方便绘制任意的矢量图形。
DrawingContext比较类似WinForm中的Graphics 类,是基础的绘图对象,用于绘制各种图形,它主要API有如下几种:
常用的基础的绘图API有:
基本绘图API跟GDI中的类似,大家发现了没有?WPF并没有DrawCurve的方法,虽然z有DrawGeometry方法可以绘制图形,但是找不到没有“张力”的参数。由于没有提供与DrawCurve方法等价的方法,WPF中没有提供方法调用来绘制光滑曲线,我们可以通过一系列贝塞尔曲线绘制一个平滑的曲线。
贝塞尔曲线(Bézier curve),又称贝兹曲线或贝济埃曲线,是应用于二维图形应用程序的数学曲线。贝塞尔曲线是计算机图形图像造型的基本工具,是图形造型运用得最多的基本线条之一。它通过控制曲线上的四个点(起始点、终止点以及两个相互分离的中间点)来创造、编辑图形。其中起重要作用的是位于曲线中央的控制线。这条线是虚拟的,中间与贝塞尔曲线交叉,两端是控制端点。移动两端的端点时贝塞尔曲线改变曲线的曲率(弯曲的程度);移动中间点(也就是移动虚拟的控制线)时,贝塞尔曲线在起始点和终止点锁定的情况下做均匀移动。
上图显示了这四个点是如何决定曲线形状的。曲线从起始点(A)开始,向第一个控制点(B)的方向移动。它在终点(D)结束,从第二个控制点(C)的方向来。图中的蓝色线显示了端点和控制点之间的方向。
从起点和终点到控制点的距离决定了曲线与蓝色线的距离。如果控制点较远,则曲线沿蓝色线较长。
要绘制一条连接一系列点的平滑曲线,可以构建多个从这些点开始和结束的贝塞尔曲线。为了使曲线平滑,你需要在相邻的曲线上对齐控制点,使它们的上图蓝色指向相同的方向。下图显示两条贝塞尔曲线平滑地连接在一起。第一条曲线的第二个控制点(标记为“control 1b”)和第二条曲线的第一个控制点(标记为“control 2a”)与连接两条Bezier曲线的点共线。
根据需要我们可以移动控制点控制1b和控制2a离它们控制的点更近或更远,只要这三个点是共线的。例如,您可以将控件2a移动到更靠近点的位置,使第二条贝塞尔曲线在开始时变得更紧。就像GDI绘图中DrawCurve方法提供了一个参数tension(它允许您调整控制点与曲线上的点的距离)一样。当你构建一系列贝塞尔曲线时,你可以单独放置每个控制点。
WPF提供了一个类PolyBezierSegment,你可以用来保存一组连接的Bezier曲线:PolyBezierSegment。该对象包含一个起始点和一组点,这些点包括控制点和Bezier曲线的曲线点。这将非常有用(需要一些工作),但是不能简单地显示一个PolyBezierSegment。
首先,使用您想要连接的点来找到适当的控制点。然后使用它们来构建一个包含PolyBezierSegment对象和所有其他必要的中间对象的路径。这样就可以使用WPF构建平滑的曲线。
那么如何定义控制点呢?看看右边的图片,它显示了三条连接点A、B、C和D的贝塞尔曲线。现在关注蓝色曲线。它需要两个控制点,一个在B点之后,一个在C点之前。
为了找到数据点B附近的控制点,我们查看由点B的两个相邻点A和C定义的线段。红色虚线段将这些点连接起来。现在我们从点B沿着线段的方向移动。绿色虚线段表示平移后的红色线段,它与点B相交。我们沿着这段线段移动来放置控制点的距离取决于曲线的张力。当您查看代码时,您将看到它是如何工作的。
请注意,您使用同一段来定义特定数据点两侧的控制点。在图中,你使用相同的绿色虚线段来定义点B之前和之后的控制点。因为这些控制点在与点B相交的一条线上,点B两边的两条Bezier曲线将会平滑地相交。
要找到蓝色曲线在点C附近的控制点,您可以类似地查看点B和D之间的部分。
建立这一系列曲线有两种特殊情况。起始点和结束点两边都没有邻居,所以它们被用来代替它们缺少的邻居。例如,在前面的图片中,红色虚线将从点A指向点b,这意味着曲线开始指向第二个点。
类似地,点D的红色虚线段从点D点指向点C,所以曲线结束时远离倒数第二个点。
定义寻找控制点的方法:
参数points:是绘制平滑曲线的一组点数据。
参数tension:张力参数决定控制点与数据点的距离。
返回值Point[]:并返回一个数组,该数组包含这些点和它们之间的控制点。
private Point[] MakeCurvePoints(Point[] points, double tension)
{
if (points.Length < 2) return null;
double control_scale = tension / 0.5 * 0.175;
List<Point> result_points = new List<Point>();
result_points.Add(points[0]);
for (int i = 0; i < points.Length - 1; i++)
{
// Get the point and its neighbors.
Point pt_before = points[Math.Max(i - 1, 0)];
Point pt = points[i];
Point pt_after = points[i + 1];
Point pt_after2 = points[Math.Min(i + 2, points.Length - 1)];
double dx1 = pt_after.X - pt_before.X;
double dy1 = pt_after.Y - pt_before.Y;
Point p1 = points[i];
Point p4 = pt_after;
double dx = pt_after.X - pt_before.X;
double dy = pt_after.Y - pt_before.Y;
Point p2 = new Point(
pt.X + control_scale * dx,
pt.Y + control_scale * dy);
dx = pt_after2.X - pt.X;
dy = pt_after2.Y - pt.Y;
Point p3 = new Point(
pt_after.X - control_scale * dx,
pt_after.Y - control_scale * dy);
// Save points p2, p3, and p4.
result_points.Add(p2);
result_points.Add(p3);
result_points.Add(p4);
}
// Return the points.
return result_points.ToArray();
}
需要设置一个control_scale,用来设置控制点与数据点的距离。
接下来,代码创建一个result_points列表来保存数据点和控制点。它将曲线的“第一个点”添加到列表中。
然后,该方法循环遍历数据点,在到达最后一个数据点之前停止。对于每个数据点,代码必须找到从该数据点开始的贝塞尔曲线的控制点。
程序找到这个点之前的点,这个点之后的点,以及这个点之后的两个位置。如果数据点是第一个或最后一个点,那么这个位置之前或这个位置之后的两个点将不存在。在这种情况下,代码将使用数据点来获取前一节中描述的红色虚线,用于那些特殊情况。
然后,代码计算在这个点之前和之后的点之间X和Y坐标的变化。它将这些值乘以缩放因子control_scale,并将结果添加到当前点的坐标中,以获得控制点p2的位置。
然后,该方法执行类似的计算,以找到曲线的第二个控制点p3。
下面的方法接受一个包含数据和控制点的数组作为输入,并构建一个包含适当的PolyBezierSegment的Path对象。
创建保存PolyBezierSegment所需的WPF对象
private Path MakeBezierPath(Point[] points)
{
// Create a Path to hold the geometry.
Path path = new Path();
// Add a PathGeometry.
PathGeometry path_geometry = new PathGeometry();
path.Data = path_geometry;
// Create a PathFigure.
PathFigure path_figure = new PathFigure();
path_geometry.Figures.Add(path_figure);
// Start at the first point.
path_figure.StartPoint = points[0];
// Create a PathSegmentCollection.
PathSegmentCollection path_segment_collection =
new PathSegmentCollection();
path_figure.Segments = path_segment_collection;
// Add the rest of the points to a PointCollection.
PointCollection point_collection =
new PointCollection(points.Length - 1);
for (int i = 1; i < points.Length; i++)
point_collection.Add(points[i]);
// Make a PolyBezierSegment from the points.
PolyBezierSegment bezier_segment = new PolyBezierSegment();
bezier_segment.Points = point_collection;
// Add the PolyBezierSegment to othe segment collection.
path_segment_collection.Add(bezier_segment);
return path;
}
最后,参考 Graphics对象的DrawCurve(Pen pen, Point[] points, float tension)方法,
定义一个MakeCurve方法从一组点建立一系列贝塞尔曲线,该方法将连接点数组和张力值作为参数。它调用MakeCurvePoints来创建控制点,然后调用MakeBezierPath来构建Bezier曲线。
private Path MakeCurve(Point[] points, double tension)
{
if (points.Length < 2) return null;
Point[] result_points = MakeCurvePoints(points, tension);
// Use the points to create the path.
return MakeBezierPath(result_points.ToArray());
}