当秒表达到某个确切的时间跨度时,将操作排队到 运行
Queue actions to run when stopwatch reaches a certain exact timespan
我正在制作一个软件来在屏幕上显示计时器和 运行 用户定义的特定时间的特定操作。我找到了一种通过使用间隔为 16 毫秒(大约 60FPS)的 DispatcherTimer 在独立 window 上显示计时器的方法,但现在我必须找到一种方法来 运行 在定义的时间执行这些操作。
我制作了一个组件 QueueableStopwatch
来完成这项工作。它的工作方式如下:
- 给它一个包含
QueueAction
个对象的数组
- 在内部为 QueueActions 创建一个数组 运行 一次(按间隔排序)
- 在内部为要重复的 QueueActions 创建另一个数组
- 它有一个内部
Stopwatch
来计算时间
- 组件有方法
Start()
和Stop()
Start()
启动内部秒表并在单独的线程上启动“队列处理”循环
Stop()
停止秒表,当 Stopwatch.IsRunning
变为 false 时,“队列处理”循环自行停止
“队列处理”循环执行以下操作:
- 将对下一个
QueueAction
的引用保留为 运行。如果内部 Stopwatch.Elapsed
> 引用 QueueAction.Interval
它的 运行 并且引用更新到 运行 once QueueActions 数组 中的下一个
- 我们运行在条件
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.
我正在制作一个软件来在屏幕上显示计时器和 运行 用户定义的特定时间的特定操作。我找到了一种通过使用间隔为 16 毫秒(大约 60FPS)的 DispatcherTimer 在独立 window 上显示计时器的方法,但现在我必须找到一种方法来 运行 在定义的时间执行这些操作。
我制作了一个组件 QueueableStopwatch
来完成这项工作。它的工作方式如下:
- 给它一个包含
QueueAction
个对象的数组 - 在内部为 QueueActions 创建一个数组 运行 一次(按间隔排序)
- 在内部为要重复的 QueueActions 创建另一个数组
- 它有一个内部
Stopwatch
来计算时间 - 组件有方法
Start()
和Stop()
Start()
启动内部秒表并在单独的线程上启动“队列处理”循环Stop()
停止秒表,当Stopwatch.IsRunning
变为 false 时,“队列处理”循环自行停止
“队列处理”循环执行以下操作:
- 将对下一个
QueueAction
的引用保留为 运行。如果内部Stopwatch.Elapsed
> 引用QueueAction.Interval
它的 运行 并且引用更新到 运行 once QueueActions 数组 中的下一个
- 我们运行在条件
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.