大数跨境
0
0

WinForm 也能有酷炫开关?手写旋转按钮控件

WinForm 也能有酷炫开关?手写旋转按钮控件 DotNet技术匠
2025-12-01
0
导读:WinForms 自定义控件:实现一个旋转开关按钮。

前言

在 WinForm 应用程序开发中,原生控件虽然稳定可靠,但往往难以满足对界面美观和交互体验有更高要求的场景。

本文将介绍一个名为 RotatingSwitchButton 的自定义控件实现过程——它模拟了物理旋转开关的外观与行为,融合了动画、阴影、金属质感和精细刻度等现代 UI 元素,在保留 WinForm 轻量级特性的基础上,显著提升了视觉表现力。

正文

这个旋转开关控件的核心目标是:用纯 GDI+ 绘图实现一个既好看又实用的状态切换元件。它不像普通 CheckBox 那样简单,而是通过旋钮的位置(向上为"开",向下为"关")直观传达状态,并辅以平滑动画增强交互反馈。

状态与动画控制

控件内部使用 isOn 布尔值记录当前状态,currentAngle 表示旋钮的实时角度。

切换状态时,并非直接跳转,而是启动一个基于 Timer 的动画循环:

private bool isOn = false;
private float currentAngle = 0f;
private readonly float targetOnAngle = -90f;  // 向上位置
private readonly float targetOffAngle = 90f;   // 向下位置

动画采用缓动函数 EaseInOutQuad,使运动更自然,避免机械感:

private float EaseInOutQuad(float t)
{
    return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
}

每帧根据目标角度逐步逼近,直到误差小于阈值后停止定时器,确保性能不受长期运行影响。

阴影与深度感

为提升立体感,控件支持可配置的阴影效果。通过 PathGradientBrush 创建径向渐变,模拟柔和投影:

private void DrawShadow(Graphics g, float centerX, float centerY, float radius)
{
    using (var shadowPath = new GraphicsPath())
    {
        float shadowSize = radius * 2 + shadowBlur * 2;
        float shadowX = centerX - radius - shadowBlur + shadowDepth * 0.5f;
        float shadowY = centerY - radius - shadowBlur + shadowDepth;
        shadowPath.AddEllipse(shadowX, shadowY, shadowSize, shadowSize);
        using (var shadowBrush = new PathGradientBrush(shadowPath))
        {
            Color centerShadowColor = Color.FromArgb(
                (int)(255 * shadowOpacity),
                shadowColor.R,
                shadowColor.G,
                shadowColor.B);
            shadowBrush.CenterColor = centerShadowColor;
            shadowBrush.SurroundColors = new[] { Color.FromArgb(0, shadowColor) };
            shadowBrush.FocusScales = new PointF(0.8f0.8f);
            g.FillPath(shadowBrush, shadowPath);
        }
    }
}

用户可通过 EnableShadowShadowDepthShadowOpacity 等属性灵活调整阴影表现。

视觉细节打磨

  • 外环:使用线性渐变模拟金属拉丝质感,并叠加高光路径增强光泽。

  • 刻度线:每隔 6 度绘制短线,30 度处加长,90° 和 270°(即 ON/OFF 位置)进一步突出:

for (int i = 0; i < 360; i += 6)
{
    float scaleLength = (i % 30 == 0) ? 0.12f : 0.08f;
    if (i == 90 || i == 270)
        scaleLength = 0.15f// 主要刻度线
    // 绘制逻辑...
}
  • 旋钮:采用多层绘制——底层阴影、主体径向渐变、顶部高光、中心指示线,营造真实金属旋钮效果。

  • 标签:在上下两侧分别标注 "ON" 和 "OFF",当前状态高亮显示,非激活状态置灰,语义清晰。

项目效果

完整控件代码

以下为控件完整实现(已保留原始格式):

namespace AppControls
{
    publicclassRotatingSwitchButton : Control
    {
        privatebool isOn = false;
        privatefloat currentAngle = 0f;
        privatereadonlyfloat targetOnAngle = -90f;  // 负角度表示向上
        privatereadonlyfloat targetOffAngle = 90f;  // 正角度表示向下
        private Timer animationTimer;

        privatereadonly Color onColor = Color.FromArgb(76217100);
        privatereadonly Color offColor = Color.FromArgb(2555948);
        privatereadonly Color knobColor = Color.White;
        privatereadonly Color gradientStart = Color.FromArgb(240240240);
        privatereadonly Color gradientEnd = Color.FromArgb(200200200);

        privatebool enableShadow = true;
        privatefloat shadowDepth = 5f;
        privatefloat shadowOpacity = 0.3f;
        private Color shadowColor = Color.FromArgb(76000);
        privatefloat shadowBlur = 10f;

        [Category("Appearance")]
        [Description("启用或禁用控件阴影")]
        publicbool EnableShadow
        {
            get => enableShadow;
            set
            {
                if (enableShadow != value)
                {
                    enableShadow = value;
                    Invalidate();
                }
            }
        }

        [Category("Appearance")]
        [Description("设置阴影深度")]
        publicfloat ShadowDepth
        {
            get => shadowDepth;
            set
            {
                if (shadowDepth != value)
                {
                    shadowDepth = value;
                    Invalidate();
                }
            }
        }

        [Category("Appearance")]
        [Description("设置阴影透明度 (0.0 - 1.0)")]
        publicfloat ShadowOpacity
        {
            get => shadowOpacity;
            set
            {
                value = Math.Max(0, Math.Min(1value));
                if (shadowOpacity != value)
                {
                    shadowOpacity = value;
                    Invalidate();
                }
            }
        }

        [Category("Appearance")]
        [Description("设置阴影颜色")]
        public Color ShadowColor
        {
            get => shadowColor;
            set
            {
                if (shadowColor != value)
                {
                    shadowColor = value;
                    Invalidate();
                }
            }
        }

        [Category("Appearance")]
        [Description("设置阴影模糊程度")]
        publicfloat ShadowBlur
        {
            get => shadowBlur;
            set
            {
                if (shadowBlur != value)
                {
                    shadowBlur = value;
                    Invalidate();
                }
            }
        }

        publicbool IsOn
        {
            get => isOn;
            set
            {
                if (isOn != value)
                {
                    isOn = value;
                    StartAnimation();
                    OnValueChanged(EventArgs.Empty);
                }
            }
        }

        publicevent EventHandler ValueChanged;

        public RotatingSwitchButton()
        {
            SetStyle(ControlStyles.SupportsTransparentBackColor |
                   ControlStyles.OptimizedDoubleBuffer |
                   ControlStyles.AllPaintingInWmPaint |
                   ControlStyles.UserPaint, true);
            Size = new Size(100100);
            BackColor = Color.Transparent;
            currentAngle = targetOffAngle;
            animationTimer = new Timer();
            animationTimer.Interval = 16;
            animationTimer.Tick += AnimationTimer_Tick;
            Padding = new Padding((int)(shadowDepth + shadowBlur));
        }

        private void StartAnimation()
        {
            animationTimer.Start();
        }

        private float EaseInOutQuad(float t)
        {
            return t < 0.5f ? 2 * t * t : -1 + (4 - 2 * t) * t;
        }

        private void AnimationTimer_Tick(object sender, EventArgs e)
        {
            float targetAngle = isOn ? targetOnAngle : targetOffAngle;
            if (Math.Abs(currentAngle - targetAngle) < 0.1f)
            {
                currentAngle = targetAngle;
                animationTimer.Stop();
            }
            else
            {
                float totalDistance = Math.Abs(targetAngle - currentAngle);
                float step = totalDistance * EaseInOutQuad(0.15f);
                currentAngle += (currentAngle < targetAngle) ? step : -step;
            }
            Invalidate();
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
            e.Graphics.CompositingQuality = CompositingQuality.HighQuality;

            float centerX = Width / 2f;
            float centerY = Height / 2f;
            float outerRadius = Math.Min(Width, Height) / 2f - (shadowDepth + shadowBlur);
            float innerRadius = outerRadius * 0.7f;

            if (enableShadow)
            {
                DrawShadow(e.Graphics, centerX, centerY, outerRadius);
            }

            DrawOuterRing(e.Graphics, centerX, centerY, outerRadius);
            DrawLabels(e.Graphics, centerX, centerY, outerRadius);
            DrawInnerRing(e.Graphics, centerX, centerY, innerRadius);
            DrawKnob(e.Graphics, centerX, centerY, innerRadius);
        }

        private void DrawShadow(Graphics g, float centerX, float centerY, float radius)
        {
            using (var shadowPath = new GraphicsPath())
            {
                float shadowSize = radius * 2 + shadowBlur * 2;
                float shadowX = centerX - radius - shadowBlur + shadowDepth * 0.5f;
                float shadowY = centerY - radius - shadowBlur + shadowDepth;
                shadowPath.AddEllipse(shadowX, shadowY, shadowSize, shadowSize);
                using (var shadowBrush = new PathGradientBrush(shadowPath))
                {
                    Color centerShadowColor = Color.FromArgb(
                        (int)(255 * shadowOpacity),
                        shadowColor.R,
                        shadowColor.G,
                        shadowColor.B);
                    shadowBrush.CenterColor = centerShadowColor;
                    shadowBrush.SurroundColors = new[] { Color.FromArgb(0, shadowColor) };
                    shadowBrush.FocusScales = new PointF(0.8f0.8f);
                    g.FillPath(shadowBrush, shadowPath);
                }
            }
        }

        private void DrawOuterRing(Graphics g, float centerX, float centerY, float radius)
        {
            using (var gradientBrush = new LinearGradientBrush(
                new RectangleF(centerX - radius, centerY - radius, radius * 2, radius * 2),
                gradientStart, gradientEnd, 45f))
            {
                g.FillEllipse(gradientBrush, centerX - radius, centerY - radius, radius * 2, radius * 2);
            }
            using (var pen = new Pen(Color.FromArgb(100255255255), 1.5f))
            {
                g.DrawEllipse(pen, centerX - radius, centerY - radius, radius * 2, radius * 2);
            }
            using (var highlightPath = new GraphicsPath())
            {
                highlightPath.AddEllipse(centerX - radius * 0.9f, centerY - radius * 0.9f,
                    radius * 1.8f, radius * 1.8f);
                using (var highlightBrush = new PathGradientBrush(highlightPath))
                {
                    highlightBrush.CenterColor = Color.FromArgb(30255255255);
                    highlightBrush.SurroundColors = new[] { Color.FromArgb(0255255255) };
                    g.FillPath(highlightBrush, highlightPath);
                }
            }
        }

        private void DrawLabels(Graphics g, float centerX, float centerY, float radius)
        {
            using (var font = new Font("Arial"12f, FontStyle.Bold))
            {
                DrawRotatedText(g, "ON", font, isOn ? onColor : Color.Gray,
                    centerX, centerY, radius * 0.8f-90);
                DrawRotatedText(g, "OFF", font, !isOn ? offColor : Color.Gray,
                    centerX, centerY, radius * 0.8f90);
            }
        }

        private void DrawRotatedText(Graphics g, string text, Font font, Color color,
         float centerX, float centerY, float radius, float angle
)

        {
            using (var brush = new SolidBrush(color))
            {
                var size = g.MeasureString(text, font);
                float x = centerX + (float)(radius * Math.Cos(angle * Math.PI / 180)) - size.Width / 2;
                float y = centerY + (float)(radius * Math.Sin(angle * Math.PI / 180)) - size.Height / 2;
                g.TranslateTransform(x + size.Width / 2, y + size.Height / 2);
                g.RotateTransform(0); // 保持文本水平
                g.DrawString(text, font, brush, -size.Width / 2, -size.Height / 2);
                g.ResetTransform();
            }
        }

        private void DrawInnerRing(Graphics g, float centerX, float centerY, float radius)
        {
            using (var pen = new Pen(Color.FromArgb(100130130130), 1f))
            {
                g.DrawEllipse(pen, centerX - radius, centerY - radius, radius * 2, radius * 2);
                for (int i = 0; i < 360; i += 6)
                {
                    float scaleLength = (i % 30 == 0) ? 0.12f : 0.08f;
                    if (i == 90 || i == 270)
                        scaleLength = 0.15f;
                    float startX = centerX + (float)(radius * (1 - scaleLength) * Math.Cos(i * Math.PI / 180));
                    float startY = centerY + (float)(radius * (1 - scaleLength) * Math.Sin(i * Math.PI / 180));
                    float endX = centerX + (float)(radius * Math.Cos(i * Math.PI / 180));
                    float endY = centerY + (float)(radius * Math.Sin(i * Math.PI / 180));
                    using (var scalePen = new Pen(Color.FromArgb(
                        i % 30 == 0 ? 120 : 80,
                        130130130), i % 30 == 0 ? 1.5f : 0.8f))
                    {
                        g.DrawLine(scalePen, startX, startY, endX, endY);
                    }
                }
            }
        }

        private void DrawKnob(Graphics g, float centerX, float centerY, float radius)
        {
            float knobRadius = radius * 0.3f;
            float knobX = centerX + (float)(radius * 0.6f * Math.Cos(currentAngle * Math.PI / 180));
            float knobY = centerY + (float)(radius * 0.6f * Math.Sin(currentAngle * Math.PI / 180));

            using (var shadowBrush = new SolidBrush(Color.FromArgb(30000)))
            {
                g.FillEllipse(shadowBrush, knobX - knobRadius + 2, knobY - knobRadius + 2,
                    knobRadius * 2, knobRadius * 2);
            }

            using (var knobPath = new GraphicsPath())
            {
                knobPath.AddEllipse(knobX - knobRadius, knobY - knobRadius, knobRadius * 2, knobRadius * 2);
                using (var knobBrush = new PathGradientBrush(knobPath))
                {
                    knobBrush.CenterColor = Color.White;
                    knobBrush.SurroundColors = new[] { Color.FromArgb(230230230) };
                    g.FillPath(knobBrush, knobPath);
                }
            }

            using (var highlightBrush = new PathGradientBrush(new PointF[] {
                new PointF(knobX - knobRadius * 0.5f, knobY - knobRadius * 0.5f),
                new PointF(knobX + knobRadius * 0.5f, knobY - knobRadius * 0.5f),
                new PointF(knobX, knobY + knobRadius * 0.5f)
            }))
            {
                highlightBrush.CenterColor = Color.FromArgb(50255255255);
                highlightBrush.SurroundColors = new[] { Color.Transparent };
                g.FillPath(highlightBrush, new GraphicsPath());
            }

            using (var indicatorPen = new Pen(Color.FromArgb(100000), 2f))
            {
                float lineLength = knobRadius * 0.8f;
                float endX = knobX + (float)(lineLength * Math.Cos(currentAngle * Math.PI / 180));
                float endY = knobY + (float)(lineLength * Math.Sin(currentAngle * Math.PI / 180));
                g.DrawLine(indicatorPen, knobX, knobY, endX, endY);
            }
        }

        protected override void OnClick(EventArgs e)
        {
            base.OnClick(e);
            IsOn = !IsOn;
        }

        protected virtual void OnValueChanged(EventArgs e)
        {
            ValueChanged?.Invoke(this, e);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                animationTimer?.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}
总结

控价展示了如何在 WinForm 这一"传统"框架下,通过精细的 GDI+ 绘图和合理的动画设计,打造出具有现代感的 UI 控件。它不依赖第三方库,完全基于 .NET 内置能力实现,部署简单、兼容性好。对于需要提升专业软件界面质感、又不愿引入 WPF 或 Electron 等重型方案的项目来说,这类自定义控件提供了一条务实而高效的路径。

该控件适用于工业监控面板、设备控制界面、系统设置模块等场景,既能清晰传达状态,又能提升整体视觉品质。更重要的是,它的实现思路——分层绘制、资源及时释放、动画与交互解耦——为其他自定义控件开发提供了可复用的经验。

关键词

#WinForm#自定义控件#旋转开关#GDI+、#动画#阴影效果#金属质感#刻度绘制#状态切换

最后
如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号[DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

作者:技术老小子

出处:mp.weixin.qq.com/s/7D6CyGvm3rACjWW6i64iBg
声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!



END



方便大家交流、资源共享和共同成长
纯技术交流群,需要加入的小伙伴请扫码,并备注加群



推荐阅读






WinForm 中创建自定义仪表盘控件
WinForm 自定义控件-信号灯(工业)
WinForm 自定义控件-警灯(工业)

.NET 8 + WPF 的 Modbus 智能温湿监控系统

.NET 8 微服务框架长什么样?集成 AI 智能体、自动调度与实时通信

基于 JSON 配置的 .NET 桌面应用自动更新方案

基于 .NET 的可视化流程编辑工业视觉框架

WPF 轻量级插件框架:动态菜单、浮动窗口、热加载 DLL,开箱即用

WPF 智能仓储上位机系统,集成数据采集与轻量级 MES 功能

C# + FFmpeg 一键转码,轻松解决海康视频网页播放难题

C# 工业级全局键鼠行为监测与日志记录工具

开源 .NET 工作流引擎 + 可视化设计,轻松搞定 OA/CRM/ERP 开发
WinForm + STM32 打造稳定好用的工业设备远程升级工具
Visual Studio 2026 上手体验,AI 懂你、界面清爽、协作无缝


觉得有收获?不妨分享让更多人受益

关注「DotNet技术匠」,共同提升技术实力


收藏
点赞
分享
在看

【声明】内容源于网络
0
0
DotNet技术匠
「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
内容 1715
粉丝 0
DotNet技术匠 「DotNet技术匠」聚焦.NET核心,分享深度干货、实战技巧、最新资讯、优质资源,助你领跑技术赛道,赋能开发者成长。
总阅读277
粉丝0
内容1.7k