你是否在项目中遇到过这样的需求:需要开发一个专业的路径绘制工具,支持工业级精度和复杂路径操作?传统的GDI+性能有限,WPF又过于复杂。今天,我将带你用C#和SkiaSharp打造一个完整的工业级路径绘制系统。
本文将手把手教你:如何设计专业的UI布局、实现高性能图形渲染、处理复杂路径算法,以及导出多种工业格式(G代码、DXF等)。无论你是CAD软件开发者,还是工业控制系统工程师,这套解决方案都能为你节省大量开发时间。
在开发工业级绘图软件时,我们常常面临这些挑战:
性能瓶颈:GDI+在处理大量图形元素时性能急剧下降
精度问题:浮点运算误差影响工业级精度要求
格式兼容:需要支持多种工业标准格式输出
UI复杂性:专业软件需要丰富的交互体验
SkiaSharp作为Google Skia的.NET绑定,为我们提供了:

C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SkiaSharp;
namespace AppIndustrialPathDrawing.Models
{
public class PathPoint
{
public float X { get; set; }
public float Y { get; set; }
public PathPointType Type { get; set; }
public float[] ControlPoints { get; set; }
public string Description { get; set; }
public DateTime CreatedTime { get; set; }
public PathPoint()
{
CreatedTime = DateTime.Now;
Type = PathPointType.Line;
}
public PathPoint(float x, float y, PathPointType type = PathPointType.Line) : this()
{
X = x;
Y = y;
Type = type;
}
public SKPoint ToSKPoint()
{
return new SKPoint(X, Y);
}
public override string ToString()
{
return $"({X:F2}, {Y:F2}) - {Type}";
}
}
public enum PathPointType
{
Move, // 移动到点
Line, // 直线到点
Curve, // 曲线到点
Arc, // 弧线到点
Cubic, // 三次贝塞尔曲线
Quadratic // 二次贝塞尔曲线
}
}
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using SkiaSharp;
namespace AppIndustrialPathDrawing.Models
{
public class IndustrialPath
{
public string Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public List<PathPoint> Points { get; set; }
public PathType Type { get; set; }
public DrawingSettings Settings { get; set; }
public DateTime CreatedTime { get; set; }
public DateTime ModifiedTime { get; set; }
public float TotalLength { get; private set; }
public float MaxVelocity { get; set; } = 100.0f; // mm/s
public float Acceleration { get; set; } = 50.0f; // mm/s²
public float Tolerance { get; set; } = 0.01f; // mm
public string MaterialType { get; set; } = "Steel";
public float ToolDiameter { get; set; } = 5.0f; // mm
public IndustrialPath()
{
Id = Guid.NewGuid().ToString();
Points = new List<PathPoint>();
Settings = new DrawingSettings();
CreatedTime = DateTime.Now;
ModifiedTime = DateTime.Now;
}
public void AddPoint(PathPoint point)
{
Points.Add(point);
ModifiedTime = DateTime.Now;
CalculateLength();
}
public bool RemovePoint(PathPoint point)
{
bool removed = Points.Remove(point);
if (removed)
{
ModifiedTime = DateTime.Now;
CalculateLength();
}
return removed;
}
public void CalculateLength()
{
TotalLength = 0;
for (int i = 1; i < Points.Count; i++)
{
var p1 = Points[i - 1];
var p2 = Points[i];
switch (p2.Type)
{
case PathPointType.Line:
TotalLength += CalculateLineLength(p1, p2);
break;
case PathPointType.Curve:
case PathPointType.Cubic:
case PathPointType.Quadratic:
TotalLength += CalculateCurveLength(p1, p2);
break;
case PathPointType.Arc:
TotalLength += CalculateArcLength(p1, p2);
break;
}
}
}
private float CalculateLineLength(PathPoint p1, PathPoint p2)
{
float dx = p2.X - p1.X;
float dy = p2.Y - p1.Y;
return (float)Math.Sqrt(dx * dx + dy * dy);
}
private float CalculateCurveLength(PathPoint p1, PathPoint p2)
{
// 简化一些
return CalculateLineLength(p1, p2) * 1.2f;
}
private float CalculateArcLength(PathPoint p1, PathPoint p2)
{
// 简化一些
return CalculateLineLength(p1, p2) * 1.57f; // π/2 近似
}
/// <summary>
/// 创建 SkiaSharp 路径对象
/// </summary>
public SKPath CreateSKPath()
{
var path = new SKPath();
if (Points.Count == 0) return path;
var firstPoint = Points[0];
path.MoveTo(firstPoint.X, firstPoint.Y);
for (int i = 1; i < Points.Count; i++)
{
var point = Points[i];
switch (point.Type)
{
case PathPointType.Line:
path.LineTo(point.X, point.Y);
break;
case PathPointType.Quadratic:
if (point.ControlPoints != null && point.ControlPoints.Length >= 2)
{
path.QuadTo(point.ControlPoints[0], point.ControlPoints[1],
point.X, point.Y);
}
else
{
path.LineTo(point.X, point.Y);
}
break;
case PathPointType.Cubic:
if (point.ControlPoints != null && point.ControlPoints.Length >= 4)
{
path.CubicTo(point.ControlPoints[0], point.ControlPoints[1],
point.ControlPoints[2], point.ControlPoints[3],
point.X, point.Y);
}
else
{
path.LineTo(point.X, point.Y);
}
break;
case PathPointType.Arc:
var rect = new SKRect(point.X - 50, point.Y - 50, point.X + 50, point.Y + 50);
path.ArcTo(rect, 0, 90, false);
break;
default:
path.LineTo(point.X, point.Y);
break;
}
}
return path;
}
public SKRect GetBounds()
{
if (Points.Count == 0) return SKRect.Empty;
float minX = Points.Min(p => p.X);
float maxX = Points.Max(p => p.X);
float minY = Points.Min(p => p.Y);
float maxY = Points.Max(p => p.Y);
return new SKRect(minX, minY, maxX, maxY);
}
}
public enum PathType
{
Linear, // 直线路径
Curved, // 曲线路径
Complex, // 复杂路径
Machining, // 加工路径
Welding, // 焊接路径
Cutting // 切割路径
}
}
C#using AppIndustrialPathDrawing.Models;
using AppIndustrialPathDrawing.Services;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
namespace AppIndustrialPathDrawing
{
public partial class FrmMain : Form
{
private IndustrialPath currentPath;
private DrawingSettings drawingSettings;
private float zoomFactor = 1.0f;
private SKPoint panOffset = SKPoint.Empty;
private bool isPanning = false;
private SKPoint lastPanPoint;
private ColorDialog colorDialog;
SKTypeface typeface;
public FrmMain()
{
InitializeComponent();
InitializeApplication();
// 加载支持中文的字体
typeface = SKTypeface.FromFamilyName(
"Microsoft YaHei", // 或 "SimHei", "SimSun" 等常用中文字体
SKFontStyleWeight.Normal,
SKFontStyleWidth.Normal,
SKFontStyleSlant.Upright);
}
private void InitializeApplication()
{
currentPath = new IndustrialPath
{
Name = "新建路径",
Type = PathType.Linear
};
drawingSettings = new DrawingSettings();
colorDialog = new ColorDialog();
SetupEventHandlers();
SetupListView();
UpdateUI();
}
private void SetupEventHandlers()
{
// SKControl 事件
skCanvas.PaintSurface += SkCanvas_PaintSurface;
skCanvas.MouseDown += SkCanvas_MouseDown;
skCanvas.MouseMove += SkCanvas_MouseMove;
skCanvas.MouseUp += SkCanvas_MouseUp;
skCanvas.MouseWheel += SkCanvas_MouseWheel;
// 按钮事件
btnAddPoint.Click += BtnAddPoint_Click;
btnRemovePoint.Click += BtnRemovePoint_Click;
btnCalculatePath.Click += BtnCalculatePath_Click;
btnClearPath.Click += BtnClearPath_Click;
btnStrokeColor.Click += BtnStrokeColor_Click;
btnFillColor.Click += BtnFillColor_Click;
// 数值控件事件
nudStartX.ValueChanged += CoordinateChanged;
nudStartY.ValueChanged += CoordinateChanged;
nudEndX.ValueChanged += CoordinateChanged;
nudEndY.ValueChanged += CoordinateChanged;
// 轨迹栏事件
trkStrokeWidth.ValueChanged += TrkStrokeWidth_ValueChanged;
// 复选框事件
chkShowGrid.CheckedChanged += DisplayOptionChanged;
chkShowCoordinates.CheckedChanged += DisplayOptionChanged;
chkAntiAlias.CheckedChanged += DisplayOptionChanged;
// 单选按钮事件
rbLinearPath.CheckedChanged += PathTypeChanged;
rbCurvePath.CheckedChanged += PathTypeChanged;
rbComplexPath.CheckedChanged += PathTypeChanged;
// 菜单事件
tsmiNew.Click += TsmiNew_Click;
tsmiOpen.Click += TsmiOpen_Click;
tsmiSave.Click += TsmiSave_Click;
tsmiExport.Click += TsmiExport_Click;
// 工具栏事件
tsbNew.Click += TsmiNew_Click;
tsbOpen.Click += TsmiOpen_Click;
tsbSave.Click += TsmiSave_Click;
tsbZoomIn.Click += TsbZoomIn_Click;
tsbZoomOut.Click += TsbZoomOut_Click;
tsbZoomFit.Click += TsbZoomFit_Click;
// ListView 事件
lvPathPoints.SelectedIndexChanged += LvPathPoints_SelectedIndexChanged;
}
private void SetupListView()
{
lvPathPoints.View = View.Details;
lvPathPoints.FullRowSelect = true;
lvPathPoints.GridLines = true;
lvPathPoints.MultiSelect = false;
lvPathPoints.Columns.Add("序号", 50);
lvPathPoints.Columns.Add("X坐标", 80);
lvPathPoints.Columns.Add("Y坐标", 80);
lvPathPoints.Columns.Add("类型", 80);
lvPathPoints.Columns.Add("描述", 100);
}
#region SkiaSharp 绘制事件
private void SkCanvas_PaintSurface(object sender, SKPaintSurfaceEventArgs e)
{
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.White);
// 应用缩放和平移变换
canvas.Save();
canvas.Translate(panOffset.X, panOffset.Y);
canvas.Scale(zoomFactor);
// 绘制网格
if (drawingSettings.ShowGrid)
{
DrawGrid(canvas);
}
// 绘制坐标轴
if (drawingSettings.ShowCoordinates)
{
DrawCoordinateAxes(canvas);
}
// 绘制路径
DrawIndustrialPath(canvas);
// 绘制控制点
if (drawingSettings.ShowControlPoints)
{
DrawControlPoints(canvas);
}
// 绘制方向箭头
if (drawingSettings.ShowDirection)
{
DrawDirectionArrows(canvas);
}
canvas.Restore();
// 绘制UI元素(不受变换影响)
DrawUIElements(canvas, e.Info);
}
private void DrawGrid(SKCanvas canvas)
{
using (var paint = drawingSettings.CreateGridPaint())
{
var bounds = GetVisibleBounds();
float spacing = drawingSettings.GridSpacing;
// 绘制垂直线
for (float x = (float)(Math.Floor(bounds.Left / spacing) * spacing);
x <= bounds.Right; x += spacing)
{
canvas.DrawLine(x, bounds.Top, x, bounds.Bottom, paint);
}
// 绘制水平线
for (float y = (float)(Math.Floor(bounds.Top / spacing) * spacing);
y <= bounds.Bottom; y += spacing)
{
canvas.DrawLine(bounds.Left, y, bounds.Right, y, paint);
}
}
}
private void DrawCoordinateAxes(SKCanvas canvas)
{
using (var axisPaint = new SKPaint
{
Color = SKColors.Black,
StrokeWidth = 1.0f,
Style = SKPaintStyle.Stroke,
IsAntialias = true
})
using (var textPaint = drawingSettings.CreateTextPaint())
{
var bounds = GetVisibleBounds();
// X轴
canvas.DrawLine(bounds.Left, 0, bounds.Right, 0, axisPaint);
// Y轴
canvas.DrawLine(0, bounds.Top, 0, bounds.Bottom, axisPaint);
// 绘制刻度标签
float spacing = drawingSettings.GridSpacing;
for (float x = spacing; x <= bounds.Right; x += spacing * 5)
{
canvas.DrawText(x.ToString("F0"), x, -5, textPaint);
}
for (float y = spacing; y <= bounds.Bottom; y += spacing * 5)
{
canvas.DrawText(y.ToString("F0"), 5, y, textPaint);
}
}
}
private void DrawIndustrialPath(SKCanvas canvas)
{
if (currentPath.Points.Count < 2) return;
using (var pathPaint = drawingSettings.CreatePathPaint())
using (var skPath = currentPath.CreateSKPath())
{
canvas.DrawPath(skPath, pathPaint);
}
// 绘制路径段信息
if (drawingSettings.ShowDimensions)
{
DrawPathDimensions(canvas);
}
}
private void DrawControlPoints(SKCanvas canvas)
{
using (var pointPaint = new SKPaint
{
Color = drawingSettings.ControlPointColor,
Style = SKPaintStyle.Fill,
IsAntialias = true
})
using (var outlinePaint = new SKPaint
{
Color = SKColors.Black,
Style = SKPaintStyle.Stroke,
StrokeWidth = 1.0f,
IsAntialias = true
})
{
for (int i = 0; i < currentPath.Points.Count; i++)
{
var point = currentPath.Points[i];
float radius = drawingSettings.ControlPointRadius;
// 起始点用不同颜色
if (i == 0)
{
pointPaint.Color = SKColors.Green;
}
else if (i == currentPath.Points.Count - 1)
{
pointPaint.Color = SKColors.Red;
}
else
{
pointPaint.Color = drawingSettings.ControlPointColor;
}
canvas.DrawCircle(point.X, point.Y, radius, pointPaint);
canvas.DrawCircle(point.X, point.Y, radius, outlinePaint);
// 绘制点编号
using (var textPaint = new SKPaint
{
Color = SKColors.Black,
TextSize = 12,
IsAntialias = true,
TextAlign = SKTextAlign.Center
})
{
canvas.DrawText((i + 1).ToString(), point.X, point.Y - radius - 5, textPaint);
}
}
}
}
private void DrawDirectionArrows(SKCanvas canvas)
{
if (currentPath.Points.Count < 2) return;
using (var arrowPaint = new SKPaint
{
Color = drawingSettings.DirectionArrowColor,
Style = SKPaintStyle.Fill,
IsAntialias = true
})
{
for (int i = 1; i < currentPath.Points.Count; i++)
{
var start = currentPath.Points[i - 1];
var end = currentPath.Points[i];
DrawArrow(canvas, start.ToSKPoint(), end.ToSKPoint(), arrowPaint);
}
}
}
private void DrawArrow(SKCanvas canvas, SKPoint start, SKPoint end, SKPaint paint)
{
float dx = end.X - start.X;
float dy = end.Y - start.Y;
float length = (float)Math.Sqrt(dx * dx + dy * dy);
if (length < 10) return; // 太短的线段不绘制箭头
// 计算箭头位置(线段中点)
float midX = start.X + dx * 0.5f;
float midY = start.Y + dy * 0.5f;
// 计算箭头方向
float angle = (float)Math.Atan2(dy, dx);
float arrowLength = drawingSettings.DirectionArrowSize;
using (var path = new SKPath())
{
// 绘制箭头
path.MoveTo(midX, midY);
path.LineTo(
midX - arrowLength * (float)Math.Cos(angle - Math.PI / 6),
midY - arrowLength * (float)Math.Sin(angle - Math.PI / 6));
path.MoveTo(midX, midY);
path.LineTo(
midX - arrowLength * (float)Math.Cos(angle + Math.PI / 6),
midY - arrowLength * (float)Math.Sin(angle + Math.PI / 6));
canvas.DrawPath(path, paint);
}
}
private void DrawPathDimensions(SKCanvas canvas)
{
using (var textPaint = new SKPaint
{
Color = drawingSettings.DimensionColor,
TextSize = drawingSettings.DimensionFontSize,
IsAntialias = true,
TextAlign = SKTextAlign.Center
})
{
for (int i = 1; i < currentPath.Points.Count; i++)
{
var start = currentPath.Points[i - 1];
var end = currentPath.Points[i];
float dx = end.X - start.X;
float dy = end.Y - start.Y;
float length = (float)Math.Sqrt(dx * dx + dy * dy);
float midX = start.X + dx * 0.5f;
float midY = start.Y + dy * 0.5f;
canvas.DrawText($"{length:F1}mm", midX, midY - 10, textPaint);
}
}
}
private void DrawUIElements(SKCanvas canvas, SKImageInfo info)
{
// 绘制缩放信息
using (var textPaint = new SKPaint
{
Typeface = typeface, // 设置字体
Color = SKColors.Black,
TextSize = 14,
IsAntialias = true
})
{
canvas.DrawText($"缩放: {zoomFactor:P0}", 10, 30, textPaint);
canvas.DrawText($"点数: {currentPath.Points.Count}", 10, 50, textPaint);
canvas.DrawText($"长度: {currentPath.TotalLength:F1}mm", 10, 70, textPaint);
}
}
private SKRect GetVisibleBounds()
{
float margin = 100 / zoomFactor;
return new SKRect(
-panOffset.X / zoomFactor - margin,
-panOffset.Y / zoomFactor - margin,
(-panOffset.X + skCanvas.Width) / zoomFactor + margin,
(-panOffset.Y + skCanvas.Height) / zoomFactor + margin
);
}
#endregion
#region 鼠标事件处理
private void SkCanvas_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Middle)
{
isPanning = true;
lastPanPoint = new SKPoint(e.X, e.Y);
skCanvas.Cursor = Cursors.Hand;
}
else if (e.Button == MouseButtons.Left)
{
// 将屏幕坐标转换为世界坐标
var worldPoint = ScreenToWorld(new SKPoint(e.X, e.Y));
// 添加新的路径点
var newPoint = new PathPoint(worldPoint.X, worldPoint.Y);
if (rbCurvePath.Checked && currentPath.Points.Count > 0)
{
var lastPoint = currentPath.Points.Last();
newPoint.Type = PathPointType.Quadratic;
newPoint.ControlPoints = PathCalculationService.CalculateBezierControlPoints(lastPoint, newPoint);
}
else if (rbComplexPath.Checked && currentPath.Points.Count > 0)
{
var lastPoint = currentPath.Points.Last();
newPoint.Type = PathPointType.Cubic;
newPoint.ControlPoints = PathCalculationService.CalculateCubicBezierControlPoints(lastPoint, newPoint);
}
currentPath.AddPoint(newPoint);
UpdateUI();
skCanvas.Invalidate();
}
}
private void SkCanvas_MouseMove(object sender, MouseEventArgs e)
{
var worldPoint = ScreenToWorld(new SKPoint(e.X, e.Y));
// 更新状态栏坐标显示
tsslCoordinates.Text = $"坐标: ({worldPoint.X:F1}, {worldPoint.Y:F1})";
if (isPanning)
{
var currentPoint = new SKPoint(e.X, e.Y);
panOffset.X += currentPoint.X - lastPanPoint.X;
panOffset.Y += currentPoint.Y - lastPanPoint.Y;
lastPanPoint = currentPoint;
skCanvas.Invalidate();
}
}
private void SkCanvas_MouseUp(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Middle)
{
isPanning = false;
skCanvas.Cursor = Cursors.Default;
}
}
private void SkCanvas_MouseWheel(object sender, MouseEventArgs e)
{
float zoomDelta = e.Delta > 0 ? 1.1f : 0.9f;
var mousePoint = new SKPoint(e.X, e.Y);
// 以鼠标位置为中心缩放
panOffset.X = mousePoint.X - (mousePoint.X - panOffset.X) * zoomDelta;
panOffset.Y = mousePoint.Y - (mousePoint.Y - panOffset.Y) * zoomDelta;
zoomFactor *= zoomDelta;
// 限制缩放范围
zoomFactor = Math.Max(0.1f, Math.Min(10.0f, zoomFactor));
tsslZoom.Text = $"缩放: {zoomFactor:P0}";
skCanvas.Invalidate();
}
private SKPoint ScreenToWorld(SKPoint screenPoint)
{
return new SKPoint(
(screenPoint.X - panOffset.X) / zoomFactor,
(screenPoint.Y - panOffset.Y) / zoomFactor
);
}
#endregion
#region 控件事件处理
private void BtnAddPoint_Click(object sender, EventArgs e)
{
var newPoint = new PathPoint((float)nudStartX.Value, (float)nudStartY.Value);
if (rbCurvePath.Checked && currentPath.Points.Count > 0)
{
var lastPoint = currentPath.Points.Last();
newPoint.Type = PathPointType.Quadratic;
newPoint.ControlPoints = PathCalculationService.CalculateBezierControlPoints(lastPoint, newPoint);
}
else if (rbComplexPath.Checked && currentPath.Points.Count > 0)
{
var lastPoint = currentPath.Points.Last();
newPoint.Type = PathPointType.Cubic;
newPoint.ControlPoints = PathCalculationService.CalculateCubicBezierControlPoints(lastPoint, newPoint);
}
currentPath.AddPoint(newPoint);
UpdateUI();
skCanvas.Invalidate();
}
private void BtnRemovePoint_Click(object sender, EventArgs e)
{
if (lvPathPoints.SelectedIndices.Count > 0)
{
int index = lvPathPoints.SelectedIndices[0];
if (index < currentPath.Points.Count)
{
currentPath.Points.RemoveAt(index);
currentPath.CalculateLength();
UpdateUI();
skCanvas.Invalidate();
}
}
}
private void BtnCalculatePath_Click(object sender, EventArgs e)
{
if (currentPath.Points.Count < 2)
{
MessageBox.Show("至少需要两个点才能计算路径。", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
// 优化路径
var optimizedPoints = PathCalculationService.OptimizePath(currentPath.Points, 2.0f);
currentPath.Points.Clear();
optimizedPoints.ForEach(p => currentPath.AddPoint(p));
// 计算加工时间
float machiningTime = PathCalculationService.CalculateMachiningTime(currentPath);
MessageBox.Show($"路径优化完成!\n" +
$"优化后点数: {currentPath.Points.Count}\n" +
$"总长度: {currentPath.TotalLength:F2} mm\n" +
$"预估加工时间: {machiningTime:F1} 秒",
"路径计算结果", MessageBoxButtons.OK, MessageBoxIcon.Information);
UpdateUI();
skCanvas.Invalidate();
}
private void BtnClearPath_Click(object sender, EventArgs e)
{
if (MessageBox.Show("确定要清除当前路径吗?", "确认",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes)
{
currentPath.Points.Clear();
currentPath.CalculateLength();
UpdateUI();
skCanvas.Invalidate();
}
}
private void BtnStrokeColor_Click(object sender, EventArgs e)
{
colorDialog.Color = Color.FromArgb(drawingSettings.StrokeColor.Alpha,
drawingSettings.StrokeColor.Red,
drawingSettings.StrokeColor.Green,
drawingSettings.StrokeColor.Blue);
if (colorDialog.ShowDialog() == DialogResult.OK)
{
drawingSettings.StrokeColor = new SKColor(
colorDialog.Color.R,
colorDialog.Color.G,
colorDialog.Color.B,
colorDialog.Color.A);
btnStrokeColor.BackColor = colorDialog.Color;
skCanvas.Invalidate();
}
}
private void BtnFillColor_Click(object sender, EventArgs e)
{
colorDialog.Color = Color.FromArgb(drawingSettings.FillColor.Alpha,
drawingSettings.FillColor.Red,
drawingSettings.FillColor.Green,
drawingSettings.FillColor.Blue);
if (colorDialog.ShowDialog() == DialogResult.OK)
{
drawingSettings.FillColor = new SKColor(
colorDialog.Color.R,
colorDialog.Color.G,
colorDialog.Color.B,
colorDialog.Color.A);
btnFillColor.BackColor = colorDialog.Color;
skCanvas.Invalidate();
}
}
private void TrkStrokeWidth_ValueChanged(object sender, EventArgs e)
{
drawingSettings.StrokeWidth = trkStrokeWidth.Value;
lblStrokeWidthValue.Text = trkStrokeWidth.Value.ToString();
skCanvas.Invalidate();
}
private void CoordinateChanged(object sender, EventArgs e)
{
// 实时更新坐标预览
skCanvas.Invalidate();
}
private void DisplayOptionChanged(object sender, EventArgs e)
{
drawingSettings.ShowGrid = chkShowGrid.Checked;
drawingSettings.ShowCoordinates = chkShowCoordinates.Checked;
drawingSettings.AntiAlias = chkAntiAlias.Checked;
skCanvas.Invalidate();
}
private void PathTypeChanged(object sender, EventArgs e)
{
if (rbLinearPath.Checked)
currentPath.Type = PathType.Linear;
else if (rbCurvePath.Checked)
currentPath.Type = PathType.Curved;
else if (rbComplexPath.Checked)
currentPath.Type = PathType.Complex;
}
private void LvPathPoints_SelectedIndexChanged(object sender, EventArgs e)
{
btnRemovePoint.Enabled = lvPathPoints.SelectedIndices.Count > 0;
}
#endregion
#region 菜单和工具栏事件
private void TsmiNew_Click(object sender, EventArgs e)
{
if (currentPath.Points.Count > 0)
{
if (MessageBox.Show("当前路径尚未保存,确定要新建吗?", "确认",
MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes)
return;
}
currentPath = new IndustrialPath
{
Name = "新建路径",
Type = PathType.Linear
};
UpdateUI();
skCanvas.Invalidate();
}
private void TsmiOpen_Click(object sender, EventArgs e)
{
using (var openFileDialog = new OpenFileDialog())
{
openFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
openFileDialog.FilterIndex = 1;
if (openFileDialog.ShowDialog() == DialogResult.OK)
{
var loadedPath = ExportService.LoadFromJSON(openFileDialog.FileName);
if (loadedPath != null)
{
currentPath = loadedPath;
UpdateUI();
skCanvas.Invalidate();
}
}
}
}
private void TsmiSave_Click(object sender, EventArgs e)
{
using (var saveFileDialog = new SaveFileDialog())
{
saveFileDialog.Filter = "JSON文件 (*.json)|*.json|所有文件 (*.*)|*.*";
saveFileDialog.FilterIndex = 1;
saveFileDialog.FileName = currentPath.Name + ".json";
if (saveFileDialog.ShowDialog() == DialogResult.OK)
{
ExportService.SaveToJSON(currentPath, saveFileDialog.FileName);
}
}
}
private void TsmiExport_Click(object sender, EventArgs e)
{
using (var exportDialog = new SaveFileDialog())
{
exportDialog.Filter = "PNG图片 (*.png)|*.png|SVG矢量图 (*.svg)|*.svg|" +
"G代码 (*.gcode)|*.gcode|DXF文件 (*.dxf)|*.dxf|所有文件 (*.*)|*.*";
exportDialog.FilterIndex = 1;
exportDialog.FileName = currentPath.Name;
if (exportDialog.ShowDialog() == DialogResult.OK)
{
string extension = System.IO.Path.GetExtension(exportDialog.FileName).ToLower();
bool success = false;
switch (extension)
{
case ".png":
success = ExportService.ExportToPNG(currentPath, drawingSettings, exportDialog.FileName);
break;
case ".svg":
success = ExportService.ExportToSVG(currentPath, drawingSettings, exportDialog.FileName);
break;
case ".gcode":
success = ExportService.ExportToGCode(currentPath, exportDialog.FileName);
break;
case ".dxf":
success = ExportService.ExportToDXF(currentPath, exportDialog.FileName);
break;
}
if (success)
{
MessageBox.Show("导出成功!", "提示",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
}
}
private void TsbZoomIn_Click(object sender, EventArgs e)
{
zoomFactor *= 1.2f;
zoomFactor = Math.Min(10.0f, zoomFactor);
tsslZoom.Text = $"缩放: {zoomFactor:P0}";
skCanvas.Invalidate();
}
private void TsbZoomOut_Click(object sender, EventArgs e)
{
zoomFactor /= 1.2f;
zoomFactor = Math.Max(0.1f, zoomFactor);
tsslZoom.Text = $"缩放: {zoomFactor:P0}";
skCanvas.Invalidate();
}
private void TsbZoomFit_Click(object sender, EventArgs e)
{
if (currentPath.Points.Count == 0) return;
var bounds = currentPath.GetBounds();
if (bounds.IsEmpty) return;
float scaleX = (skCanvas.Width - 100) / bounds.Width;
float scaleY = (skCanvas.Height - 100) / bounds.Height;
zoomFactor = Math.Min(scaleX, scaleY);
panOffset.X = (skCanvas.Width - bounds.Width * zoomFactor) / 2 - bounds.Left * zoomFactor;
panOffset.Y = (skCanvas.Height - bounds.Height * zoomFactor) / 2 - bounds.Top * zoomFactor;
tsslZoom.Text = $"缩放: {zoomFactor:P0}";
skCanvas.Invalidate();
}
#endregion
#region UI 更新
private void UpdateUI()
{
// 更新路径点列表
UpdatePathPointsList();
// 更新状态栏
tsslPathLength.Text = $"路径长度: {currentPath.TotalLength:F1}mm";
// 更新按钮状态
btnRemovePoint.Enabled = lvPathPoints.SelectedIndices.Count > 0;
btnCalculatePath.Enabled = currentPath.Points.Count >= 2;
btnClearPath.Enabled = currentPath.Points.Count > 0;
// 更新窗体标题
this.Text = $"工业级路径绘制系统 v1.0 - {currentPath.Name}" +
(currentPath.Points.Count > 0 ? $" ({currentPath.Points.Count} 点)" : "");
}
private void UpdatePathPointsList()
{
lvPathPoints.Items.Clear();
for (int i = 0; i < currentPath.Points.Count; i++)
{
var point = currentPath.Points[i];
var item = new ListViewItem((i + 1).ToString());
item.SubItems.Add(point.X.ToString("F2"));
item.SubItems.Add(point.Y.ToString("F2"));
item.SubItems.Add(point.Type.ToString());
item.SubItems.Add(point.Description ?? "");
item.Tag = point;
lvPathPoints.Items.Add(item);
}
}
#endregion
}
}
C#public static class IndustrialExporter
{
/// <summary>
/// 导出G代码 - 工业制造标准
/// </summary>
public static bool ExportToGCode(IndustrialPath path, string filePath)
{
try
{
var gcode = new StringBuilder();
// G代码文件头
gcode.AppendLine("; Generated by Industrial Path Drawing System");
gcode.AppendLine($"; Date: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
gcode.AppendLine($"; Tool Diameter: {path.ToolDiameter}mm");
gcode.AppendLine($"; Feed Rate: {path.MaxVelocity}mm/min");
gcode.AppendLine();
// 机床初始化
gcode.AppendLine("G21 ; Set units to millimeters");
gcode.AppendLine("G90 ; Absolute positioning");
gcode.AppendLine("G94 ; Feed rate per minute");
gcode.AppendLine($"F{path.MaxVelocity:F0}");
gcode.AppendLine();
// 移动到起始点
if (path.Points.Count > 0)
{
var start = path.Points[0];
gcode.AppendLine($"G0 X{start.X:F3} Y{start.Y:F3} ; Rapid move to start");
gcode.AppendLine("M3 S1000 ; Start spindle");
float plungeDepth = path.ToolDiameter / 2;
gcode.AppendLine($"G1 Z-{plungeDepth:F3} F300 ; Plunge");
}
// 生成路径G代码
GeneratePathGCode(gcode, path);
// 程序结束
gcode.AppendLine("G0 Z5 ; Retract");
gcode.AppendLine("M5 ; Stop spindle");
gcode.AppendLine("G28 ; Home");
gcode.AppendLine("M30 ; End program");
File.WriteAllText(filePath, gcode.ToString(), Encoding.UTF8);
return true;
}
catch (Exception ex)
{
throw new ExportException($"G代码导出失败: {ex.Message}", ex);
}
}
/// <summary>
/// 路径转G代码核心算法
/// </summary>
private static void GeneratePathGCode(StringBuilder gcode, IndustrialPath path)
{
for (int i = 1; i < path.Points.Count; i++)
{
var point = path.Points[i];
var prevPoint = path.Points[i - 1];
switch (point.Type)
{
case PathPointType.Line:
gcode.AppendLine($"G1 X{point.X:F3} Y{point.Y:F3}");
break;
case PathPointType.Arc:
// 顺时针圆弧
float radius = CalculateArcRadius(prevPoint, point);
gcode.AppendLine($"G2 X{point.X:F3} Y{point.Y:F3} R{radius:F3}");
break;
case PathPointType.Curve:
// 曲线转换为多段直线
var segments = InterpolateCurve(prevPoint, point, 10);
foreach (var segment in segments)
{
gcode.AppendLine($"G1 X{segment.X:F3} Y{segment.Y:F3}");
}
break;
}
}
}
}
C#/// <summary>
/// DXF格式导出 - AutoCAD兼容
/// </summary>
public static bool ExportToDXF(IndustrialPath path, string filePath)
{
try
{
using var writer = new StreamWriter(filePath, false, Encoding.ASCII);
// DXF文件头
WriteDXFHeader(writer);
// 图层定义
WriteDXFLayers(writer);
// 实体部分
writer.WriteLine("0");
writer.WriteLine("SECTION");
writer.WriteLine("2");
writer.WriteLine("ENTITIES");
// 绘制路径实体
WritePathEntities(writer, path);
// 文件结尾
writer.WriteLine("0");
writer.WriteLine("ENDSEC");
writer.WriteLine("0");
writer.WriteLine("EOF");
return true;
}
catch (Exception ex)
{
throw new ExportException($"DXF导出失败: {ex.Message}", ex);
}
}
这套系统已成功应用于:
C#// 路径优化算法
public static class PathOptimizer
{
/// <summary>
/// 道格拉斯-普克算法简化路径
/// </summary>
public static List<PathPoint> SimplifyPath(List<PathPoint> points, float tolerance)
{
if (points.Count < 3) return points;
// 实现道格拉斯-普克算法
return DouglasPeucker(points, 0, points.Count - 1, tolerance);
}
}
// 碰撞检测
public static class CollisionDetector
{
/// <summary>
/// 检查路径是否与障碍物碰撞
/// </summary>
public static bool CheckPathCollision(IndustrialPath path, List<SKRect> obstacles)
{
var pathBounds = path.CreateSKPath().Bounds;
return obstacles.Any(obstacle =>
SKRect.Intersect(pathBounds, obstacle) != SKRect.Empty);
}
}


C#// 正确的资源管理
using (var paint = new SKPaint())
using (var path = new SKPath())
{
// 绘制操作
canvas.DrawPath(path, paint);
} // 自动释放资源
// 线程安全的画布更新
private void SafeUpdateCanvas()
{
if (InvokeRequired)
{
Invoke(new Action(() => skCanvas.Invalidate()));
}
else
{
skCanvas.Invalidate();
}
}
你在开发图形应用时遇到过什么性能瓶颈?欢迎在评论区分享你的经验和遇到的问题。
如果这篇文章对你有帮助,请转发给更多需要的同行!让我们一起推动C#在工业软件领域的发展。
关注我,获取更多C#实战干货和工业软件开发技巧!
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!