在 Windows 窗体应用程序开发中,自定义控件的设计不仅是 UI 能力的体现,更是提升用户体验的关键环节。传统的 GDI+ 绘图方式虽然可用,但在视觉表现力、性能和跨平台支持方面存在明显局限。
本文将深入讲解如何利用 SkiaSharp 图形库,在 .NET 平台上开发一个功能完整、视觉效果出众的自定义时钟控件。我们将从零开始构建基础结构,逐步引入高级视觉效果,并最终实现高度可定制化的控件设计。
一、SkiaSharp 简介
SkiaSharp 是 Google Skia 图形引擎的 .NET 封装,提供了强大而高效的 2D 图形绘制能力。相较于系统自带的System.Drawing,它具有以下显著优势:
- 跨平台支持:可在 Windows、macOS 和 Linux 上无缝运行
- 高性能渲染:适用于实时动画与复杂图形场景
- 丰富的 API 支持:涵盖路径、变换、渐变、遮罩滤镜等高级特性
- 与 .NET 生态深度集成:尤其适合搭配
SKControl 在 WinForms 中使用
通过SKControl控件,我们可以在 Windows Forms 应用中轻松调用 SkiaSharp 的绘图接口,实现高质量的自定义 UI。
二、基础时钟控件实现
1. 控件结构设计
我们的时钟控件继承自SKControl,包含以下核心组件:
- 表盘外圈与刻度线
- 时针、分针、秒针
- 中心装饰点
- 定时器用于每秒刷新时间
2. 初始化与计时器设置
public ClockControl()
{
// 设置控件基本属性
BackColor = Color.White;
Size = new Size(300, 300);
// 初始化计时器,每秒更新一次
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += OnTimerElapsed;
_timer.AutoReset = true;
_timer.Enabled = true;
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
// 在UI线程上刷新控件
if (InvokeRequired)
{
BeginInvoke(new Action(() => Invalidate()));
}
else
{
Invalidate();
}
}
该代码初始化了一个 300×300 像素的控件,并启动一个每秒触发一次的定时器,通过Invalidate()触发重绘,确保指针随时间动态更新。
3. 绘制表盘和刻度
private void DrawClockFace(SKCanvas canvas, float centerX, float centerY, float radius)
{
// 绘制外圆
using var paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Black,
StrokeWidth = 2,
IsAntialias = true
};
canvas.DrawCircle(centerX, centerY, radius, paint);
// 绘制刻度线和数字
for (int i = 0; i < 60; i++)
{
float angle = i * 6; // 每分钟6度
bool isHourMark = i % 5 == 0;
// 根据是否小时刻度设置不同长度和粗细
float innerRadius = isHourMark ? radius - 15 : radius - 5;
float strokeWidth = isHourMark ? 3 : 1;
// 绘制刻度线...
// 绘制小时数字...
}
}
此方法使用三角函数计算每个刻度位置,区分小时(粗长)与分钟(细短)刻度,并为后续数字标记预留空间。
4. 绘制时钟指针
private void DrawClockHand(SKCanvas canvas, float angle, float length, SKColor color)
{
// 保存画布状态
canvas.Save();
// 设置画笔
using var paint = new SKPaint
{
Color = color,
StrokeWidth = 4,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
// 移动到时钟中心
float centerX = Width / 2;
float centerY = Height / 2;
// 应用旋转
canvas.Translate(centerX, centerY);
canvas.RotateDegrees(angle);
// 绘制指针
canvas.DrawLine(0, 0, 0, -length, paint);
// 恢复画布状态
canvas.Restore();
}
该方法巧妙利用画布变换(Translate + Rotate),避免手动计算坐标,使指针绘制逻辑简洁清晰。
三、高级样式与视觉效果
1. 阴影效果增强立体感
// 绘制表盘阴影
if (ShowShadows)
{
using var shadowPaint = new SKPaint
{
Color = SKColors.Black.WithAlpha(40),
IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 10)
};
canvas.DrawCircle(centerX + 5, centerY + 5, radius, shadowPaint);
}
通过SKMaskFilter.CreateBlur实现柔和阴影,显著提升控件的视觉深度。
2. 风格切换:现代 vs 传统
// 现代风格秒针
if (UseModernStyle)
{
// 绘制带有尾部的指针
canvas.DrawLine(0, length * 0.2f, 0, -length, paint);
// 绘制圆形尾部
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle(0, length * 0.2f, 4, paint);
// 绘制秒针头部
SKPath path = new SKPath();
path.MoveTo(-3, -length + 15);
path.LineTo(3, -length + 15);
path.LineTo(0, -length);
path.Close();
canvas.DrawPath(path, paint);
}
else
{
// 传统风格秒针
canvas.DrawLine(0, 0, 0, -length, paint);
}
借助SKPath,我们可绘制箭头状指针头部,赋予现代风格更强的设计感。
四、自定义与定制选项
为提升控件通用性,提供以下可配置属性:
// 样式设置
public SKColor BackgroundColor { get; set; } = SKColors.White;
public SKColor FaceColor { get; set; } = SKColors.WhiteSmoke;
public SKColor BorderColor { get; set; } = new SKColor(40, 40, 40);
public SKColor HourHandColor { get; set; } = new SKColor(60, 60, 60);
public SKColor MinuteHandColor { get; set; } = new SKColor(80, 80, 80);
public SKColor SecondHandColor { get; set; } = SKColors.OrangeRed;
public SKColor MarkersColor { get; set; } = new SKColor(40, 40, 40);
public SKColor NumbersColor { get; set; } = new SKColor(40, 40, 40);
public bool ShowShadows { get; set; } = true;
public bool ShowSecondHand { get; set; } = true;
public bool UseModernStyle { get; set; } = true;
用户可自由调整颜色、开关秒针、启用/禁用阴影、切换风格,满足多样化 UI 需求。
五、实践与性能考量
1. 资源管理
所有SKPaint对象均使用using语句确保及时释放,避免内存泄漏:
using var paint = new SKPaint
{
// 属性设置
};
并在Dispose中清理定时器:
protected override void Dispose(bool disposing)
{
if (disposing)
{
_timer?.Stop();
_timer?.Dispose();
}
base.Dispose(disposing);
}
2. 抗锯齿与流畅绘制
全局启用抗锯齿:
IsAntialias = true
确保边缘平滑,提升视觉质量。
3. 响应窗口大小变化
重写OnResize触发重绘:
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}
保证控件在缩放后仍保持比例协调。这些设计思路亦可迁移至仪表盘、进度环等复杂 UI 场景,充分释放图形库的潜力。
六、完整代码
核心绘制逻辑位于OnPaintSurface方法中,整合所有功能模块:
namespace AppRotate
{
public class ClockControl : SKControl
{
private System.Timers.Timer _timer;
private float _hourHandLength;
private float _minuteHandLength;
private float _secondHandLength;
private float _clockRadius;
// 样式设置
public SKColor BackgroundColor { get; set; } = SKColors.White;
public SKColor FaceColor { get; set; } = SKColors.WhiteSmoke;
public SKColor BorderColor { get; set; } = new SKColor(40, 40, 40);
public SKColor HourHandColor { get; set; } = new SKColor(60, 60, 60);
public SKColor MinuteHandColor { get; set; } = new SKColor(80, 80, 80);
public SKColor SecondHandColor { get; set; } = SKColors.OrangeRed;
public SKColor MarkersColor { get; set; } = new SKColor(40, 40, 40);
public SKColor NumbersColor { get; set; } = new SKColor(40, 40, 40);
public bool ShowShadows { get; set; } = true;
public bool ShowSecondHand { get; set; } = true;
public bool UseModernStyle { get; set; } = true;
public ClockControl()
{
// 设置控件基本属性
BackColor = Color.White;
Size = new Size(300, 300);
// 初始化计时器,每秒更新一次
_timer = new System.Timers.Timer(1000);
_timer.Elapsed += OnTimerElapsed;
_timer.AutoReset = true;
_timer.Enabled = true;
}
private void OnTimerElapsed(object sender, ElapsedEventArgs e)
{
// 在UI线程上刷新控件
if (InvokeRequired)
{
BeginInvoke(new Action(() => Invalidate()));
}
else
{
Invalidate();
}
}
protected override void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
base.OnPaintSurface(e);
SKSurface surface = e.Surface;
SKCanvas canvas = surface.Canvas;
// 清除背景
canvas.Clear(BackgroundColor);
// 计算尺寸
int width = e.Info.Width;
int height = e.Info.Height;
float centerX = width / 2f;
float centerY = height / 2f;
_clockRadius = Math.Min(width, height) / 2f * 0.85f;
// 设置指针长度
_hourHandLength = _clockRadius * 0.5f;
_minuteHandLength = _clockRadius * 0.7f;
_secondHandLength = _clockRadius * 0.8f;
// 绘制时钟表盘
DrawClockFace(canvas, centerX, centerY, _clockRadius);
// 获取当前时间
DateTime now = DateTime.Now;
// 计算指针角度
float hourAngle = 30 * (now.Hour % 12) + 0.5f * now.Minute;
float minuteAngle = 6 * now.Minute + 0.1f * now.Second;
float secondAngle = 6 * now.Second;
// 绘制时针
DrawClockHand(canvas, hourAngle, _hourHandLength, HourHandColor, 6);
// 绘制分针
DrawClockHand(canvas, minuteAngle, _minuteHandLength, MinuteHandColor, 4);
// 绘制秒针
if (ShowSecondHand)
{
DrawSecondHand(canvas, secondAngle, _secondHandLength, SecondHandColor);
}
// 绘制中心点
DrawClockCenter(canvas, centerX, centerY);
}
private void DrawClockFace(SKCanvas canvas, float centerX, float centerY, float radius)
{
// 绘制表盘背景
if (ShowShadows)
{
// 添加阴影效果
using var shadowPaint = new SKPaint
{
Color = SKColors.Black.WithAlpha(40),
IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 10)
};
canvas.DrawCircle(centerX + 5, centerY + 5, radius, shadowPaint);
}
// 绘制表盘
using (var facePaint = new SKPaint
{
Style = SKPaintStyle.Fill,
Color = FaceColor,
IsAntialias = true
})
{
canvas.DrawCircle(centerX, centerY, radius, facePaint);
}
// 绘制外圆
using var borderPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = BorderColor,
StrokeWidth = UseModernStyle ? 3 : 2,
IsAntialias = true
};
canvas.DrawCircle(centerX, centerY, radius, borderPaint);
// 绘制刻度
using var tickPaint = new SKPaint
{
Color = MarkersColor,
StrokeWidth = 1,
IsAntialias = true
};
for (int i = 0; i < 60; i++)
{
float angle = i * 6; // 每分钟6度
bool isHourMark = i % 5 == 0;
float outerRadius = radius;
float innerRadius = isHourMark ? radius - 15 : radius - 5;
float strokeWidth = isHourMark ? 3 : 1;
tickPaint.StrokeWidth = strokeWidth;
float x1 = centerX + (float)(outerRadius * Math.Sin(Math.PI * angle / 180));
float y1 = centerY - (float)(outerRadius * Math.Cos(Math.PI * angle / 180));
float x2 = centerX + (float)(innerRadius * Math.Sin(Math.PI * angle / 180));
float y2 = centerY - (float)(innerRadius * Math.Cos(Math.PI * angle / 180));
canvas.DrawLine(x1, y1, x2, y2, tickPaint);
// 绘制数字
if (isHourMark)
{
int hour = i / 5 == 0 ? 12 : i / 5;
using var textPaint = new SKPaint
{
Color = NumbersColor,
TextSize = UseModernStyle ? 24 : 20,
IsAntialias = true,
TextAlign = SKTextAlign.Center,
Typeface = SKTypeface.FromFamilyName(
UseModernStyle ? "Arial" : "Times New Roman",
UseModernStyle ? SKFontStyleWeight.Bold : SKFontStyleWeight.Normal,
SKFontStyleWidth.Normal,
SKFontStyleSlant.Upright)
};
float textRadius = radius - 35;
float textX = centerX + (float)(textRadius * Math.Sin(Math.PI * angle / 180));
float textY = centerY - (float)(textRadius * Math.Cos(Math.PI * angle / 180)) + 8; // 垂直居中调整
canvas.DrawText(hour.ToString(), textX, textY, textPaint);
}
}
// 添加装饰环
if (UseModernStyle)
{
using var decorPaint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = BorderColor.WithAlpha(100),
StrokeWidth = 1,
IsAntialias = true
};
canvas.DrawCircle(centerX, centerY, radius * 0.9f, decorPaint);
}
}
// 绘制时针和分针
private void DrawClockHand(SKCanvas canvas, float angle, float length, SKColor color, float strokeWidth)
{
// 保存画布状态
canvas.Save();
// 移动到时钟中心
float centerX = Width / 2;
float centerY = Height / 2;
canvas.Translate(centerX, centerY);
// 应用旋转
canvas.RotateDegrees(angle);
// 绘制阴影
if (ShowShadows)
{
using var shadowPaint = new SKPaint
{
Color = SKColors.Black.WithAlpha(60),
StrokeWidth = strokeWidth,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 2)
};
canvas.DrawLine(0, 0, 0, -length, shadowPaint);
}
// 设置画笔
using var paint = new SKPaint
{
Color = color,
StrokeWidth = strokeWidth,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
// 绘制指针
if (UseModernStyle)
{
// 现代风格:绘制带有尾部的指针
canvas.DrawLine(0, length * 0.2f, 0, -length, paint);
// 绘制指针头部
paint.Style = SKPaintStyle.Fill;
SKPath path = new SKPath();
path.MoveTo(-strokeWidth / 2, -length + strokeWidth * 2);
path.LineTo(strokeWidth / 2, -length + strokeWidth * 2);
path.LineTo(0, -length);
path.Close();
canvas.DrawPath(path, paint);
}
else
{
// 传统风格:简单线条
canvas.DrawLine(0, 0, 0, -length, paint);
}
// 恢复画布状态
canvas.Restore();
}
// 绘制秒针(特殊处理,使其更细长)
private void DrawSecondHand(SKCanvas canvas, float angle, float length, SKColor color)
{
canvas.Save();
float centerX = Width / 2;
float centerY = Height / 2;
canvas.Translate(centerX, centerY);
canvas.RotateDegrees(angle);
// 绘制秒针
using var paint = new SKPaint
{
Color = color,
StrokeWidth = 2,
IsAntialias = true,
StrokeCap = SKStrokeCap.Round
};
if (UseModernStyle)
{
// 现代风格秒针
canvas.DrawLine(0, length * 0.2f, 0, -length, paint);
// 绘制圆形尾部
paint.Style = SKPaintStyle.Fill;
canvas.DrawCircle(0, length * 0.2f, 4, paint);
// 绘制秒针头部
SKPath path = new SKPath();
path.MoveTo(-3, -length + 15);
path.LineTo(3, -length + 15);
path.LineTo(0, -length);
path.Close();
canvas.DrawPath(path, paint);
}
else
{
// 传统风格秒针
canvas.DrawLine(0, 0, 0, -length, paint);
}
canvas.Restore();
}
private void DrawClockCenter(SKCanvas canvas, float centerX, float centerY)
{
if (ShowShadows)
{
// 绘制中心点阴影
using var shadowPaint = new SKPaint
{
Color = SKColors.Black.WithAlpha(60),
Style = SKPaintStyle.Fill,
IsAntialias = true,
MaskFilter = SKMaskFilter.CreateBlur(SKBlurStyle.Normal, 3)
};
canvas.DrawCircle(centerX + 1, centerY + 1, 6, shadowPaint);
}
// 绘制中心圆点
using var paint = new SKPaint
{
Color = UseModernStyle ? SecondHandColor : BorderColor,
Style = SKPaintStyle.Fill,
IsAntialias = true
};
canvas.DrawCircle(centerX, centerY, 6, paint);
// 绘制内部白色圆点
paint.Color = FaceColor;
canvas.DrawCircle(centerX, centerY, 3, paint);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
Invalidate();
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_timer?.Stop();
_timer?.Dispose();
}
base.Dispose(disposing);
}
}
}
总结
本文系统地展示了如何基于 SkiaSharp 开发一个高颜值、高性能、高度可定制的 Windows Forms 时钟控件。
关键实现包括:
- 架构清晰:以
SKControl为基础,结合定时器实现动态刷新
- 绘图高效:充分利用画布变换简化指针旋转逻辑
- 视觉丰富:集成阴影、多风格、装饰环等美化元素
- 灵活配置:通过属性暴露颜色、样式、功能开关
- 工程规范:遵循资源管理、抗锯齿、响应式布局等最佳实践
该控件不仅可直接用于项目,其设计思路亦可迁移至仪表盘、进度环、自定义图表等复杂 UI 场景,充分释放 SkiaSharp 在 .NET 桌面开发中的图形潜力。