Flash LED 控件






3.26/5 (24投票s)
2004年9月27日
9分钟阅读

137830

7763
通过重写 Paint 方法来模拟 LED 灯的精美控件。一套完善的属性允许各种闪烁效果。
引言
这是一个适用于 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
语句不言自明。我将 Width
和 Height
设置为奇数,以便在控件中心有一个像素。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
处理程序负责解释 Enable
和 Active
状态。这是它的代码:
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。我将使该函数为 public
和 static
,以便能够在组件源代码之外使用它,而无需创建 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
该函数通过将两种颜色分解为 R
、G
和 B
分量,应用比例,然后返回重新组合的颜色来工作。我使用此函数将 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;
}
}
}
}
FlashIntervals
和 FlashColors
将接受带分隔符的字符串,这些字符串将转换为公共数组,可供内部代码以及外部过程访问。任何合理的定界符都将分隔输入字符串,错误项将默认为 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;
}
}
操作示例
- 要进入简单的闪烁模式(开/关),只需在
FlashIntervals
中放置一个间隔值,而在FlashColors
中不放置任何内容。 - 对于红/绿模式,不要尝试使用将关闭颜色设置为绿色的先前模式!关闭状态的气泡边框会变暗,闪光会在另一侧。相反,为
FlashColors
输入RED,Green
,为FlashIntervals
输入500,500
。 - 一个更有趣的例子是
Red,Off,Yellow,Off,Blue
。您可以直接输入这个字符串,因为Off
不是有效的颜色,或者更短:Red,,Yellow,,Blue
(不要在逗号之间输入空格——空格也是分隔符!)。对于FlashIntervals
,输入500,250,500,250,500,250
。请注意,额外的 250 值是为了在蓝色和红色之间有一个关闭间隔。 - 对于双脉冲灯,将
FlashIntervals
输入为250,250,250,1000
。它将执行:四分之一秒亮,四分之一秒灭,再四分之一秒亮,最后四分之一秒灭;然后重复。您也可以通过1000,250,250,250
编程双脉冲暗。 - 一个更有趣的例子是生成颜色渐变。在这里,让代码填充颜色和间隔的值更容易。假设有 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
,我们已经将其编程为通过从 Red
到 Yellow
的渐变来旋转颜色。我将尝试不仅改变形状,还要使标志脉动:
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
。现在,将以下代码放在 led1
的 Paint
处理程序上:
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
方法绘制自身。除了其开/关功能外,它还通过预设间隔旋转一组颜色进行闪烁。最后,我作为一个额外功能,添加了外部事件来修改嵌入功能,通过绘制存储在程序集资源中的位图。富有想象力地使用,它可以为任何应用程序添加精美的效果。