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

酷炫闪烁灯

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (5投票s)

2009年12月2日

CPOL

7分钟阅读

viewsIcon

28639

downloadIcon

365

可由多个线程操作的分组指示灯

引言

(感谢 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) 来存储需要关闭的指示灯的索引,并使用一个 BackGroundWorkerQueue 中取出这些索引,并通知相应的指示灯关闭。

声明 QueueBackgroundWorker

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 的作用是让您能够打开和关闭指示灯。

目前,指示灯组(一行或一列指示灯)是一个名为 uCoolBlinkiesUserControl。它是父控件。(我想我本可以把它做成一个基于 Panel 或其他东西的组件,但我还没有尝试过。)

uCoolBlinkies 拥有一个单独的 uOneBlinkyList (Of T)

uOneBlinky 知道如何将自己重绘为“亮”或“灭”的灯,使用红色、黄色、蓝色或绿色。当被告知关闭时,它知道如何设置其计时器以在用“熄灭”颜色重绘之前实现延迟。

uCoolBlinkies 会记住哪些灯已经亮过以及哪些需要按正确的顺序关闭。

我所有的 public 属性和方法都以“A_”为前缀,这样它们就会出现在智能感知列表的顶部,而不会与其他项混在一起。有人有更好的方法来分组自定义属性/方法吗?

要在您的项目中使用 uCoolBlinkies,只需将两个 UserControl 文件,uCoolBlinkies.vbuOneBlinky.vb,添加到您的项目中。然后 uCoolBlinkies 应该会(可能在重新生成后)出现在您的工具箱中,您可以将一个或多个拖到您的窗体上。(不要将 uOneBlinky 拖到您的窗体上。它仅由 uCoolBlinkies 使用。)

关注点

使用 QueueBackgroundWorker 来跟踪被多个线程访问时的事件顺序。也许有人可以对此提出改进建议。

使用 Threading.Timer 来可靠地延迟一个重绘事件(关闭灯),这不会让线程等待。

历史

  • 本文于2009年11月30日首次在此提交,但断断续续开发了超过10年。我在我的多线程应用程序中遇到了很多麻烦,存在一些奇怪的时序问题,而所有问题都归结于这个小小的指示灯没有正确延迟!现在我认为我已经解决了这个问题并使其线程安全,我将其提交给其他人使用,如果社区中的任何人发现我在使用 QueueThreading.Timer 方面有任何明显甚至细微的问题,我欢迎您的评论和贡献。
  • 2009年12月2日 - 进行了澄清性编辑,无程序更改
© . All rights reserved.