当秒表达到某个确切的时间跨度时,将操作排队到 运行

Queue actions to run when stopwatch reaches a certain exact timespan

我正在制作一个软件来在屏幕上显示计时器和 运行 用户定义的特定时间的特定操作。我找到了一种通过使用间隔为 16 毫秒(大约 60FPS)的 DispatcherTimer 在独立 window 上显示计时器的方法,但现在我必须找到一种方法来 运行 在定义的时间执行这些操作。

我制作了一个组件 QueueableStopwatch 来完成这项工作。它的工作方式如下:

“队列处理”循环执行以下操作:

  1. 将对下一个 QueueAction 的引用保留为 运行。如果内部 Stopwatch.Elapsed > 引用 QueueAction.Interval 它的 运行 并且引用更新到 运行 once QueueActions 数组
  2. 中的下一个
  3. 我们运行在条件Stopwatch.Elapsed / QueueAction.Interval - QueueAction.TimesExecuted >= 1下待重复数组中的每个QueueAction。如果 运行 我们将 QueueAction.TimesExecuted 增加一。

此解决方案是否足以作为应用程序的“核心”实施运行宁关键行动?

Stopwatch.IsRunning 的使用是否会像记录的那样导致意外行为 here

这是组件代码:

public class QueueAction
{
    /// <summary>
    /// Interval to run the action
    /// </summary>
    public TimeSpan Interval { get; set; }
    /// <summary>
    /// The current action to run
    /// </summary>
    public Action Action { get; set; }
    /// <summary>
    /// Dispatcher the action will be ran into
    /// </summary>
    public Dispatcher Dispatcher { get; set; }
    /// <summary>
    /// True if the action will be repeated
    /// </summary>
    public bool Repeat { get; set; }
}
public class QueueableStopwatch
{
    private Stopwatch _stopwatch = new Stopwatch();

    public TimeSpan Elapsed => _stopwatch.Elapsed;

    private RepeatableQueueAction[] _repeatQueue = { };
    private QueueAction[] _singleQueue = { };

    public QueueAction[] Queue
    {
        get => _singleQueue;
        set
        {
            _repeatQueue = value.Where(action => action.Repeat).Select(action => new RepeatableQueueAction { QueueAction = action }).ToArray();
            _singleQueue = value.Where(action => !action.Repeat).OrderBy(action => action.Interval.TotalMilliseconds).ToArray();
        }
    }

    public void Start()
    {
        if (_stopwatch.IsRunning)
            throw new InvalidOperationException("The chronometer is already running");

        _stopwatch.Start();

        if(_singleQueue.Length > 0)
        {
            new Task(() =>
            {
                int i = 0;
                QueueAction selectedAction = selectedAction = _singleQueue[i];
                do
                {
                    if (i < _singleQueue.Length && selectedAction.Interval <= _stopwatch.Elapsed) // Single time run queue
                    {
                        selectedAction.Dispatcher.Invoke(() => selectedAction.Action());
                        i++;
                        if(i < _singleQueue.Length)
                            selectedAction = _singleQueue[i];
                    }

                    foreach(var repetitionAction in _repeatQueue) // Repeat run queue
                    {
                        if(_stopwatch.Elapsed / repetitionAction.QueueAction.Interval - repetitionAction.Repetitions >= 1)
                        {
                            repetitionAction.QueueAction.Dispatcher.Invoke(() => repetitionAction.QueueAction.Action());
                            repetitionAction.Repetitions++;
                        }
                    }
                }
                while (_stopwatch.IsRunning);
            }).Start();
        }
    }

    public void Stop()
    {
        _stopwatch.Reset();
    }

    private class RepeatableQueueAction
    {
        public QueueAction QueueAction { get; set; }
        public int Repetitions { get; set; }
    }

}

如果你想 运行 它这个 xaml 做的工作:

MainWindow.xaml

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp1"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        Background="Black">
    <StackPanel Orientation="Vertical">
        <Label Name="lblMessage" Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="56"/>
        <Button Click="Button_Click" Content="Stop" HorizontalAlignment="Center"/>
    </StackPanel>
</Window>

MainWindow.cs

public partial class MainWindow : Window
{

    QueueableStopwatch stopwatch = new QueueableStopwatch();

    public MainWindow()
    {
        InitializeComponent();
        stopwatch.Queue = new QueueAction[]
        {
            new QueueAction
            {
                Dispatcher = lblMessage.Dispatcher,
                Interval = TimeSpan.FromSeconds(7),
                Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]I run every 7 seconds",
                Repeat = true
            },
            new QueueAction
            {
                Dispatcher = lblMessage.Dispatcher,
                Interval = TimeSpan.FromSeconds(10),
                Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
            },
            new QueueAction
            {
                Dispatcher = lblMessage.Dispatcher,
                Interval = TimeSpan.FromSeconds(3),
                Action = () => lblMessage.Content = $"[{stopwatch.Elapsed}]3 seconds elapsed"
            }
        };
        stopwatch.Start();
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
        stopwatch.Stop();
    }
}

秒表不适合这项工作,您实际上是在线程上等待旋转,浪费 CPU 时间并可能导致其他线程被饿死。

框架已经以timers的形式提供了这个功能。如果您想在 UI 线程上执行 运行 操作,调度计时器将是合适的。因此,对于您要安排的每个操作,创建一个相应的计时器。如果您想预先决定是否应该重复该操作,您可能需要一个包装器。

var timer = new DispatcherTimer(){ Interval =  TimeSpan.FromSeconds(10) };
timer.Tick += (o, e) => {
    lblMessage.Content = $"[{stopwatch.Elapsed}]Queued first but ran at 10 seconds"
    timer.Stop();
    };
timer.Start();

计时器的分辨率取决于OS,但通常为 1-16 毫秒。对于 UI 程序,这应该足够了,无论如何屏幕​​都会有各种小的延迟。如果您需要更好的分辨率,可以使用 mediatimer.