【WPF】DatePickerを年月表示にする【C#】

【WPF】DatePickerを年月表示にする【C#】

はじめに

今回は DatePicker の表示を年月のみの表示にカスタマイズする方法について解説します。

下記はカスタマイズ後のイメージです。

dependency-property-calendar2.gif

依存関係プロパティ(DependencyProperty) でコントロールをカスタマイズ

DatePicker コントロールを年月表示にするには、日付の部分を除外するカスタマイズが必要になります。

それを実現するためにDependencyPropertyを使用して既存のコントロールの挙動を変更します。

依存関係プロパティ(DependencyProperty) とは?

DependencyPropertyとは以下のような役割を持った特殊なプロパティです。

記事によっては「データバインディングのターゲットに指定できる」とだけ解説されているものもありますが、WPF の画面要素であるコントロールを柔軟に実装することが出来る仕組みです。

まずは簡単なカスタムコントロールを作成して依存関係プロパティの使い方を確認して行きます。

Button クリック時にメッセージボックスを表示させる

既存の Button 要素のクリック時にメッセージボックスを表示させる振る舞いを持たせたカスタムコントロールを実装をしていきます。

CustomControlというフォルダを作成後、CustomButton.csというファイルを作成し以下のように記述します。

"CustomControl/CustomButton.cs"
using System.Windows;
using System.Windows.Controls;

namespace MVVM.DatePickerYearMonth.CustomControl
{
    public sealed class CustomButton : Button // プロパティを追加するコントロールを継承
    {
        /// <summary>
        /// 依存関係プロパティの定義
        /// コードビハインドからアクセスする場合はこの名前を使用する
        /// </summary>
        public static readonly DependencyProperty IsShowMessageProperty =
            DependencyProperty.Register(
                // 登録するプロパティの名前
                "IsShowMessageDP",
                // プロパティに指定する値の型
                typeof(bool),
                // この依存関係プロパティの所有者
                typeof(CustomButton),
                // プロパティのメタデータ
                // 初期値とプロパティの値が変更された際に実行される関数を指定(コールバック関数はnullでも可)
                new PropertyMetadata(false, OnIsShowMessageChanged));

        /// <summary>
        /// XAML側からアクセスするためのプロパティ
        /// </summary>
        public bool IsShowMessageDP
        {
            get => (bool)GetValue(IsShowMessageProperty);
            set => SetValue(IsShowMessageProperty, value);
        }

        /// <summary>
        /// 値変更時に実行される振る舞い
        /// </summary>
        /// <param name="d"></param>
        /// <param name="e"></param>
        private static void OnIsShowMessageChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            if (o is Button button)
            {
                // プロパティの値がtrueだった場合はクリックイベントに処理を登録
                if ((bool)e.NewValue)
                {
                    button.Click += OnButtonClick;
                }
                else
                {
                    button.Click -= OnButtonClick;
                }
            }
        }

        /// <summary>
        /// ボタンクリック時に実行されるメソッド
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private static void OnButtonClick(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Button clicked!");
        }
    }
}

IsShowMessageDPは XAML 側から値を指定する際に必要なプロパティになります。

こうすることで XAML 側から以下のように値を指定することが出来ます。

"MainWindow.xaml"
<Window x:Class="MVVM.DatePickerYearMonth.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:local="clr-namespace:MVVM.DatePickerYearMonth.CustomControl"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="450"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>

        <!--  カスタムコントロールを読み込み  -->
        <local:CustomButton Width="120"
                            Height="30"
                            Content="ボタン"
                            ★↓CLRラッパー
                            IsShowMessageDP="True" />
    </Grid>
</Window>

コードビハインドから値を指定する場合はIsShowMessagePropertyを指定します。このプロパティは XAML 側から値の設定は出来ません。

"MainWindow.xaml.cs"
using System.Windows;

namespace MVVM.DatePickerYearMonth
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            // コントロールをインスタンス化
            var b = new CustomControl.CustomButton();
            // メッセージボックスを表示するフラグを指定
            b.IsShowMessageDP = true;

        }
    }
}

実行すると以下のような、クリック時に常にメッセージボックスを表示させるカスタム Button コントロールが作成されます。

dependency-property-button.gif

添付プロパティでの実装

依存関係プロパティでは、コントロールを継承して直接定義していました。

添付プロパティではDependencyObjectが継承されたクラスに対して任意のプロパティを文字通り添付する仕組みです。

ほとんど依存関係プロパティのときと同じ実装ですが、添付プロパティを登録するにはRegisterAttachedメソッドを使用するなど若干異なる点があります。

"AttachedProperty/ButtonExtends.cs"
using System.Windows.Controls;
using System.Windows;

namespace MVVM.DatePickerYearMonth.AttachedProperty
{
    public class ButtonExtends
    {

        public static readonly DependencyProperty IsShowMessagePropertyAT =
            // 添付プロパティを登録するにはRegisterAttachedメソッドを使用する
            DependencyProperty.RegisterAttached(
                // 登録するプロパティの名前
                "IsShowMessageAT",
                typeof(bool),
                typeof(ButtonExtends),
                new PropertyMetadata(false, OnIsShowMessageChanged));

        /// <summary>
        /// ゲッターには登録したプロパティ名にGetを命名する必要がある
        /// </summary>
        /// <param name="o"></param>
        /// <returns></returns>
        public static bool GetIsShowMessageAT(DependencyObject o)
            => (bool)o.GetValue(IsShowMessagePropertyAT);

        /// <summary>
        /// ゲッターと同様の命名規則
        /// </summary>
        /// <param name="o"></param>
        /// <param name="value"></param>
        public static void SetIsShowMessageAT(DependencyObject o, bool value)
           => o.SetValue(IsShowMessagePropertyAT, value);

        private static void OnIsShowMessageChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            if (o is Button button)
            {

                if ((bool)e.NewValue)
                {
                    button.Click += OnButtonClick;
                }
                else
                {
                    button.Click -= OnButtonClick;
                }
            }
        }

        private static void OnButtonClick(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("AttachedProperty!");
        }
    }
}

注意点として、ゲッターとセッターには登録したプロパティ名IsShowMessageATSetGetを指定する必要があります。

XAML 側からは以下のようにアクセスします。

"MainWindow.xaml"
<Window x:Class="MVVM.DatePickerYearMonth.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        ★↓名前空間を追加
        xmlns:attachd="clr-namespace:MVVM.DatePickerYearMonth.AttachedProperty"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:MVVM.DatePickerYearMonth.CustomControl"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        Title="MainWindow"
        Width="450"
        Height="450"
        WindowStartupLocation="CenterScreen"
        mc:Ignorable="d">
    <Grid>
        <StackPanel VerticalAlignment="Center">

            <StackPanel>

                <StackPanel HorizontalAlignment="Center" Orientation="Horizontal">

                    <StackPanel>
                        <Label Width="120" Content="依存関係プロパティ" />

                        <!--  カスタムコントロールを読み込み  -->
                        <local:CustomButton Width="120"
                                            Height="30"
                                            Content="ボタン"
                                            IsShowMessageDP="True" />

                    </StackPanel>

                    <StackPanel Margin="42,0,0,0">
                        <Label Width="120" Content="添付プロパティ" />

                        <!--  添付プロパティを付与する  -->
+                       <Button Width="120"
+                               Height="30"
+                               attachd:ButtonExtends.IsShowMessageAT="True"
+                               Content="ボタン" />

                    </StackPanel>
                </StackPanel>

            </StackPanel>
        </StackPanel>
    </Grid>
</Window>

ビルド後は以下のような動作になります。

dependency-property-button2.gif

カレンダーの表示モードを制御する

ここまで簡単な例で依存関係プロパティと添付プロパティによるコントロールのカスタマイズについて見てきました。

次に、本題の DatePicker をカスタマイズするための実装を見ていきます。今回は添付プロパティで実装したいと思います。

既存の DatePicker をカスタマイズするために以下の 4 つのクラスを作成します。

  • DatePicker コントロールを拡張して、年と月のみを表示させる機能を提供するクラス(DatePickerCalendarBehavior.cs
  • DatePicker コントロールの日付表示形式をカスタマイズするためのクラス(DatePickerCalendarBehavior.cs
  • ビジュアルツリーにアクセスするためのクラス(FrameworkElementExtensions.cs
  • 日付フォーマットを変換するためのコンバータークラス(DatePickerDateTimeConverter.cs
"AttachedProperty/DatePickerCalendarBehavior.cs"
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Threading;

namespace MVVM.DatePickerYearMonth.AttachedProperty
{
    public class DatePickerCalendarBehavior
    {
        public static readonly DependencyProperty IsMonthYearProperty =
            DependencyProperty.RegisterAttached(
                   "IsMonthYear",
                    typeof(bool),
                    typeof(DatePickerCalendarBehavior),
                    new PropertyMetadata(OnIsMonthYearChanged));

        public static bool GetIsMonthYear(DependencyObject o)
            => (bool)o.GetValue(IsMonthYearProperty);

        public static void SetIsMonthYear(DependencyObject o, bool value)
            => o.SetValue(IsMonthYearProperty, value);

        private static void OnIsMonthYearChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            // イベントが発生したオブジェクトをDatePickerにキャスト
            var datePicker = (DatePicker)o;

            // メインスレッドで実行するための処理
            Application.Current.Dispatcher
                // BeginInvokeは非同期で実行される
                .BeginInvoke(
                    // // アプリケーションがロードされ、レンダリングが完了したあとに実行する優先度
                    DispatcherPriority.Loaded,
                    // 実行されるActionデリゲートを定義
                    new Action<DatePicker, DependencyPropertyChangedEventArgs>(SetCalendarEventHandlers), datePicker, e);
        }

        private static void SetCalendarEventHandlers(DatePicker datePicker, DependencyPropertyChangedEventArgs e)
        {
            // プロパティの値に変更があったか確認
            if (e.NewValue == e.OldValue) return;

            if ((bool)e.NewValue)
            {
                datePicker.CalendarOpened += DatePickerOnCalendarOpened;
                datePicker.CalendarClosed += DatePickerOnCalendarClosed;
            }
            else
            {
                datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
                datePicker.CalendarClosed -= DatePickerOnCalendarClosed;
            }
        }

        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs routedEventArgs)
        {
            var calendar = GetDatePickerCalendar(sender);
            // カレンダーが開かれた時に年月を選択するビューを表示する(1年分のカレンダー)
            // デフォルトのモードはMonth
            calendar.DisplayMode = CalendarMode.Year;

            // カレンダーの表示モードが変更された時に実行されるイベントハンドラに処理を登録
            calendar.DisplayModeChanged += CalendarOnDisplayModeChanged;
        }

        private static void DatePickerOnCalendarClosed(object sender, RoutedEventArgs routedEventArgs)
        {
            var datePicker = (DatePicker)sender;
            var calendar = GetDatePickerCalendar(sender);
            // カレンダーの選択されれた値を登録
            datePicker.SelectedDate = calendar.SelectedDate;

            // 表示モードの変更が不要になるので、カレンダーが閉じられた時にイベントハンドラの購読を解除
            calendar.DisplayModeChanged -= CalendarOnDisplayModeChanged;
        }

        private static void CalendarOnDisplayModeChanged(object sender, CalendarModeChangedEventArgs e)
        {
            var calendar = (Calendar)sender;

            // カレンダーの表示モードが月表示モードであるか判定
            if (calendar.DisplayMode != CalendarMode.Month) return;

            // 選択された日付を取得する(選択した月の1日が渡ってくる)
            calendar.SelectedDate = GetSelectedCalendarDate(calendar.DisplayDate);

            // 選択されたCalendarから親要素のDatePickerを検索する
            // 選択した日付を DatePicker コントロールに反映させる必要がある
            var datePicker = GetCalendarsDatePicker(calendar);

            // 該当のDatePickerを閉じる
            datePicker.IsDropDownOpen = false;
        }

        private static Calendar GetDatePickerCalendar(object sender)
        {
            var datePicker = (DatePicker)sender;
            // DatePickerコントロールのテンプレート内で定義されているポップアップ部分を取得
            var popup = (Popup)datePicker.Template.FindName("PART_Popup", datePicker);
            // ポップアップのカレンダー部分を取得
            return ((Calendar)popup.Child);
        }

        private static DatePicker GetCalendarsDatePicker(FrameworkElement child)
        {
            var parent = (FrameworkElement)child.Parent;
            // ブレークポイント
            if (parent.Name == "PART_Root")
            {
                return (DatePicker)parent.TemplatedParent;
            }

            return GetCalendarsDatePicker(parent);
        }

        private static DateTime? GetSelectedCalendarDate(DateTime? selectedDate)
        {
            if (!selectedDate.HasValue) return null;

            // 日付から年月を取得し、その月の1日を返す
            return new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, 1);
        }
    }
}

ここでは、DatePicker要素の子要素であるCalendarが開かれた際に発火するイベントハンドラに処理を登録・解除することで年月表示モードに変更したり、選択された値を取得する処理を記述しています。

ポイントとしてはDatePickerコントロールのテンプレート内から"PART_Popup"という名前の Popup コントロールを取得しているところです。

"PART_Popup"DatePickerコントロールのテンプレート内で定義されているポップアップ部分の名前です。

コントロールテンプレートはコントロールのビジュアル構造を定義するもので、コントロールの見た目と挙動をカスタマイズするために使用されます。

"PART_Root"DatePickerコントロールのテンプレート内の最上位要素を指します。

この見た目を構成しているコントロールテンプレートを検索することで、選択されたカレンダーの値やポップアップを閉じるべきDatePicker要素を特定しています。

日付の表示モードを制御する

次に、日付を選択したときの表示形式をカスタマイズする添付プロパティを実装します。

実装は先程と似たような形になっていますが、コンバーターを使用してバインディングされた値のフォーマットを変更する処理を加えています。

"AttachedProperty/DatePickerCalendarBehavior.cs"
using MVVM.DatePickerYearMonth.Converter;
using MVVM.DatePickerYearMonth.DatePickerExtensions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;

namespace MVVM.DatePickerYearMonth.AttachedProperty
{
    public class DatePickerDateFormatBehavior
    {

        public static readonly DependencyProperty DateFormatProperty =
            DependencyProperty.RegisterAttached(
                    "DateFormat",
                    typeof(string),
                    typeof(DatePickerDateFormatBehavior),
                    new PropertyMetadata(OnDateFormatChanged));

        public static string GetDateFormat(DependencyObject o)
        {
            return (string)o.GetValue(DateFormatProperty);
        }

        public static void SetDateFormat(DependencyObject o, string value)
        {
            o.SetValue(DateFormatProperty, value);
        }

        private static void OnDateFormatChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            // イベントが発生したオブジェクトをDatePickerにキャスト
            var datePicker = (DatePicker)o;

            // メインスレッドで実行するための処理
            Application.Current.Dispatcher
                // BeginInvokeは非同期で実行される
                .BeginInvoke(
                    // アプリケーションがロードされ、レンダリングが完了したあとに実行する優先度
                    DispatcherPriority.Loaded,
                    // 実行されるActionデリゲートを定義
                    new Action<DatePicker>(ApplyDateFormat), datePicker);
        }

        private static void ApplyDateFormat(DatePicker datePicker)
        {
            // DatePickerコントロールのSelectedDateプロパティに対するバインディングを設定する
            var binding = new Binding("SelectedDate")
            {
                // バインディングのソースとしてDatePickerコントロールを指定する
                RelativeSource = new RelativeSource { AncestorType = typeof(DatePicker) },
                // 日付のフォーマットをカスタマイズする独自のコンバーターを指定
                Converter = new DatePickerDateTimeConverter(),
                // コンバーターに渡すパラメータを指定
                ConverterParameter = new Tuple<DatePicker, string>(datePicker, GetDateFormat(datePicker))
            };

            // DatePickerのコントロールテンプレートからテキストボックスを取得する
            var textBox = GetTemplateTextBox(datePicker);
            // テキストボックスのプロパティに生成した Binding("SelectedDate") を指定
            textBox.SetBinding(TextBox.TextProperty, binding);

            // Textbox選択時、エンターキーで発生するイベントに対しても処理を追加
            textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
            textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;

            // DatePickerのボタン要素を検索
            var dropDownButton = GetTemplateButton(datePicker);

            datePicker.CalendarOpened -= DatePickerOnCalendarOpened;
            datePicker.CalendarOpened += DatePickerOnCalendarOpened;

            // テキストボックスがちらつく問題を防ぐために、ドロップダウンボタンのPreviewMouseUpを処理する。
            dropDownButton.PreviewMouseUp -= DropDownButtonPreviewMouseUp;
            dropDownButton.PreviewMouseUp += DropDownButtonPreviewMouseUp;
        }

        private static ButtonBase GetTemplateButton(DatePicker datePicker)
        {
            // DatePickerのコントロールテンプレートから PART_Button という名前の要素を検索
            // PART_ButtonはCalendarの開閉をするボタン
            return (ButtonBase)datePicker.Template.FindName("PART_Button", datePicker);
        }

        private static TextBox GetTemplateTextBox(Control control)
        {
            // DatePickerのコントロールテンプレートから PART_TextBox という名前の要素を検索
            return (TextBox)control?.Template?.FindName("PART_TextBox", control);
        }

        private static void DropDownButtonPreviewMouseUp(object sender, MouseButtonEventArgs e)
        {
            // パターンマッチングを使用して型チェックと変数への代入を行う
            if (!(sender is FrameworkElement fe)) return;
            // 親要素を遡ってDatePickerを検索
            var datePicker = fe.TryFindParent<DatePicker>();

            if (datePicker == null || datePicker.SelectedDate == null) return;

            // カレンダーを開閉するボタンを取得
            var dropDownButton = GetTemplateButton(datePicker);

            // イベントの発生がドロップダウンボタンであり、ドロップダウンカレンダーが閉じていた場合
            // WPFではイベントが伝播し、同じイベントに対して複数のオブジェクトが反応することがある
            if (e.OriginalSource == dropDownButton && datePicker.IsDropDownOpen == false)
            {
                // カレンダーを表示する
                datePicker.SetCurrentValue(DatePicker.IsDropDownOpenProperty, true);

                // 日付を表示する
                datePicker.SetCurrentValue(DatePicker.DisplayDateProperty, datePicker.SelectedDate.Value);

                // ドロップダウンボタンがマウスのキャプチャ(マウスイベントを独占的に受け取る状態)を解放する
                dropDownButton.ReleaseMouseCapture();

                // イベントの終了処理
                // DatePickerの標準的なイベント処理が実行されるのを防ぐ
                e.Handled = true;
            }
        }

        private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key != Key.Return)
                return;

            e.Handled = true;

            var textBox = (TextBox)sender;
            var datePicker = (DatePicker)textBox.TemplatedParent;
            var dateStr = textBox.Text;
            var formatStr = GetDateFormat(datePicker);
            datePicker.SelectedDate = DatePickerDateTimeConverter.StringToDateTime(datePicker, formatStr, dateStr);
        }

        private static void DatePickerOnCalendarOpened(object sender, RoutedEventArgs e)
        {
            var datePicker = (DatePicker)sender;
            var textBox = GetTemplateTextBox(datePicker);
            var formatStr = GetDateFormat(datePicker);
            textBox.Text = DatePickerDateTimeConverter.DateTimeToString(formatStr, datePicker.SelectedDate);
        }
    }
}

次にコンバーターの処理を見ていきます。

日付のフォーマットを制御するコンバーター

バインドの間にデータを変換するには、IValueConverterインターフェイスを実装するクラスを作成します。これには、ConvertメソッドとConvertBackメソッドが含まれています。

Convertメソッドはバインドされたデータ(バインドソース)を XAML 側で使用できるデータ型(バインドターゲット)への変換行い、ConvertBackメソッドはその逆の処理を記述します。

"Converter/DatePickerDateTimeConverter.cs"
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;

namespace MVVM.DatePickerYearMonth.Converter
{
    /// <summary>
    /// 日付フォーマットを変換するためのコンバーター
    /// </summary>
    public sealed class DatePickerDateTimeConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var formatStr = ((Tuple<DatePicker, string>)parameter).Item2;
            var selectedDate = (DateTime?)value;
            return DateTimeToString(formatStr, selectedDate);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var tupleParam = ((Tuple<DatePicker, string>)parameter);
            var dateStr = (string)value;
            return StringToDateTime(tupleParam.Item1, tupleParam.Item2, dateStr);
        }

        public static string DateTimeToString(string formatStr, DateTime? selectedDate)
        {
            return selectedDate.HasValue ? selectedDate.Value.ToString(formatStr) : null;
        }

        public static DateTime? StringToDateTime(DatePicker datePicker, string dateFormat, string dateString)
        {
            DateTime parsedDate;
            // 指定されたフォーマットで日付文字列を解析する
            bool isParsed = DateTime.TryParseExact(dateString,
                                                   dateFormat,
                                                   CultureInfo.CurrentCulture,
                                                   DateTimeStyles.None,
                                                   out parsedDate);

            if (!isParsed)
            {
                isParsed = DateTime.TryParse(dateString, CultureInfo.CurrentCulture, DateTimeStyles.None, out parsedDate);
            }

            // 解析に成功した場合は解析された日付を、失敗した場合はDatePickerの選択された日付を返す
            return isParsed ? parsedDate : datePicker.SelectedDate;
        }
    }
}

Convertメソッドでは、DatePickerコントロールで選択されたDateTimeという値を、添付プロパティで指定されたyyyy年MMMというフォーマットで変換する処理を行っています。

object parameterは以下のようなDatePickerオブジェクトと指定したフォーマットyyyy年MMMが渡ってくるので、Tupleオブジェクトでyyyy年MMMの部分飲み取り出しています。

converter-parameter.png

object valueにはカレンダーを選択したときの値が渡ってきます。

ConvertBackメソッドには、ターゲットソースの XAML の UI 要素(例えば TextBox)からバインディングソースであるプロパティにデータが流れた場合に実行される処理を記述します。

ビジュアルツリーのヘルパークラス

最後に、ビジュアルツリーを使用してイベントが発生した時に親要素や子要素をたどるための処理を実装したヘルパークラスを作成します。

WPF には論理ツリービジュアルツリーという仕組みがあり、論理ツリーは開発者が理解しやすいように階層構造になっている XAML マークアップのことです。

ビジュアルツリーは XAML ファイルがパーサーによって解析されて、オブジェクトとして実行され実際に画面にレンダリングされるコードのことです。

今回動作をカスタマイズしたDatePickerのコードをたどっていくとFrameworkElementを継承している事がわかります。(更に潜っていくと色々なクラスを継承しています)

visual-tree.png

XAML 側で指定しなかったもっと細かい UI を構成する横幅やマージンを指定するプロパティ等が実装されていることが分かります。

これらのコードが実行されることで画面が表示されるのですが、XAML にはこれらのプロパティを細かくは指定していません。これら全ての画面情報を XAML 側に記述していればコード量が増え画面の実装をすることが現実的ではないことは想像できます。

つまり、論理ツリーとは画面の構成を最低限の情報で階層的に表現したもので、ビジュアルツリーとは論理ツリーでは画面表示に足りない情報を実装しているものということになります。

"DatePickerExtensions/FrameworkElementExtensions.cs"
using System.Windows;
using System.Windows.Media;

namespace MVVM.DatePickerYearMonth.DatePickerExtensions
{
    public static class FrameworkElementExtensions
    {
        public static T TryFindParent<T>(this DependencyObject child) where T : DependencyObject
        {
            // 親アイテムを取得
            var parentObject = GetParentObject(child);

            // ツリーの末端に到達した場合
            if (parentObject == null) return null;

            // 親が検索対象の型に一致するかチェック
            if (parentObject is T parent) return parent;

            // 再帰を使って次の階層へ遡っていく
            return TryFindParent<T>(parentObject);
        }

        public static DependencyObject GetParentObject(this DependencyObject child)
        {
            if (child == null) return null;

            if (child is ContentElement contentElement)
            {
                var parent = ContentOperations.GetParent(contentElement) ??
                             (contentElement as FrameworkContentElement)?.Parent;

                if (parent != null) return parent;
            }

            // フレームワーク要素(DockPanelなど)の親も検索
            if (child is FrameworkElement frameworkElement)
            {
                var parent = frameworkElement.Parent;
                if (parent != null) return parent;
            }

            // ContentElement 及び FrameworkElement でない場合は child 要素のビジュアルツリー上の親要素を返す
            return VisualTreeHelper.GetParent(child);
        }
    }
}

ここではビジュアルツリーを通して親要素や子要素を検索するための静的メソッドを実装しています。

ここまでの実装を元にプログラムを実行してみます。

dependency-property-calendar.gif

DatePicker を年月表示にカスタマイズすることが出来ました。

今回作成したサンプルのリポジトリです。

参考記事