WinForm 开发指示灯自定义控件

346 阅读5分钟

前言

在工业控制、监控系统等应用中,指示灯是直观展示设备状态的核心组件。

本文基于C#和GDI+技术,详细介绍如何开发一个功能丰富的指示灯自定义控件,支持3D立体效果、动态动画及多样化交互特性。

正文

1、控件核心特性

视觉效果:3D立体渲染、呼吸/旋转/波纹动画、圆形/方形双形态

交互功能:闪烁效果(可调频率)、鼠标悬停高亮、禁用状态

自定义配置

  • 开/关颜色(OnColor/OffColor

  • 边框样式(BorderWidth/BorderColor

  • 动画开关(IsBlinking/IsBreathing/IsRotating/IsRippling

2、控件效果

3、完整代码

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Drawing2D;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Timer = System.Windows.Forms.Timer;

namespace AppControls
{
    public class IndicatorLight : Control
    {
        #region 私有字段
        private bool isOn = false;
        private Color onColor = Color.Lime;
        private Color offColor = Color.DarkGray;
        private bool is3D = true;
        private bool isBlinking = false;
        private int blinkInterval = 500;
        private Timer blinkTimer;
        private bool isCircular = true;
        private int borderWidth = 1;
        private Color borderColor = Color.DarkGray;
        private bool temporaryState = false;
        #endregion

        #region 动画相关字段  
        private bool isBreathing = false;
        private bool isRotating = false;
        private bool isRippling = false;
        private float breathingOpacity = 1.0f;
        private float rotationAngle = 0f;
        private List<float> rippleRadii = new List<float>();
        private const float BREATHING_STEP = 0.05f;
        private const float ROTATION_STEP = 2f;
        private const float RIPPLE_STEP = 2f;
        private const int MAX_RIPPLES = 3;
        private Timer animationTimer;
        private bool breathingDirection = false; // false为淡出,true为淡入  
        #endregion

        #region 属性
        [Category("Animation")]
        public bool IsBreathing
        {
            get { return isBreathing; }
            set
            {
                isBreathing = value;
                if (isBreathing)
                    animationTimer.Start();
                else if (!IsAnimating())
                    animationTimer.Stop();
                Invalidate();
            }
        }

        [Category("Animation")]
        public bool IsRotating
        {
            get { return isRotating; }
            set
            {
                isRotating = value;
                if (isRotating)
                    animationTimer.Start();
                else if (!IsAnimating())
                    animationTimer.Stop();
                Invalidate();
            }
        }

        [Category("Animation")]
        public bool IsRippling
        {
            get { return isRippling; }
            set
            {
                isRippling = value;
                if (isRippling)
                    animationTimer.Start();
                else
                {
                    rippleRadii.Clear();
                    if (!IsAnimating())
                        animationTimer.Stop();
                }
                Invalidate();
            }
        }

        [Category("Appearance")]
        public bool IsOn
        {
            get { return isOn; }
            set
            {
                isOn = value;
                Invalidate();
            }
        }

        [Category("Appearance")]
        public Color OnColor
        {
            get { return onColor; }
            set
            {
                onColor = value;
                Invalidate();
            }
        }

        [Category("Appearance")]
        public Color OffColor
        {
            get { return offColor; }
            set
            {
                offColor = value;
                Invalidate();
            }
        }

        [Category("Appearance")]
        public bool Is3D
        {
            get { return is3D; }
            set
            {
                is3D = value;
                Invalidate();
            }
        }

        [Category("Behavior")]
        public bool IsBlinking
        {
            get { return isBlinking; }
            set
            {
                isBlinking = value;
                if (isBlinking)
                    blinkTimer.Start();
                else
                {
                    blinkTimer.Stop();
                    temporaryState = false;
                }
                Invalidate();
            }
        }

        [Category("Behavior")]
        public int BlinkInterval
        {
            get { return blinkInterval; }
            set
            {
                blinkInterval = value;
                blinkTimer.Interval = value;
            }
        }

        [Category("Appearance")]
        public bool IsCircular
        {
            get { return isCircular; }
            set
            {
                isCircular = value;
                Invalidate();
            }
        }

        [Category("Appearance")]
        public int BorderWidth
        {
            get { return borderWidth; }
            set
            {
                borderWidth = value;
                Invalidate();
            }
        }

        [Category("Appearance")]
        public Color BorderColor
        {
            get { return borderColor; }
            set
            {
                borderColor = value;
                Invalidate();
            }
        }
        #endregion

        #region 构造函数
        public IndicatorLight()
        {
            SetStyle(ControlStyles.SupportsTransparentBackColor, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.ResizeRedraw, true);
            Size = new Size(30, 30);
            BackColor = Color.Transparent;

            blinkTimer = new Timer();
            blinkTimer.Interval = blinkInterval;
            blinkTimer.Tick += BlinkTimer_Tick;

            animationTimer = new Timer();
            animationTimer.Interval = 50; // 20fps  
            animationTimer.Tick += AnimationTimer_Tick;
        }
        #endregion

        private void AnimationTimer_Tick(object sender, EventArgs e)
        {
            bool needInvalidate = false;

            // 处理呼吸效果  
            if (isBreathing)
            {
                if (!breathingDirection)
                {
                    breathingOpacity -= BREATHING_STEP;
                    if (breathingOpacity <= 0.3f)
                    {
                        breathingOpacity = 0.3f;
                        breathingDirection = true;
                    }
                }
                else
                {
                    breathingOpacity += BREATHING_STEP;
                    if (breathingOpacity >= 1.0f)
                    {
                        breathingOpacity = 1.0f;
                        breathingDirection = false;
                    }
                }
                needInvalidate = true;
            }

            // 处理旋转效果  
            if (isRotating)
            {
                rotationAngle = (rotationAngle + ROTATION_STEP) % 360;
                needInvalidate = true;
            }

            // 处理波纹效果  
            if (isRippling)
            {
                // 更新现有波纹  
                for (int i = rippleRadii.Count - 1; i >= 0; i--)
                {
                    rippleRadii[i] += RIPPLE_STEP;
                    if (rippleRadii[i] > Math.Max(Width, Height))
                    {
                        rippleRadii.RemoveAt(i);
                    }
                }

                // 添加新波纹  
                if (rippleRadii.Count < MAX_RIPPLES && (rippleRadii.Count == 0 || rippleRadii[rippleRadii.Count - 1] > 20))
                {
                    rippleRadii.Add(0f);
                }
                needInvalidate = true;
            }

            if (needInvalidate)
                Invalidate();
        }

        private bool IsAnimating()
        {
            return isBreathing || isRotating || isRippling || isBlinking;
        }

        #region 事件处理
        private void BlinkTimer_Tick(object sender, EventArgs e)
        {
            temporaryState = !temporaryState;
            Invalidate();
        }
        #endregion

        #region 绘制方法
        protected override void OnPaint(PaintEventArgs e)
        {
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

            bool currentState = isBlinking ? temporaryState : isOn;
            Color currentColor = currentState ? onColor : offColor;

            // 应用呼吸效果的透明度  
            if (isBreathing)
            {
                currentColor = Color.FromArgb(
                    (int)(255 * breathingOpacity),
                    currentColor.R,
                    currentColor.G,
                    currentColor.B
                );
            }

            // 保存当前图形状态  
            GraphicsState state = e.Graphics.Save();

            // 应用旋转  
            if (isRotating)
            {
                e.Graphics.TranslateTransform(Width / 2f, Height / 2f);
                e.Graphics.RotateTransform(rotationAngle);
                e.Graphics.TranslateTransform(-Width / 2f, -Height / 2f);
            }

            using (GraphicsPath path = new GraphicsPath())
            {
                Rectangle bounds = new Rectangle(
                    borderWidth,
                    borderWidth,
                    Width - 2 * borderWidth - 1,
                    Height - 2 * borderWidth - 1
                );

                if (isCircular)
                    path.AddEllipse(bounds);
                else
                    path.AddRectangle(bounds);

                // 绘制主体  
                if (is3D)
                {
                    // 3D效果绘制代码保持不变  
                    using (PathGradientBrush gradientBrush = new PathGradientBrush(path))
                    {
                        gradientBrush.CenterColor = LightenColor(currentColor, 50);
                        gradientBrush.SurroundColors = new Color[] { currentColor };
                        gradientBrush.FocusScales = new PointF(0.8f, 0.8f);
                        e.Graphics.FillPath(gradientBrush, path);
                    }

                    // 高光效果  
                    DrawHighlight(e.Graphics, bounds);
                }
                else
                {
                    using (SolidBrush brush = new SolidBrush(currentColor))
                    {
                        e.Graphics.FillPath(brush, path);
                    }
                }

                // 绘制边框  
                if (borderWidth > 0)
                {
                    using (Pen borderPen = new Pen(borderColor, borderWidth))
                    {
                        e.Graphics.DrawPath(borderPen, path);
                    }
                }
            }

            // 恢复图形状态  
            e.Graphics.Restore(state);

            // 绘制波纹效果  
            if (isRippling && rippleRadii.Count > 0)
            {
                DrawRipples(e.Graphics);
            }

            base.OnPaint(e);
        }
        #endregion

        private void DrawHighlight(Graphics g, Rectangle bounds)
        {
            using (GraphicsPath highlightPath = new GraphicsPath())
            {
                // 计算高光区域(左上角域)  
                Rectangle highlightBounds = new Rectangle(
                    bounds.X + bounds.Width / 4,
                    bounds.Y + bounds.Height / 4,
                    bounds.Width / 4,
                    bounds.Height / 4
                );
                highlightPath.AddEllipse(highlightBounds);

                // 创建径向渐变画刷实现高光效果 
                using (PathGradientBrush highlightBrush = new PathGradientBrush(highlightPath))
                {
                    highlightBrush.CenterColor = Color.FromArgb(180, Color.White);
                    highlightBrush.SurroundColors = new Color[] { Color.FromArgb(0, Color.White) };
                    g.FillPath(highlightBrush, highlightPath);
                }
            }
        }

        private void DrawRipples(Graphics g)
        {
            float centerX = Width / 2f;
            float centerY = Height / 2f;

            foreach (float radius in rippleRadii)
            {
                // 计算波纹透明度  
                float alpha = 1.0f - (radius / Math.Max(Width, Height));
                if (alpha <= 0) continue;

                // 绘制波纹圆圈  
                using (Pen ripplePen = new Pen(Color.FromArgb((int)(alpha * 255), onColor), 2))
                {
                    g.DrawEllipse(ripplePen,
                        centerX - radius,
                        centerY - radius,
                        radius * 2,
                        radius * 2);
                }
            }
        }

        #region 辅助方法
        private Color LightenColor(Color color, int amount)
        {
            return Color.FromArgb(
                color.A,
                Math.Min(color.R + amount, 255),
                Math.Min(color.G + amount, 255),
                Math.Min(color.B + amount, 255)
            );
        }
        #endregion

        #region 公共方法
        public void StartBlinking()
        {
            IsBlinking = true;
        }

        public void StopBlinking()
        {
            IsBlinking = false;
        }

        public void Toggle()
        {
            IsOn = !IsOn;
        }

        public void StartBreathing()
        {
            IsBreathing = true;
        }

        public void StopBreathing()
        {
            IsBreathing = false;
        }

        public void StartRotating()
        {
            IsRotating = true;
        }

        public void StopRotating()
        {
            IsRotating = false;
        }

        public void StartRippling()
        {
            IsRippling = true;
        }

        public void StopRippling()
        {
            IsRippling = false;
        }
        #endregion

        #region 析构函数
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                blinkTimer?.Dispose();
                animationTimer?.Dispose();
            }
            base.Dispose(disposing);
        }
        #endregion
    }
}

4、功能说明

动画效果

  • 呼吸效果:通过调整透明度(breathingOpacity)实现明暗渐变。

  • 旋转效果:使用旋转变换(RotateTransform)动态旋转指示灯。

  • 波纹效果:以圆心扩散同心圆(rippleRadii),模拟水波纹。

状态控制方法

// 示例:启动波纹动画
indicator.StartRippling();
// 示例:切换开关状态
indicator.Toggle();

渲染流程

  • 计算当前颜色(开/关状态 + 呼吸透明度)

  • 应用旋转变换(若启用)

  • 绘制3D效果:路径渐变(PathGradientBrush)模拟光照

  • 叠加高光区域(DrawHighlight

  • 绘制波纹动画(DrawRipples

总结

该指示灯控件通过GDI+的路径渲染、矩阵变换和动画计时器,实现了工业级的可视化效果。其模块化设计(如分离动画逻辑与渲染逻辑)便于扩展新特性,可广泛应用于SCADA系统、设备监控面板等场景。开发者可通过属性面板快速配置样式,或通过公共方法动态控制状态。

关键词:指示灯、自定义控件、C#、GDI+、3D效果、闪烁动画、工业控制、PathGradientBrush、状态可视化

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:技术老小子

出处:mp.weixin.qq.com/s/Ub-xzPU3w2dZDL4HreVL1g

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!