[C#][Windows Formsアプリ][MonthCalendar] MonthCalendar:MaxSelectionCount で選択できる最大日数を制御する

スポンサーリンク

はじめに

本記事では、Windows Forms の MonthCalendar コントロールで選択できる最大日数を MaxSelectionCount で制御する方法を解説します。
「単一日のみ選ばせたい」「最大 7 日まで」「当月でも 31 日を超えさせない」などの要件に、最短で安全に対応できる実装パターンとサンプルコードを示します。

説明

MaxSelectionCount は、ユーザーが選択できる範囲の最大日数(開始日と終了日を含む合計)を制限します。
日数の数え方は (End - Start).Days + 1(両端含む)で、いわゆる「オフバイワン」に注意が必要です。

実務でのポイント:

単一日固定なら MaxSelectionCount = 1 が最短。
任意範囲をコードから設定するときは、開始 <= 終了の正規化MinDate/MaxDate への丸め最大日数の強制を毎回行うのが定石。
・最大日数を超える入力への対応は、切り詰める(サイレント)エラー表示で設定しないか、方針を揃えると UX が安定します。

サンプルコード

以下は、MaxSelectionCount を UI から変更しつつ、範囲設定を安全に行う最小アプリです。
NumericUpDown で最大日数を変更
TextBox から任意の開始/終了日を入力し「範囲を設定」
CheckBox で「超過時に切り詰める(サイレント)」か「エラー表示」を切替
・現在の選択と選択日数を常に表示

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

namespace MonthCalendarMaxSelectionCountSample
{
    public class MainForm : Form
    {
        private MonthCalendar calendar;
        private NumericUpDown nudMaxDays;
        private TextBox txtStart, txtEnd;
        private Button btnApplyRange, btnSingleDay, btnWeek, btnMonth;
        private CheckBox chkTrimIfOver;
        private Label lblStatus;
        private ListBox lstLog;

        public MainForm()
        {
            Text = "MonthCalendar - MaxSelectionCount で最大日数を制御";
            Width = 920;
            Height = 560;

            calendar = new MonthCalendar
            {
                Location = new Point(20, 20),
                MaxSelectionCount = 7
            };

            lstLog = new ListBox
            {
                Location = new Point(340, 20),
                Size = new Size(540, 280)
            };

            // --- MaxSelectionCount の編集 ---
            var lblMax = new Label { Text = "MaxSelectionCount:", Location = new Point(20, 210), AutoSize = true };
            nudMaxDays = new NumericUpDown
            {
                Location = new Point(160, 206),
                Width = 80,
                Minimum = 1,
                Maximum = 366,
                Value = calendar.MaxSelectionCount
            };
            nudMaxDays.ValueChanged += (s, e) =>
            {
                calendar.MaxSelectionCount = (int)nudMaxDays.Value;
                Log($"[MaxSet] MaxSelectionCount = {calendar.MaxSelectionCount}");
                UpdateStatus();
            };

            // --- 任意範囲の設定 ---
            var lblStart = new Label { Text = "開始 (yyyy-MM-dd):", Location = new Point(20, 250), AutoSize = true };
            txtStart = new TextBox { Location = new Point(160, 246), Width = 110, Text = DateTime.Today.ToString("yyyy-MM-dd") };

            var lblEnd = new Label { Text = "終了 (yyyy-MM-dd):", Location = new Point(20, 282), AutoSize = true };
            txtEnd = new TextBox { Location = new Point(160, 278), Width = 110, Text = DateTime.Today.ToString("yyyy-MM-dd") };

            chkTrimIfOver = new CheckBox
            {
                Text = "最大超過時は終了日を切り詰める(サイレント)",
                Location = new Point(20, 312),
                AutoSize = true,
                Checked = true
            };

            btnApplyRange = new Button { Text = "範囲を設定", Location = new Point(20, 344), Width = 250 };
            btnApplyRange.Click += (s, e) => ApplyRangeFromTextBoxes();

            // --- プリセット ---
            btnSingleDay = new Button { Text = "単一日(今日)", Location = new Point(20, 392), Width = 120 };
            btnSingleDay.Click += (s, e) => SelectSingleDay(DateTime.Today);

            btnWeek = new Button { Text = "当週(7日)", Location = new Point(150, 392), Width = 120 };
            btnWeek.Click += (s, e) =>
            {
                var any = DateTime.Today;
                // 月曜はじまり
                var start = any.AddDays(-(int)any.DayOfWeek + (int)DayOfWeek.Monday);
                var end = start.AddDays(6);
                SelectRangeSafely(start, end);
            };

            btnMonth = new Button { Text = "当月", Location = new Point(280, 392), Width = 120 };
            btnMonth.Click += (s, e) =>
            {
                var start = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
                var end = start.AddMonths(1).AddDays(-1);
                SelectRangeSafely(start, end);
            };

            lblStatus = new Label { Location = new Point(20, 432), AutoSize = true, Font = new Font(this.Font, FontStyle.Bold) };

            // --- イベントログ ---
            calendar.DateChanged += (s, e) =>
            {
                Log($"DateChanged  Start={e.Start:yyyy-MM-dd}, End={e.End:yyyy-MM-dd}");
                UpdateStatus();
            };

            // --- レイアウト登録 ---
            Controls.Add(calendar);
            Controls.Add(lstLog);
            Controls.Add(lblMax);
            Controls.Add(nudMaxDays);
            Controls.Add(lblStart);
            Controls.Add(txtStart);
            Controls.Add(lblEnd);
            Controls.Add(txtEnd);
            Controls.Add(chkTrimIfOver);
            Controls.Add(btnApplyRange);
            Controls.Add(btnSingleDay);
            Controls.Add(btnWeek);
            Controls.Add(btnMonth);
            Controls.Add(lblStatus);

            Load += (s, e) =>
            {
                SelectSingleDay(DateTime.Today);
                UpdateStatus();
            };
        }

        // ========== コアロジック ==========

        private void ApplyRangeFromTextBoxes()
        {
            if (!TryParseDate(txtStart.Text, out var sdt) || !TryParseDate(txtEnd.Text, out var edt))
            {
                MessageBox.Show("日付は yyyy-MM-dd 形式で入力してください。");
                return;
            }
            SelectRangeSafely(sdt, edt);
        }

        /// <summary>開始/終了を正規化・丸め・最大日数チェック付きで安全に設定</summary>
        private void SelectRangeSafely(DateTime start, DateTime end)
        {
            Normalize(ref start, ref end);
            ClampToBounds(ref start, ref end);

            int desired = DaysInclusive(start, end);
            int max = calendar.MaxSelectionCount;

            if (desired > max)
            {
                if (chkTrimIfOver.Checked)
                {
                    end = start.AddDays(max - 1); // 切り詰め(サイレント)
                    Log($"[Trimmed] {desired}日 -> {max}日に切り詰め({start:yyyy-MM-dd}..{end:yyyy-MM-dd})");
                }
                else
                {
                    MessageBox.Show($"選択できるのは最大 {max} 日までです。");
                    return;
                }
            }

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

        /// <summary>単一日選択(MaxSelectionCount を尊重)</summary>
        private void SelectSingleDay(DateTime day)
        {
            day = Clamp(day, calendar.MinDate, calendar.MaxDate);
            calendar.SetSelectionRange(day, day);
            Log($"[Single] {day:yyyy-MM-dd}");
        }

        // ========== ユーティリティ ==========

        private static void Normalize(ref DateTime start, ref DateTime end)
        {
            if (start > end) (start, end) = (end, start);
        }

        private void ClampToBounds(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 v, DateTime min, DateTime max)
            => v < min ? min : (v > max ? max : v);

        private static int DaysInclusive(DateTime start, DateTime end)
            => (end - start).Days + 1;

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

        private void UpdateStatus()
        {
            var s = calendar.SelectionStart.Date;
            var e = calendar.SelectionEnd.Date;
            int days = DaysInclusive(s, e);
            lblStatus.Text = $"選択期間: {s:yyyy-MM-dd} ~ {e:yyyy-MM-dd}({days} 日) / MaxSelectionCount = {calendar.MaxSelectionCount}";
        }

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

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

つまづきポイント

1) 日数の数え方(オフバイワン)
MaxSelectionCount両端含むため、(End - Start).Days + 1 で計算します。例:同一日なら 1 日、10/01~10/07 は 7 日です。

2) コードで大きい範囲を設定したい
最大日数を超える設定は、方針を決めて切り詰める/エラー表示のいずれかに統一しましょう。本サンプルはチェックボックスで切替できます。

3) start と end の逆転
ユーザー入力やドラッグ順により start > end になることがあります。毎回 Normalize() で入れ替えておくのが定石です。

4) MinDate/MaxDate の外
会計期間などで範囲外になりやすいケースは、Clamp() でカレンダーの範囲内へ丸めてから設定しましょう。

5) イベントの選び方
コードで範囲を変えたときは DateChanged が発火しますが、DateSelected は発火しません。
「入力中の逐次処理」は DateChanged、「確定後の重い処理」はユーザー操作に限定してハンドリングする、などの分離が有効です。

まとめ

MaxSelectionCount を正しく使えば、単一日~複数日まで安全に範囲選択を制御できます。
実装の肝は「正規化」「丸め」「最大日数の扱い(切り詰め or エラー)」の 3 点セットを毎回適用すること。サンプルを土台に、要件に合わせた UX を素早く実装してみてください。

 

Please follow and like us:

コメント

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