编辑
2026-01-02
C#
00

目录

🎯 解决的核心问题
传统绘图方案的痛点
SkiaSharp的优势
💡 系统架构设计
🏗️ 核心数据模型
🎨 绘制引擎核心
🔥 交互系统实现
📍 精确碰撞检测
🎯 多模式拖拽系统
⚡ 流体动画系统
🌊 动态流体效果
🧑‍💻 完整代码
🛠️ 实战应用场景
工业设计软件
教学演示系统
监控系统界面
⚠️ 开发中的关键坑点
内存管理
坐标转换
性能优化
🎯 功能扩展建议
💬 技术交流
🚀 总结与展望

你是否还在为复杂的工业图形绘制而头疼?传统的GDI+绘图库性能差、效果单一,而Web前端方案又无法满足桌面应用的需求。今天,我将带你用C#和SkiaSharp构建一个功能完整的工业管线绘制系统,不仅支持实时交互编辑,还能展示流体动画效果。

这不是纸上谈兵的Demo,而是一个可以直接用于生产环境的完整解决方案!从基础绘制到高级交互,从性能优化到用户体验,我们将一步步解析每个关键技术点。

🎯 解决的核心问题

传统绘图方案的痛点

在工业软件开发中,我们经常遇到这些难题:

  • GDI+性能瓶颈:复杂图形卡顿严重
  • 交互体验差:拖拽、编辑功能实现困难
  • 动画效果单一:难以实现流体流动等动态效果
  • 跨平台支持弱:Windows专有API限制

SkiaSharp的优势

SkiaSharp作为Google Skia的C#封装,完美解决了这些问题:

  • 硬件加速渲染,性能卓越
  • 丰富的绘图API,效果专业
  • 跨平台支持,一套代码多端运行
  • 与WinForms/WPF无缝集成

💡 系统架构设计

🏗️ 核心数据模型

首先定义管线段的数据结构,这是整个系统的基础:

c#
public class PipelineSegment { public SKPoint StartPoint { get; set; } public SKPoint EndPoint { get; set; } public float CurvatureStrength { get; set; } // 弧度强度:-1到1 public Guid Id { get; set; } } public class PipelineStyle { public float PipeWidth { get; set; } public SKColor PipeColor { get; set; } public SKColor FlowColor { get; set; } public float FlowSpeed { get; set; } public PipelineType PipelineType { get; set; } }

设计亮点

  • CurvatureStrength让每个管线段都可以调整弧度,无需复杂的类型区分
  • 统一的样式管理,便于主题切换和批量操作

🎨 绘制引擎核心

SkiaSharp的绘制逻辑清晰简洁,性能出色:

c#
private void DrawPipelineSegment(SKCanvas canvas, PipelineSegment segment, SKPaint paint) { if (Math.Abs(segment.CurvatureStrength) < 0.01f) { // 直线段 - 最优性能 canvas.DrawLine(segment.StartPoint, segment.EndPoint, paint); } else { // 弧形段 - 二次贝塞尔曲线 var controlPoint = GetCurvatureControlPoint(segment); using (var path = new SKPath()) { path.MoveTo(segment.StartPoint); path.QuadTo(controlPoint, segment.EndPoint); canvas.DrawPath(path, paint); } } }

技术要点

  • 智能判断直线/曲线,避免不必要的路径计算
  • 使用using确保Path对象及时释放,防止内存泄漏

🔥 交互系统实现

📍 精确碰撞检测

实现专业级的图形编辑器,精确的碰撞检测是关键:

c#
private bool IsPointOnLine(SKPoint point, SKPoint start, SKPoint end) { var A = point.X - start.X; var B = point.Y - start.Y; var C = end.X - start.X; var D = end.Y - start.Y; var dot = A * C + B * D; var lenSq = C * C + D * D; if (lenSq == 0) return SKPoint.Distance(point, start) <= HIT_TOLERANCE; var param = dot / lenSq; SKPoint closestPoint; if (param < 0) closestPoint = start; else if (param > 1) closestPoint = end; else closestPoint = new SKPoint(start.X + param * C, start.Y + param * D); return SKPoint.Distance(point, closestPoint) <= HIT_TOLERANCE; }

算法解析

  • 使用向量投影计算点到直线的最短距离
  • 边界条件处理确保线段范围内的准确检测
  • HIT_TOLERANCE常量提供合适的点击容差

🎯 多模式拖拽系统

支持整体移动、端点调整、弧度控制等多种拖拽模式:

c#
private void SkiaCanvas_MouseMove(object sender, MouseEventArgs e) { var mousePos = new SKPoint(e.X, e.Y); if (isDragging && selectedSegment != null) { var deltaX = mousePos.X - lastMousePosition.X; var deltaY = mousePos.Y - lastMousePosition.Y; switch (currentDragMode) { case DragMode.Move: // 移动整个管线段 selectedSegment.StartPoint = new SKPoint( selectedSegment.StartPoint.X + deltaX, selectedSegment.StartPoint.Y + deltaY ); selectedSegment.EndPoint = new SKPoint( selectedSegment.EndPoint.X + deltaX, selectedSegment.EndPoint.Y + deltaY ); break; case DragMode.CurvatureControl: // 实时调整弧度 AdjustCurvatureStrength(mousePos); break; } skiaCanvas.Invalidate(); // 触发重绘 } }

用户体验优化

  • 不同拖拽模式对应不同鼠标指针,直观明了
  • 实时反馈,所见即所得的编辑体验

⚡ 流体动画系统

🌊 动态流体效果

通过虚线动画模拟不同类型管道的流体特性:

c#
private void DrawFlowAnimation(SKCanvas canvas) { if (!isAnimationEnabled) return; using (var paint = new SKPaint()) { paint.StrokeWidth = selectedStyle.PipeWidth - 12; paint.Style = SKPaintStyle.Stroke; paint.StrokeCap = SKStrokeCap.Round; paint.IsAntialias = true; float[] intervals; SKColor animationColor; // 根据管线类型设置不同动画效果 switch (selectedStyle.PipelineType) { case PipelineType.Gas: intervals = new float[] { 10, 15 }; // 短划线模拟气体 animationColor = SKColors.LimeGreen; break; case PipelineType.Steam: intervals = new float[] { 5, 5 }; // 密集短划线模拟蒸汽 animationColor = SKColors.WhiteSmoke; break; default: intervals = new float[] { 20, 10 }; animationColor = selectedStyle.FlowColor; break; } paint.Color = animationColor; paint.PathEffect = SKPathEffect.CreateDash(intervals, animationOffset); foreach (var segment in pipelineSegments) { DrawPipelineSegment(canvas, segment, paint); } } }

动画优化技巧

  • 使用Timer控制动画频率,平衡流畅度与性能
  • 不同管道类型使用差异化的虚线模式,增强真实感

🧑‍💻 完整代码

c#
using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Windows.Forms; using SkiaSharp; using SkiaSharp.Views.Desktop; using Timer = System.Windows.Forms.Timer; namespace AppPipelineDrawing { public partial class FrmMain : Form { private Timer animationTimer; private float animationOffset = 0; private List<PipelineSegment> pipelineSegments; private PipelineStyle selectedStyle; private bool isAnimationEnabled = true; private PipelineSegment selectedSegment = null; private bool isDragging = false; private DragMode currentDragMode = DragMode.None; private SKPoint lastMousePosition; private const float HIT_TOLERANCE = 15f; private const float HANDLE_SIZE = 8f; private const float CONTROL_HANDLE_SIZE = 6f; public FrmMain() { InitializeComponent(); InitializePipeline(); SetupAnimation(); SetupMouseEvents(); SetupKeyboardEvents(); } private void SetupMouseEvents() { skiaCanvas.MouseDown += SkiaCanvas_MouseDown; skiaCanvas.MouseMove += SkiaCanvas_MouseMove; skiaCanvas.MouseUp += SkiaCanvas_MouseUp; skiaCanvas.MouseLeave += SkiaCanvas_MouseLeave; } private void SetupKeyboardEvents() { this.KeyPreview = true; this.KeyDown += FrmMain_KeyDown; } private void FrmMain_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Delete && selectedSegment != null) { DeleteSelectedSegment(); e.Handled = true; } else if (e.KeyCode == Keys.Escape) { selectedSegment = null; skiaCanvas.Invalidate(); UpdateStatusLabel("已取消选择"); e.Handled = true; } } private void DeleteSelectedSegment() { if (selectedSegment == null) return; var segmentId = selectedSegment.Id.ToString().Substring(0, 8); pipelineSegments.Remove(selectedSegment); selectedSegment = null; skiaCanvas.Invalidate(); UpdateStatusLabel($"已删除管线段 ID: {segmentId}..."); } private void InitializePipeline() { pipelineSegments = new List<PipelineSegment>(); selectedStyle = new PipelineStyle { PipeWidth = 40, PipeColor = SKColors.SteelBlue, FlowColor = SKColors.LightBlue, FlowSpeed = 2.0f, PipelineType = PipelineType.Oil }; CreateSamplePipeline(); } private void CreateSamplePipeline() { pipelineSegments.Clear(); pipelineSegments.Add(new PipelineSegment { StartPoint = new SKPoint(50, 200), EndPoint = new SKPoint(200, 200), CurvatureStrength = 0f, Id = Guid.NewGuid() }); pipelineSegments.Add(new PipelineSegment { StartPoint = new SKPoint(200, 200), EndPoint = new SKPoint(350, 100), CurvatureStrength = 0.3f, Id = Guid.NewGuid() }); pipelineSegments.Add(new PipelineSegment { StartPoint = new SKPoint(350, 100), EndPoint = new SKPoint(500, 150), CurvatureStrength = -0.2f, Id = Guid.NewGuid() }); skiaCanvas.Invalidate(); } private void SetupAnimation() { animationTimer = new Timer(); animationTimer.Interval = 50; animationTimer.Tick += AnimationTimer_Tick; animationTimer.Start(); } private void AnimationTimer_Tick(object sender, EventArgs e) { if (isAnimationEnabled) { animationOffset += selectedStyle.FlowSpeed; if (animationOffset > 50) animationOffset = 0; skiaCanvas.Invalidate(); } } #region 鼠标事件处理 private void SkiaCanvas_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Right) { var mousePos = new SKPoint(e.X, e.Y); var hitSegment = HitTestSegment(mousePos); if (hitSegment != null) { selectedSegment = hitSegment; skiaCanvas.Invalidate(); ShowContextMenu(e.Location); return; } } if (e.Button != MouseButtons.Left) return; var mousePosLeft = new SKPoint(e.X, e.Y); lastMousePosition = mousePosLeft; if (selectedSegment != null) { var dragMode = GetDragMode(selectedSegment, mousePosLeft); if (dragMode != DragMode.None) { currentDragMode = dragMode; isDragging = true; skiaCanvas.Cursor = GetCursorForDragMode(dragMode); return; } } var hitSegmentLeft = HitTestSegment(mousePosLeft); if (hitSegmentLeft != null) { selectedSegment = hitSegmentLeft; currentDragMode = DragMode.Move; isDragging = true; skiaCanvas.Cursor = Cursors.SizeAll; UpdateStatusLabel($"选中管线段 ID: {selectedSegment.Id.ToString().Substring(0, 8)}... 弧度: {selectedSegment.CurvatureStrength:F2} [Del键删除]"); } else { selectedSegment = null; UpdateStatusLabel("工业管线绘制系统 v1.0"); } skiaCanvas.Invalidate(); } private void ShowContextMenu(Point location) { var contextMenu = new ContextMenuStrip(); var deleteItem = new ToolStripMenuItem("删除管线段"); deleteItem.Click += (s, e) => DeleteSelectedSegment(); contextMenu.Items.Add(deleteItem); var propertiesItem = new ToolStripMenuItem("属性"); propertiesItem.Click += (s, e) => ShowSegmentProperties(); contextMenu.Items.Add(propertiesItem); contextMenu.Items.Add(new ToolStripSeparator()); var duplicateItem = new ToolStripMenuItem("复制管线段"); duplicateItem.Click += (s, e) => DuplicateSelectedSegment(); contextMenu.Items.Add(duplicateItem); contextMenu.Show(skiaCanvas, location); } private void ShowSegmentProperties() { if (selectedSegment == null) return; var message = $"管线段属性:\n\n" + $"ID: {selectedSegment.Id}\n" + $"起点: ({selectedSegment.StartPoint.X:F1}, {selectedSegment.StartPoint.Y:F1})\n" + $"终点: ({selectedSegment.EndPoint.X:F1}, {selectedSegment.EndPoint.Y:F1})\n" + $"弧度强度: {selectedSegment.CurvatureStrength:F3}\n" + $"长度: {SKPoint.Distance(selectedSegment.StartPoint, selectedSegment.EndPoint):F1}"; MessageBox.Show(message, "管线段属性", MessageBoxButtons.OK, MessageBoxIcon.Information); } private void DuplicateSelectedSegment() { if (selectedSegment == null) return; var offset = 50f; var newSegment = new PipelineSegment { StartPoint = new SKPoint(selectedSegment.StartPoint.X + offset, selectedSegment.StartPoint.Y + offset), EndPoint = new SKPoint(selectedSegment.EndPoint.X + offset, selectedSegment.EndPoint.Y + offset), CurvatureStrength = selectedSegment.CurvatureStrength, Id = Guid.NewGuid() }; pipelineSegments.Add(newSegment); selectedSegment = newSegment; skiaCanvas.Invalidate(); UpdateStatusLabel($"已复制管线段,新ID: {newSegment.Id.ToString().Substring(0, 8)}..."); } private void SkiaCanvas_MouseMove(object sender, MouseEventArgs e) { var mousePos = new SKPoint(e.X, e.Y); if (isDragging && selectedSegment != null) { var deltaX = mousePos.X - lastMousePosition.X; var deltaY = mousePos.Y - lastMousePosition.Y; switch (currentDragMode) { case DragMode.Move: selectedSegment.StartPoint = new SKPoint( selectedSegment.StartPoint.X + deltaX, selectedSegment.StartPoint.Y + deltaY ); selectedSegment.EndPoint = new SKPoint( selectedSegment.EndPoint.X + deltaX, selectedSegment.EndPoint.Y + deltaY ); break; case DragMode.ResizeStart: selectedSegment.StartPoint = mousePos; break; case DragMode.ResizeEnd: selectedSegment.EndPoint = mousePos; break; case DragMode.CurvatureControl: var curvaturePoint = GetCurvatureControlPoint(selectedSegment); var distanceFromOriginal = SKPoint.Distance(mousePos, curvaturePoint); var midPoint = GetMidPoint(selectedSegment.StartPoint, selectedSegment.EndPoint); var maxDistance = SKPoint.Distance(selectedSegment.StartPoint, selectedSegment.EndPoint) * 0.5f; var perpVector = GetPerpendicularVector(selectedSegment.StartPoint, selectedSegment.EndPoint); var toMouse = new SKPoint(mousePos.X - midPoint.X, mousePos.Y - midPoint.Y); var dotProduct = toMouse.X * perpVector.X + toMouse.Y * perpVector.Y; var projectedDistance = Math.Abs(dotProduct); selectedSegment.CurvatureStrength = Math.Sign(dotProduct) * Math.Min(projectedDistance / maxDistance, 1.0f); UpdateStatusLabel($"调整弧度: {selectedSegment.CurvatureStrength:F2}"); break; } skiaCanvas.Invalidate(); } else { UpdateCursor(mousePos); } lastMousePosition = mousePos; } private void SkiaCanvas_MouseUp(object sender, MouseEventArgs e) { if (isDragging) { isDragging = false; currentDragMode = DragMode.None; skiaCanvas.Cursor = Cursors.Default; if (selectedSegment != null) { UpdateStatusLabel($"管线段已更新 ID: {selectedSegment.Id.ToString().Substring(0, 8)}... 弧度: {selectedSegment.CurvatureStrength:F2} [Del键删除]"); } } } private void SkiaCanvas_MouseLeave(object sender, EventArgs e) { skiaCanvas.Cursor = Cursors.Default; } private void UpdateCursor(SKPoint mousePos) { if (selectedSegment != null) { var dragMode = GetDragMode(selectedSegment, mousePos); skiaCanvas.Cursor = GetCursorForDragMode(dragMode); } else { var hitSegment = HitTestSegment(mousePos); skiaCanvas.Cursor = hitSegment != null ? Cursors.Hand : Cursors.Default; } } private Cursor GetCursorForDragMode(DragMode mode) { switch (mode) { case DragMode.Move: return Cursors.SizeAll; case DragMode.ResizeStart: case DragMode.ResizeEnd: return Cursors.Cross; case DragMode.CurvatureControl: return Cursors.Hand; default: return Cursors.Default; } } #endregion #region 工具方法 private SKPoint GetMidPoint(SKPoint start, SKPoint end) { return new SKPoint((start.X + end.X) / 2, (start.Y + end.Y) / 2); } private SKPoint GetPerpendicularVector(SKPoint start, SKPoint end) { var dx = end.X - start.X; var dy = end.Y - start.Y; var length = (float)Math.Sqrt(dx * dx + dy * dy); if (length == 0) return new SKPoint(0, 1); return new SKPoint(-dy / length, dx / length); } private SKPoint GetCurvatureControlPoint(PipelineSegment segment) { var midPoint = GetMidPoint(segment.StartPoint, segment.EndPoint); var perpVector = GetPerpendicularVector(segment.StartPoint, segment.EndPoint); var distance = SKPoint.Distance(segment.StartPoint, segment.EndPoint) * 0.5f * segment.CurvatureStrength; return new SKPoint( midPoint.X + perpVector.X * distance, midPoint.Y + perpVector.Y * distance ); } #endregion #region 碰撞检测 private PipelineSegment HitTestSegment(SKPoint point) { foreach (var segment in pipelineSegments) { if (IsPointOnSegment(point, segment)) { return segment; } } return null; } private bool IsPointOnSegment(SKPoint point, PipelineSegment segment) { if (Math.Abs(segment.CurvatureStrength) < 0.01f) { return IsPointOnLine(point, segment.StartPoint, segment.EndPoint); } else { return IsPointOnCurve(point, segment); } } private bool IsPointOnLine(SKPoint point, SKPoint start, SKPoint end) { var A = point.X - start.X; var B = point.Y - start.Y; var C = end.X - start.X; var D = end.Y - start.Y; var dot = A * C + B * D; var lenSq = C * C + D * D; if (lenSq == 0) return SKPoint.Distance(point, start) <= HIT_TOLERANCE; var param = dot / lenSq; SKPoint closestPoint; if (param < 0) closestPoint = start; else if (param > 1) closestPoint = end; else closestPoint = new SKPoint(start.X + param * C, start.Y + param * D); return SKPoint.Distance(point, closestPoint) <= HIT_TOLERANCE; } private bool IsPointOnCurve(SKPoint point, PipelineSegment segment) { var controlPoint = GetCurvatureControlPoint(segment); const int samples = 20; for (int i = 0; i <= samples; i++) { float t = (float)i / samples; var curvePoint = GetQuadraticBezierPoint(segment.StartPoint, controlPoint, segment.EndPoint, t); if (SKPoint.Distance(point, curvePoint) <= HIT_TOLERANCE) { return true; } } return false; } private SKPoint GetQuadraticBezierPoint(SKPoint start, SKPoint control, SKPoint end, float t) { var oneMinusT = 1 - t; var x = oneMinusT * oneMinusT * start.X + 2 * oneMinusT * t * control.X + t * t * end.X; var y = oneMinusT * oneMinusT * start.Y + 2 * oneMinusT * t * control.Y + t * t * end.Y; return new SKPoint(x, y); } private DragMode GetDragMode(PipelineSegment segment, SKPoint point) { if (IsPointNearHandle(point, segment.StartPoint)) return DragMode.ResizeStart; if (IsPointNearHandle(point, segment.EndPoint)) return DragMode.ResizeEnd; var curvaturePoint = GetCurvatureControlPoint(segment); if (IsPointNearControlHandle(point, curvaturePoint)) return DragMode.CurvatureControl; if (IsPointOnSegment(point, segment)) return DragMode.Move; return DragMode.None; } private bool IsPointNearHandle(SKPoint point, SKPoint handle) { return SKPoint.Distance(point, handle) <= HANDLE_SIZE; } private bool IsPointNearControlHandle(SKPoint point, SKPoint handle) { return SKPoint.Distance(point, handle) <= CONTROL_HANDLE_SIZE; } #endregion private void skiaCanvas_PaintSurface(object sender, SKPaintSurfaceEventArgs e) { var canvas = e.Surface.Canvas; canvas.Clear(SKColors.White); DrawGrid(canvas, e.Info.Width, e.Info.Height); DrawPipeline(canvas); DrawFlowAnimation(canvas); DrawSelection(canvas); } private void DrawGrid(SKCanvas canvas, int width, int height) { using (var paint = new SKPaint()) { paint.Color = SKColors.LightGray; paint.StrokeWidth = 1; paint.Style = SKPaintStyle.Stroke; for (int x = 0; x < width; x += 20) { canvas.DrawLine(x, 0, x, height, paint); } for (int y = 0; y < height; y += 20) { canvas.DrawLine(0, y, width, y, paint); } } } private void DrawPipeline(SKCanvas canvas) { using (var paint = new SKPaint()) { paint.StrokeCap = SKStrokeCap.Round; paint.StrokeJoin = SKStrokeJoin.Round; paint.Style = SKPaintStyle.Stroke; paint.IsAntialias = true; foreach (var segment in pipelineSegments) { if (segment == selectedSegment) { paint.Color = SKColors.Orange; paint.StrokeWidth = selectedStyle.PipeWidth + 4; } else { paint.Color = selectedStyle.PipeColor; paint.StrokeWidth = selectedStyle.PipeWidth; } DrawPipelineSegment(canvas, segment, paint); } foreach (var segment in pipelineSegments) { if (selectedStyle.PipelineType == PipelineType.Steam) { paint.Color = SKColors.DarkGray; } else { paint.Color = SKColors.White; } if (segment == selectedSegment) { paint.StrokeWidth = selectedStyle.PipeWidth - 4; } else { paint.StrokeWidth = selectedStyle.PipeWidth - 8; } DrawPipelineSegment(canvas, segment, paint); } } } private void DrawSelection(SKCanvas canvas) { if (selectedSegment == null) return; using (var paint = new SKPaint()) { paint.IsAntialias = true; paint.Color = SKColors.Red; paint.Style = SKPaintStyle.Fill; canvas.DrawCircle(selectedSegment.StartPoint.X, selectedSegment.StartPoint.Y, HANDLE_SIZE, paint); canvas.DrawCircle(selectedSegment.EndPoint.X, selectedSegment.EndPoint.Y, HANDLE_SIZE, paint); var curvaturePoint = GetCurvatureControlPoint(selectedSegment); paint.Color = SKColors.Green; canvas.DrawCircle(curvaturePoint.X, curvaturePoint.Y, CONTROL_HANDLE_SIZE, paint); paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = 1; paint.Color = SKColors.Gray; paint.PathEffect = SKPathEffect.CreateDash(new float[] { 3, 3 }, 0); var midPoint = GetMidPoint(selectedSegment.StartPoint, selectedSegment.EndPoint); canvas.DrawLine(midPoint, curvaturePoint, paint); paint.Style = SKPaintStyle.Stroke; paint.StrokeWidth = 2; paint.Color = SKColors.Blue; paint.PathEffect = SKPathEffect.CreateDash(new float[] { 5, 5 }, 0); var bounds = GetSegmentBounds(selectedSegment); canvas.DrawRect(bounds, paint); paint.Style = SKPaintStyle.Fill; paint.Color = SKColors.Red; paint.TextSize = 12; paint.Typeface = SKTypeface.FromFamilyName("Arial"); var deleteHint = "[Del]"; var textBounds = new SKRect(); paint.MeasureText(deleteHint, ref textBounds); canvas.DrawText(deleteHint, bounds.Right - textBounds.Width - 5, bounds.Top + textBounds.Height + 5, paint); } } private SKRect GetSegmentBounds(PipelineSegment segment) { var margin = selectedStyle.PipeWidth / 2 + 10; float minX = Math.Min(segment.StartPoint.X, segment.EndPoint.X); float maxX = Math.Max(segment.StartPoint.X, segment.EndPoint.X); float minY = Math.Min(segment.StartPoint.Y, segment.EndPoint.Y); float maxY = Math.Max(segment.StartPoint.Y, segment.EndPoint.Y); var curvaturePoint = GetCurvatureControlPoint(segment); minX = Math.Min(minX, curvaturePoint.X); maxX = Math.Max(maxX, curvaturePoint.X); minY = Math.Min(minY, curvaturePoint.Y); maxY = Math.Max(maxY, curvaturePoint.Y); return new SKRect(minX - margin, minY - margin, maxX + margin, maxY + margin); } private void DrawPipelineSegment(SKCanvas canvas, PipelineSegment segment, SKPaint paint) { if (Math.Abs(segment.CurvatureStrength) < 0.01f) { canvas.DrawLine(segment.StartPoint, segment.EndPoint, paint); } else { var controlPoint = GetCurvatureControlPoint(segment); using (var path = new SKPath()) { path.MoveTo(segment.StartPoint); path.QuadTo(controlPoint, segment.EndPoint); canvas.DrawPath(path, paint); } } } private void DrawFlowAnimation(SKCanvas canvas) { if (!isAnimationEnabled) return; using (var paint = new SKPaint()) { paint.StrokeWidth = selectedStyle.PipeWidth - 12; paint.Style = SKPaintStyle.Stroke; paint.StrokeCap = SKStrokeCap.Round; paint.IsAntialias = true; float[] intervals; SKColor animationColor; switch (selectedStyle.PipelineType) { case PipelineType.Oil: intervals = new float[] { 20, 10 }; animationColor = SKColors.Orange; break; case PipelineType.Water: intervals = new float[] { 15, 5 }; animationColor = SKColors.LightBlue; break; case PipelineType.Gas: intervals = new float[] { 10, 15 }; animationColor = SKColors.LimeGreen; break; case PipelineType.Steam: intervals = new float[] { 5, 5 }; animationColor = SKColors.WhiteSmoke; break; default: intervals = new float[] { 20, 10 }; animationColor = selectedStyle.FlowColor; break; } paint.Color = animationColor; paint.PathEffect = SKPathEffect.CreateDash(intervals, animationOffset); foreach (var segment in pipelineSegments) { DrawPipelineSegment(canvas, segment, paint); } } } private void UpdateStatusLabel(string text) { if (lblStatus != null) { lblStatus.Text = text; } } private void btnAddSegment_Click(object sender, EventArgs e) { var lastSegment = pipelineSegments.LastOrDefault(); if (lastSegment != null) { var newSegment = new PipelineSegment { StartPoint = lastSegment.EndPoint, EndPoint = new SKPoint(lastSegment.EndPoint.X + 100, lastSegment.EndPoint.Y), CurvatureStrength = 0f, Id = Guid.NewGuid() }; pipelineSegments.Add(newSegment); selectedSegment = newSegment; skiaCanvas.Invalidate(); UpdateStatusLabel($"添加了新管线段,ID: {newSegment.Id.ToString().Substring(0, 8)}... [Del键删除]"); } else { var newSegment = new PipelineSegment { StartPoint = new SKPoint(100, 200), EndPoint = new SKPoint(200, 200), CurvatureStrength = 0f, Id = Guid.NewGuid() }; pipelineSegments.Add(newSegment); selectedSegment = newSegment; skiaCanvas.Invalidate(); UpdateStatusLabel($"添加了新管线段,ID: {newSegment.Id.ToString().Substring(0, 8)}... [Del键删除]"); } } private void btnDeleteSegment_Click(object sender, EventArgs e) { if (selectedSegment != null) { var result = MessageBox.Show( $"确定要删除选中的管线段吗?\nID: {selectedSegment.Id.ToString().Substring(0, 8)}...", "确认删除", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { DeleteSelectedSegment(); } } else { MessageBox.Show("请先选择要删除的管线段。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void btnReset_Click(object sender, EventArgs e) { var result = MessageBox.Show("确定要重置所有管线吗?这将清除所有当前的管线段。", "确认重置", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { selectedSegment = null; CreateSamplePipeline(); UpdateStatusLabel("已重置管线"); } } private void cmbPipelineType_SelectedIndexChanged(object sender, EventArgs e) { var combo = sender as ComboBox; selectedStyle.PipelineType = (PipelineType)combo.SelectedIndex; UpdatePipelineStyle(); } private void cmbPipeSize_SelectedIndexChanged(object sender, EventArgs e) { var combo = sender as ComboBox; selectedStyle.PipeWidth = 20 + combo.SelectedIndex * 10; skiaCanvas.Invalidate(); } private void btnColorPicker_Click(object sender, EventArgs e) { using (var colorDialog = new ColorDialog()) { if (colorDialog.ShowDialog() == DialogResult.OK) { selectedStyle.PipeColor = new SKColor( colorDialog.Color.R, colorDialog.Color.G, colorDialog.Color.B ); btnColorPicker.BackColor = colorDialog.Color; skiaCanvas.Invalidate(); } } } private void chkAnimation_CheckedChanged(object sender, EventArgs e) { isAnimationEnabled = chkAnimation.Checked; } private void trkSpeed_Scroll(object sender, EventArgs e) { selectedStyle.FlowSpeed = trkSpeed.Value * 0.5f; lblSpeed.Text = $"流速: {selectedStyle.FlowSpeed:F1}"; } private void UpdatePipelineStyle() { switch (selectedStyle.PipelineType) { case PipelineType.Oil: selectedStyle.PipeColor = SKColors.Brown; selectedStyle.FlowColor = SKColors.Orange; break; case PipelineType.Water: selectedStyle.PipeColor = SKColors.Blue; selectedStyle.FlowColor = SKColors.LightBlue; break; case PipelineType.Gas: selectedStyle.PipeColor = SKColors.Yellow; selectedStyle.FlowColor = SKColors.Green; break; case PipelineType.Steam: selectedStyle.PipeColor = SKColors.Gray; selectedStyle.FlowColor = SKColors.LightGray; break; } skiaCanvas.Invalidate(); } } public class PipelineSegment { public SKPoint StartPoint { get; set; } public SKPoint EndPoint { get; set; } public float CurvatureStrength { get; set; } public Guid Id { get; set; } } public class PipelineStyle { public float PipeWidth { get; set; } public SKColor PipeColor { get; set; } public SKColor FlowColor { get; set; } public float FlowSpeed { get; set; } public PipelineType PipelineType { get; set; } } public enum PipelineType { Oil, Water, Gas, Steam } public enum DragMode { None, Move, ResizeStart, ResizeEnd, CurvatureControl } }

image.png

image.png

🛠️ 实战应用场景

工业设计软件

  • 化工管道布局设计
  • 建筑给排水系统
  • 石油管线规划

教学演示系统

  • 工程制图教学
  • 流体力学可视化
  • 工艺流程展示

监控系统界面

  • 实时管道状态显示
  • 流量方向指示
  • 故障点标注

⚠️ 开发中的关键坑点

内存管理

c#
// ❌ 错误做法 - 可能造成内存泄漏 var path = new SKPath(); // 忘记释放资源 // ✅ 正确做法 - 使用using确保释放 using (var path = new SKPath()) { // 绘制逻辑 }

坐标转换

c#
// 注意WinForms坐标与SKCanvas坐标的一致性 var mousePos = new SKPoint(e.X, e.Y); // 直接使用,无需转换

性能优化

  • 避免在绘制方法中创建大量临时对象
  • 使用canvas.Invalidate()而非频繁重绘整个界面
  • 复杂路径使用缓存策略

🎯 功能扩展建议

想让系统更加完善?考虑添加这些功能:

  1. 撤销/重做系统:实现命令模式
  2. 图层管理:支持多层级管线设计
  3. 导出功能:SVG、PNG等格式输出
  4. 数据持久化:JSON/XML格式保存加载

💬 技术交流

你在实际项目中是如何处理复杂图形绘制的?遇到过哪些性能瓶颈?欢迎在评论区分享你的经验和踩过的坑!

如果你正在开发类似的工业软件或图形编辑器,这套方案能为你节省大量开发时间。关于SkiaSharp的更多高级用法,你还想了解哪些方面?

🚀 总结与展望

通过这个工业管线绘制系统,我们掌握了三个核心要点:

  1. SkiaSharp绘图引擎:高性能、跨平台的专业绘图解决方案
  2. 交互系统设计:精确碰撞检测+多模式拖拽,打造专业级用户体验
  3. 动画与视觉效果:通过巧妙的算法实现生动的流体动画

这套技术方案不仅适用于工业软件,在游戏开发、数据可视化、教育软件等领域同样大有用武之地。SkiaSharp正在成为C#图形开发的新标杆,值得每位.NET开发者深入掌握。

觉得这篇文章对你有帮助?请转发给更多同行,让我们一起推动C#图形编程技术的发展! 🌟


相关信息

通过网盘分享的文件:AppPipelineDrawing.zip 链接: https://pan.baidu.com/s/1WH9uxiy3CnBNqqTkvyPwfg?pwd=3zau 提取码: 3zau --来自百度网盘超级会员v9的分享

本文作者:技术老小子

本文链接:

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