はじめに
本記事では、Windows Forms の MonthCalendar コントロールで範囲選択を扱う際の定石パターンを、SelectionRange プロパティと SetSelectionRange メソッドを中心に解説します。
「単一日」「任意の開始・終了日」「当週・当月」といった頻出要件を、再利用しやすいヘルパー関数で最短実装できるようにします。
説明
SelectionRange は現在の選択範囲(Start と End)を取得・設定できるプロパティです。
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 問題
ユーザーが「終了→開始」の順にクリック・ドラッグするなど、start と end が逆転しがちです。Normalize() のように毎回正規化しましょう。
2) MaxSelectionCount を超える
MaxSelectionCount の制約に当たると意図通り選択できないことがあります。サンプルのように切り詰めるか、警告表示のどちらかを決めて統一。
3) MinDate/MaxDate の外に出る
期首・期末などで範囲がはみ出しやすいです。セット前に ClampToCalendarBounds() で丸めるのが定石。
4) DateChanged と DateSelected の混同
SetSelectionRange / SelectionRange をコードから変えると DateChanged は発火しますが、DateSelected は発火しません。処理のフック先を間違えないように。
5) 週の開始曜日
国・要件により週の開始曜日が異なります。ヘルパーの引数(例:startOfWeek)で可変にしておくと再利用性が上がります。
まとめ
範囲選択は SelectionRange と SetSelectionRange を軸に、正規化・丸め込み・最大日数の扱いを定石化すると安定します。
本記事のヘルパー群(単一日/当週/当月/任意範囲)は、そのまま自プロジェクトへコピーしても機能する最小単位です。まずは UI ボタンやユーザー操作から呼び出し、ログで挙動を確認してみてください。

コメント