C# 工业路径绘制系统:UI 架构、SkiaSharp 渲染、工业格式导出

32 阅读4分钟

前言

大家是否在项目中遇到过这样的需求:需要开发一个专业的路径绘制工具,支持工业级精度和复杂路径操作?传统的 GDI+ 性能有限,WPF 又过于复杂。

本文将使用 C# + SkiaSharp 开发一个完整的工业级路径绘制系统。手把手教大家如何设计专业的 UI 布局、实现高性能图形渲染、处理复杂路径算法,并支持导出多种工业格式(如 G 代码、DXF 等)。不管是 CAD 软件开发,还是工业控制系统,这套解决方案都能节省大量开发时间。

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

传统方案的局限性

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

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

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

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

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

SkiaSharp 的优势

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

  • 硬件加速:GPU 渲染支持,性能提升 10 倍以上

  • 跨平台:Windows、macOS、Linux 全平台支持

  • 工业精度:支持亚像素级精确渲染

  • 丰富 API:完整的 2D 图形绘制能力

系统架构

核心类结构

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   // 二次贝塞尔曲线
    }
}

工业路径核心类

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

工业格式导出实战

G 代码导出 — CNC 机床标准

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 标准

/// <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 打印:打印路径可视化和编辑

  • 机器人导航:路径规划和轨迹优化

扩展功能

// 路径优化算法
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);
    }
}

常见提醒

SkiaSharp 使用注意事项

1、内存泄漏:务必正确释放 SKPathSKPaint 等资源

2、坐标系统:注意 SkiaSharp 的坐标原点在左上角

3、线程安全:SkiaSharp 对象不是线程安全的

4、性能监控:大量路径点时要注意渲染性能

解决方案

// 正确的资源管理
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、架构清晰解耦:路径数据、渲染逻辑、导出模块彼此独立,易于维护和扩展

3、贴近工业场景:支持 G 代码、DXF 等真实生产环境所需的格式输出

4、可扩展性强:预留了路径优化、碰撞检测等高级功能接口

可进一步集成实时通信协议(如 Modbus)、三维路径支持(STEP/IGES)或 AI 辅助路径生成,打造更完整的工业软件生态。

关键词

C#、SkiaSharp、工业路径绘制、G代码导出、DXF格式、高性能图形渲染、路径算法、数控加工、激光切割、机器人导航、WinForms、跨平台绘图、道格拉斯-普克算法、碰撞检测、工业物联网

mp.weixin.qq.com/s/16zTfkqsr4WcUUOW-E19lw

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!