找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1248

积分

0

好友

184

主题
发表于 4 天前 | 查看: 13| 回复: 0

在 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 时钟控件。

关键实现包括

  1. 架构清晰:以SKControl为基础,结合定时器实现动态刷新
  2. 绘图高效:充分利用画布变换简化指针旋转逻辑
  3. 视觉丰富:集成阴影、多风格、装饰环等美化元素
  4. 灵活配置:通过属性暴露颜色、样式、功能开关
  5. 工程规范:遵循资源管理、抗锯齿、响应式布局等最佳实践

该控件不仅可直接用于项目,其设计思路亦可迁移至仪表盘、进度环、自定义图表等复杂 UI 场景,充分释放 SkiaSharp 在 .NET 桌面开发中的图形潜力。




上一篇:WPF与DevExpress构建现代化工控系统:开源框架实现企业级UI与权限管理
下一篇:Linux路由处理内核原理深度解析:从数据结构到负载均衡实战
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 22:31 , Processed in 0.111665 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表