カスタムDatePicker：TextBoxを使った便利な日付選択機能の実装

## はじめに
Windowsフォームアプリケーションで日付を入力する際、デフォルトの日付ピッカー（DateTimePicker）を使用するのが一般的ですが、デザインや機能をカスタマイズしたい場合には適していません。本記事では、TextBox をベースにしたカスタム日付ピッカーの実装方法を紹介します。

本カスタム日付ピッカーでは、以下のような機能を提供します。
* TextBox 内に日付を表示
* MonthCalendar をポップアップ表示
* BackColorをカスタマイズ可能
* カスタムのカレンダーアイコンを追加可能
* 初期選択デートやリセット機能の実装

## 実装方法

**1. CustomDatePickerDemo クラスの作成**
TextBox を継承して、クリックすると MonthCalendar を表示するカスタム日付ピッカーを作成します。

```
public class CustomDatePickerDemo : TextBox
{
    private MonthCalendar monthCalendar;
    private Form popupForm;
    private Form parentForm;

    // 選択された日付を取得および設定するための SelectedDate プロパティ
    private DateTime selectedDate = DateTime.Now;

    public DateTime SelectedDate
    {
        get { return selectedDate; }
        set
        {
            selectedDate = value;

            // カレンダーの日付を設定
            if (monthCalendar != null)
            {
                monthCalendar.SetDate(selectedDate);
            }

            // TextBox のテキストを選択された日付で更新
            this.Text = selectedDate.ToString("yyyy年MM月dd日");
        }
    }

    protected override void OnParentChanged(EventArgs e)
    {
        base.OnParentChanged(e);
        if (this.Parent != null)
        {
            parentForm = this.FindForm();
            if (parentForm != null)
            {
                parentForm.MouseDown += ParentForm_MouseDown; // TextBox 外部のクリックを検出
            }
        }
    }

    private void ShowCalendar()
    {
        if (popupForm != null)
        {
            popupForm.Close();
            popupForm = null;
        }

        popupForm = new Form
        {
            FormBorderStyle = FormBorderStyle.None,
            ShowInTaskbar = false,
            StartPosition = FormStartPosition.Manual,
            AutoSize = false,
        };

        monthCalendar = new MonthCalendar
        {
            MaxSelectionCount = 1
        };

        // テキストが空の場合、カレンダーの選択日を現在の日付に設定
        if (string.IsNullOrEmpty(this.Text))
        {
            monthCalendar.SetDate(DateTime.Now);
        }
        else
        {
            // テキストが空でない場合、カレンダーの選択日を現在の SelectedDate に設定
            monthCalendar.SetDate(SelectedDate);
        }

        // カレンダーが表示されたときに選択日を設定
        monthCalendar.DateSelected += MonthCalendar_DateSelected;
        popupForm.Controls.Add(monthCalendar);

        // カレンダーのサイズに基づいてポップアップのサイズを調整
        popupForm.ClientSize = new Size(
            monthCalendar.Width + 48,
            monthCalendar.Height + 6
        );

        // TextBox の下にポップアップを配置
        Point screenLocation = this.PointToScreen(new Point(0, this.Height));
        popupForm.Location = screenLocation;

        popupForm.Deactivate += (s, e) => CloseCalendar();

        popupForm.Show();
    }

    private void MonthCalendar_DateSelected(object sender, DateRangeEventArgs e)
    {
        // カレンダーで日付が選択されたときに、TextBox のテキストを更新
        this.Text = e.Start.ToString("yyyy年MM月dd日");

        // SelectedDate プロパティを更新
        SelectedDate = e.Start;

        CloseCalendar();
        if (parentForm != null)
        {
            parentForm.ActiveControl = null; // TextBox からフォーカスを外す
        }
    }

    private void ParentForm_MouseDown(object sender, MouseEventArgs e)
    {
        if (popupForm == null)
            return;

        Point mousePosition = parentForm.PointToScreen(e.Location);

        if (this.Bounds.Contains(this.Parent.PointToClient(mousePosition)))
            return;

        if (popupForm.Bounds.Contains(mousePosition))
            return;

        // クリックが外部だった場合、カレンダーを閉じる
        CloseCalendar();
    }

    private void CloseCalendar()
    {
        if (popupForm != null)
        {
            popupForm.Close();
            popupForm = null
        }
    }
}

```
**２. BackColorのカスタマイズ**
CustomBackColor プロパティを追加することで、TextBox の背景色を変更できます。
```
public Color CustomBackColor
{
    get { return this.BackColor; }
    set { this.BackColor = value; }
}
```
**3. カレンダーアイコンの追加**
カレンダーアイコンを PictureBox で追加し、クリック時に ShowCalendar() を実行することで、より直感的なUIを実現できます。
```
private PictureBox calendarIcon;
public CustomDatePickerDemo()
{
    calendarIcon = new PictureBox
    {
        Image = Properties.Resources.calendarDark, // 画像リソースを指定
        SizeMode = PictureBoxSizeMode.StretchImage,
        Size = new Size(20, 20),
        Cursor = Cursors.Hand,
        Location = new Point(this.Width + 28, (this.Height - 20) / 2),
        BackColor = Color.Transparent
    };
    this.Controls.Add(calendarIcon);
    calendarIcon.Click += (s, e) => ShowCalendar();
    this.Click += (s, e) => ShowCalendar();

}
```
**4.フォームでの利用**
CustomDatePickerDemo をフォームに追加し、複数の TextBox に適用できます。
```
public partial class CustomDatePickerForm : Form
{
    public CustomDatePickerDemo customDatePicker;

    public CustomDatePickerForm()
    {
        InitializeComponent();

        customDatePicker = new CustomDatePickerDemo
        {
            Location = new Point(45, 20),
            Width = 150,
            CustomBackColor = Color.Yellow
        };
        this.panel1.Controls.Add(customDatePicker);

        CustomDatePickerDemo customDatePicker2 = new CustomDatePickerDemo
        {
            Location = new Point(45, 20),
            Width = 150,
            CustomBackColor = Color.Yellow,
            SelectedDate = new DateTime(2022, 3, 2)
        };
        this.panel2.Controls.Add(customDatePicker2);
    }

    private void btnOk_Click(object sender, EventArgs e)
    {
        MessageBox.Show(customDatePicker.SelectedDate.ToString("yyyy年MM月dd日"));
    }
}
```

**5. リセット機能の実装**
リセットボタンをクリックすると、TextBox の内容をクリアし、カレンダーを開く際には最小日付を設定します。
```
private void reset_Click(object sender, EventArgs e)
{
    customDatePicker.Text = "";
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3951101/dd834ede-becb-e05e-d090-cab958870959.png)


## まとめ
本記事では、カスタム日付ピッカーの実装方法について解説しました。TextBox をベースに MonthCalendar をポップアップ表示し、背景色の変更やカレンダーアイコンの追加、リセット機能を実装しました。

このカスタム日付ピッカーを活用することで、デザインの自由度を高めつつ、ユーザーにとって使いやすいUIを提供できます。