65.9K
CodeProject 正在变化。 阅读更多。
Home

Flash LED 控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.26/5 (24投票s)

2004年9月27日

9分钟阅读

viewsIcon

137830

downloadIcon

7763

通过重写 Paint 方法来模拟 LED 灯的精美控件。一套完善的属性允许各种闪烁效果。

Sample Image - FlashLED.jpg

引言

这是一个适用于 Windows 应用程序的小型项目。它创建了一个模拟 LED 灯的控件。该 LED 是圆形且彩色的,它通过重写的 OnPaint 方法绘制自身。除了其开/关功能外,它还使用计时器以单独设置的间隔轮流显示有限数量的颜色。富有想象力地使用,它将为您的应用程序添加精美的效果。

构建它

创建一个新的 C# 窗口控件库项目。在开始之前,将继承从 UserControl 更改为仅仅 Control,将所有 UserControl 重命名为 Led,并删除所有与 InitialiseComponent 相关的内容——Led 不会有子控件。接着,将命名空间从任意内容更改为适合您的控件集合的名称,例如 TH.WinControls(其中 TH 代表我的姓名首字母——但也可以是您的)。右键单击解决方案资源管理器中的 Led 项目,从弹出菜单中选择“属性”,并将默认命名空间更改为上述名称。然后向您的项目添加一个位图,将其命名为与项目相同的名称,例如 Led.bmp,在编辑其内容之前,在解决方案资源管理器中选择另一个项目,然后重新选择 Led.bmp。在“属性”窗格中,将“生成操作”更改为“嵌入资源”。只有这样才能进入位图编辑器并开始设计一个漂亮的位图,大小为 16 X 16 像素,16 色,例如

现在,您可以构建项目并将控件添加到工具箱中。在此阶段,控件只是出现在调色板上,没有任何功能。在其构造函数中添加以下行:

    private Timer tick; 
      
    public Led():base() {
      SetStyle(ControlStyles.AllPaintingInWmPaint, true);
      SetStyle(ControlStyles.DoubleBuffer, true);
      SetStyle(ControlStyles.UserPaint,    true); 
      SetStyle(ControlStyles.ResizeRedraw, true); 
      
      Width  = 17; 
      Height = 17;

      Paint += new System.Windows.Forms.PaintEventHandler(this._Paint);
      
      tick = new Timer();
      tick.Enabled = false;
      tick.Tick += new System.EventHandler(this._Tick);
    }

SetStyle 语句不言自明。我将 WidthHeight 设置为奇数,以便在控件中心有一个像素。17 是实现完美圆形(我们的 LED 将是圆形)的最佳尺寸。

此时也创建 Timer,但暂时保持禁用状态。现在,我们必须添加两个引用的处理程序——_Paint,它将绘制控件,以及 _Tick,它将处理 Flash 模式下的颜色变化。

技巧:为了能够构建每个阶段,只需为 _Timer_Paint 添加空的处理程序。要知道方法签名是什么样子,请在主窗体中创建 Paint 的处理程序,同时在那里放置一个 Timer 并创建其处理程序。然后将相关代码复制到控件文件中并调整相应的名称。

    private void _Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
    }
    private void _Tick(object sender, System.EventArgs e) {
    }

Paint

添加一个新属性以激活 LED,再添加两个属性以指定两个状态(活动/非活动)的颜色。

    private bool _Active = true;
    [Category("Behavior"),
    DefaultValue(true)]
    public bool Active {
      get { return _Active; }
      set { 
        _Active = value; 
        Invalidate();
      }
    }

    private Color _ColorOn = Color.Red;
    [Category("Appearance")]
    public Color ColorOn {
      get { return _ColorOn; }
      set { 
        _ColorOn = value; 
        Invalidate();
      }
    }

    private Color _ColorOff = SystemColors.Control;
    [Category("Appearance")]
    public Color ColorOff {
      get { return _ColorOff; }
      set { 
        _ColorOff = value; 
        Invalidate();
      }
    }

请注意 Invalidate 调用,它将确保只要有可用于此任务的时间片,Paint 就会重新绘制组件。

_Paint 处理程序负责解释 EnableActive 状态。这是它的代码:

    private void _Paint(object sender, System.Windows.Forms.PaintEventArgs e) {  
      e.Graphics.Clear(BackColor);
      if (Enabled) {
        if (Active) {
          e.Graphics.FillEllipse(new SolidBrush(ColorOn),1,1,Width-3,Height-3);
          e.Graphics.DrawArc(new Pen(FadeColor(ColorOn, 
                     Color.White,1,2),2),3,3,Width-7, 
                     Height-7,-90.0F,-90.0F);
          e.Graphics.DrawEllipse(new Pen(FadeColor(ColorOn, 
                     Color.Black),1),1,1,Width-3,Height-3);
        }
        else {
          e.Graphics.FillEllipse(new SolidBrush(ColorOff),1,1,Width-3,Height-3);
          e.Graphics.DrawArc(new Pen(FadeColor(ColorOff, 
                     Color.Black,2,1),2),3,3,Width-7,Height-7,0.0F,90.0F);
          e.Graphics.DrawEllipse(new Pen(FadeColor(ColorOff, 
                     Color.Black),1),1,1,Width-3,Height-3);
        }
      }
      else e.Graphics.DrawEllipse(new 
                      Pen(System.Drawing.SystemColors.ControlDark,1), 
                      1,1,Width-3,Height-3);
    }

为了更好地操作颜色,我引入了一个辅助函数 FadeColor,它将以给定比例将第一个参数中给定的颜色与第二个参数混合。我将默认比例设置为 1:1。我将使该函数为 publicstatic,以便能够在组件源代码之外使用它,而无需创建 LED;事实上,它与 LED 的形状和行为无关。

    #region helper color functions
    public static Color FadeColor(Color c1, Color c2, int i1, int i2) {
      int r=(i1*c1.R+i2*c2.R)/(i1+i2); 
      int g=(i1*c1.G+i2*c2.G)/(i1+i2); 
      int b=(i1*c1.B+i2*c2.B)/(i1+i2); 

      return Color.FromArgb(r,g,b);
    }

    public static Color FadeColor(Color c1, Color c2) {
      return FadeColor(c1,c2,1,1);
    }
    #endregion

该函数通过将两种颜色分解为 RGB 分量,应用比例,然后返回重新组合的颜色来工作。我使用此函数将 LED 颜色混合到气泡的边缘,并添加使气泡具有体积的闪光。当 LED 亮起时,边缘会用白色照亮,而当熄灭时,边缘会用黑色变暗。闪光也是如此,但比例不同。

现在您可以编译解决方案,将 LED 拖到测试窗体上,更改其颜色,并在设计模式下直接将其打开/关闭!

现在闪烁它

现在是关键:我希望能够添加不止一个闪烁间隔,而且——为什么不呢——不止一组两种开/关颜色。事实上,我希望能够添加任意数量的颜色,并以平滑渐变或侵入式红-黄-品红警告的形式按顺序触发它们。通过属性预设这种行为应该简单且无错误,同时能够通过代码编写冗长的序列。

为了实现这一点,我将添加两个属性:

    private string _FlashIntervals="250";
    public int [] flashIntervals = new int[1] {250};
    [Category("Appearance"),
    DefaultValue("250")]
    public string FlashIntervals {
      get { return _FlashIntervals; }
      set { 
        _FlashIntervals = value; 
        string [] fi = _FlashIntervals.Split(new char[] {',','/','|',' ','\n'});
        flashIntervals = new int[fi.Length];
        for (int i=0; i<fi.Length; i++)
          try {
            flashIntervals[i] = int.Parse(fi[i]);
          } catch {
            flashIntervals[i] = 25;
          }
      }
    }

    private string _FlashColors=string.Empty; 
    public Color [] flashColors;
    [Category("Appearance"),
    DefaultValue("")]
    public string FlashColors {
      get { return _FlashColors; }
      set { 
        _FlashColors = value; 
        if (_FlashColors==string.Empty) {
          flashColors=null;
        } else {
          string [] fc = _FlashColors.Split(new char[] {',','/','|',' ','\n'});
          flashColors = new Color[fc.Length];
          for (int i=0; i<fc.Length; i++)
            try {
              flashColors[i] = (fc[i]!="")?Color.FromName(fc[i]):Color.Empty;
            } catch {
              flashColors[i] = Color.Empty;
            }
        }
      }
    }

FlashIntervalsFlashColors 将接受带分隔符的字符串,这些字符串将转换为公共数组,可供内部代码以及外部过程访问。任何合理的定界符都将分隔输入字符串,错误项将默认为 Color.Empty(用于关闭 LED 的颜色)和 25 毫秒(用于间隔)——足够小,不会引起麻烦。

两个数组不必长度相同:如果颜色表较大,多余的项将被忽略;如果颜色少于间隔,多余的间隔将使 LED 亮起和熄灭。空颜色也是如此。

    public int tickIndex;
    private void _Tick(object sender, System.EventArgs e) {
      tickIndex=(++tickIndex)%(flashIntervals.Length);
      tick.Interval=flashIntervals[tickIndex];
      try {
        if ((flashColors==null)
        ||(flashColors.Length<tickIndex)
        ||(flashColors[tickIndex]==Color.Empty))
          Active = !Active;
        else {
          ColorOn = flashColors[tickIndex];
          Active=true;
        }
      } catch {
        Active = !Active;
      }
    }

如果我们有一个 Flash 属性来切换此模式,这将完成我们的闪烁功能

    private bool _Flash = false;
    [Category("Behavior"),
    DefaultValue(false)]
    public bool Flash {
      get { return _Flash; }
      set { 
        _Flash = value && (flashIntervals.Length>0); 
        tickIndex = 0;
        tick.Interval = flashIntervals[tickIndex];
        tick.Enabled = _Flash;
        Active = true;
      }
    }

操作示例

  1. 要进入简单的闪烁模式(开/关),只需在 FlashIntervals 中放置一个间隔值,而在 FlashColors 中不放置任何内容。
  2. 对于红/绿模式,不要尝试使用将关闭颜色设置为绿色的先前模式!关闭状态的气泡边框会变暗,闪光会在另一侧。相反,为 FlashColors 输入 RED,Green,为 FlashIntervals 输入 500,500
  3. 一个更有趣的例子是 Red,Off,Yellow,Off,Blue。您可以直接输入这个字符串,因为 Off 不是有效的颜色,或者更短:Red,,Yellow,,Blue(不要在逗号之间输入空格——空格也是分隔符!)。对于 FlashIntervals,输入 500,250,500,250,500,250。请注意,额外的 250 值是为了在蓝色和红色之间有一个关闭间隔。
  4. 对于双脉冲灯,将 FlashIntervals 输入为 250,250,250,1000。它将执行:四分之一秒亮,四分之一秒灭,再四分之一秒亮,最后四分之一秒灭;然后重复。您也可以通过 1000,250,250,250 编程双脉冲暗。
  5. 一个更有趣的例子是生成颜色渐变。在这里,让代码填充颜色和间隔的值更容易。假设有 40 个等间隔的 50 毫秒,渐变从黄色到红色再返回。为了获得渐变的所有颜色,我们可以方便地使用公开的静态 FadeColor 方法。因此,只需在测试窗体的代码隐藏中添加以下代码,例如在窗体的构造函数中(或添加一个按钮):
          Color
          c; led5.flashColors
          = new Color[40]; led5.flashIntervals
          = new int[40]; for
          (int i= 0; i<20; i++) { c=
            TH.WinComponents.Led.FadeColor(Color.Yellow,Color.Red,
                                (i<=20)?i:20,(i>20)?20:20-i);
            led5.flashColors[   i]=c; led5.flashIntervals[   i]=50;
            led5.flashColors[39-i]=c; led5.flashIntervals[39-i]=50;
          }
          led5.Flash=!led5.Flash;

我本该在此结束,但一些细节迫使我继续,实际上是更多。

透明度

我们的控件在气泡外真的透明吗?让我们一探究竟!我们进入测试表单,从一个角到另一个角画一条线:

    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
      e.Graphics.DrawLine(new Pen(Color.White,2),0,0, 
        ClientRectangle.Width,ClientRectangle.Height);
    }
    private void Form1_SizeChanged(object sender, System.EventArgs e) {
      Invalidate();
    }

(首先,我们学习如何使用 ClientRectangle 处理窗体区域内的坐标。然后,每次窗体调整大小时,我们被迫使窗体失效。否则,会发生以下情况:

什么?——Windows 只使添加到原始窗体以及鼠标移动经过的区域无效。所以,把 Invalidate 调用放回去,并把线条加宽。现在,我们学到了什么?

令人惊讶:控件一点也不透明!让我们在 LED 的构造函数中添加以下几行。(也禁用大的那个,只是为了看看 Enable 是如何工作的)

      SetStyle(ControlStyles.SupportsTransparentBackColor, true);
      BackColor = Color.Transparent;

更有可能...

额外:外部效应

至此,我们的控制基本完成。然而,让我们看看我们还能从控制代码外部实现什么。为此,我们必须将一些代码连接到 LED 事件,而我们需要附加代码的第一个事件是 Paint。不幸的是,我们已经在控件代码内部使用了 Paint 事件。嗯,Paint 有点像委托——我们用 += 运算符附加代码,所以我们假设我们也可以附加其他处理程序。然而,在内部处理程序中,LED 的区域已经渲染完成——我们只能在气泡上添加图形元素。这意味着我们必须改变一些东西。

让我们从 LED 的构造函数中删除对 _Paint 处理程序的引用,并用以下几行更改此方法的声明:

    protected override void OnPaint(PaintEventArgs e) {
      if (Paint!=null) Paint(this,e);
      else {
        base.OnPaint(e);
        // ... the same as above _Paint method

尝试编译这段代码,我们发现原始的 Paint 事件不能用于表达式的左侧;我们也不能调用它。幸运的是,我们可以用一个新的事件来替换它:

    public new event PaintEventHandler Paint;

现在,我们可以用一个全新的内部渲染来替换控件的内部渲染。让我们开始在三角形内部绘制一个感叹号——“注意!”的通用标志。我们将使用 led5,我们已经将其编程为通过从 RedYellow 的渐变来旋转颜色。我将尝试不仅改变形状,还要使标志脉动:

    private void led5_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
      TH.WinComponents.Led l = sender as TH.WinComponents.Led;
      if (l.Enabled && l.Active) {
        int cx,cy,w,h,d;
        d  = (l.tickIndex<20)?l.tickIndex:39-l.tickIndex;
        cx = l.Width/2;  cy = l.Height/2;
        w  = 2*d+8;      h  = 2*d+8;
        try {
          // Triangle
          Point startPoint = new Point(cx,cy-h/2);
          e.Graphics.FillPolygon(new SolidBrush(l.ColorOn),
            new Point[] {startPoint,
                         new Point(cx-w/2,cy+h/2),
                         new Point(cx+w/2,cy+h/2),
                         startPoint });
          // Exclamation mark
          e.Graphics.DrawLine(new Pen(Color.Red,4),cx,cy-h/2+9,cx,cy+h/2-11);
          e.Graphics.DrawLine(new Pen(Color.Red,6),cx,cy-h/2+11,cx,cy);
          e.Graphics.DrawLine(new Pen(Color.Red,4),cx,cy+h/2-7,cx,cy+h/2-3);
        } catch {}
      }
    }

这个感叹号并不特别好看,尤其是在它很小的时候,但那时,我会在三角形中将其溶解成红色。此外,当形状变得太小时可能会出现一些错误,但空的 catch 块会处理这些问题。

另一个挑战:使用外部位图:右键单击 LED 项目并添加一个新项作为位图文件,创建一个小的位图文件。让我们画一颗心并以这种方式命名。我希望这颗心像真正的心脏一样以双脉冲跳动。为此,准备 Flash 模式,将其 FlashIntervals 属性分配为 100,250,100,750。现在,将以下代码放在 led1Paint 处理程序上:

    Bitmap hart;
    private void led6_Paint(object sender, System.Windows.Forms.PaintEventArgs e) {
      TH.WinComponents.Led l = sender as TH.WinComponents.Led;
      Rectangle r = new Rectangle(0,0,40,40);
      switch (l.tickIndex) {
        case 0 : r = new Rectangle(0,0,47,47); 
                     hart.RotateFlip(RotateFlipType.Rotate90FlipNone); 
                     break;
        case 1 : r = new Rectangle(3,3,40,40); break;
        case 2 : r = new Rectangle(1,1,46,46); break;
        case 3 : r = new Rectangle(3,3,40,40); break;
      }
      e.Graphics.DrawImage(hart,r);
    }

为了使这段代码工作,我们必须在某个地方加载并准备 heart 位图。让我们在表单的构造函数中完成此操作:

      hart = new Bitmap(@"C:\CS\WinComponents\Led\Hart.bmp"); 
      hart.MakeTransparent(Color.White);

直接引用并交付可执行文件中的文件并不好,所以让我们将其包含在程序集的资源中。点击测试程序中的 Hart.bmp 文件,然后在属性面板中,将“生成操作”更改为“嵌入资源”。现在修改初始化代码:

//hart = new Bitmap(@"C:\CS\WinComponents\Led\Hart.bmp");
Stream s = 
    Assembly.GetCallingAssembly().GetManifestResourceStream("Led.Hart.bmp");
hart = new Bitmap(s);

注意:资源名称 Led.Hart.bmp 中的 Led 代表测试程序的程序集名称。

结论

我提出了一个小型项目,它创建了一个模拟 LED 灯的控件。该 LED 控件通过其 OnPaint 方法绘制自身。除了其开/关功能外,它还通过预设间隔旋转一组颜色进行闪烁。最后,我作为一个额外功能,添加了外部事件来修改嵌入功能,通过绘制存储在程序集资源中的位图。富有想象力地使用,它可以为任何应用程序添加精美的效果。

© . All rights reserved.