酷炫闪烁灯





4.00/5 (5投票s)
可由多个线程操作的分组指示灯

引言
(感谢 Gadwin PrintScreen 提供的截图。这是一款非常棒的程序。)
Cool Blinkies
是一个线程安全且稳定的指示灯组,允许同时点亮多个指示灯
CoolBlinkies1.A_AllowMultipleOn = True
或只点亮单个指示灯
CoolBlinkies1.A_AllowMultipleOn = False
它具有不同程度的“latency
”(延迟)
CoolBlinkies1.A_Latency = uOneBlinky.LightLatencies.Medium
根据此列表
Public Enum LightLatencies
None
[Short]
Medium
[Long]
End Enum
Latency
(延迟)让用户即使在指示灯只亮了一瞬间的情况下也能看到它亮过。如果您需要更长的 latency
,可以查看 uOneBlinky:Private Sub SetLatency
。
您可以设置灯组中指示灯的数量
CoolBlinkies1.A_NumberOfLights = 5
指示灯的颜色
CoolBlinkies1.A_LightColour = uOneBlinky.LightColours.Red
根据此列表
Public Enum LightColours
Red
Blue
Green
Yellow
End Enum
将它设置为一排指示灯
CoolBlinkies1.A_Orientation = Orientations.Horizontal
或如上图所示的一列指示灯。
CoolBlinkies1.A_Orientation = Orientations.Vertical
您可以设置指示灯之间的边距
CoolBlinkies1.A_Margin = 3
以及指示灯在点亮后是否保持常亮
CoolBlinkies1.A_MomentaryOn = False
或在一段时间后(“latency
”)自行熄灭
CoolBlinkies1.A_MomentaryOn = True
这取决于上面描述的 A_Latency
属性。
如果您使用 A_MomentaryOn
功能,您还可以将“第一个”或“第零个”指示灯作为“默认”指示灯
CoolBlinkies1.A_FirstLightIsOnByDefault = True
在这种情况下,A_MomentaryOn
属性应为 True
。然后您所要做的就是告诉灯组要点亮哪个指示灯
A_LightState (Index) = uOneBlinky.LightStates.LightOn
该指示灯会在 latency
(延迟)时间过后自行熄灭。然后“默认”指示灯将重新亮起。
这可以用于这样的场景,例如,“默认”指示灯旁边的文本是“空闲”。通常您希望该指示灯常亮,当您想显示“非空闲”状态时,只需点亮另一个指示灯,无需其他操作。延迟时间过后,另一个指示灯会自行熄灭,“默认”指示灯将重新亮起。
背景
挑战:多线程控制一行或一列指示灯。
看起来很简单,只是打开和关闭它们,对吗?但有时指示灯的亮起和熄灭可能非常快,以至于用户根本无法察觉到指示灯曾经亮过。所以,您希望指示灯能保持点亮足够长的时间,以便人眼能够注意到。这意味着在用“熄灭颜色”绘制指示灯之前需要进行延迟。这就是“latency
”(延迟)。
您不应该在发出请求的同一线程上执行这种“延迟”,因为这会延误该线程。相反,指示灯应该接收到关闭自身的请求,并让另一个线程来执行延迟,而不是阻塞调用线程。这样调用可以立即返回,没有延迟。
在尝试使用线程处理后,我最终决定使用 Threading.Timer
来实现。(顺便说一下,这与 Windows.Forms.Timer
不同。)
我发现 Threading.Timer
可靠、精确且灵活。每个单独的指示灯,在被要求关闭时,都会使用(并重用)一个 Threading.Timer
实例,根据其“Latency
”属性延迟一段时间,然后再用“熄灭颜色”重绘自己。
声明 Threading.Timer
(以及一个无限延迟以防止计时器自动重复)
Private mLatencyTimer As Threading.Timer
Private mNoAutoRepeatInfiniteTimeSpan As TimeSpan _
= New TimeSpan(Threading.Timeout.Infinite)
设置 Threading.Timer
Public Sub New( )
...
'now initialize the latency timer.
'it will return and draw the light dark
Dim QuickDelayForInitialSetup As New TimeSpan(100)
mLatencyTimer = New Threading.Timer( _
AddressOf LatencyTimerControl, Nothing, _
QuickDelayForInitialSetup, _
mNoAutoRepeatInfiniteTimeSpan)
End Sub
这是 Threading.Timer
构造函数引用的 sub
Private Sub LatencyTimerControl()
'when we get here, we need to turn
'the light off if we are still delaying towards off.
If mCurrentLightState = LightStates.DelayingTowardsOff _
Or mMomentaryOn = True Then
'turn it off and paint it
mCurrentLightState = LightStates.LightOff
Invalidate()
'Let parent know a light turned itself off,
'in case it needs to turn on the DefaultLight
RaiseEvent LatencyDone(mMyPosition)
End If
End Sub
但是当有多个线程可能请求同一个指示灯打开/关闭时,还有另一个问题。
当一个灯组中的一个指示灯亮起时,该灯组必须关闭上一个亮着的指示灯(除非您已将 AllowMultipleOn
属性设置为 True
)。您希望它的行为像一组 RadioButtons
(单选按钮):如果一个指示灯被“选中”,那么上一个被选中的必须被取消选中。
但在多线程可能会点亮一个灯组中不同指示灯的情况下,“灯组”(父控件,uCoolBlinkies
)必须记住正确的关闭顺序。您不能将这些值存储在类级别的字段
中,因为一个可重入的线程可能会在它被用来关闭相应的指示灯之前改变这个值。
在尝试了各种想法之后,我最终决定使用一个 Queue (Of T)
来存储需要关闭的指示灯的索引,并使用一个 BackGroundWorker
从 Queue
中取出这些索引,并通知相应的指示灯关闭。
声明 Queue
和 BackgroundWorker
Private mTurnOffQueue As New Queue(Of Integer)
Private WithEvents mTurnOffBW As BackgroundWorker
设置 BackgroundWorker
并安排其关闭
Private Sub uCoolBlinkies_Load _
(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Load
mTurnOffBW = New BackgroundWorker
mTurnOffBW.WorkerSupportsCancellation = True
mTurnOffBW.RunWorkerAsync()
End Sub
Private Sub uCoolBlinkies_Disposed _
(ByVal sender As Object, ByVal e As System.EventArgs) _
Handles Me.Disposed
mStopBW = True
mTurnOffBW.CancelAsync()
End Sub
这是如何将它链接到实际执行工作的 sub
Private Sub mTurnOffBW_DoWork(ByVal sender As Object, _
ByVal e As DoWorkEventArgs) _
Handles mTurnOffBW.DoWork
Dim BW As BackgroundWorker = CType(sender, BackgroundWorker)
CheckForTurnOffs(BW)
End Sub
Private Sub CheckForTurnOffs(ByVal ABW As BackgroundWorker)
Do
If mTurnOffQueue.Count = 0 Then
Thread.Sleep(100)
Else
mLights(mTurnOffQueue.Dequeue).A_LightState = _
uOneBlinky.LightStates.LightOff
End If
Loop Until mStopBW
End Sub
上面的 sub CheckForTurnOffs
持续工作,如果 Queue
中没有项目,则休眠;否则,它会 Dequeue
(出队)一个需要关闭的灯的索引,并告诉该灯关闭自己。
当需要点亮一个灯时,您告诉它点亮,然后如果您不允许同时点亮多个灯,您可能需要告诉另一个灯关闭。下面我点亮一个灯,然后关闭上一个灯,除非它尚未被定义(Index = -1
)。要关闭灯,我只需将该灯的索引推入队列,然后就不管它了。一旦它进入了队列
,我就可以将 mLastLightOn
更新为我刚刚点亮的当前灯。
'turn the new light on
mLights(Index).A_LightState = uOneBlinky.LightStates.LightOn
'now set up for turning it off.
If Not mAllowMultipleOn Then
If mLastLightOn > -1 Then mTurnOffQueue.Enqueue(mLastLightOn)
mLastLightOn = Index
End If
Using the Code
我建议您运行这个项目。(确保 Tester
项目是“启动项目”)。几乎所有的属性都在用户界面上进行了演示。
当您试玩这个演示时,请记住,在您的项目中,指示灯不会由 TrackBar
控制。TrackBar
的作用是让您能够打开和关闭指示灯。

目前,指示灯组(一行或一列指示灯)是一个名为 uCoolBlinkies
的 UserControl
。它是父控件。(我想我本可以把它做成一个基于 Panel
或其他东西的组件
,但我还没有尝试过。)
uCoolBlinkies
拥有一个单独的 uOneBlinky
的 List (Of T)
。
uOneBlinky
知道如何将自己重绘为“亮”或“灭”的灯,使用红色、黄色、蓝色或绿色。当被告知关闭时,它知道如何设置其计时器以在用“熄灭”颜色重绘之前实现延迟。
uCoolBlinkies
会记住哪些灯已经亮过以及哪些需要按正确的顺序关闭。
我所有的 public
属性和方法都以“A_
”为前缀,这样它们就会出现在智能感知列表的顶部,而不会与其他项混在一起。有人有更好的方法来分组自定义属性/方法吗?
要在您的项目中使用 uCoolBlinkies
,只需将两个 UserControl
文件,uCoolBlinkies.vb 和 uOneBlinky.vb,添加到您的项目中。然后 uCoolBlinkies
应该会(可能在重新生成后)出现在您的工具箱中,您可以将一个或多个拖到您的窗体上。(不要将 uOneBlinky
拖到您的窗体上。它仅由 uCoolBlinkies
使用。)
关注点
使用 Queue
和 BackgroundWorker
来跟踪被多个线程访问时的事件顺序。也许有人可以对此提出改进建议。
使用 Threading.Timer
来可靠地延迟一个重绘事件(关闭灯),这不会让线程等待。
历史
- 本文于2009年11月30日首次在此提交,但断断续续开发了超过10年。我在我的多线程应用程序中遇到了很多麻烦,存在一些奇怪的时序问题,而所有问题都归结于这个小小的指示灯没有正确延迟!现在我认为我已经解决了这个问题并使其线程安全,我将其提交给其他人使用,如果社区中的任何人发现我在使用
Queue
和Threading.Timer
方面有任何明显甚至细微的问题,我欢迎您的评论和贡献。 - 2009年12月2日 - 进行了澄清性编辑,无程序更改