更好的解决方案、计时器、秒表、时间跨度

Better solution, timer, stopwatch, timespan

我正在开发用于跟踪各种活动持续时间的小工具。

在此示例中,我们有 3 个活动,驾驶、步行和等待。

每个活动都是 Form1 上的一个按钮

示例:

等等,可以是一项或多项活动。在测量结束时,我有每个 activity.

的总持续时间

下面这段代码实际上运行良好。从 Form1 调用函数,开始测量,稍后我在 public 个可用变量中有值。

模块:

Dim SW, SW1, SW2 As New Stopwatch
Dim WithEvents Tmr, Tmr1, Tmr2 As New Timer
Dim stws() = {SW, SW1, SW2}
Dim tmrs() = {Tmr, Tmr1, Tmr2}
Public Drive, Walk, Wait As String

Public Function WhichButton(btn As Button)

WhichButton = btn.Text

        Select Case WhichButton
            Case "Drive"
                For Each s As Stopwatch In stws
                    s.Stop()
                Next
                For Each t As Timer In tmrs
                    t.Stop()
                Next
                SW.Start()
                Tmr.Start()
            Case "Wait"
                For Each s As Stopwatch In stws
                    s.Stop()
                Next
                For Each t As Timer In tmrs
                    t.Stop()
                Next
                SW.Start()
                Tmr1.Start()
            Case "Walk"
                For Each s As Stopwatch In stws
                    s.Stop()
                Next
                For Each t As Timer In tmrs
                    t.Stop()
                Next
                SW2.Start()
                Tmr2.Start()
        End Select
End Function

Private Sub Tmr_Tick(sender As Object, e As System.EventArgs) Handles Tmr.Tick
    Dim elapsed As TimeSpan = SW.Elapsed
    Drive = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub

Private Sub Tmr1_Tick(sender As Object, e As System.EventArgs) Handles Tmr1.Tick
    Dim elapsed As TimeSpan = SW1.Elapsed
    Walk = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub

Private Sub Tmr2_Tick(sender As Object, e As System.EventArgs) Handles Tmr2.Tick
    Dim elapsed As TimeSpan = SW2.Elapsed
    Wait = $"{elapsed.Hours:00}:{elapsed.Minutes:00}.{elapsed.Seconds:00}"
End Sub

我来这里是因为我对这个解决方案不满意,而且我不了解高级解决方案。这里的问题是我可以有 X 个按钮,可以添加新按钮或删除几个按钮,这取决于情况,我不想为每个按钮编写代码块。此外,如果我更改按钮的文本 属性,Select 大小写将不起作用。 所以我想为每个按钮动态创建计时器和秒表。

我想从这个开始:

 Dim timers As List(Of Timer) = New List(Of Timer)

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load

    For Each btn As Button In Panel1.Controls.OfType(Of Button)
        timers.Add(New Timer() With {.Tag = btn.Name})
        AddHandler btn.Click, AddressOf Something
    Next

End Sub

Public Sub Something(sender As Object, e As EventArgs)
    Dim btn = DirectCast(sender, Button)

    Dim tmr As Timer = timers.SingleOrDefault(Function(t) t.Tag IsNot Nothing AndAlso t.Tag.ToString = btn.Name)

End Sub

在这里我可以通过标签引用定时器 属性 但我不知道如何实现秒表和时间跨度。 感谢阅读,如有任何帮助,建议、伪代码、代码示例,我们将不胜感激。

首先,使用三个 Timers 没有意义。一个 Timer 可以处理所有 3 次。其次,根据您发布的内容,使用 Timer 毫无意义。我认为 Timer 有用的唯一原因是不断在 UI 中显示当前经过的时间,但您没有这样做。如果您不打算显示这些变量,则重复设置这些 String 变量是没有意义的。只需在需要时从适当的 Stopwatch 中获取 Elapsed 值即可。

至于你的 Buttons' Click 事件处理程序,它也很糟糕。通用事件处理程序的全部意义在于,您希望对每个对象执行相同的操作,因此您只需编写一次代码。如果您最终为该公共事件处理程序中的每个对象编写单独的代码,那么这就失去了意义,并使您的代码更复杂而不是更少。您应该为每个 Button.

使用单独的事件处理程序

如果您要使用通用事件处理程序,至少要提取通用代码。在所有三个 Case 块中都有相同的两个 For Each 循环。这应该在 Select Case 之前完成,然后只在每个 Case.

中启动适当的 Stopwatch

不过我认为您不应该使用 Buttons。你实际上应该使用 RadioButtons。您可以将它们的 Appearance 属性 设置为 Button,然后它们看起来就像普通的 Buttons,但仍然表现得像 RadioButtons。当您单击一个时,它会保留按下的外观以指示它已被选中,单击另一个将释放先前按下的那个。在这种情况下,您的代码可能如下所示:

Private ReadOnly driveStopwatch As New Stopwatch
Private ReadOnly waitStopwatch As New Stopwatch
Private ReadOnly walkStopwatch As New Stopwatch

Private Sub driveRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged
    If driveRadioButton.Checked Then
        driveStopwatch.Start()
    Else
        driveStopwatch.Stop()
    End If
End Sub

Private Sub waitRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles waitRadioButton.CheckedChanged
    If waitRadioButton.Checked Then
        waitStopwatch.Start()
    Else
        waitStopwatch.Stop()
    End If
End Sub

Private Sub walkRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles walkRadioButton.CheckedChanged
    If walkRadioButton.Checked Then
        walkStopwatch.Start()
    Else
        walkStopwatch.Stop()
    End If
End Sub

因为选中 RadioButton 会自动取消选中任何其他,每个 CheckedChanged 事件处理程序只需要担心自己的 Stopwatch

如果你想在它停止时显示特定 Stopwatch 的经过时间,你可以在它停止时这样做,例如

Private Sub driveRadioButton_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged
    If driveRadioButton.Checked Then
        driveStopwatch.Start()
    Else
        driveStopwatch.Stop()
        driveLabel.Text = driveStopwatch.Elapsed.ToString("hh\:mm\:ss")
    End If
End Sub

我认为 TimeSpan.ToString 的重载首先在 .NET 4.5 中可用,因此除非您的目标是 .NET 4.0 或更早版本,否则您应该使用它。

如果你确实想不断显示当前经过的时间,那么正如我所说,你只需要一个Timer。您只需让它一直 运行 并根据当前 运行ning 的 Stopwatch 适当更新,例如

Private Sub displayTimer_Tick(sender As Object, e As EventArgs) Handles displayTimer.Tick
    If driveStopwatch.IsRunning Then
        driveLabel.Text = driveStopwatch.Elapsed.ToString("hh\:mm\:ss")
    ElseIf waitStopwatch.IsRunning Then
        waitLabel.Text = waitStopwatch.Elapsed.ToString("hh\:mm\:ss")
    ElseIf walkStopwatch.IsRunning Then
        walkLabel.Text = walkStopwatch.Elapsed.ToString("hh\:mm\:ss")
    End If
End Sub

您还没有向我们展示您是如何显示经过时间的,所以这只是一个猜测。在这个场景中,当 Stopwatch 停止时,您绝对应该更新 Label,因为 Timer 不会在下一个 Tick 上更新 Label

你可能想要一个 Button 可以停止的地方 and/or 重置所有三个 Stopwatches。这意味着在所有三个 RadioButtons 上将 Checked 设置为 False,然后在所有三个 Stopwatches 上调用 Reset。您可能也想 clear/reset Labels

像这样使用 RadioButtons 也存在潜在问题。如果您的 RadioButtons 之一在 Tab 键顺序中排在第一位,那么在您加载表单时它将默认获得焦点。聚焦 RadioButton 将检查它,因此这意味着您将默认启动 Stopwatch。如果这不是您想要的,请确保其他控件位于 Tab 键顺序中的第一个。如果由于某种原因你不能这样做,处理表单的 Shown 事件,将 ActiveControl 设置为 Nothing,取消选中 RadioButton 并重置相应的 StopwatchLabel.

作为最后一条一般信息,请注意我已经为所有内容命名,这样即使事先不了解该项目的人也不会怀疑所有内容是什么以及它们的用途。 SWSW1SW2 这样的名字是不好的。即使您意识到 SW 意味着 Stopwatch,您也不知道每个值的实际用途。在 Intellisense 的今天,它只是懒惰地使用这样的名称。每个有经验的开发人员都可以告诉你一个故事,关于一段时间后回过头来阅读他们自己的代码,却不知道他们所说的各种东西的意思。不要陷入那个陷阱,确保你早日养成良好的习惯。

编辑:

作为奖励,您可以通过以下方式正确使用通用事件处理程序。首先,定义一个自定义 Stopwatch class 具有关联的 Label:

Public Class StopwatchEx
    Inherits Stopwatch

    Public Property Label As Label

End Class

一旦建立关联,您就会自动知道使用哪个 Label 来显示 Stopwatch 的经过时间。接下来,定义一个具有关联 Stopwatch:

的自定义 RadioButton class
Public Class RadioButtonEx
    Inherits RadioButton

    Public Property Stopwatch As StopwatchEx

End Class

接下来,在您的表单上使用自定义 class 而不是标准 RadioButtons。您可以直接从工具箱添加它们(您的自定义控件将在构建项目后自动添加),或者您可以编辑设计器代码文件并更改代码中控件的类型。后一种选择存在一定的风险,因此请务必事先创建备份。完成后,更改 Stopwatches 的类型并处理表单的 Load 事件以创建关联:

Private ReadOnly driveStopwatch As New StopwatchEx
Private ReadOnly waitStopwatch As New StopwatchEx
Private ReadOnly walkStopwatch As New StopwatchEx

Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
    'Associate Stopwatches with RadioButtons
    driveRadioButton.Stopwatch = driveStopwatch
    waitRadioButton.Stopwatch = waitStopwatch
    walkRadioButton.Stopwatch = walkStopwatch

    'Associate Labels with Stopwatches
    driveStopwatch.Label = driveLabel
    waitStopwatch.Label = waitLabel
    walkStopwatch.Label = walkLabel
End Sub

您现在可以使用一个方法来处理所有三个 CheckedChanged 事件 RadioButtons 因为您现在可以对所有三个执行完全相同的操作:

Private Sub RadioButtons_CheckedChanged(sender As Object, e As EventArgs) Handles driveRadioButton.CheckedChanged,
                                                                                  waitRadioButton.CheckedChanged,
                                                                                  walkRadioButton.CheckedChanged
    Dim rb = DirectCast(sender, RadioButtonEx)
    Dim sw = rb.Stopwatch

    If rb.Checked Then
        sw.Start()
    Else
        sw.Stop()
        sw.Label.Text = sw.Elapsed.ToString("hh\:mm\:ss")
    End If
End Sub

引发事件的 RadioButton 会告诉您要使用哪个 Stopwatch,那个会告诉您要使用哪个 Label,因此无需为每个事件编写不同的代码。

TimerTick事件处理程序也可以用通用代码处理每个Stopwatch

Private Sub displayTimer_Tick(sender As Object, e As EventArgs) Handles displayTimer.Tick
    For Each sw In {driveStopwatch, waitStopwatch, walkStopwatch}
        If sw.IsRunning Then
            sw.Label.Text = sw.Elapsed.ToString("hh\:mm\:ss")
            Exit For
        End If
    Next
End Sub

您可以在 class 级别创建数组,但是,由于它只在这个地方使用,因此在这里创建它是有意义的。性能影响微不足道,它通过在使用它们的地方创建东西使代码更具可读性。

请注意,我在这段代码中确实使用了变量名的缩写。这是出于两个原因。首先,它们是在不同时间引用不同对象的变量。这意味着不可能使用特定于对象用途的名称。您可以使用基于上下文的名称,例如currentRadioButton,但由于第二个原因,我在这里不这样做。

第二个原因是它们是在非常有限的范围内使用的局部变量。 rbsw 变量的使用不超过声明它们的几行,因此很难不理解它们是什么。如果你这样命名一个字段,那么当你在代码中看到它时,你必须去别处寻找它是什么。在这段代码中,如果您正在查看其中一个变量的用法,那么声明也在视线范围内,因此您必须视而不见才能看不到您正在处理的是什么类型。基本上,如果一个变量的使用距离它的声明很远,那么我建议使用一个有意义的、描述性的名称。如果它只在其声明的几行内使用,那么一个简短的名称就可以了。我通常倾向于使用类型的首字母,就像我在这里所做的那样。如果您需要该类型的多个局部变量,我通常更喜欢使用描述性名称来消除它们的歧义,而不是使用数字。但是,有时确实没有特定目的的方法来做到这一点,在这种情况下,数字是可以的,例如在没有上下文的情况下比较两个 Strings 可能会使用 s1s2 作为变量名称。