基礎

新しい WPF カレンダー コントロールのカスタマイズ

Charles Petzold

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

目次

Calendar: 簡単な概要説明
Calendar のスタイル設定
新しい Calendar テンプレートの設定
CalendarItem テンプレートの置き換え
強力な CalendarDayButton テンプレート
Calendar からの派生
DatePicker コントロール

2006 年に、Microsoft が Windows Presentation Foundation (WPF) をリリースしたときに、いくつかのコントロール、特にカレンダー コントロールが含まれていないとだれもが思いました。それらのコントロールが含まれていないのは不思議でした。Windows フォームには、MonthCalendar コントロールと DateTimePicker コントロールがあるのに、どうしてそれらを WPF に移植しないのでしょうか。

しかし、コントロールを WPF に移植するのは、プロパティ名やイベント名を変更するだけの簡単な問題ではありません。WPF コントロールには、"外観がありません"。つまり、コントロールの視覚的な外観は、他の要素およびコントロールのビジュアル ツリーに分離されている必要があります。この既定のテンプレートは、完全に置き換えることができる方法で構築および文書化する必要があります。こうすることで、開発者および設計者はコントロールをカスタマイズして、コントロールにまったく新しい外観を与えることができます。

2008 年 1 月号の筆者の記事「非コモン コントロール用のテンプレート」で MonthCalendar コントロールを設計しているときに気が付きましたが、置き換え可能なテンプレートを使用してコントロールを設計するのは厄介です。さまざまな外観に対応できるようにコントロールをできるだけ汎用化させる一方で、コードとテンプレート間のインターフェイスが極端に複雑にならないようにする必要があります。カレンダーの場合は、文化的習慣に大きく依存しているため、別の次元の複雑さが伴います。また、カスタム テンプレートでそうした習慣をどこまで無視できるようにする必要があるのかが明確ではありません。

WPF カレンダー コントロールの実装に時間がかかったのは、それが理由かもしれません。マイクロソフトは、昨年末に WPF Toolkit で Calendar コントロールと DatePicker コントロールを (いくつかの関連クラスと共に) リリースしました (新しい DataGrid も含まれていますが、ここでは説明しません)。これらのコントロールは Silverlight の同じコントロールとの互換性が非常に高く、おそらく WPF の次期リリースに組み込まれることになるでしょう。

この記事の残りの部分では、皆さんが既に WPF Toolkit のソース コードとバイナリをダウンロードし、インストーラを実行して WPFToolkit.dll を登録していることを前提とします。この DLL を登録したら、Visual Studio の WPF プロジェクトで使用するために、プロジェクトの [参照設定] タグを右クリックし、[参照の追加] をクリックして、[.NET] タブをクリックします。WPF Toolkit DLL は、一覧の下の方にあります。

Calendar: 簡単な概要説明

Calendar クラスおよび DatePicker クラス (およびサポートしているいくつかの列挙体およびデリゲート) には、Microsoft.Windows.Controls 名前空間が存在します。一部の関連クラス、特に CalendarItem、CalendarButton、CalendarDayButton、および DatePickerTextBox は、Microsoft.Windows.Controls.Primitives 名前空間に含まれています。これらは WPF コントロールの通常の名前空間ではないため、C# コードに新しく using ディレクティブを追加し、XAML ファイル内に適切な XML 名前空間宣言を作成する必要があると思います。新しい Visual State Manager (この後説明します) に関連するクラスには、より一般的な System.Windows 名前空間が存在します。

Calendar クラスは Control から派生します。Calendar を単純にインスタンス化すると、図 1 の左側に示したビューが表示されます。単一の月のみ表示され、Windows フォームの MonthCalendar のように複数の月は表示されません。また、4 週または 5 週しかない月でも、常に 6 週表示されます。ヘッダー (月と年が表示されている一番上の部分) をクリックすると、図 1 の中央にある年のビューに切り替わります。いずれかの月をクリックすると月のビューに戻り、ヘッダーを再度クリックすると右側の 10 年のビューが表示されます。いずれかの年をクリックすると、その年のビューに戻ります。

fig01.gif

図 1 Calendar の 3 つの表示モード

これらの 3 つのビューは Calendar の DisplayMode プロパティに対応しています。そのプロパティは CalendarMode 列挙体の 3 つのメンバ (Month、Year、または Decade) のいずれかに設定されます。

ヘッダーの両側にある 2 つのボタンをクリックすると、ひと月、1 年、または 10 年を前後に移動できます。

DisplayDate プロパティは DateTime 型で、表示する月、年、または 10 年を示します。DisplayDateStart プロパティと DisplayDateEnd プロパティは null を許容する DateTime 型で、カレンダーの移動を特定の範囲に制限します。

IsTodayHighlighted プロパティは既定値が true で、今日の日付の後ろに暗い色の正方形を描画します。また、複数の日付を選択することもできます。SelectionMode プロパティには、CalendarSelectionMode 列挙体のメンバ (SingleDate (既定)、SingleRange、MultipleRange、または None) を設定します。SingleDate の場合、SelectedDate プロパティ (null を許容する DateTime 型) が選択された日付を示します。それ以外の場合は、DateTime 型の ObservableCollection から派生する、SelectedDatesCollection 型の SelectedDates プロパティが選択された日付を示します。

BlackoutDates プロパティを使用すると、さまざまな日付の範囲を選択できないように設定できます。このプロパティは CalendarBlackoutDateCollection 型で、DateTime 型の Start プロパティおよび End プロパティを定義するクラスである CalendarDateRange 型の ObservableCollection から派生します。

FirstDayOfWeek プロパティは DayOfWeek 型です。通常、既定値は Sunday ですが、一部のロケール (France など) では Monday になります。

Calendar では、DisplayModeChanged、DisplayDateChanged、SelectionModeChanged、および SelectedDatesChanged という 4 つのイベントが定義されます。

Calendar のスタイル設定

Calendar クラスの残りのパブリック プロパティには CalendarItemStyle、CalendarButtonStyle、および CalendarDayButtonStyle があり、これらはすべて Style 型です。ただし、これらのプロパティについて理解するには、Calendar コントロールを作成および構造化する方法を理解する必要があります。そうした知識は XAML を分析することで得ることができます。

Calendar コントロールのソース コードはすべて CodePlex サイトからダウンロードできますが、そのコントロールのカスタマイズに興味がある場合、最も重要となるファイルは間違いなく Toolkit-release\Calendar\Themes サブディレクトリにある Generic.xaml です。Generic.xaml ファイルには、Calendar コントロール自体と Calendar コントロールを構成する他のコントロールの既定のスタイル (テンプレートを含む) が含まれています。Calendar のテンプレートを作成する場合は、Generic.xaml を詳しく分析する必要があります。

Calendar クラスによって、コントロールのすべてのプロパティとイベントが定義され、ユーザー入力処理の一部も実行されます。しかし、Generic.xaml の Calendar テンプレートを確認すると、表示されない StackPanel コントロールと CalendarItem コントロールのみで Calendar が構成されていることがわかります。

CalendarItem クラスも Control から派生します。このクラスには、コントロールの視覚的な外観全体が含まれています。これには、一番上の 3 つのボタン (以下、まとめてナビゲーション ボタンと呼びます) と 2 つのグリッドがあります。一方のグリッドには、月の日付 (ヘッダーの曜日を含む) が表示されます。もう一方のグリッドには、12 か月または 12 年のどちらかが表示されます。一度に表示できるのは、2 つのグリッドのうちの 1 つだけです。

また、CalendarItem によって、この 2 つのグリッドの設定が行われます。月の日付を表示する場合は、CalendarDayButton 型のオブジェクトが作成されます。12 か月または 12 年の場合は、CalendarButton 型のオブジェクトが作成されます。両方のクラスは Button から派生します。

Generic.xaml には、Calendar、CalendarItem、CalendarButton、および CalendarDayButton の既定のスタイルとテンプレートが含まれています。CalendarItem、CalendarButton、および CalendarDayButton に新しいスタイル (テンプレートを含む) を設定するには、Calendar によって定義される CalendarItemStyle、CalendarButtonStyle、または CalendarDayButtonStyle の各プロパティを直接、あるいは Calendar の Style プロパティを通して設定します。

Calendar 自体の Style を設定する方法は、Calendar 固有のプロパティ (DisplayMode、SelectionMode など) や Calendar によって継承されるプロパティ (HorizontalAlignment、Margin など) を設定する場合に便利です。ここでは、より問題が発生しやすいプロパティのクラス (フォントやブラシに関連するプロパティ) に重点を置いて説明します。

Calendar コントロールは関連するコントロールに基づいて忠実に作成されます。そのため、フォントに手を加えると、途端にコントロールの視覚的なバランスが悪くなることがあります。たとえば、Calendar の FontFamily プロパティを設定すると、曜日見出し以外のすべてのテキストが影響を受けます (見出しの FontFamily は、CalendarItem の既定のテンプレート内の DateTemplate でハードコーディングされています)。月の日付のテキストを少し小さくすると、月全体が左に移動したように見えます。

Calendar の FontSize プロパティを設定しても効果はありません。実際に FontSize を変更するには、CalendarButton スタイル、CalendarDayButton スタイル、および CalendarItem テンプレート内の 2 か所で FontSize を変更する必要があります。別の方法で解決しましょう。Calendar コントロールを通常よりも少し大きくまたは小さくする場合は、FontSize を使用する代わりに、ScaleTransform を定義して、それを Calendar の LayoutTransform プロパティに設定するか、Viewbox に Calendar を配置することをお勧めします。

Calendar で現在の日付、選択された日付、および選択できない日付に印を付けるために使用される色は、さまざまなテンプレートでハードコーディングされています。簡単に変更できるブラシは、Calendar コントロールの Background プロパティと Calendar コントロールを囲む BorderBrush の 2 つだけです。両方とも Calendar の既定のスタイルで線形グラデーション ブラシに設定された後、テンプレート バインディングを利用して CalendarItem に渡されます。既定の Background ブラシを置き換える方法は後で説明しますが、置き換えると、図 1 に示したナビゲーション ボタンの背景色が影響を受けることを覚えておいてください。

新しい Calendar テンプレートの設定

コントロールのテンプレートを置き換えるには、まず、ドキュメントまたは実際のソース コード (利用可能な場合) のどちらかでクラス定義を探します。そこで TemplatePart 属性のコレクションを確認します。これらの TemplatePart 属性には、新しいテンプレートを完全に機能させるために必要な名前付きコントロールまたは要素が示されています。Calendar コントロールの場合、新しいテンプレートには少なくとも PART_CalendarItem という名前の CalendarItem と PART_Root という名前の Panel の 2 つのパーツが必要です (既定の Calendar テンプレートの PART_Root Panel は StackPanel です)。

Calendar のテンプレートを置き換えるようなことはなさそうですが、その必要性がないと断言するのはやめておきます。もしかしたら、Calendar を構成するビジュアル ツリーに、選択されたすべての日付を表示するスクロール可能な ItemsControl などを追加する必要があるかもしれませんね。

そうしたアプリケーションの例を示す SelectedDateLister プロジェクトが、このコラムのダウンロード可能なソース コードに含まれています。図 2 はそのプロジェクトの MainWindow.xaml ファイルの抜粋で、Calendar の新しいテンプレートに関する箇所を示しており、図 3 はそのビューを示しています。

図 2 Calendar の新しいテンプレート

<ControlTemplate TargetType="toolkit:Calendar">
    <StackPanel Name="PART_Root" 
                Orientation="Horizontal">
        <primitives:CalendarItem 
            Name="PART_CalendarItem" 
            Style="{TemplateBinding CalendarItemStyle}"
            Background="{TemplateBinding Background}" 
            BorderBrush="{TemplateBinding BorderBrush}" 
            BorderThickness="{TemplateBinding BorderThickness}"
            VerticalAlignment="Center" />
        <Border BorderBrush="{TemplateBinding BorderBrush}"
                BorderThickness="{TemplateBinding BorderThickness}"
                Margin="4 4 0 4">
            <ScrollViewer 
                    VerticalScrollBarVisibility="Auto"
                    Height="{Binding ElementName=PART_CalendarItem,
                                     Path=ActualHeight}"
                    Width="100">
                <ItemsControl 
                    ItemsSource=
                        "{Binding RelativeSource={RelativeSource 
                                      AncestorType=toolkit:Calendar}, 
                                  Path=SelectedDates}" />
            </ScrollViewer>
        </Border>
    </StackPanel>
</ControlTemplate>

fig03.gif

図 3 選択された日付が一覧表示される Calendar

プリミティブの XML 名前空間プレフィックスに CalendarItem コントロールの参照が必要なことに注目してください。XAML ファイルのルート要素で、そのプレフィックスは Microsoft.Windows.Controls.Primitives の .NET 名前空間と WPFToolkit アセンブリに関連付けられています。また、プレフィックス toolkit は同じアセンブリの Microsoft.Windows.Controls 名前空間に関連付けられています。Calendar は、次のように XAML で単純にインスタンス化されます。

<toolkit:Calendar />

新しいテンプレートを含むスタイルは暗黙的に適用されます。

また、図 2 で、ItemsControl の ItemsSource プロパティを Calendar の SelectedDates プロパティに接続するために、RelativeSource を指定した Binding を使用していることにも注目してください。この場合、SelectedDates は依存関係プロパティによってサポートされないため、TemplateBinding を使用できません。

TemplateBinding で設定する必要がある CalendarItem のプロパティを、私がどうやって突き止めたかわかりますか。Generic.xaml の既定の Calendar テンプレートを確認した、というのが答えです。実際は、確認しただけでなく、コピーして、自分のソース コードに貼り付けました。既存のテンプレートを変更するには、コピー アンド ペーストが一番だと思います。

CalendarItem テンプレートの置き換え

Calendar コントロールの内側の外観をより細かく制御するには、CalendarItem のテンプレートを置き換える必要があります。既定のテンプレートは相当な量の XAML で記述されています (Generic.xaml ファイルは 250 行ほどあります)。そのテンプレートに、一番上に表示されるナビゲーション ボタンの埋め込まれたテンプレートが 3 つ含まれています。

Silverlight についてまだ詳しく知らない、熟練した WPF プログラマは、3 つのボタンの埋め込まれたテンプレートに見慣れない名前のクラス (VisualStateManager、VisualStateGroup、および VisualState) への参照を目にして驚かれることでしょう。これらのクラスは、Visual State Manager の主要なコンポーネントです。Visual State Manager は、Silverlight に既に導入されており、次のメジャー リリースで WPF に組み込まれるものと思われます (WPF Toolkit に、暫定版の Visual State Manager のソース コードが含まれています)。

Visual State Manager は、現在テンプレートで使用されているトリガの多くを置き換えることを目的としています。通常、テンプレートのトリガは IsEnabled、IsMouseOver、IsSelected などの特定のプロパティの値に基づいています。一方、Visual State Manager は、Disabled、MouseOver、Selected などの名前を使用する、より構造化された表示状態の概念に基づいています。一見するとこれらの表示状態はトリガと 1 対 1 で対応しているようですが、必ずしもそうではありません。実際に使用する場合、トリガの結果の視覚的外観は表示される順番によって異なる場合があるため、トリガを相互に連携させる必要があるときに、複数のトリガをうまく定義できないことがよくあります。別の方法で表示状態を識別することで、プログラマの意図をより明確にします。プロパティの設定と表示状態の関連付けはコード内で行われます。

XAML で Visual State Manager のクラスを参照するには、WPFToolkit アセンブリの System.Windows 名前空間に関連付けられた XML プレフィックス (vsm など) の定義を追加する必要があります。

CalendarItem のカスタム テンプレートには、図 4 に示す 8 つの名前付きパーツが必要です。

図 4 CalendarItem テンプレートの名前付きパーツ
パーツ
PART_Root FrameworkElement
PART_HeaderButton Button
PART_PreviousButton Button
PART_NextButton Button
DayTitleTemplate DataTemplate のキー名
PART_MonthView Grid
PART_YearView Grid
PART_DisabledVisual FrameworkElement

これらの名前の 1 つが実際にリソースのキー名であることに注目してください。CalendarItem テンプレートの Resources セクションに、次のように Text プロパティを設定した TextBlock 要素を含む DataTemplate を "DayTitleTemplate" というキー名で含める必要があります。

Text="{Binding}"

この DateTemplate を使用して曜日見出しを表示します。これらのヘッダーに使用される CalendarDayButton アイテムでは、Content に null が設定される一方で、DataContext にテキスト ヘッダー (Su、Mo、Tu など) が設定されるという奇妙なバインディングが行われます (CalendarDayButton の DataContext プロパティについては後で詳しく説明します)。

CalendarItem テンプレートの既定のビジュアル ツリーは、入れ子になったいくつかのグリッドで構成されています。外側の単一セルの Grid は PART_Root という名前です。この Grid には、Border と、コントロールが無効になった場合に半透明の覆いをかけるための PART_DisabledVisual という名前の Rectangle が含まれています。

Border には、コントロールの主要な構造体となる 3 つの列と 2 つの行から成る別の Grid が含まれています。上の行には 3 つの名前付きのナビゲーション ボタンが、下の行には 3 列にまたがる 2 つのグリッド (PART_MonthView および PART_YearView) が含まれています。

PART_MonthView という名前の Grid には、7 つの列 (7 つの曜日) と 7 つの行 (曜日見出しに 1 行と月の日付に 6 行) があり、PART_YearView には、4 つの列と 3 つの行 (1 年の 12 か月または "10 年" ビューのときの 12 年) があります。CalenderItem のコード部分でこれらのグリッドを設定する子 CalendarDayButton および子 CalendarButton が作成されると、Grid.Row 添付プロパティおよび Grid.Column 添付プロパティがそれぞれの子に明示的に設定されます。

fig05.gif

図 5 必要最低限の新しい CalendarItem テンプレート

そのため、カスタムの CalendarItem テンプレートが既存のレイアウトと大きく異なることはほとんどありません。試しにカスタマイズする場合は、BareBonesCalendarItem プロジェクトの MainWindow.xaml ファイルに、カスタム テンプレートを完成させ、機能させるために最低限必要なマークアップがほとんど用意されています。図 5 に完成したビューを示します。少し変えるために、構造全体を DockPanel から作成して、前後の月に移動するためのボタンを両側に配置しました。これらのナビゲーション ボタンにテンプレートを適用しなかったため、実際のボタンのような外観になっています。これだけ簡単なものでも、テンプレートは 80 行ほどの長さがあります。少し奇妙に見えますが、うまく機能します。

新しい CalendarItem テンプレートをより安全に、より合理的に定義するには、Generic.xaml から既定のテンプレートを自分の XAML ファイルにコピーし、特定の部分だけを変更します。

これは、ColorfulCalendarItem プロジェクトで行った手法です。MainWindow.xaml ファイルでは、Generic.xaml の名前空間宣言と一致するように、Calendar クラスを参照するためのローカル (ツールキットではなく) の XML 名前空間プレフィックスを定義しています。

CalendarItem テンプレートでは、次のように外側の Border に対して新しい Background ブラシを追加しました。

<LinearGradientBrush StartPoint="0 0"   EndPoint="0 1">
    <GradientStop Offset="0"   Color="#FFFFC0" />
    <GradientStop Offset="0.5" Color="#FFE0B0" />
    <GradientStop Offset="1"   Color="#FFD0A8" />
</LinearGradientBrush>

また、次のように、ヘッダー中央のボタンに対して ContentPresenter を囲む新しい Border を追加し、こちらにも背景を設定しました。

<Border Padding="12 0"
        CornerRadius="6">
    <Border.Background>
        <LinearGradientBrush StartPoint="0 0" EndPoint="0 1">
            <GradientStop Offset="0" Color="#FFC4A0" />
            <GradientStop Offset="1" Color="#FF9450" />
        </LinearGradientBrush>
    </Border.Background>

その結果を図 6 に示します。

fig06.gif

図 6 CalendarItem テンプレートで定義した新しい色

強力な CalendarDayButton テンプレート

CalendarItem テンプレートには、カレンダーの最上部に表示される 3 つのナビゲーション ボタンの埋め込まれたテンプレートが含まれていますが、下部のグリッドを設定する CalendarButton オブジェクトと CalendarDayButton オブジェクトへの参照は含まれていません。CalendarItem.cs ファイルを分析するとわかりますが、これらのボタンはコード内でインスタンス化されます。ただし、Calendar の CalendarButtonStyle プロパティおよび CalendarDayButtonStyle プロパティに Style オブジェクトを設定することで、これらのボタンの新しいテンプレートを定義できます。

CalendarDayButton のカスタム テンプレートは非常に大きな可能性を秘めています。このカスタム テンプレートを使用すると、1 ~ 31 の通常の 2 桁の数字以外に各日付の追加情報を表示できます。しかし、CalendarDayButton のコンテンツに追加するものはすべてそのボタンによって表される具体的な日付に依存する必要があり、一見、スマートな方法では実現できないように見えます。そうした情報を簡単には入手できないように思えるからです。

心配しないでください。CalendarDayButton の Content には 24 などの単純なテキスト文字列が設定されますが、各 CalendarDayButton の DataContext プロパティには、正確な年月を含むその日付の実際の DateTime オブジェクトが設定されます (また、この機能は曜日見出しに使用される CalendarDayButton オブジェクトの識別に役立ちます。これらのボタンの Content は null に設定されていますが、DataContext には Su、Mo などのテキスト文字列が設定されます。そのため、これらの見出しの CalendarItem には DataTemplate が必要です)。

DateTime オブジェクトを使用できると、新しい CalendarDayButton テンプレートの何らかのカスタム クラスを参照することで、さまざまな機能拡張を簡単に実装できるようになります。既定の CalendarDayButton テンプレートは 120 行ほどの XAML で記述されているため、そうしたテンプレートをゼロから記述することはないでしょう。CalendarItem と同様に、Generic.xaml から既定の CalendarDayButton テンプレートを自分のプロジェクトにコピーしてから、それを変更することになると思います。

たとえば、RedLetterDays プロジェクトで示すように、各日付の上にマウス ポインタを移動したときに表示されるヒントに、休日やその他の重要な日を表示することが考えられます。この機能をバインディング コンバータとして実装するのは理にかなっているように思えます。実質的に、コンバータによって DateTime オブジェクトが String オブジェクトに変換されるからです。

図 7 は、このバインディング コンバータの重要な部分を示しています。コンストラクタによって、3 月 (この記事の執筆時) から 6 月 (この記事の公開時) の日付のアイテムが設定されます。実際のプログラムでは、ファイルからこれらの日付やテキスト文字列にアクセスすることになると思います。

図 7 RedLetterDays のバインディング コンバータの大部分

public class RedLetterDayConverter : IValueConverter {
    static Dictionary<DateTime, string> dict = 
        new Dictionary<DateTime, string>();

    static RedLetterDayConverter() {
        dict.Add(new DateTime(2009, 3, 17), "St. Patrick's Day");
        dict.Add(new DateTime(2009, 3, 20), "First day of spring");
        dict.Add(new DateTime(2009, 4, 1), "April Fools");
        dict.Add(new DateTime(2009, 4, 22), "Earth Day");
        dict.Add(new DateTime(2009, 5, 1), "May Day");
        dict.Add(new DateTime(2009, 5, 10), "Mother's Day");
        dict.Add(new DateTime(2009, 6, 21), "First Day of Summer");
    }

    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture) {

        string text;
        if (!dict.TryGetValue((DateTime)value, out text))
            text = null;
        return text;
    }

    ...

}

MainWindow.xaml ファイルに含まれている CalendarDayButton のテンプレートには、コードをいくつか追加しています。まず、バインディング コンバータがリソースとして格納されます。

<ControlTemplate.Resources>
    <src:RedLetterDayConverter x:Key="conv" />
</ControlTemplate.Resources>

次に、ヒントを表示するために、CalendarDayButton テンプレートの外側の Grid でそのバインディング コンバータを参照します。

<Grid ToolTip="{Binding Converter={StaticResource conv}, 
Mode=OneWay}">

そのバインディングに Path 設定が抜けているように見える場合は、ボタンの DataContext は DateTime オブジェクトであり、DataContext はビジュアル ツリーを通じて継承されることを覚えておいてください。ヒントに DateTime を表示する場合は、単純に次のように使用します。

<Grid ToolTip="{Binding}">

また、テンプレートには、現在の日付および選択された日付を強調表示するために使用される数あるオブジェクトの中から Rectangle オブジェクトが追加されています。

<Rectangle x:Name="RedLetterDayBackground" 
    IsHitTestVisible="False" 
    Fill="#80FF0000" />

バインディング コンバータから null が返されると、DataTrigger によって、この Rectangle は非表示になります。

<DataTrigger 
    Binding="{Binding 
        Converter={StaticResource conv}}"
    Value="{x:Null}">
    <Setter TargetName="RedLetterDayBackground" 
            Property="Visibility" 
            Value="Hidden" />
</DataTrigger>

図 8 は、3 月の 3 日間の休日を示しています。

fig08.gif

図 8 RedLetterDays の表示

一般的に、CalendarDayButton テンプレートのビジュアル ツリーには、DateTime 型の依存関係プロパティを持つ FrameworkElement の派生クラスや、DataContext が DateTime 型であると仮定される FrameworkElement の派生クラスも含めることができます。

このコラムの読者の中には、2008 年 1 月号の記事で作成した、月の満ち欠けを利用したカレンダーを思い出す人もいるかもしれません。元の MoonDisk クラスは、太陽によって照らされる月のようすをシミュレーションするために、Viewport3D から派生させました。また、そのクラスから DateTime 依存関係プロパティを削除し、その DataContext を DateTime にキャストするように多少変更しました。MoonPhaseCalendar プロジェクトには、その MoonDisk クラスが含まれており、次のように CalendarDayButton テンプレートのビジュアル ツリー内でそのクラスを参照しています。

<Grid Width="48" Height="48">
    <ContentPresenter 
        x:Name="NormalText"
        HorizontalAlignment="Left"
        VerticalAlignment="Top">
        <TextElement.Foreground>
            <SolidColorBrush x:Name="selectedText" 
                Color="#FF333333"/>
        </TextElement.Foreground>
    </ContentPresenter>
    <src:MoonDisk Margin="6" />
</Grid>

MoonDisk では DateTime 型の DataContext が継承されるため、バインディングがないことに注目してください。2009 年 7 月の結果を図 9 に示します。

fig09.gif

図 9 MoonPhaseCalendar の表示

Calendar からの派生

CalendarItem、CalendarButton、および CalendarDayButton はすべてシール クラスです。そのため、それらから派生させることはできません。CalendarButton および CalendarDayButton から派生させることができるとしても、古いボタンではなく新しいボタンをインスタンス化するように CalendarItem の一部を書き換える必要があります。

ただし、機能を追加するために、Calendar 自体から派生させることはできます。たとえば、特定の日をクリックしたときに、図 10 に示すようなモードレス ダイアログが表示され、その日の予定を入力および確認できるように、Calendar コントロールを拡張できます。そのために、DailyReminders プロジェクトを用意しています。

fig10.gif

図 10 DailyReminders ダイアログ ボックス

この機能を実装するには、Calendar から派生するクラス (DailyRemindersCalendar と呼んでいます) で、ユーザーが CalendarDayButton をクリックしたことを検出できる必要があります。通常、これは非常に簡単にできるでしょう。Calendar から派生するクラスのコンストラクタで、Mouse.ClickEvent とこれらのイベントを処理するメソッドを引数として指定して AddHandler を呼び出します。ご存知のとおり、WPF によってルーティング イベントが実装されるため、子ボタンからの Click イベントはビジュアル ツリーを上方向へ移動します。ハンドラは、RoutedEvent オブジェクトの引数 Source を確認し、さらに DataContext が DateTime 型のオブジェクトであるかどうかを確認することでさまざまな子ボタンを識別します。

しかし、これは不可能であることがわかりました。CalendarItem クラスによって、CalendarDayButton のボタンアップ イベントとボタンダウン イベントのハンドラがインストールされ、Click イベントが無効になるからです。

そこで、OnMouseDoubleClick メソッドをオーバーライドするように DailyRemindersCalendar を記述しました。しかし、このことが別の問題を引き起こしました。Calendar の派生クラスのそのイベントでは、MouseButtonEventArgs の Source プロパティと OriginalSource プロパティはどちらも Button 型ではないのです。Source は null に、OriginalSource は TextBlock または Path か Rectangle のいずれかになります。

しかし、ここでも、ビジュアル ツリーによる DataContext の継承が役に立ちます。OnMouseDoubleClick のオーバーライドでは、OriginalSource オブジェクトに DateTime 型の DataContext がある場合、メソッドには CalendarDayButton がクリックされていることがわかり、その DateTime オブジェクトによって、どれがクリックされたかがメソッドに正確に伝えられます。その後、メソッドで DailyRemindersDialog をインスタンス化することで、コード内でユーザー インターフェイス全体 (TextBlock コントロールおよび TextBox コントロールを含む Grid) を作成できます (実際、DailyReminders プログラム全体は、全体的にマークアップのないコードで構成されています)。

DailyRemindersDialog はモードレスで呼び出されるため、別の日のダイアログを同時に表示できます。ただし、DailyRemindersDialog では、同じ日のダイアログを複数表示することはできません。複数表示すると、日々の予定を適切に保存および取得する際に問題が発生します。DailyRemindersStorage クラスでは、プログラムのこの部分を処理するために、分離ストレージの XML ファイルを参照します。DailyRemindersDialog を閉じると、更新された情報がそのファイルに保存されます。

DatePicker コントロール

DatePicker は主に DatePickerTextBox と Calendar コントロールを呼び出すドロップダウン リストで構成されているため、DatePicker ではなく新しい Calendar コントロールについてのみ説明してきました。

この DatePickerTextBox はまったく洗練されていません。DatePickerTextBox には、Windows フォームの DateTimePicker コントロールで筆者が非常に気に入っている、日付や時刻の個々のコンポーネント (年、月、日、時間など) をクリックしたり、カーソル キーや数字キーを使用して値を変更したりする機能が実装されていません。

しかし、スタンドアロンの Calendar コントロールに適用できるスタイルとテンプレートはすべて、DatePicker のドロップダウン リストから呼び出される Calendar コントロールにも適用できます。DatePicker コントロールには、CalendarStyle という名前の Style 型のプロパティがあります。このプロパティに設定する Style オブジェクトには、Calendar によって定義されるプロパティ (CalendarItemStyle、CalendarButtonStyle、CalendarDayButtonStyle など) のセッターを含めることができます。

埋め込まれた一連のスタイルとテンプレートを通じて、Calendar コントロールと DatePicker の両方に実質的にアクセスし、ニーズや好みに合わせてカスタマイズできます。

ご意見やご質問は mmnet30@microsoft.com まで英語でお送りください。

Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine』(Wiley、2008 年) があります。彼の Web サイトは www.charlespetzold.com です。