web-dev-qa-db-ja.com

WPFカウントダウンタイマーの作り方

結果を次のように表示するwpfカウントダウンタイマーを作成したい hh:mm:ss テキストボックスに、私は誰かの助けに感謝します。

15
user2419978

DispatcherTimerクラス( msdn )を使用できます。

TimeSpan構造( msdn )で保持できる期間。

TimeSpanhh:mm:ssにフォーマットする場合は、ToStringメソッドを「c」引数( msdn )で呼び出す必要があります。

例:

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>

コードビハインド:

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();            
        }
    }
}
26
kmatyaszek

この目的でDispatcherTimerを使用することに何の問題もありません。ただし、新しいTPLベースのasync/awaitパラダイムをIMHOにすると、コードの読み書きが簡単になります。コードビハインドから直接UI要素の値を設定するのではなく、WPFプログラムに常に適切なMVVMプラクティスを使用することもお勧めします。

これは、質問で説明されているようにカウントダウンタイマーを実装するプログラムの例ですが、これらのより現代的な方法を使用しています…

もちろんビューモデルは、興味深いコードの大部分が存在する場所であり、そこにさえも、実際のカウントダウンを実装する単一のメソッド_StartCountdown()があります。

ViewModel.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));
    }
}

コメントで述べたように、上記はそのまま動作しますが、特定の出力が必要な場合は、ユーザー固有のニーズに合わせて出力を調整するためにIValueConverterを実装すると便利です。これらの例をいくつか示します。

tcToLocalConverter.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:

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();
    }
}

そしてもちろん、UIを定義するためのいくつかのXAML(UI要素のいずれにも名前がなく、分離コードがそれらのいずれかに明示的にアクセスする必要がない場合):

MainWindow.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>
9
Peter Duniho