编辑
2025-12-22
C#
00

目录

💡 问题分析:工业绘图软件的核心痛点
🔍 传统方案的局限性
🎯 SkiaSharp的优势
🏗️ 系统架构设计
📋 核心类结构
🎨 工业路径核心类
🖥️ UI布局设计最佳实践
🔧 工业格式导出实战
📄 G代码导出 - CNC机床标准
📊 DXF格式导出 - CAD标准
🔥 实战应用场景
🏭 工业应用实例
💡 扩展功能建议
⚠️ 常见坑点提醒
🚨 SkiaSharp使用注意事项
💊 解决方案
✨ 总结与展望
🎯 核心要点总结
🚀 进阶学习建议
💬 互动交流

你是否在项目中遇到过这样的需求:需要开发一个专业的路径绘制工具,支持工业级精度和复杂路径操作?传统的GDI+性能有限,WPF又过于复杂。今天,我将带你用C#和SkiaSharp打造一个完整的工业级路径绘制系统。

本文将手把手教你:如何设计专业的UI布局、实现高性能图形渲染、处理复杂路径算法,以及导出多种工业格式(G代码、DXF等)。无论你是CAD软件开发者,还是工业控制系统工程师,这套解决方案都能为你节省大量开发时间。

💡 问题分析:工业绘图软件的核心痛点

🔍 传统方案的局限性

在开发工业级绘图软件时,我们常常面临这些挑战:

性能瓶颈:GDI+在处理大量图形元素时性能急剧下降

精度问题:浮点运算误差影响工业级精度要求

格式兼容:需要支持多种工业标准格式输出

UI复杂性:专业软件需要丰富的交互体验

🎯 SkiaSharp的优势

SkiaSharp作为Google Skia的.NET绑定,为我们提供了:

  • 硬件加速:GPU渲染支持,性能提升10倍以上
  • 跨平台:Windows、macOS、Linux全平台支持
  • 工业精度:支持亚像素级精确渲染
  • 丰富API:完整的2D图形绘制能力

🏗️ 系统架构设计

image.png

📋 核心类结构

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 // 切割路径 } }

🖥️ UI布局设计最佳实践

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 } }

🔧 工业格式导出实战

📄 G代码导出 - CNC机床标准

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; } } } }

📊 DXF格式导出 - CAD标准

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); } }

🔥 实战应用场景

🏭 工业应用实例

这套系统已成功应用于:

  • CNC数控加工:路径规划和G代码生成
  • 激光切割:复杂图形的路径优化
  • 3D打印:打印路径可视化和编辑
  • 机器人导航:路径规划和轨迹优化

💡 扩展功能建议

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); } }

image.png

image.png

⚠️ 常见坑点提醒

🚨 SkiaSharp使用注意事项

  1. 内存泄漏:务必正确释放SKPath、SKPaint等资源
  2. 坐标系统:注意SkiaSharp的坐标原点在左上角
  3. 线程安全:SkiaSharp对象不是线程安全的
  4. 性能监控:大量路径点时要注意渲染性能

💊 解决方案

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(); } }

✨ 总结与展望

🎯 核心要点总结

  1. SkiaSharp + WinForms 是工业级绘图应用的完美组合,性能优异且开发效率高
  2. 模块化设计 让代码更易维护,路径算法、UI界面、导出功能完全解耦
  3. 工业标准格式 支持让软件具备真正的商业价值,可直接用于生产环境

🚀 进阶学习建议

  • 深入学习SkiaSharp的高级特性:着色器、路径效果、图像处理
  • 研究工业控制协议:Modbus、Ethernet/IP等
  • 掌握更多CAD格式:STEP、IGES等三维格式

💬 互动交流

你在开发图形应用时遇到过什么性能瓶颈?欢迎在评论区分享你的经验和遇到的问题。

如果这篇文章对你有帮助,请转发给更多需要的同行!让我们一起推动C#在工业软件领域的发展。


关注我,获取更多C#实战干货和工业软件开发技巧!

本文作者:技术老小子

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!