dragan10

カスタムコントロール - 最終章

2008-10-19 15:52:04

一ヶ月にわたってカスタムコントロールの作成に関する半ダースほどの短い記事をポストしてきました。これらのカスタムコントロールはVisual State Managerと、その基盤となるエンジン部分、特に依存性プロパティおよびパーツ・ステートモデルとやりとりします。

 *カスタムコントロールへの招待 その1(未訳)
 *カスタムコントロールへの招待 その2(未訳)
 *カスタムコントロールへの作成(未訳)
 * Generic.xaml(未訳)
 * 依存性プロパティ その1
 * 依存性プロパティ その2
 * 依存性プロパティ - 優先順位

これらを背景に、カスタムコントロールにビジュアルステートを実装する段階に進む準備が整いました。キーとなるコンセプトは次の通りです:コントロールのロジックと、コントロールのビジュアルとの厳格な分離。これがもっともはっきりするのは、コントロールがクリックされたときです。

見かけから言えば、多くのコントロールはクリックに対する反応として、そのビジュアルをいくらか変化させてます。論理的には、何らかの振る舞いを持って反応します - あるいは、「リスナーたち」が反応できるイベントを発生させるかも知れません。これらを分離しておくことが、パーツ・ステートモデルのキーなのです。

作成するカスタムコントロール(今回は早送りで、もっとゆっくり丁寧な説明は今後のチュートリアルやビデオでします)は、Contolから導出します。このコントロールは独自の形で描かれ、スキンを変更することができるように設計され、パーツ・ステートモデルをサポートします。

このコントロールのCommonStatesには三つのステートがあります。
    * Normal
    * MouseOver
    * Pressed

そして、CustomStatesには二つのステートがあります。
    * Normal
    * On

複数のStateGroup(この場合はCommonStatesとCustomStates) は独立しているので、どちらのStateGroupにもNormalが含まれていてかまわないことに注意してください。

ステートの変化

カスタムコントロールは、CommonStatesとCustumStatesが共にNormalの場合、灰色のボールとして描かれますが、ステートが変化すると見かけも変わります。加えて、ステートがNormalの時とそうでないときとで表示が変わるテンプレートを追加し、それを完成させるためにコントロールはクリックされたときにイベントを発行し、コントロール(それがテンプレート化されているか否かによらず)を持っているページがそのクリックに反応できるようにします。ヒュー!

本当に大事なところはチュートリアルとビデオのために取っておきますが、ここで注目していただきたいのはここです:どうやってこのコントロールは、ビジュアルに関する契約をVisual State Manager(とExpression Blend)に伝えているのでしょうか?

これは、属性を使って実現されています。属性はCustomControlの定義の上に置かれ、コントロールとVisual State Managerとの間の契約を決定します。属性はステートグループとその中のステートを宣言します。

[TemplatePart       ( Name = "Core",      Type = typeof( FrameworkElement ) )]
[TemplateVisualState( Name = "Normal",    GroupName = "CommonStates" )]
[TemplateVisualState( Name = "MouseOver", GroupName = "CommonStates" )]
[TemplateVisualState( Name = "Pressed",   GroupName = "CommonStates" )]
[TemplateVisualState( Name = "On",        GroupName = "CustomStates" )]
[TemplateVisualState( Name = "Norm",      GroupName = "CustomStates" )]
Public class CustomControl : Control
{

一行目は、作成しているコントロールの一部を定義しています:コントロールの名前と型を決めています(この場合、型はFrameworkElementです - これは十分な汎用性を持っており、どんなUIControlも内部に持てますし、加えてSilverlightのレイアウト・システムに加わりうるどんな要素も、また生存期間があってデータバインディングに関するサポートを要するどんな要素も内部に持つことができます)。

五つのTemplateVisualState属性は、二つのステートグループを定義しています:CommonStates とCustomStatesです。

CLRのイベントを状態変化に変換するのは、カスタムクラスの仕事です。たとえば、CLRは"mouse over"については何も知りません - これはWindowsやSilverlightが認識するステートではないのです。しかし、SilverlightはMouseEnterやMouseLeaveを認識してくれますし、それさえあれば私たちがやりたいことには十分なのです。

まずメンバー変数を三つ作成します。

private FrameworkElement corePart;
private bool isMouseOver;
private bool isPressed;

最初の変数は私たちのコントロールそのもので、テンプレートを適用したあとすぐに取得します。

public override void OnApplyTemplate()
{
   base.OnApplyTemplate();
   CorePart = GetTemplateChild( "Core" ) as FrameworkElement;

(これは、もちろんXaml中で適切な要素が"Core"となっている必要があります。当然そうしますが・・・)

<Ellipse
   x:Name="Core"
    Width="200"
    Height="200" RenderTransformOrigin="0.5,0.5" >

corePartができたので、他の二つのプライベート変数にはイベントハンドラを代入して、CLRイベントをビジュアルステートの変化に変換することができます。しかし、頭のいい開発者から知恵を借りて、すこしトリッキーなやり方をしましょう。Coreのプロパティを設定するときにCoreのイベントハンドラを設定するのですが、このときに安全なやり方を取ります(仮にCoreがまだ存在していないか、Coreがnullだった場合を考慮します)。以下がデータメンバーに対応する完成版のプロパティです。

private FrameworkElement CorePart
{
   get
   {
      return corePart;
   }
   set
   {
      FrameworkElement oldCorePart = corePart;
      if ( oldCorePart != null )
      {
         oldCorePart.MouseEnter -= new MouseEventHandler( corePart_MouseEnter );
         oldCorePart.MouseLeave -= new MouseEventHandler( corePart_MouseLeave );
         oldCorePart.MouseLeftButtonDown -= new MouseButtonEventHandler( corePart_MouseLeftButtonDown );
         oldCorePart.MouseLeftButtonUp -= new MouseButtonEventHandler( corePart_MouseLeftButtonUp );
      }
      corePart = value;
      if ( corePart != null )
      {
         corePart.MouseEnter += new MouseEventHandler( corePart_MouseEnter );
         corePart.MouseLeave += new MouseEventHandler( corePart_MouseLeave );
         corePart.MouseLeftButtonDown += new MouseButtonEventHandler( corePart_MouseLeftButtonDown );
         corePart.MouseLeftButtonUp += new MouseButtonEventHandler( corePart_MouseLeftButtonUp );
      }
   }
}

これは、見かけほどひどいできではありません。単に、「Coreのプロパティを設定するときには、まず最初に既存のメンバー変数のコピーを作る。そのメンバー変数がnullでなければ、それを全イベントハンドラから外す。次に、もし与えられたのがnullでない新しいcorepartであれば、その新しい値にイベントハンドラ群を登録する」という処理をやっているだけです。

イベントハンドラは、MouseEnter/MouseLeave およびbuttonDownとbuttonUpを探します。

これらはビジュアルイベントに翻訳され、どれかが起きるたびにGotoStateというプライベートメソッドが呼ばれます。つまりこうです。

void corePart_MouseEnter( object sender, MouseEventArgs e )
{
   isMouseOver = true;
   GoToState( true );
}

これは、「CLRがマウスがCoreの上を通過したと言ってきたら、それはMouseOverステートに移行しろという意味なので、フラグをセットして、ビジュアルのトランジションを使うという引数を渡してGoToStateメソッドを呼ぶ(つまり、新しいステートに移行する際に、間が抜けて見えないようにトランジションのタイミングを使うということ)」ということです。

GoToStateはVisual State Machineを呼び出し、(推測されたとおり)フラグと、トランジションを使うかどうかの指定に基づいて、どのステートに移行するかを指示します。

private void GoToState( bool useTransitions )
{
   if ( isPressed )
   {
      VisualStateManager.GoToState( this, "Pressed", useTransitions );
   }
   else if ( isMouseOver )
   {
      VisualStateManager.GoToState( this, "MouseOver", useTransitions );
   }
   else
   {
      VisualStateManager.GoToState( this, "Normal", useTransitions );
   }

   if ( IsOn )
   {
      VisualStateManager.GoToState( this, "On", useTransitions );
   }
   else
   {
      VisualStateManager.GoToState( this, "Norm", useTransitions );
   }
}

Visual State Managerが新しいステートに移行する際には、generic.xamlもしくはあなたが書いたテンプレート内のストーリーボードを使います。たとえば、isMouseOverに対する反応がgeneric.xamlでMouseOverというVisualState内に記録されているとして、このMouseOverではコントロールが目立つようにバウンドするものとすれば、こんな風になります。

<vsm:VisualState x:Name="MouseOver">
    <Storyboard x:Key="Bounce" RepeatBehavior="forever" >
        <DoubleAnimationUsingKeyFrames
         BeginTime="00:00:00"
         Duration="00:00:01"
         Storyboard.TargetName="Core"
         Storyboard.TargetProperty="(UIElement.RenderTransform).
         (TransformGroup.Children)[3].(TranslateTransform.Y)">
            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="25"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="0"/>
            <SplineDoubleKeyFrame KeyTime="00:00:00.75" Value="50"/>
            <SplineDoubleKeyFrame KeyTime="00:00:01" Value="0"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</vsm:VisualState>

テンプレートは同じステートを取りますが、ご期待通り、やや違う振る舞いをします。

<vsm:VisualState x:Name="MouseOver">
    <Storyboard>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                Duration="00:00:00.0010000"
                Storyboard.TargetName="Core"
                Storyboard.TargetProperty="(UIElement.RenderTransform).
                                       (TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1.25"/>
        </DoubleAnimationUsingKeyFrames>
        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
                 Duration="00:00:00.0010000"
                 Storyboard.TargetName="Core"
                 Storyboard.TargetProperty="(UIElement.RenderTransform).
                                       (TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
            <SplineDoubleKeyFrame KeyTime="00:00:00" Value="1.25"/>
        </DoubleAnimationUsingKeyFrames>
    </Storyboard>
</vsm:VisualState>

告白しなければなりませんが、このあたりがカスタムコントロールを私が気に入っているところなのです。深く見ていけばいくほど、よりたくさんおもしろい物が見つかります。とはいうものの、ビデオ無しで探検していけるのはこのあたりまでだと思いますので、そろそろ他の重要な機能に関するポストを書いていくことに意識を向けようと思います。

(これはRTW直前のポストなので、十分に信頼できる内容にはなっていないかも知れません。RTWで問題が起こらないように全速力で作業しておりますので、ご容赦ください)

読んでくださってありがとう。

-jesse

 (原文はこちら


※このエントリは ブロガーにより投稿されたものです。朝日インタラクティブ および ZDNet Japan編集部の見解・意向を示すものではありません。
  • 新着記事
  • 特集
  • ブログ