C# 如何制作wpf倒数计时器?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16748371/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-10 01:50:26  来源:igfitidea点击:

How to make a wpf countdown timer?

c#wpftimercountdown

提问by user2419978

I want to create wpf countdown timer that display the result as hh:mm:ssinto textbox, I would be thankful for anyone help.

我想创建 wpf 倒数计时器,将结果显示为hh:mm:ss到文本框中,我会感谢任何人的帮助。

采纳答案by kmatyaszek

You can use DispatcherTimerclass (msdn).

您可以使用DispatcherTimer类(msdn)。

Duration of time you can hold in TimeSpanstructure (msdn).

您可以在TimeSpan结构中保留的时间长度( msdn)。

If you want formatting TimeSpanto hh:mm:ssyou should invoke ToStringmethod with "c" argument (msdn).

如果您想格式化TimeSpanhh:mm:ss您应该ToString使用“c”参数(msdn)调用方法。

Example:

例子:

XAML:

XAML:

<Window x:Class="CountdownTimer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <TextBlock Name="tbTime" />
    </Grid>
</Window>

Code-behind:

代码隐藏:

using System;
using System.Windows;
using System.Windows.Threading;

namespace CountdownTimer
{
    public partial class MainWindow : Window
    {
        DispatcherTimer _timer;
        TimeSpan _time;

        public MainWindow()
        {
            InitializeComponent();

            _time = TimeSpan.FromSeconds(10);

            _timer = new DispatcherTimer(new TimeSpan(0, 0, 1), DispatcherPriority.Normal, delegate
                {
                    tbTime.Text = _time.ToString("c");
                    if (_time == TimeSpan.Zero) _timer.Stop();
                    _time = _time.Add(TimeSpan.FromSeconds(-1));                    
                }, Application.Current.Dispatcher);

            _timer.Start();            
        }
    }
}

回答by Peter Duniho

There is nothing wrong with using DispatcherTimerfor this purpose. However, IMHO the newer TPL-based async/awaitparadigm makes for code that is easier to write and read. It is also better to always use good MVVM practices for WPF programs, rather than to set UI element values directly from code-behind.

DispatcherTimer用于此目的没有任何问题。但是,恕我直言,较新的基于 TPL 的async/await范式使代码更易于编写和阅读。最好始终对 WPF 程序使用良好的 MVVM 实践,而不是直接从代码隐藏设置 UI 元素值。

Here's an example of a program that implements a countdown-timer as described in the question, but using these more modern practices…

这是一个实现问题中描述的倒数计时器的程序示例,但使用这些更现代的做法......

The view model is of course where the bulk of the interesting code resides, and even there the main thing is the single method _StartCountdown(), which implements the actual countdown:

视图模型当然是大部分有趣代码所在的地方,即使在那里,主要的是单个方法_StartCountdown(),它实现了实际的倒计时:

ViewModel.cs:

视图模型.cs:

class ViewModel
{
    private async void _StartCountdown()
    {
        Running = true;

        // NOTE: UTC times used internally to ensure proper operation
        // across Daylight Saving Time changes. An IValueConverter can
        // be used to present the user a local time.

        // NOTE: RemainingTime is the raw data. It may be desirable to
        // use an IValueConverter to always round up to the nearest integer
        // value for whatever is the least-significant component displayed
        // (e.g. minutes, seconds, milliseconds), so that the displayed
        // value doesn't reach the zero value until the timer has completed.

        DateTime startTime = DateTime.UtcNow, endTime = startTime + Duration;
        TimeSpan remainingTime, interval = TimeSpan.FromMilliseconds(100);

        StartTime = startTime;
        remainingTime = endTime - startTime;

        while (remainingTime > TimeSpan.Zero)
        {
            RemainingTime = remainingTime;
            if (RemainingTime < interval)
            {
                interval = RemainingTime;
            }

            // NOTE: arbitrary update rate of 100 ms (initialized above). This
            // should be a value at least somewhat less than the minimum precision
            // displayed (e.g. here it's 1/10th the displayed precision of one
            // second), to avoid potentially distracting/annoying "stutters" in
            // the countdown.

            await Task.Delay(interval);
            remainingTime = endTime - DateTime.UtcNow;
        }

        RemainingTime = TimeSpan.Zero;
        StartTime = null;
        Running = false;
    }

    private TimeSpan _duration;
    public TimeSpan Duration
    {
        get { return _duration; }
        set { _UpdateField(ref _duration, value); }
    }

    private DateTime? _startTime;
    public DateTime? StartTime
    {
        get { return _startTime; }
        private set { _UpdateField(ref _startTime, value); }
    }

    private TimeSpan _remainingTime;
    public TimeSpan RemainingTime
    {
        get { return _remainingTime; }
        private set { _UpdateField(ref _remainingTime, value); }
    }

    private bool _running;
    public bool Running
    {
        get { return _running; }
        private set { _UpdateField(ref _running, value, _OnRunningChanged); }
    }

    private void _OnRunningChanged(bool obj)
    {
        _startCountdownCommand.RaiseCanExecuteChanged();
    }

    private readonly DelegateCommand _startCountdownCommand;
    public ICommand StartCountdownCommand { get { return _startCountdownCommand; } }

    public ViewModel()
    {
        _startCountdownCommand = new DelegateCommand(_StartCountdown, () => !Running);
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void _UpdateField<T>(ref T field, T newValue,
        Action<T> onChangedCallback = null,
        [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(field, newValue))
        {
            return;
        }

        T oldValue = field;

        field = newValue;
        onChangedCallback?.Invoke(oldValue);
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

As noted in the comments, the above will work as-is, but if you want specific output, it's useful to have IValueConverterimplementations to adjust the output to suit user-specific needs. Here are some examples of those:

正如评论中所指出的,上述内容将按原样工作,但如果您想要特定的输出,那么让IValueConverter实现来调整输出以满足用户特定的需求是很有用的。以下是其中的一些示例:

UtcToLocalConverter.cs:

UtcToLocalConverter.cs:

class UtcToLocalConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return null;

        if (value is DateTime)
        {
            DateTime dateTime = (DateTime)value;

            return dateTime.ToLocalTime();
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return null;

        if (value is DateTime)
        {
            DateTime dateTime = (DateTime)value;

            return dateTime.ToUniversalTime();
        }

        return Binding.DoNothing;
    }
}

TimeSpanRoundUpConverter.cs:

TimeSpanRoundUpConverter.cs:

class TimeSpanRoundUpConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (!(value is TimeSpan && parameter is TimeSpan))
        {
            return Binding.DoNothing;
        }

        return RoundUpTimeSpan((TimeSpan)value, (TimeSpan)parameter);
    }

    private static TimeSpan RoundUpTimeSpan(TimeSpan value, TimeSpan roundTo)
    {
        if (value < TimeSpan.Zero) return RoundUpTimeSpan(-value, roundTo);

        double quantization = roundTo.TotalMilliseconds, input = value.TotalMilliseconds;
        double normalized = input / quantization;
        int wholeMultiple = (int)normalized;
        double fraction = normalized - wholeMultiple;

        return TimeSpan.FromMilliseconds((fraction == 0 ? wholeMultiple : wholeMultiple + 1) * quantization);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

And of course, some XAML to define the UI (where none of the UI elements have names, nor does the code-behind need to access any of them explicitly):

当然,还有一些 XAML 来定义 UI(其中没有 UI 元素具有名称,代码隐藏也不需要显式访问它们中的任何一个):

MainWindow.xaml:

主窗口.xaml:

<Window x:Class="TestSO16748371CountdownTimer.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:l="clr-namespace:TestSO16748371CountdownTimer"
        xmlns:s="clr-namespace:System;assembly=mscorlib"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
  <Window.DataContext>
    <l:ViewModel/>
  </Window.DataContext>

  <Window.Resources>
    <l:UtcToLocalConverter x:Key="utcToLocalConverter1"/>
    <l:TimeSpanRoundUpConverter x:Key="timeSpanRoundUpConverter1"/>
    <s:TimeSpan x:Key="timeSpanRoundTo1">00:00:01</s:TimeSpan>
  </Window.Resources>

  <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="Auto"/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition Height="Auto"/>
      <RowDefinition/>
    </Grid.RowDefinitions>

    <TextBlock Text="Duration: "/>
    <TextBox Text="{Binding Duration}" Grid.Column="1"/>

    <TextBlock Text="Start time:" Grid.Row="1"/>
    <TextBlock Text="{Binding StartTime, Converter={StaticResource utcToLocalConverter1}}" Grid.Row="1" Grid.Column="1"/>

    <TextBlock Text="Remaining time:" Grid.Row="2"/>
    <TextBlock Text="{Binding RemainingTime,
      StringFormat=hh\:mm\:ss,
      Converter={StaticResource timeSpanRoundUpConverter1},
      ConverterParameter={StaticResource timeSpanRoundTo1}}" Grid.Row="2" Grid.Column="1"/>

    <Button Content="Start Countdown" Command="{Binding StartCountdownCommand}" Grid.Row="3" VerticalAlignment="Top"/>

  </Grid>
</Window>