你是否还在为复杂的工业图形绘制而头疼?传统的GDI+绘图库性能差、效果单一,而Web前端方案又无法满足桌面应用的需求。今天,我将带你用C#和SkiaSharp构建一个功能完整的工业管线绘制系统,不仅支持实时交互编辑,还能展示流体动画效果。
这不是纸上谈兵的Demo,而是一个可以直接用于生产环境的完整解决方案!从基础绘制到高级交互,从性能优化到用户体验,我们将一步步解析每个关键技术点。
在工业软件开发中,我们经常遇到这些难题:
SkiaSharp作为Google Skia的C#封装,完美解决了这些问题:
首先定义管线段的数据结构,这是整个系统的基础:
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);
}
}
}
动画优化技巧:
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
}
}


c#// ❌ 错误做法 - 可能造成内存泄漏
var path = new SKPath();
// 忘记释放资源
// ✅ 正确做法 - 使用using确保释放
using (var path = new SKPath())
{
// 绘制逻辑
}
c#// 注意WinForms坐标与SKCanvas坐标的一致性
var mousePos = new SKPoint(e.X, e.Y); // 直接使用,无需转换
canvas.Invalidate()而非频繁重绘整个界面想让系统更加完善?考虑添加这些功能:
你在实际项目中是如何处理复杂图形绘制的?遇到过哪些性能瓶颈?欢迎在评论区分享你的经验和踩过的坑!
如果你正在开发类似的工业软件或图形编辑器,这套方案能为你节省大量开发时间。关于SkiaSharp的更多高级用法,你还想了解哪些方面?
通过这个工业管线绘制系统,我们掌握了三个核心要点:
这套技术方案不仅适用于工业软件,在游戏开发、数据可视化、教育软件等领域同样大有用武之地。SkiaSharp正在成为C#图形开发的新标杆,值得每位.NET开发者深入掌握。
觉得这篇文章对你有帮助?请转发给更多同行,让我们一起推动C#图形编程技术的发展! 🌟
相关信息
通过网盘分享的文件:AppPipelineDrawing.zip 链接: https://pan.baidu.com/s/1WH9uxiy3CnBNqqTkvyPwfg?pwd=3zau 提取码: 3zau --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!