はじめに
本記事では、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 を素早く実装してみてください。

コメント