💡 想要开发一款媲美画图软件的应用?厌倦了GDI+的局限性?本文将手把手教你使用SkiaSharp构建功能完整的绘图板应用,涵盖多图层管理、图形选择、现代化UI设计等核心功能。无论你是桌面开发新手还是想要提升技能的资深开发者,都能从中收获满满的干货!
在C#桌面开发中,很多开发者在实现绘图功能时都会遇到以下问题:
🚫 GDI+性能瓶颈
🚫 功能实现复杂
🚫 扩展性差
SkiaSharp是Google Skia图形库的.NET封装,具有以下优势:
我们采用分层架构 + 对象模式,将复杂的绘图功能拆分为:
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}";
}
}
}
}

通过本文的实战演练,我们成功构建了一个功能完整的绘图应用,核心收获包括:
🔥 三大技术要点:
💡 扩展思路:基于这套架构,你可以轻松添加更多功能,如撤销重做、图形变换、滤镜效果等。SkiaSharp的强大功能为你的创意提供了无限可能!
📢 互动时间
如果这篇文章对你有帮助,请点赞并转发给更多需要的同行!让我们一起在C#开发的路上越走越远!🚀
关注我,获取更多C#实战干货和最新技术分享!
🎨 从零打造专业级绘图应用:SkiaSharp + WinForms 实战指南
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!