雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

わんくま同盟東京勉強会 #38 LT アフターフォロー - カウントダウンタイマー

ってことで、だいぶ遅くなりましたが LT で作ったカウントダウンタイマーの紹介をしておきます。

プロジェクト構成

まずはプロジェクトの構成です。

  • WpfAppication1.proj
    • Application.xaml
      • Application.xaml.cs
    • TSViewConverter.cs
    • TSCheckConverter.cs
    • Window1.xaml

Window 1 つにコンバーター 2 つのシンプル(?)な WPF アプリケーションです。
短時間で構築する必要があるのと、そもそもデータが TimeSpan 1 個でロジックらしいロジックも存在しないので、コードビハインドに全部書きました。
# 試しに手元で MVVM で書き直してみましたが……逆に面倒です。。。

ちなみに、ちょっとズルをしてコンバーター 2 つは 3 分間クッキング方式で先に用意しておきました。
# ……だって、あの場で新しいクラスを追加とかしていたら、テンプレートが展開されるだけで 10 秒以上とられちゃうんだもの><

TSViewConverter.cs

using System;
using System.Windows.Data;

namespace Mkns
{
  public class TSViewConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      var data = (TimeSpan)value;
      if (data.Ticks < 0)
      {
        data = data.Negate();
      }
      return String.Format("{0:00}:{1:00}.{2:000}", data.Minutes, data.Seconds, data.Milliseconds);
    }

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

まずは先に用意しておいたコンバーターの紹介です。
こちらはウィンドウに表示する文字列用のコンバーターです。
今回は TimeSpan 値をそのまま TextBlock にバインドしたかった(簡略化のため)ので、こちらの意図通りにフォーマットさせたくてコンバーターのお世話になりました。
…… TimeSpan がカスタムフォーマットに対応してくれていればもっと簡単だったのにー。 :p
# そういや、どうせ先に用意しておくならちゃんと逆変換の方を NotSupportedException に書き換えておけばよかったかも。

TSCheckConverter.cs

using System;
using System.Windows.Data;

namespace Mkns
{
  public class TSCheckConverter : IValueConverter
  {
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
      return (((TimeSpan)value).Ticks < 0);
    }

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

先に用意しておいたコンバーターの紹介 2 つめ。
こちらはトリガーイベント用のコンバーターです。
最初、時間超過時の文字色変更はコードビハインドに直接コードを書いていました。
でも、 WPF 的にデザインに関することは全部 XAML で書きたいなぁと思っていたら、コンバーターを使った方法と云うのを見つけて嬉々として利用しました♪
DataTrigger は値一致条件のみなので使いづらいなぁ、と常々思っていたのですが、コンバーターを使えば結構複雑な条件指定も出来そうですね。

Window1.xaml

<Window x:Class="WpfApplication3.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Background="Black" Foreground="White"
    xmlns:y="clr-namespace:Mkns"
    MouseDoubleClick="Window_MouseDoubleClick"
    Title="Window1" Height="300" Width="300">
  <Window.Resources>
    <y:TSViewConverter x:Key="vc" />
    <y:TSCheckConverter x:Key="cc" />
    <Style x:Key="ts" TargetType="TextBlock">
      <Setter Property="Foreground" Value="White" />
      <Style.Triggers>
        <DataTrigger Binding="{Binding tr, Converter={StaticResource cc}}" Value="True">
          <Setter Property="Foreground" Value="Red" />
        </DataTrigger>
      </Style.Triggers>
    </Style>
  </Window.Resources>
  <Grid>
    <Viewbox Stretch="Fill">
      <TextBlock Text="{Binding tr, Converter={StaticResource vc}}" Style="{StaticResource ts}" />
    </Viewbox>
  </Grid>
</Window>

ここでのトピックスは、 Viewbox でしょうかね。
初めて Viewbox を使ったときは感動したものです。
Windows Form の頃に頑張ってウィンドウのサイズから文字サイズを計算して描画していたのが馬鹿馬鹿しくなりますね。。。
ちなみにタイプ数減らしたくて Key の値を 2 文字とかにしてますw
実際のプログラミングではやっちゃいけないですねww

Window1.xaml.cs

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

namespace WpfApplication3
{
  /// <summary>
  /// Window1.xaml の相互作用ロジック
  /// </summary>
  public partial class Window1 : Window
  {
    DispatcherTimer timer;
    DateTime t;

    public static readonly DependencyProperty trProp = DependencyProperty.Register("tr", typeof(TimeSpan), typeof(Window));

    public TimeSpan tr
    {
      get { return (TimeSpan)this.GetValue(trProp); }
      set { this.SetValue(trProp, value); }
    }
    
    public Window1()
    {
      InitializeComponent();
      this.DataContext = this;
      timer = new DispatcherTimer() { Interval = new TimeSpan(10000) };
      timer.Tick += (s, e) => { tr = t - DateTime.Now; };
    }

    private void Window_MouseDoubleClick(object sender, MouseButtonEventArgs e)
    {
      t = DateTime.Now.AddSeconds(5);
      timer.Start();

    }
  }
}

実際にライブコーディングしたのはこのソース位でした。( XAML も書いたけど)
キモは、 TimeSpan 値を依存プロパティ (DependencyProperty) として追加したところと、 DataContext に自身を設定したところ位でしょうか。
こうする事でバインドを WPF 側に任せてしまえるのでラクですね。
ちなみに、タイマー部分のロジックはえいやっで書いてしまいましたが、カウントダウンタイマーの基本的な考え方は以下の通りです。

  • タイマー開始時の現在時刻 (DateTime.Now) から指定時間後をターゲットタイムとして保存
  • タイマーイベントで、保存したターゲットタイムと現在時刻との差( = 残り時間)を計算

たったこれだけなんですねー。(だからこそ 5 分で書けたんだけど :p )