[C#][Windows Formsアプリ][MonthCalendar] SelectionRange と SetSelectionRange:範囲選択の定石パターン(Windows Forms / MonthCalendar)

スポンサーリンク

はじめに

本記事では、Windows Forms の MonthCalendar コントロールで範囲選択を扱う際の定石パターンを、SelectionRange プロパティと SetSelectionRange メソッドを中心に解説します。
「単一日」「任意の開始・終了日」「当週・当月」といった頻出要件を、再利用しやすいヘルパー関数で最短実装できるようにします。

説明

SelectionRange は現在の選択範囲(StartEnd)を取得・設定できるプロパティです。
SetSelectionRange(DateTime start, DateTime end) は、開始日と終了日をまとめて設定するメソッドです。どちらを使っても最終的な選択状態は同じですが、「2つの日付をまとめて安全に設定」したい場合は SetSelectionRange が意図を表現しやすく、コードも読みやすくなります。

実務では次のような注意と定石を押さえると安全です。

開始と終了の正規化start > end の場合は入れ替える。
範囲の丸め込みMinDate/MaxDate の外にはみ出さないようクランプする。
最大選択日数MaxSelectionCount を超えないように制御する(必要なら分割やエラー表示)。
操作パターンの定型化単一日当週当月任意2点ドラッグなどをヘルパー関数で共通化。

サンプルコード

以下は、範囲選択を扱うユーティリティを備えた最小サンプルです。
UI:MonthCalendar / 「今日」・「当週」・「当月」ボタン / 任意範囲をテキストボックスから設定 / ログ表示。

using System;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms;

namespace MonthCalendarRangePatterns
{
    public class MainForm : Form
    {
        private MonthCalendar calendar;
        private Button btnToday, btnThisWeek, btnThisMonth;
        private TextBox txtStart, txtEnd;
        private Button btnSetRange;
        private ListBox lstLog;

        public MainForm()
        {
            Text = "MonthCalendar 範囲選択: SelectionRange と SetSelectionRange";
            Width = 860;
            Height = 520;

            calendar = new MonthCalendar
            {
                Location = new Point(20, 20),
                MaxSelectionCount = 31, // 例: 1~31日の範囲を想定(要件に合わせて変更)
            };

            // ログ
            lstLog = new ListBox
            {
                Location = new Point(320, 20),
                Size = new Size(500, 300)
            };

            // プリセット選択ボタン
            btnToday = new Button
            {
                Text = "今日 (単一日)",
                Location = new Point(20, 220),
                Width = 180
            };
            btnToday.Click += (s, e) => SelectSingleDay(DateTime.Today);

            btnThisWeek = new Button
            {
                Text = "当週 (月曜はじまり)",
                Location = new Point(20, 260),
                Width = 180
            };
            btnThisWeek.Click += (s, e) => SelectCurrentWeek(DateTime.Today, startOfWeek: DayOfWeek.Monday);

            btnThisMonth = new Button
            {
                Text = "当月",
                Location = new Point(20, 300),
                Width = 180
            };
            btnThisMonth.Click += (s, e) => SelectCurrentMonth(DateTime.Today);

            // 任意範囲入力
            var lblStart = new Label { Text = "開始日 (yyyy-MM-dd):", Location = new Point(20, 360), AutoSize = true };
            txtStart = new TextBox { Location = new Point(180, 356), Width = 120 };
            txtStart.Text = DateTime.Today.ToString("yyyy-MM-dd");

            var lblEnd = new Label { Text = "終了日 (yyyy-MM-dd):", Location = new Point(20, 392), AutoSize = true };
            txtEnd = new TextBox { Location = new Point(180, 388), Width = 120 };
            txtEnd.Text = DateTime.Today.ToString("yyyy-MM-dd");

            btnSetRange = new Button
            {
                Text = "範囲を設定",
                Location = new Point(20, 424),
                Width = 280
            };
            btnSetRange.Click += (s, e) =>
            {
                if (!TryParseDate(txtStart.Text, out var sdt) || !TryParseDate(txtEnd.Text, out var edt))
                {
                    MessageBox.Show("日付の形式は yyyy-MM-dd で入力してください。");
                    return;
                }
                SelectRange(sdt, edt, enforceMaxRange: true);
            };

            // イベント: 選択が変わるたびにログ
            calendar.DateChanged += (s, e) =>
            {
                Log($"DateChanged  Start={e.Start:yyyy-MM-dd}, End={e.End:yyyy-MM-dd}");
            };

            Controls.Add(calendar);
            Controls.Add(lstLog);
            Controls.Add(btnToday);
            Controls.Add(btnThisWeek);
            Controls.Add(btnThisMonth);
            Controls.Add(lblStart);
            Controls.Add(txtStart);
            Controls.Add(lblEnd);
            Controls.Add(txtEnd);
            Controls.Add(btnSetRange);

            // 初期表示: 今日(単一日)
            Load += (s, e) => SelectSingleDay(DateTime.Today);
        }

        // --- 定石ヘルパー ---

        /// <summary>単一日選択(SelectionRange.Start と End を同日に)</summary>
        private void SelectSingleDay(DateTime day)
        {
            // Min/Max の範囲に丸める
            day = Clamp(day, calendar.MinDate, calendar.MaxDate);
            // まとめて指定するなら SetSelectionRange が読みやすい
            calendar.SetSelectionRange(day, day);
            Log($"[Single] {day:yyyy-MM-dd}");
        }

        /// <summary>任意の開始・終了で範囲選択。必要に応じて MaxSelectionCount を強制</summary>
        private void SelectRange(DateTime start, DateTime end, bool enforceMaxRange)
        {
            Normalize(ref start, ref end); // start <= end に整える
            ClampToCalendarBounds(ref start, ref end); // MinDate/MaxDate 内に丸める

            if (enforceMaxRange)
            {
                var max = calendar.MaxSelectionCount;
                var days = (end - start).Days + 1;
                if (days > max)
                {
                    // 方式1: 終了日を切り詰める(サイレント)
                    end = start.AddDays(max - 1);
                    // 方式2: メッセージを出すなら下記(必要に応じて切替)
                    // MessageBox.Show($"最大 {max} 日までです。");
                }
            }

            calendar.SetSelectionRange(start, end);
            Log($"[Range] {start:yyyy-MM-dd} .. {end:yyyy-MM-dd}");
        }

        /// <summary>当週(週の開始曜日を指定可能)</summary>
        private void SelectCurrentWeek(DateTime anyDayInWeek, DayOfWeek startOfWeek)
        {
            DateTime start = anyDayInWeek;
            while (start.DayOfWeek != startOfWeek)
                start = start.AddDays(-1);

            DateTime end = start.AddDays(6);
            SelectRange(start, end, enforceMaxRange: true);
            Log($"[Week] {start:yyyy-MM-dd} .. {end:yyyy-MM-dd} (StartOfWeek={startOfWeek})");
        }

        /// <summary>当月</summary>
        private void SelectCurrentMonth(DateTime anyDayInMonth)
        {
            var start = new DateTime(anyDayInMonth.Year, anyDayInMonth.Month, 1);
            var end = start.AddMonths(1).AddDays(-1);
            SelectRange(start, end, enforceMaxRange: true);
            Log($"[Month] {start:yyyy-MM-dd} .. {end:yyyy-MM-dd}");
        }

        // --- ユーティリティ ---

        /// <summary>start > end のとき入れ替える</summary>
        private static void Normalize(ref DateTime start, ref DateTime end)
        {
            if (start > end) (start, end) = (end, start);
        }

        /// <summary>MinDate/MaxDate の範囲に収める</summary>
        private void ClampToCalendarBounds(ref DateTime start, ref DateTime end)
        {
            start = Clamp(start, calendar.MinDate, calendar.MaxDate);
            end   = Clamp(end,   calendar.MinDate, calendar.MaxDate);
            Normalize(ref start, ref end);
        }

        private static DateTime Clamp(DateTime value, DateTime min, DateTime max)
            => value < min ? min : (value > max ? max : value);

        private static bool TryParseDate(string text, out DateTime date)
            => DateTime.TryParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture,
                                      DateTimeStyles.None, out date);

        private void Log(string message)
            => lstLog.Items.Insert(0, $"{DateTime.Now:HH:mm:ss.fff}  {message}");

        [STAThread]
        public static void Main()
        {
            Application.EnableVisualStyles();
            Application.Run(new MainForm());
        }
    }
}

つまづきポイント

1) start > end 問題
ユーザーが「終了→開始」の順にクリック・ドラッグするなど、startend が逆転しがちです。Normalize() のように毎回正規化しましょう。

2) MaxSelectionCount を超える
MaxSelectionCount の制約に当たると意図通り選択できないことがあります。サンプルのように切り詰めるか、警告表示のどちらかを決めて統一。

3) MinDate/MaxDate の外に出る
期首・期末などで範囲がはみ出しやすいです。セット前に ClampToCalendarBounds() で丸めるのが定石。

4) DateChanged と DateSelected の混同
SetSelectionRange / SelectionRangeコードから変えると DateChanged は発火しますが、DateSelected は発火しません。処理のフック先を間違えないように。

5) 週の開始曜日
国・要件により週の開始曜日が異なります。ヘルパーの引数(例:startOfWeek)で可変にしておくと再利用性が上がります。

まとめ

範囲選択は SelectionRangeSetSelectionRange を軸に、正規化・丸め込み・最大日数の扱いを定石化すると安定します。
本記事のヘルパー群(単一日/当週/当月/任意範囲)は、そのまま自プロジェクトへコピーしても機能する最小単位です。まずは UI ボタンやユーザー操作から呼び出し、ログで挙動を確認してみてください。

 

Please follow and like us:

コメント

タイトルとURLをコピーしました