WPF的2D绘图
纲要:
- Shape类介绍及使用
- DrawingVisual类介绍及使用
- WritableBitmap类双缓存介绍及使用
- 图形变换
Shape的特点
1、Shape与其它的WPF控件一样,也继承FrameworkElement,属于UI 元素,所以具有大多数控件通用的属性和事件,如果创建图元规模较小的程序,采用Shape应该是比较好的选择。
2、shape对象派生自FrameworkElement类,因此使用这些对象会使应用程序的内存消耗显著增加。
3、如果对于图形内容确实不需要FrameworkElement功能,请考虑使用更轻量的Drawing对象。
Shape的继承层次:
Rectangle:矩形
Ellipse:椭圆
Line:直线
Polyline:多段线
Polygon:封闭多段线
Path:路径
♦ 上面图形相对比较简单,下面介绍Path路径:
WPF提供两个类来描述路径数据:
■ StreamGeometry
■ PathFigureCollection
使用场景:
StreamGeometry:当你建立路径后,不再需要修改时,
PathFigureCollection:如果还需要对路径数值进行修改
<Canvas>
<!--PathFigureCollection的表示方法-->
<Path Stroke="Black" StrokeThickness="1" Fill="#CCCCFF">
<Path.Data>
<PathGeometry Figures="M 10,100 L 100,100 100,50"/>
</Path.Data>
</Path>
<!--StreamGeometry的表示方法-->
<Path Stroke="Black" Data="M 10,100 L 100,100 100,50" Canvas.Top="100"/>
</Canvas>
运行效果如下:
♦ StreamGeometry指令说明
1. 移动指令 Move Command(M):M 起始点/m 起始点
例如:M 100,50或m 100,50
M:表示绝对值;
m:表示相对于前一点的值,如果前一点没有指定,则使用(0,0)。
2. 绘制指令(Draw Command):
(1) 直线:Line(L)
(2) 水平直线: Horizontal line(H)
(3) 垂直直线: Vertical line(V)
(4) 三次方程式贝塞尔曲线: Cubic Bezier curve(C)
(5) 二次方程式贝塞尔曲线: Quadratic Bezier curve(Q)
(6) 平滑三次方程式贝塞尔曲线: Smooth cubic Bezier curve(S)
(7) 平滑二次方程式贝塞尔曲线: smooth quadratic Bezier curve(T)
(8) 椭圆圆弧: elliptical Arc(A)
3. 关闭指令 Close Command:Z / z
直线举例:
<Path Stroke="Red" Data="M 100,100 L200,200"/>
<Path Stroke="Red" Data="M 100,100 L200,200 200 300"/>
运行效果:
♦ PathFigureCollection几何图形
Data的类型是Geomery(几何图形),是一个抽象类,不能直接使用
下面针对各图形举例说明:
1、Path路径直线几何图形
<Path Stroke="Red" StrokeThickness="3" >
<Path.Data>
<LineGeometry StartPoint="20, 20" EndPoint="120, 120"/>
</Path.Data>
</Path>
<Path Stroke="Red" StrokeThickness="3" Data="M 10,100 L 100,100 100,50" />
2、矩形图形(RectangleGeometry)
<Path Stroke="Red" Fill="Yellow" StrokeThickness="2">
<Path.Data>
<RectangleGeometry Rect="20,20,120,120" RadiusX="10" RadiusY="10"/>
</Path.Data>
</Path>
3、椭圆几何图形(EllipseGeometry)
<Path Stroke="Red" Fill="LawnGreen">
<Path.Data>
<EllipseGeometry Center="80,80" RadiusX="60" RadiusY="60"/>
</Path.Data>
</Path>
4、PathGeometry
PathGeometry 之所以如此重要,是因为 Path的 Figures属性可以容纳 PathFigure对象,而 PathFigure 的 Segments属性又可以容纳各种线段结合成的复杂图形。
代码轮廓:
<Path>
<Path.Data>
<PathGeometry>
<PathFigure>
<!--各种线段-->
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
4.1、Segments线段
注意:这些线段是没有起点,起点就是前一个线段的终点
4.1.1、圆弧线段(ArcSegment)
<Path Stroke="Red">
<Path.Data>
<PathGeometry>
<PathFigure IsClosed="False" StartPoint="50,50">
<ArcSegment Point="100,100" Size="50,50" SweepDirection="Clockwise"
IsLargeArc="False" RotationAngle="45"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
<Path Stroke="Red" StrokeThickness="2" Data="M50 50 A50 50 45 0 1 100 100"/>
这两段代码实现的图形是相同的
ArcSegment设置许多属性,它们分别是:
Point:指明圆弧连接的终点
Size:指明完整椭圆的横轴半径和纵轴半径
IsLargeArc:指明是否使用大弧去连接 . . .
SweepDirection :指明圆弧是顺时针方向还是逆时针方向
RotationAngle:指明圆弧椭圆的旋转角度
4.1.2、贝塞尔曲线(BezierSegment)
3次方贝塞尔曲线由 4 个点决定:
(1)起点:前一线段的终点 或者 PathFigure 的 StartPoint
(2)终点:Point3 属性,即曲线的终点位置
(3)两个控制点:Point1 和 Point2 属性
<Path Stroke="Black" StrokeThickness="2" Margin="30">
<Path.Data>
<PathGeometry>
<PathFigure StartPoint="0,0">
<BezierSegment Point1="250,0" Point2="50,200"
Point3="300,200"/>
</PathFigure>
</PathGeometry>
</Path.Data>
</Path>
贝塞尔曲线原理链接:
https://blog.csdn.net/danshiming/article/details/125821526
几何图形代码示例:
public MainWindow()
{
InitializeComponent();
Path myPath = new Path();
myPath.Stroke = Brushes.Black;
myPath.StrokeThickness = 1;
StreamGeometry geometry = new StreamGeometry();
using (StreamGeometryContext ctx = geometry.Open())
{
ctx.BeginFigure(new Point(10, 100), true /* is filled */, true /* is closed */);
ctx.LineTo(new Point(100, 100), true, false);
ctx.LineTo(new Point(100, 50), true, false);
}
geometry.Freeze();
myPath.Data = geometry;
StackPanel mainPanel = new StackPanel();
mainPanel.Children.Add(myPath);
this.Content = mainPanel;
}
DrawingVisual类
DrawingVisual类是WPF中最轻量级的绘图类,继承自ContainerVisual、ContainerVisual又继承自Visual(Visual有以下能力:输出显示、坐标变换、区域剪裁、命中测试、边框计算。)
DrawingVisual无法单独存在,必须放在一个容器中(需要有布局系统)呈现.
DrawingVisual继承自ContainerVisual,该类有一个RenderOpen方法,返回一个可用于定义可视化内容的DrawingContext对象,绘制完毕后需要调用DrawingContext.Close()方法来结束绘图。
DrawingContext中有各种绘图的方法:
DrawText
DrawLine
DrawRectangle
DrawRoundedRectangle
DrawEllipse
DrawGeometry
DrawImage
等等
DrawingVisual示例1:
private List<DrawingVisual> _drawingVisuals = new List<DrawingVisual>();
public MainWindow()
{
DrawingVisual _drawingVisual = new DrawingVisual();
_drawingVisuals.Add(_drawingVisual);
DrawingContext dc = _drawingVisual.RenderOpen();
dc.DrawGeometry(Brushes.White, new Pen(Brushes.White, 1), new PathGeometry());
dc.Close();
}
//必须重载这两个方法,不然是画不出来的
// 重载自己的VisualTree的孩子的个数
protected override int VisualChildrenCount
{
get { return _drawingVisuals.Count; }
}
// 重载当WPF框架向自己要孩子的时候,返回返回DrawingVisual
protected override Visual GetVisualChild(int index)
{
return _drawingVisuals[index];
}
DrawingVisual示例2:
public partial class Window5 : Window
{
public Window5()
{
InitializeComponent();
this.Content = new RectangleElement();
}
}
public class RectangleElement : UIElement
{
protected override void OnRender(DrawingContext drawingContext)
{
drawingContext.DrawRectangle(Brushes.Red, null, new Rect(0, 0, 100, 20));
base.OnRender(drawingContext);
}
}
注意:OnRender渲染界面时、通过将drawingContext实现绘制,绘制完后**不能调用Close()**方法关闭。
Bitmap类和WritableBitmap
Bitmap类和WritableBitmap的区别:
1、可写性:
WriteableBitmap 可以在运行时对其进行修改和绘制,
Bitmap 只能读取位图数据。
2、存储方式:
WriteableBitmap 是基于内存中的位图数组存储的,
Bitmap 是基于磁盘文件存储的。
3、可用性:
WriteableBitmap 可以在 WPF 应用程序中使用,
Bitmap 可以在 Windows 应用程序中使用。
4、性能:
WriteableBitmap 的读写性能比 Bitmap 更高,因为它不需要进行磁盘读写操作。
5、方便性:
WriteableBitmap 提供了简单的 API 用于在运行时修改和绘制位图,
Bitmap 需要使用 GDI+ 进行图形操作。
在图元或数据量很大时,可考虑WritableBitmap可写位图实现绘图,以提高性能,以下时WritableBitmap的一些特点:
1、WriteableBitmap类使用两个缓冲区。后台缓冲区在系统内存中分配并累积当前未显示的内容。前台缓冲区分配在系统内存中,包含当前显示的内容。渲染系统将前端缓冲区复制到显存中进行显示。
2、两个线程使用这些缓冲区。用户界面 (UI) 线程生成 UI 但不将其呈现到屏幕上。UI 线程响应用户输入、计时器和其他事件。一个应用程序可以有多个 UI 线程。呈现线程组合并呈现来自 UI 线程的更改。每个应用程序只有一个渲染线程。
3、UI 线程将内容写入后台缓冲区。渲染线程从前端缓冲区读取内容并将其复制到显存。使用更改的矩形区域跟踪对后台缓冲区的更改。
4、WriteableBitmap在需要编辑图片的时候,是非常有效的手段,通过它我们可以做到在内存中直接修改图片的字节流,并自动通知到界面自动刷新,且效率较高,因为不需要每次重新加载图像
WritableBitmap调用流程
为了更好地控制更新,以及对后台缓冲区的多线程访问,请使用以下工作流程。
1、调用Lock方法为更新保留后台缓冲区。
2、通过访问BackBuffer属性获取指向后台缓冲区的指针。
3、将更改写入后台缓冲区。当WriteableBitmap被锁定时,其他线程可能会将更改写入后台缓冲区。
4、调用AddDirtyRect方法来指示已更改的区域。
5、调用Unlock方法释放后台缓冲区并允许在屏幕上显示。
6、当更新被发送到渲染线程时,渲染线程将更改后的矩形从后台缓冲区复制到前台缓冲区。渲染系统控制这种交换以避免死锁和重绘伪像
using CommonLib;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using drawing = System.Drawing;
namespace Controls.AxisSeries
{
/// <summary>
/// 高性能曲线图
/// </summary>
public class PolyLineAsync : UserControl
{
#region 字段
/// <summary>
/// 有效高度
/// </summary>
private double height;
#endregion
#region 属性
/// <summary>
/// 横向比例系数
/// </summary>
public double Kx { get; private set; }
/// <summary>
/// 纵向比例系数
/// </summary>
public double Ky { get; private set; }
/// <summary>
/// 面板
/// </summary>
public Canvas Root { get; private set; }
/// <summary>
/// 横轴的数据范围
/// </summary>
public Range HorizontalRange
{
get { return (Range)GetValue(HorizontalRangeProperty); }
set { SetValue(HorizontalRangeProperty, value); }
}
/// <summary>
/// 纵轴的数据范围
/// </summary>
public Range VerticalRange
{
get { return (Range)GetValue(VerticalRangeProperty); }
set { SetValue(VerticalRangeProperty, value); }
}
/// <summary>
/// 数据源
/// </summary>
public DataSeries DataSeries
{
get { return (DataSeries)GetValue(DataSeriesProperty); }
set { SetValue(DataSeriesProperty, value); }
}
#endregion
#region 依赖属性
/// <summary>
/// 横轴的数据范围
/// </summary>
public static readonly DependencyProperty HorizontalRangeProperty = DependencyProperty.Register(nameof(HorizontalRange),
typeof(Range), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));
/// <summary>
/// 纵轴的数据范围
/// </summary>
public static readonly DependencyProperty VerticalRangeProperty = DependencyProperty.Register("VerticalRange",
typeof(Range), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));
/// <summary>
/// 数据源
/// </summary>
public static readonly DependencyProperty DataSeriesProperty = DependencyProperty.Register("DataSeries",
typeof(DataSeries), typeof(PolyLineAsync), new PropertyMetadata(null, OnParamterChanged));
#endregion
#region 构造函数
public PolyLineAsync()
{
Root = new Canvas();
Content = Root;
DataSeries = new DataSeries();
Random r = new Random();
for (int i = 0; i < 50; i++)
{
DataSeries.Add(new drawing.Point
(
r.Next(0, 300),
r.Next(-100, 100)
));
}
SizeChanged += (s, e) => { ResetScale(); Refresh(); };
}
#endregion
#region 内部方法
/// <summary>
/// 刷新
/// </summary>
private void Refresh()
{
Root.Children.Clear();
if (Kx == 0 || Ky == 0) return;
if (DataSeries.Any())
{
var points = new List<drawing.PointF>();
DataSeries.ToList().ForEach(p => {
points.Add(Normalize(p));
});
if (points.Count <= 1) return;
WriteableBitmap bitmap = new WriteableBitmap((int)this.RenderSize.Width,(int)this.RenderSize.Height,96, 96,PixelFormats.Bgra32,null);
Image image = new Image { Source = bitmap};
bitmap.Lock();
using (drawing.Bitmap buff = new drawing.Bitmap((int)bitmap.Width, (int)bitmap.Height, bitmap.BackBufferStride, System.Drawing.Imaging.PixelFormat.Format32bppArgb, bitmap.BackBuffer))
{
using (drawing.Graphics g = drawing.Graphics.FromImage(buff))
{
Color color = (Foreground as SolidColorBrush).Color;
var brush = drawing.Color.FromArgb(color.A, color.R, color.G, color.A);
var pen = new drawing.Pen(brush, 1); // 颜色和线条宽度
g.DrawLines(pen, points.ToArray());
g.Flush();
}
}
bitmap.AddDirtyRect(new Int32Rect(0, 0, (int)bitmap.Width, (int)bitmap.Height));
bitmap.Unlock();
Root.Children.Add(image);
}
}
/// <summary>
/// 重置大小
/// </summary>
/// <param name="polyLineFigure"></param>
private void ResetScale()
{
this.Kx = 0;
this.Ky = 0;
if (this.HorizontalRange == null || this.VerticalRange == null) return;
if (this.HorizontalRange.Distance == 0 || this.VerticalRange.Distance == 0) return;
double width = double.IsNaN(this.Width) ? this.RenderSize.Width : this.Width;
this.Kx = width / this.HorizontalRange.Distance;
this.height = double.IsNaN(this.Height) ? this.RenderSize.Height : this.Height;
this.Ky = this.height / this.VerticalRange.Distance;
}
/// <summary>
/// 属性变更回调函数
/// </summary>
/// <param name="d"></param>
/// <param name="e"></param>
private static void OnParamterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
(d as PolyLineAsync)?.ResetScale();
}
/// <summary>
/// 数据点投影到视图点
/// </summary>
/// <param name="v"></param>
/// <returns></returns>
private drawing.PointF Normalize(drawing.Point p) => new drawing.PointF
(
(float)((p.X - HorizontalRange.Min) * Kx),
(float)(height - (p.Y - VerticalRange.Min) * Ky)
);
#endregion
}
}