编辑
2026-01-20
C#
00

目录

🔍 问题分析:传统绘图开发的痛点
💡 解决方案:SkiaSharp + 架构设计
🔥 为什么选择SkiaSharp?
🎯 核心架构设计
🛠️ 代码实战:核心功能实现
🎨 图形对象基类设计
🗂️ 多图层管理系统
🖱️ 精准的鼠标交互处理
🎨 完整程序
🎉 总结与展望

💡 想要开发一款媲美画图软件的应用?厌倦了GDI+的局限性?本文将手把手教你使用SkiaSharp构建功能完整的绘图板应用,涵盖多图层管理、图形选择、现代化UI设计等核心功能。无论你是桌面开发新手还是想要提升技能的资深开发者,都能从中收获满满的干货!

🔍 问题分析:传统绘图开发的痛点

在C#桌面开发中,很多开发者在实现绘图功能时都会遇到以下问题:

🚫 GDI+性能瓶颈

  • 复杂图形绘制时性能下降明显
  • 抗锯齿效果不够理想
  • 内存管理容易出现泄漏

🚫 功能实现复杂

  • 图层管理逻辑繁琐
  • 图形选择和移动难以实现
  • UI界面缺乏现代化设计

🚫 扩展性差

  • 添加新工具需要大量重构
  • 跨平台支持困难
  • 与现代图形库集成复杂

💡 解决方案:SkiaSharp + 架构设计

🔥 为什么选择SkiaSharp?

SkiaSharp是Google Skia图形库的.NET封装,具有以下优势:

  • 高性能:硬件加速渲染,支持GPU
  • 跨平台:Windows、macOS、Linux全覆盖
  • 功能丰富:矢量图形、文字渲染、图像处理
  • 现代化:支持最新的图形标准和效果

🎯 核心架构设计

我们采用分层架构 + 对象模式,将复杂的绘图功能拆分为:

markdown
📁 应用架构 ├── 🎨 UI层 (FrmMain) ├── 🛠️ 工具管理层 (DrawingTool) ├── 📐 图形对象层 (DrawableObject) └── 🗂️ 图层管理层 (LayerManager)

🛠️ 代码实战:核心功能实现

🎨 图形对象基类设计

首先,我们定义一个抽象的图形对象基类,为所有图形提供统一接口:

c#
public abstract class DrawableObject { public string Id { get; set; } public DrawableType Type { get; set; } public SKPaint Paint { get; set; } public bool IsSelected { get; set; } public SKRect BoundingRect { get; protected set; } protected DrawableObject() { Id = Guid.NewGuid().ToString(); IsSelected = false; } // 核心方法:每个图形都必须实现 public abstract void Draw(SKCanvas canvas); public abstract void DrawSelection(SKCanvas canvas); public abstract bool HitTest(SKPoint point); public abstract void Move(float deltaX, float deltaY); // 🔥 选择效果的通用实现 protected SKPaint CreateSelectionPaint() { return new SKPaint { Color = SKColors.Blue, Style = SKPaintStyle.Stroke, StrokeWidth = 2, PathEffect = SKPathEffect.CreateDash(new float[] { 5, 5 }, 0) }; } }

💡 设计亮点

  • 使用抽象类确保接口一致性
  • 内置选择状态管理
  • 提供通用的选择效果渲染

🗂️ 多图层管理系统

图层管理是专业绘图软件的核心功能,我们来看看如何优雅实现:

c#
public class LayerManager { private List<Layer> layers; private int activeLayerIndex; public LayerManager() { layers = new List<Layer>(); activeLayerIndex = -1; } // 🔥 智能图层添加 public void AddLayer(int width, int height, string name = null) { if (string.IsNullOrEmpty(name)) name = $"图层 {layers.Count + 1}"; var layer = new Layer(width, height, name); layers.Add(layer); activeLayerIndex = layers.Count - 1; } // 🎯 跨图层对象选择(核心功能) public DrawableObject SelectObjectInAllLayers(SKPoint point) { // 从顶层开始查找,符合用户直觉 for (int i = layers.Count - 1; i >= 0; i--) { if (layers[i].Visible) { var selectedObj = layers[i].SelectObject(point); if (selectedObj != null) { // 清除其他图层的选择状态 for (int j = 0; j < layers.Count; j++) { if (j != i) layers[j].ClearSelection(); } SetActiveLayer(i); return selectedObj; } } } ClearAllSelections(); return null; } public void ResizeLayers(int newWidth, int newHeight) { foreach (var layer in layers) { layer.ResizeLayer(newWidth, newHeight); } } }

🖱️ 精准的鼠标交互处理

鼠标交互是绘图应用的灵魂,看看如何处理复杂的交互逻辑:

c#
private void pbCanvas_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; var skPoint = new SKPoint(e.X, e.Y); // 🎯 选择工具的智能处理 if (currentTool.Type == ToolType.Select) { selectedObject = layerManager.SelectObjectInAllLayers(skPoint); if (selectedObject != null) { isDragging = true; dragStartPoint = skPoint; pbCanvas.Cursor = Cursors.SizeAll; lblStatus.Text = $"已选中 {selectedObject.Type} 对象"; } pbCanvas.Invalidate(); return; } // 🖌️ 绘图工具的统一处理 isDrawing = true; lastPoint = startPoint = skPoint; currentPath.Clear(); currentPath.Add(skPoint); // 清除之前的选择状态 layerManager.ClearAllSelections(); selectedObject = null; } // 🔄 实时预览功能实现 private void pbCanvas_Paint(object sender, PaintEventArgs e) { using (var tempBitmap = new SKBitmap(pbCanvas.Width, pbCanvas.Height)) using (var tempCanvas = new SKCanvas(tempBitmap)) { tempCanvas.Clear(SKColors.White); // 渲染所有可见图层 foreach (var layer in layerManager.GetLayers()) { if (layer.Visible) { layer.DrawToCanvas(tempCanvas); } } // 🎨 绘制实时预览(用户体验关键) if (isDrawing && currentTool.Type != ToolType.Select) { DrawPreview(tempCanvas); } // 转换并绘制到WinForms using (var image = SKImage.FromBitmap(tempBitmap)) using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) using (var stream = new MemoryStream(data.ToArray())) using (var bitmap = new Bitmap(stream)) { e.Graphics.DrawImage(bitmap, 0, 0); } } }

🎨 完整程序

我们来打造现代化深色主题:

c#
using SkiaSharp; using SkiaSharp.Views.Desktop; using System; using System.Collections.Generic; using System.Drawing; using System.IO; using System.Windows.Forms; namespace AppDrawingBoard { public partial class FrmMain : Form { private LayerManager layerManager; private DrawingTool currentTool; private bool isDrawing = false; private SKPoint lastPoint; private SKPoint startPoint; private List<SKPoint> currentPath; private SKBitmap canvasBitmap; private DrawableObject selectedObject = null; private bool isDragging = false; private SKPoint dragStartPoint; private readonly Color[] predefinedColors = { Color.Black, Color.White, Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Orange, Color.Purple, Color.Pink, Color.Brown, Color.Gray, Color.LightGray, Color.DarkGray, Color.Cyan, Color.Magenta }; public FrmMain() { InitializeComponent(); InitializeDrawingComponents(); } private void InitializeDrawingComponents() { layerManager = new LayerManager(); currentTool = new DrawingTool(); currentPath = new List<SKPoint>(); canvasBitmap = new SKBitmap(pbCanvas.Width, pbCanvas.Height); using (var canvas = new SKCanvas(canvasBitmap)) { canvas.Clear(SKColors.White); } InitializeColorPanel(); UpdateUI(); } private void InitializeColorPanel() { flpColors.Controls.Clear(); foreach (var color in predefinedColors) { var colorButton = new Button { Size = new Size(30, 30), BackColor = color, FlatStyle = FlatStyle.Flat, Margin = new Padding(2) }; colorButton.FlatAppearance.BorderSize = 1; colorButton.FlatAppearance.BorderColor = Color.Gray; colorButton.Click += (s, e) => { currentTool.Color = new SKColor((byte)color.R, (byte)color.G, (byte)color.B); currentTool.UpdatePaint(); pnlColorPreview.BackColor = color; }; flpColors.Controls.Add(colorButton); } pnlColorPreview.BackColor = Color.Black; } private void FrmMain_Load(object sender, EventArgs e) { layerManager.AddLayer(pbCanvas.Width, pbCanvas.Height, "背景图层"); RefreshLayersList(); lblStatus.Text = "就绪 - 选择工具开始绘图"; } private void ToolRadioButton_CheckedChanged(object sender, EventArgs e) { var rb = sender as RadioButton; if (!rb.Checked) return; if (rb == rbSelect) currentTool.Type = ToolType.Select; else if (rb == rbPen) currentTool.Type = ToolType.Pen; else if (rb == rbBrush) currentTool.Type = ToolType.Brush; else if (rb == rbEraser) currentTool.Type = ToolType.Eraser; else if (rb == rbLine) currentTool.Type = ToolType.Line; else if (rb == rbRectangle) currentTool.Type = ToolType.Rectangle; else if (rb == rbEllipse) currentTool.Type = ToolType.Ellipse; currentTool.UpdatePaint(); lblStatus.Text = $"当前工具: {rb.Text}"; if (currentTool.Type != ToolType.Select) { layerManager.ClearAllSelections(); selectedObject = null; pbCanvas.Invalidate(); } if (currentTool.Type == ToolType.Select) pbCanvas.Cursor = Cursors.Default; else pbCanvas.Cursor = Cursors.Cross; } private void trkBrushSize_Scroll(object sender, EventArgs e) { currentTool.StrokeWidth = trkBrushSize.Value; currentTool.UpdatePaint(); lblSize.Text = $"大小: {trkBrushSize.Value}"; } private void btnColorPicker_Click(object sender, EventArgs e) { if (colorDialog.ShowDialog() == DialogResult.OK) { var color = colorDialog.Color; currentTool.Color = new SKColor((byte)color.R, (byte)color.G, (byte)color.B); currentTool.UpdatePaint(); pnlColorPreview.BackColor = color; } } private void btnAddLayer_Click(object sender, EventArgs e) { layerManager.AddLayer(pbCanvas.Width, pbCanvas.Height); RefreshLayersList(); lblStatus.Text = "添加了新图层"; } private void btnDeleteLayer_Click(object sender, EventArgs e) { if (layerManager.GetLayers().Count <= 1) { MessageBox.Show("至少需要保留一个图层!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var selectedIndex = lstLayers.SelectedIndex; if (selectedIndex >= 0) { layerManager.RemoveLayer(selectedIndex); RefreshLayersList(); selectedObject = null; pbCanvas.Invalidate(); lblStatus.Text = "删除了图层"; } } private void lstLayers_SelectedIndexChanged(object sender, EventArgs e) { var selectedIndex = lstLayers.SelectedIndex; if (selectedIndex >= 0) { layerManager.SetActiveLayer(layerManager.GetLayers().Count - 1 - selectedIndex); lblStatus.Text = $"切换到图层: {lstLayers.SelectedItem}"; selectedObject = null; pbCanvas.Invalidate(); } } private void RefreshLayersList() { lstLayers.Items.Clear(); var layers = layerManager.GetLayers(); for (int i = layers.Count - 1; i >= 0; i--) { lstLayers.Items.Add(layers[i].Name); } if (lstLayers.Items.Count > 0) lstLayers.SelectedIndex = 0; UpdateUI(); } private void pbCanvas_MouseDown(object sender, MouseEventArgs e) { if (e.Button != MouseButtons.Left) return; var skPoint = new SKPoint(e.X, e.Y); if (currentTool.Type == ToolType.Select) { selectedObject = layerManager.SelectObjectInAllLayers(skPoint); if (selectedObject != null) { isDragging = true; dragStartPoint = skPoint; pbCanvas.Cursor = Cursors.SizeAll; lblStatus.Text = $"已选中 {selectedObject.Type} 对象 - 拖拽移动或按Delete删除"; } else { lblStatus.Text = "未选中任何对象"; } pbCanvas.Invalidate(); return; } isDrawing = true; lastPoint = startPoint = skPoint; currentPath.Clear(); currentPath.Add(skPoint); var drawingLayer = layerManager.GetActiveLayer(); if (drawingLayer == null) return; layerManager.ClearAllSelections(); selectedObject = null; if (currentTool.Type == ToolType.Pen || currentTool.Type == ToolType.Brush || currentTool.Type == ToolType.Eraser) { drawingLayer.Canvas.DrawPoint(skPoint, currentTool.Paint); pbCanvas.Invalidate(); } } private void pbCanvas_MouseMove(object sender, MouseEventArgs e) { lblCoordinates.Text = $"X: {e.X}, Y: {e.Y}"; var currentPoint = new SKPoint(e.X, e.Y); if (currentTool.Type == ToolType.Select && isDragging && selectedObject != null) { float deltaX = currentPoint.X - dragStartPoint.X; float deltaY = currentPoint.Y - dragStartPoint.Y; selectedObject.Move(deltaX, deltaY); dragStartPoint = currentPoint; var layerForRedraw = layerManager.GetActiveLayer(); layerForRedraw?.RedrawLayer(); pbCanvas.Invalidate(); return; } if (!isDrawing) return; var currentLayer = layerManager.GetActiveLayer(); if (currentLayer == null) return; switch (currentTool.Type) { case ToolType.Pen: case ToolType.Brush: case ToolType.Eraser: currentLayer.Canvas.DrawLine(lastPoint, currentPoint, currentTool.Paint); lastPoint = currentPoint; currentPath.Add(currentPoint); pbCanvas.Invalidate(); break; case ToolType.Line: case ToolType.Rectangle: case ToolType.Ellipse: pbCanvas.Invalidate(); break; } } private void pbCanvas_MouseUp(object sender, MouseEventArgs e) { if (currentTool.Type == ToolType.Select) { isDragging = false; pbCanvas.Cursor = Cursors.Default; return; } if (!isDrawing) return; isDrawing = false; var targetLayer = layerManager.GetActiveLayer(); if (targetLayer == null) return; var endPoint = new SKPoint(e.X, e.Y); DrawableObject newObject = null; switch (currentTool.Type) { case ToolType.Pen: case ToolType.Brush: case ToolType.Eraser: if (currentPath.Count > 1) { newObject = new FreeDrawObject(currentPath, currentTool.Paint); } break; case ToolType.Line: if (Math.Abs(endPoint.X - startPoint.X) > 1 || Math.Abs(endPoint.Y - startPoint.Y) > 1) { newObject = new LineObject(startPoint, endPoint, currentTool.Paint); } break; case ToolType.Rectangle: var rect = SKRect.Create( Math.Min(startPoint.X, endPoint.X), Math.Min(startPoint.Y, endPoint.Y), Math.Abs(endPoint.X - startPoint.X), Math.Abs(endPoint.Y - startPoint.Y)); if (rect.Width > 1 && rect.Height > 1) { newObject = new RectangleObject(rect, currentTool.Paint); } break; case ToolType.Ellipse: var ellipseRect = SKRect.Create( Math.Min(startPoint.X, endPoint.X), Math.Min(startPoint.Y, endPoint.Y), Math.Abs(endPoint.X - startPoint.X), Math.Abs(endPoint.Y - startPoint.Y)); if (ellipseRect.Width > 1 && ellipseRect.Height > 1) { newObject = new EllipseObject(ellipseRect, currentTool.Paint); } break; } if (newObject != null) { targetLayer.AddObject(newObject); lblStatus.Text = $"已创建 {newObject.Type} 对象"; } pbCanvas.Invalidate(); currentPath.Clear(); } private void pbCanvas_Paint(object sender, PaintEventArgs e) { using (var tempBitmap = new SKBitmap(pbCanvas.Width, pbCanvas.Height)) using (var tempCanvas = new SKCanvas(tempBitmap)) { tempCanvas.Clear(SKColors.White); foreach (var layer in layerManager.GetLayers()) { if (layer.Visible) { layer.DrawToCanvas(tempCanvas); } } if (isDrawing && currentPath.Count > 0 && currentTool.Type != ToolType.Select) { var currentPoint = PointToClient(Cursor.Position); currentPoint = pbCanvas.PointToClient(PointToScreen(currentPoint)); var skCurrentPoint = new SKPoint(currentPoint.X, currentPoint.Y); switch (currentTool.Type) { case ToolType.Line: tempCanvas.DrawLine(startPoint, skCurrentPoint, currentTool.Paint); break; case ToolType.Rectangle: var rect = SKRect.Create( Math.Min(startPoint.X, skCurrentPoint.X), Math.Min(startPoint.Y, skCurrentPoint.Y), Math.Abs(skCurrentPoint.X - startPoint.X), Math.Abs(skCurrentPoint.Y - startPoint.Y)); tempCanvas.DrawRect(rect, currentTool.Paint); break; case ToolType.Ellipse: var ellipseRect = SKRect.Create( Math.Min(startPoint.X, skCurrentPoint.X), Math.Min(startPoint.Y, skCurrentPoint.Y), Math.Abs(skCurrentPoint.X - startPoint.X), Math.Abs(skCurrentPoint.Y - startPoint.Y)); tempCanvas.DrawOval(ellipseRect, currentTool.Paint); break; } } using (var image = SKImage.FromBitmap(tempBitmap)) using (var data = image.Encode(SKEncodedImageFormat.Png, 100)) using (var stream = new MemoryStream(data.ToArray())) using (var bitmap = new Bitmap(stream)) { e.Graphics.DrawImage(bitmap, 0, 0); } } } protected override bool ProcessCmdKey(ref Message msg, Keys keyData) { if (keyData == Keys.Delete && selectedObject != null) { var layerWithSelectedObject = layerManager.GetActiveLayer(); if (layerWithSelectedObject != null) { layerWithSelectedObject.RemoveObject(selectedObject); selectedObject = null; pbCanvas.Invalidate(); lblStatus.Text = "已删除选中对象"; } return true; } return base.ProcessCmdKey(ref msg, keyData); } private void newToolStripMenuItem_Click(object sender, EventArgs e) { if (MessageBox.Show("确定要新建画布吗?当前内容将丢失!", "确认", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { layerManager.Dispose(); layerManager = new LayerManager(); layerManager.AddLayer(pbCanvas.Width, pbCanvas.Height, "背景图层"); selectedObject = null; RefreshLayersList(); pbCanvas.Invalidate(); lblStatus.Text = "已新建画布"; } } private void openToolStripMenuItem_Click(object sender, EventArgs e) { using (var ofd = new OpenFileDialog()) { ofd.Filter = "图像文件|*.png;*.jpg;*.jpeg;*.bmp|所有文件|*.*"; if (ofd.ShowDialog() == DialogResult.OK) { try { using (var bitmap = SKBitmap.Decode(ofd.FileName)) { layerManager.Dispose(); layerManager = new LayerManager(); layerManager.AddLayer(bitmap.Width, bitmap.Height, "导入图层"); var importLayer = layerManager.GetActiveLayer(); importLayer.Canvas.DrawBitmap(bitmap, 0, 0); pbCanvas.Size = new Size(bitmap.Width, bitmap.Height); selectedObject = null; RefreshLayersList(); pbCanvas.Invalidate(); lblStatus.Text = $"已打开: {Path.GetFileName(ofd.FileName)}"; } } catch (Exception ex) { MessageBox.Show($"打开文件失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } private void saveToolStripMenuItem_Click(object sender, EventArgs e) { using (var sfd = new SaveFileDialog()) { sfd.Filter = "PNG文件|*.png|JPEG文件|*.jpg|BMP文件|*.bmp"; sfd.DefaultExt = "png"; if (sfd.ShowDialog() == DialogResult.OK) { try { using (var finalBitmap = new SKBitmap(pbCanvas.Width, pbCanvas.Height)) using (var finalCanvas = new SKCanvas(finalBitmap)) { finalCanvas.Clear(SKColors.White); foreach (var layer in layerManager.GetLayers()) { if (layer.Visible) { using (var paint = new SKPaint { Color = SKColors.White.WithAlpha((byte)(255 * layer.Opacity)) }) { finalCanvas.DrawBitmap(layer.Bitmap, 0, 0, paint); } } } using (var image = SKImage.FromBitmap(finalBitmap)) using (var data = image.Encode(GetImageFormat(sfd.FileName), 90)) using (var stream = File.OpenWrite(sfd.FileName)) { data.SaveTo(stream); } } lblStatus.Text = $"已保存: {Path.GetFileName(sfd.FileName)}"; } catch (Exception ex) { MessageBox.Show($"保存文件失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } } private SKEncodedImageFormat GetImageFormat(string fileName) { var extension = Path.GetExtension(fileName).ToLower(); switch (extension) { case ".jpg": case ".jpeg": return SKEncodedImageFormat.Jpeg; case ".bmp": return SKEncodedImageFormat.Bmp; default: return SKEncodedImageFormat.Png; } } private void exitToolStripMenuItem_Click(object sender, EventArgs e) { Close(); } private void FrmMain_FormClosing(object sender, FormClosingEventArgs e) { layerManager?.Dispose(); currentTool?.Dispose(); canvasBitmap?.Dispose(); } private void UpdateUI() { Text = $"SkiaSharp 绘图板 - {layerManager.GetLayers().Count} 个图层"; } private void FrmMain_Resize(object sender, EventArgs e) { if (layerManager != null && pbCanvas.Width > 0 && pbCanvas.Height > 0) { layerManager.ResizeLayers(pbCanvas.Width, pbCanvas.Height); pbCanvas.Invalidate(); lblStatus.Text = $"画布大小已调整为: {pbCanvas.Width} x {pbCanvas.Height}"; } } } }

image.png

🎉 总结与展望

通过本文的实战演练,我们成功构建了一个功能完整的绘图应用,核心收获包括:

🔥 三大技术要点

  1. 架构设计:分层架构 + 抽象工厂模式,让代码更清晰可维护
  2. 性能优化:合理的资源管理和渲染策略,确保应用流畅运行
  3. 用户体验:现代化UI设计和智能交互处理,提升专业度

💡 扩展思路:基于这套架构,你可以轻松添加更多功能,如撤销重做、图形变换、滤镜效果等。SkiaSharp的强大功能为你的创意提供了无限可能!


📢 互动时间

  1. 你在开发绘图应用时遇到过哪些技术难点?
  2. 对于图层管理还有什么更好的设计思路?

如果这篇文章对你有帮助,请点赞并转发给更多需要的同行!让我们一起在C#开发的路上越走越远!🚀

关注我,获取更多C#实战干货和最新技术分享!

🎨 从零打造专业级绘图应用:SkiaSharp + WinForms 实战指南

本文作者:技术老小子

本文链接:

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