Why
- 主要是最近看到了很多有关于
WPF的绘图的,主要是2D绘图,我在想可不可以用Winform绘制,查找资料发现不同于WPF,Winform没有WPF的样条元素,所以只能自己自定义然后实现,同时也意味着选中,高亮,平移旋转等基本所有的动作都需要自己写,同时也因为不是可识别的Winform控件,所以其实最难做的是选中。
How
- 安装
SkiaSharp

- 在页面设计的工具库添加一个
SKControl控件,如果没有的话重新生成解决方案就可以了,取名skControl_Main.
- 自定义控件如下

Shape
public abstract class Shape : XtraUserControl
{
protected Shape()
{
if (string.IsNullOrWhiteSpace(ID))
{
ID = Guid.NewGuid().ToString();
}
}
public static float SKScale { get; set; } = 1;
public string ID { get; set; }
public SKColor Color { get; set; } = new SKColor(System.Drawing.Color.Green.R, System.Drawing.Color.Green.G, System.Drawing.Color.Green.B);
public bool IsStroke { get; set; } = true;
public float StrokeWidth { get; set; } = 10;
public SKPoint LocationPoint { get; set; } = new SKPoint(0, 0);
public int CanveWidth { get; set; }
public int CanveHeight { get; set; }
public SKPoint StartPoint { get; set; } = new SKPoint(0, 0);
public SKPoint EndPoint { get; set; } = new SKPoint(0, 0);
public SKPath path { get; set; } = new SKPath();
public SKPath HintPath { get; set; } = new SKPath();
public bool IsSelected { get; set; } = false;
public SKRect pathBounds;
public SKColor SelectedColor { get; set; } = new SKColor(System.Drawing.Color.Red.R, System.Drawing.Color.Red.G, System.Drawing.Color.Red.B);
public abstract void Draw(SKCanvas canvas);
public virtual void GetPathData(SKPaint paint)
{
paint.GetFillPath(path, HintPath);
HintPath.GetBounds(out pathBounds);
}
public virtual bool IsHint(SKPoint point)
{
return HintPath.Contains(point.X, point.Y);
}
public virtual bool Intersects(SKRect rect)
{
return rect.Contains(pathBounds);
}
}
public class LineShape : Shape
{
public override void Draw(SKCanvas canvas)
{
if (canvas == null)
{
return;
}
path.MoveTo(StartPoint);
path.LineTo(EndPoint);
path.Close();
using (var paint = new SKPaint())
{
paint.Color = IsSelected ? SelectedColor : Color;
paint.IsStroke = IsStroke;
paint.StrokeWidth = StrokeWidth;
paint.Style = SKPaintStyle.Stroke;
//canvas.DrawLine(StartPoint.X, StartPoint.Y, EndPoint.X, EndPoint.Y, paint);
canvas.DrawPath(path, paint);
GetPathData(paint);
}
}
}
public class ArcShape : Shape
{
public float SweepAngle { get; set; } = 180;
public new float StrokeWidth { get; set; } = 10;
public override void Draw(SKCanvas canvas)
{
if (canvas == null)
{
return;
}
path.MoveTo(StartPoint);
var rect = new SKRect(StartPoint.X, StartPoint.X, EndPoint.Y, EndPoint.Y);
path.AddArc(rect, 0, SweepAngle);
//如果是圆弧 则不需要闭合
//path.Close();
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.Color = IsSelected ? SelectedColor : Color;
paint.IsStroke = IsStroke;
paint.StrokeWidth = StrokeWidth;
paint.Style = SKPaintStyle.Stroke;
canvas.DrawPath(path, paint);
GetPathData(paint);
}
}
}
public class CircleShape : Shape
{
public float Radius { get; set; } = 10;
public SKPoint CenterPoint { get; set; } = new SKPoint(0, 0);
public override void Draw(SKCanvas canvas)
{
if (canvas == null)
{
return;
}
path.MoveTo(StartPoint);
path.AddCircle(CenterPoint.X, CenterPoint.Y, Radius);
path.Close();
using (var paint = new SKPaint())
{
paint.IsAntialias = true;
paint.Color = IsSelected ? SelectedColor : Color;
paint.IsStroke = IsStroke;
paint.StrokeWidth = StrokeWidth;
paint.Style = SKPaintStyle.Stroke;
canvas.DrawPath(path, paint);
GetPathData(paint);
}
}
}
public partial class TestControl : XtraUserControl
{
public TestControl()
{
InitializeComponent();
}
private void TestControl_Load(object sender, EventArgs e)
{
skControl_Main.MouseWheel += SkControl_Main_MouseWheel;
InitShapes();
}
private void InitShapes()
{
var random = new Random();
for (int i = 0; i < 10; i++)
{
var line = new LineShape();
line.StartPoint = new SKPoint(i * random.Next(10, 100), i * random.Next(10, 100));
line.EndPoint = new SKPoint(i * random.Next(10, 100) + random.Next(120), i * random.Next(10, maxValue: 100) + random.Next(120));
shapes.Add(line);
var arc = new ArcShape();
arc.StartPoint = new SKPoint(i * random.Next(10, 100), i * random.Next(10, 100));
arc.EndPoint = new SKPoint(i * random.Next(10, 100) + random.Next(120), i * random.Next(10, 100) + random.Next(120));
arc.SweepAngle = random.Next(0, 360);
shapes.Add(arc);
var circle = new CircleShape();
circle.CenterPoint = new SKPoint(i * random.Next(10, 100), i * random.Next(10, 100));
circle.Radius = random.Next(10, 100);
shapes.Add(circle);
}
}
private List<Shape> shapes = new List<Shape>();
private float _offsetX = 0;
private float _offsetY = 0;
private bool _isPanning = false;
private Point _lastMousePoint;
private bool _isSelecting = false;
private SKPoint _selectStart;
private SKPoint _selectEnd;
private void skControl_Main_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear();
//坐标系从左上角转换到左下角
canvas.Translate(0, e.Info.Height);
canvas.Scale(1, -1);
// 平移
canvas.Translate(_offsetX, _offsetY);
// 缩放
canvas.Scale(Shape.SKScale);
foreach (var shape in shapes)
{
shape.CanveWidth = e.Info.Width;
shape.CanveHeight = e.Info.Height;
shape.Draw(canvas);
}
if (_isSelecting)
{
SKRect rect = CreateRect(_selectStart, _selectEnd);
using (var paint = new SKPaint())
{
paint.Color = SKColors.DeepSkyBlue;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
paint.PathEffect = SKPathEffect.CreateDash(new float[] { 10, 10 }, 0);
canvas.DrawRect(rect, paint);
}
}
}
private SKPoint ScreenToWorld(float x, float y)
{
float worldX = (x - _offsetX) / Shape.SKScale;
float worldY = ((skControl_Main.Height - y) - _offsetY) / Shape.SKScale;
return new SKPoint(worldX, worldY);
}
private void skControl_Main_MouseDown(object sender, MouseEventArgs e)
{
if (ModifierKeys == Keys.None && e.Button == MouseButtons.Left)
{
SKPoint worldPoint = ScreenToWorld(e.X, e.Y);
var selectShapes = shapes.Where(p => p.IsHint(worldPoint)).ToList();
selectShapes.ForEach(p =>
{
p.IsSelected = !p.IsSelected;
});
skControl_Main.Invalidate();
}
else if (e.Button == MouseButtons.Middle)
{
_isPanning = true;
_lastMousePoint = e.Location;
skControl_Main.Cursor = Cursors.Hand;
}
else if (ModifierKeys == Keys.Control && e.Button == MouseButtons.Left)
{
//框选
_isSelecting = true;
_selectStart = ScreenToWorld(e.X, e.Y);
_selectEnd = _selectStart;
}
}
private void skControl_Main_MouseMove(object sender, MouseEventArgs e)
{
if (_isPanning)
{
float dx =
e.X - _lastMousePoint.X;
float dy =
e.Y - _lastMousePoint.Y;
_offsetX += dx;
_offsetY -= dy;
_lastMousePoint = e.Location;
skControl_Main.Invalidate();
}
if (_isSelecting)
{
_selectEnd = ScreenToWorld(e.X, e.Y);
skControl_Main.Invalidate();
}
}
private void skControl_Main_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Middle)
{
_isPanning = false;
skControl_Main.Cursor = Cursors.Default;
}
if (_isSelecting)
{
_isSelecting = false;
SKRect rect = CreateRect(_selectStart, _selectEnd);
foreach (var shape in shapes)
{
shape.IsSelected = shape.Intersects(rect);
}
skControl_Main.Invalidate();
}
}
private void SkControl_Main_MouseWheel(object sender, MouseEventArgs e)
{
float oldScale = Shape.SKScale;
if (e.Delta > 0)
{
Shape.SKScale *= 1.1f;
}
else
{
Shape.SKScale /= 1.1f;
}
// 鼠标缩放中心
float mouseX = e.X;
float mouseY = e.Y;
_offsetX =
mouseX -
(mouseX - _offsetX)
* (Shape.SKScale / oldScale);
_offsetY =
mouseY -
(mouseY - _offsetY)
* (Shape.SKScale / oldScale);
skControl_Main.Invalidate();
}
private void skControl_Main_MouseDoubleClick(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
SKPoint worldPoint = ScreenToWorld(e.X, e.Y);
var selectShapes = shapes.Where(p => p.IsHint(worldPoint)).ToList();
if (!selectShapes.Any())
{
//双击 取消所有的选中
shapes.ForEach(p =>
{
p.IsSelected = false;
});
skControl_Main.Invalidate();
}
else
{
//todo 可以弹出类似于属性面板的
}
}
}
private SKRect CreateRect(SKPoint p1, SKPoint p2)
{
return new SKRect(
Math.Min(p1.X, p2.X),
Math.Min(p1.Y, p2.Y),
Math.Max(p1.X, p2.X),
Math.Max(p1.Y, p2.Y));
}
}
Tips
- 看着很简单,但其实中间还是有很多坑。首先
SKCanvas这个东西在每次绘制的时候都是新的,所以没有办法缓存,所以需要在事件PaintSurface中每次获取。
- 绘制的默认坐标系是左上角,需要转换为左下角。
- 更改了
shapes需要手动调用绘制的时候,应该使用skControl_Main.Invalidate();
- 绘制其实有两种方法,一种是
canvas.DrawLine(StartPoint.X, StartPoint.Y, EndPoint.X, EndPoint.Y, paint);这种的,一种是 canvas.DrawPath(path, paint);,需要实现高亮的话,一定只能使用第二种方法,因为第一种是绘制了就绘制了,不会有任何的路径信息,后续无法检测。
- 计算包围盒的时候需要使用
HintPath.GetBounds(out pathBounds);
- 最后
InitShapes只是测试数据而已,后续我实现了导入DXF文件,然后解析绘制,效果还不错。以下是截图

- 测试发现读取速度很快,对于大型项目也是很不错的一个处理方式。
评论区