|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel;
using System.Drawing;
using System.Globalization;
using System.Windows.Forms.Layout;
using Microsoft.Win32;
using Windows.Win32.UI.Accessibility;
namespace System.Windows.Forms;
/// <summary>
/// This control is an encapsulation of the Windows month calendar control. A month calendar control implements a
/// calendar-like user interface that provides the user with a intuitive and recognizable method of entering or
/// selecting a date.
/// </summary>
/// <remarks>
/// <para>
/// Users can also select which days to bold. The most efficient way to add the bolded dates is via an array.
/// The following is an example of this:
/// </para>
/// <code>
/// MonthCalendar mc = new MonthCalendar();
/// DateTime[] time = new DateTime[3];
/// time[0] = DateTime.Now;
/// time[1] = time[0].AddDays(2);
/// time[2] = time[1].AddDays(2);
/// mc.BoldedDates = time;
/// </code>
/// <para>
/// Removal of all bolded dates is accomplished with:
/// </para>
/// <code>
/// mc.RemoveAllBoldedDates();
/// </code>
/// <para>
/// Although less efficient, the user may need to add or remove bolded dates one at a time. To improve the performance
/// of this, neither <see cref="AddBoldedDate(DateTime)"/> nor <see cref="RemoveBoldedDate(DateTime)"/> repaints the
/// <see cref="MonthCalendar"/>. The user must call <see cref="UpdateBoldedDates"/> to force the repaint of the bolded
/// dates, otherwise the <see cref="MonthCalendar"/> will not paint properly. The following is an example of this:
/// </para>
/// <code>
/// DateTime time1 = new DateTime("3/5/98");
/// DateTime time2 = new DateTime("4/19/98");
/// mc.AddBoldedDate(time1);
/// mc.AddBoldedDate(time2);
/// mc.RemoveBoldedDate(time1);
/// mc.UpdateBoldedDates();
/// </code>
/// <para>
/// The same applies to addition and removal of annual and monthly bolded dates.
/// </para>
/// </remarks>
[DefaultProperty(nameof(SelectionRange))]
[DefaultEvent(nameof(DateChanged))]
[DefaultBindingProperty(nameof(SelectionRange))]
[Designer($"System.Windows.Forms.Design.MonthCalendarDesigner, {AssemblyRef.SystemDesign}")]
[SRDescription(nameof(SR.DescriptionMonthCalendar))]
public partial class MonthCalendar : Control
{
private static readonly Color s_defaultTitleBackColor = SystemColors.ActiveCaption;
private static readonly Color s_defaultTitleForeColor = SystemColors.ActiveCaptionText;
private static readonly Color s_trailingForeColor = SystemColors.GrayText;
private const int MonthsInYear = 12;
/// <summary>
/// This is the arbitrary number of pixels that the Win32 control
/// inserts between calendars horizontally, regardless of font.
/// </summary>
private const int InsertWidthSize = 6;
/// <summary>
/// This is the arbitrary number of pixels that the Win32 control
/// inserts between calendars vertically, regardless of font.
/// From comctl32 MonthCalendar sources CALBORDER.
/// </summary>
private const int InsertHeightSize = 6;
private const Day DefaultFirstDayOfWeek = Day.Default;
private const int DefaultMaxSelectionCount = 7;
private const int DefaultScrollChange = 0;
private static readonly Size s_defaultSingleMonthSize = new(176, 153);
private const int MaxScrollChange = 20000;
private int _extraPadding;
private Color _titleBackColor = s_defaultTitleBackColor;
private Color _titleForeColor = s_defaultTitleForeColor;
private Color _trailingForeColor = s_trailingForeColor;
private bool _showToday = true;
private bool _showTodayCircle = true;
private bool _showWeekNumbers;
private bool _rightToLeftLayout;
private Size _dimensions = new(1, 1);
private int _maxSelectionCount = DefaultMaxSelectionCount;
private DateTime _maxDate = DateTime.MaxValue;
private DateTime _minDate = DateTime.MinValue;
private int _scrollChange = DefaultScrollChange;
private bool _todayDateSet;
private DateTime _todaysDate = DateTime.Now.Date;
private DateTime _selectionStart;
private DateTime _selectionEnd;
private DateTime _focusedDate;
private SelectionRange? _currentDisplayRange;
private Day _firstDayOfWeek = DefaultFirstDayOfWeek;
private MONTH_CALDENDAR_MESSAGES_VIEW _mcCurView = MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH;
private MONTH_CALDENDAR_MESSAGES_VIEW _mcOldView = MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH;
/// <summary>
/// Bitmask for the annually bolded dates. Months start on January.
/// </summary>
private readonly int[] _monthsOfYear = new int[MonthsInYear];
/// <summary>
/// Bitmask for the dates bolded monthly.
/// </summary>
private int _datesToBoldMonthly;
private readonly List<DateTime> _boldDates = [];
private readonly List<DateTime> _annualBoldDates = [];
private readonly List<DateTime> _monthlyBoldDates = [];
private DateRangeEventHandler? _onDateChanged;
private DateRangeEventHandler? _onDateSelected;
private EventHandler? _onRightToLeftLayoutChanged;
private EventHandler? _onCalendarViewChanged;
private EventHandler? _onDisplayRangeChanged;
public MonthCalendar() : base()
{
_selectionStart = _todaysDate;
_selectionEnd = _todaysDate;
_focusedDate = _todaysDate;
SetStyle(ControlStyles.UserPaint, false);
SetStyle(ControlStyles.StandardClick, false);
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
SetStyle(ControlStyles.ApplyThemingImplicitly, true);
#pragma warning restore WFO5001
TabStop = true;
}
protected override AccessibleObject CreateAccessibilityInstance()
=> new MonthCalendarAccessibleObject(this);
protected override void RescaleConstantsForDpi(int deviceDpiOld, int deviceDpiNew)
{
base.RescaleConstantsForDpi(deviceDpiOld, deviceDpiNew);
ScaleConstants();
}
private protected override void InitializeConstantsForInitialDpi(int initialDpi) => ScaleConstants();
private void ScaleConstants()
{
const int LogicalExtraPadding = 2;
_extraPadding = LogicalToDeviceUnits(LogicalExtraPadding);
}
/// <summary>
/// The array of DateTime objects that determines which annual days are shown in bold.
/// </summary>
[Localizable(true)]
[SRDescription(nameof(SR.MonthCalendarAnnuallyBoldedDatesDescr))]
public DateTime[] AnnuallyBoldedDates
{
get => [.. _annualBoldDates];
set
{
_annualBoldDates.Clear();
for (int i = 0; i < MonthsInYear; ++i)
{
_monthsOfYear[i] = 0;
}
if (value is not null && value.Length > 0)
{
// Add each bolded date to our List.
_annualBoldDates.AddRange(value);
foreach (var dateTime in value)
{
_monthsOfYear[dateTime.Month - 1] |= 0x00000001 << (dateTime.Day - 1);
}
}
UpdateBoldedDates();
}
}
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
[SRDescription(nameof(SR.MonthCalendarMonthBackColorDescr))]
public override Color BackColor
{
get
{
if (ShouldSerializeBackColor() || Application.IsDarkModeEnabled)
{
return base.BackColor;
}
return SystemColors.Window;
}
set => base.BackColor = value;
}
#pragma warning restore WFO5001
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override Image? BackgroundImage
{
get => base.BackgroundImage;
set => base.BackgroundImage = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageChanged
{
add => base.BackgroundImageChanged += value;
remove => base.BackgroundImageChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public override ImageLayout BackgroundImageLayout
{
get => base.BackgroundImageLayout;
set => base.BackgroundImageLayout = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? BackgroundImageLayoutChanged
{
add => base.BackgroundImageLayoutChanged += value;
remove => base.BackgroundImageLayoutChanged -= value;
}
/// <summary>
/// The array of DateTime objects that determines which non-recurring
/// specified dates are shown in bold.
/// </summary>
[Localizable(true)]
public DateTime[] BoldedDates
{
get => [.. _boldDates];
set
{
_boldDates.Clear();
if (value is not null && value.Length > 0)
{
// Add each bolded date to our list.
_boldDates.AddRange(value);
}
UpdateBoldedDates();
}
}
/// <summary>
/// The number of columns and rows of months that will be displayed
/// in the MonthCalendar control.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[SRDescription(nameof(SR.MonthCalendarDimensionsDescr))]
public Size CalendarDimensions
{
get => _dimensions;
set
{
if (_dimensions.Equals(value))
{
return;
}
SetCalendarDimensions(value.Width, value.Height);
}
}
protected override CreateParams CreateParams
{
get
{
CreateParams cp = base.CreateParams;
cp.ClassName = PInvoke.MONTHCAL_CLASS;
cp.Style |= (int)PInvoke.MCS_MULTISELECT | (int)PInvoke.MCS_DAYSTATE;
if (!_showToday)
{
cp.Style |= (int)PInvoke.MCS_NOTODAY;
}
if (!_showTodayCircle)
{
cp.Style |= (int)PInvoke.MCS_NOTODAYCIRCLE;
}
if (_showWeekNumbers)
{
cp.Style |= (int)PInvoke.MCS_WEEKNUMBERS;
}
if (RightToLeft == RightToLeft.Yes && RightToLeftLayout)
{
// We want to turn on mirroring for Form explicitly.
cp.ExStyle |= (int)WINDOW_EX_STYLE.WS_EX_LAYOUTRTL;
// Don't need these styles when mirroring is turned on.
cp.ExStyle &= ~(int)(WINDOW_EX_STYLE.WS_EX_RTLREADING | WINDOW_EX_STYLE.WS_EX_RIGHT | WINDOW_EX_STYLE.WS_EX_LEFTSCROLLBAR);
}
return cp;
}
}
protected override ImeMode DefaultImeMode => ImeMode.Disable;
protected override Padding DefaultMargin => new(9);
protected override Size DefaultSize => GetMinReqRect();
/// <summary>
/// This property is overridden and hidden from statement completion
/// on controls that are based on Win32 Native Controls.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
protected override bool DoubleBuffered
{
get => base.DoubleBuffered;
set => base.DoubleBuffered = value;
}
internal void FillMonthDayStates(Span<uint> monthDayStates, SelectionRange displayRange)
{
// Run through all displayed dates to set a binary marker that the date is bolded
// if BoldedDates, AnnualArrayOfDates, or MonthlyArrayOfDates contain this date.
DateTime currentDate = displayRange.Start;
while (currentDate <= displayRange.End)
{
bool currentDateIsBolded = _boldDates.Contains(currentDate)
|| _annualBoldDates.Any(d => d.Month == currentDate.Month && d.Day == currentDate.Day)
|| _monthlyBoldDates.Any(d => d.Day == currentDate.Day);
if (currentDateIsBolded)
{
// Calculate an index of a month of the current date in the display range,
// starting from the first displayed month.
// The display range may include gray dates of the first and last months.
// So the max count of visible months is 14 and the max index is 13.
int currentMonthIndex = GetIndexInMonths(displayRange.Start, currentDate);
// Set bolded state for the current date of the current month
// to prepare the states array before sending to Windows
monthDayStates[currentMonthIndex] |= 1U << currentDate.Day - 1;
}
// Set the next day for check
currentDate = currentDate.AddDays(1);
}
}
/// <summary>
/// The first day of the week for the month calendar control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[DefaultValue(DefaultFirstDayOfWeek)]
[SRDescription(nameof(SR.MonthCalendarFirstDayOfWeekDescr))]
public Day FirstDayOfWeek
{
get => _firstDayOfWeek;
set
{
if (value is < Day.Monday or > Day.Default)
{
throw new InvalidEnumArgumentException(nameof(FirstDayOfWeek), (int)value, typeof(Day));
}
if (value == _firstDayOfWeek)
{
return;
}
_firstDayOfWeek = value;
if (IsHandleCreated)
{
if (value == Day.Default)
{
RecreateHandle();
}
else
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETFIRSTDAYOFWEEK, 0, (nint)value);
}
UpdateDisplayRange();
// Add the extra call to make the accessibility tree to rebuild correctly
OnDisplayRangeChanged(EventArgs.Empty);
}
}
}
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
[SRDescription(nameof(SR.MonthCalendarForeColorDescr))]
public override Color ForeColor
{
get
{
if (ShouldSerializeForeColor() || Application.IsDarkModeEnabled)
{
return base.ForeColor;
}
return SystemColors.WindowText;
}
set => base.ForeColor = value;
}
#pragma warning restore WFO5001
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new ImeMode ImeMode
{
get => base.ImeMode;
set => base.ImeMode = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? ImeModeChanged
{
add => base.ImeModeChanged += value;
remove => base.ImeModeChanged -= value;
}
/// <summary>
/// The maximum allowable date that can be selected. By default, there
/// is no maximum date. The maximum date is not set if max less than the
/// current minimum date.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.MonthCalendarMaxDateDescr))]
public DateTime MaxDate
{
get => DateTimePicker.EffectiveMaxDate(_maxDate);
set
{
if (value == _maxDate)
{
return;
}
ArgumentOutOfRangeException.ThrowIfLessThan(value, DateTimePicker.EffectiveMinDate(_minDate));
_maxDate = value;
SetRange();
}
}
/// <summary>
/// The maximum number of days that can be selected in a
/// month calendar control. This method does not affect the current
/// selection range.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(DefaultMaxSelectionCount)]
[SRDescription(nameof(SR.MonthCalendarMaxSelectionCountDescr))]
public int MaxSelectionCount
{
get => _maxSelectionCount;
set
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
if (value == _maxSelectionCount)
{
return;
}
if (IsHandleCreated)
{
if (PInvokeCore.SendMessage(this, PInvoke.MCM_SETMAXSELCOUNT, (WPARAM)value) == 0)
{
throw new ArgumentException(string.Format(SR.MonthCalendarMaxSelCount, value.ToString("D")), nameof(value));
}
}
_maxSelectionCount = value;
}
}
/// <summary>
/// The minimum allowable date that can be selected. By default, there
/// is no minimum date. The minimum date is not set if min greater than the
/// current maximum date. MonthCalendar does not support dates prior to 1753.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.MonthCalendarMinDateDescr))]
public DateTime MinDate
{
get => DateTimePicker.EffectiveMinDate(_minDate);
set
{
if (value == _minDate)
{
return;
}
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, DateTimePicker.EffectiveMaxDate(_maxDate));
ArgumentOutOfRangeException.ThrowIfLessThan(value, DateTimePicker.MinimumDateTime);
_minDate = value;
SetRange();
}
}
/// <summary>
/// The array of DateTime objects that determine which monthly days to bold.
/// </summary>
[Localizable(true)]
[SRDescription(nameof(SR.MonthCalendarMonthlyBoldedDatesDescr))]
public DateTime[] MonthlyBoldedDates
{
get => [.. _monthlyBoldDates];
set
{
_monthlyBoldDates.Clear();
_datesToBoldMonthly = 0;
if (value is not null && value.Length > 0)
{
// Add each bolded date to our List.
_monthlyBoldDates.AddRange(value);
foreach (var dateTime in value)
{
_datesToBoldMonthly |= 0x00000001 << (dateTime.Day - 1);
}
}
UpdateBoldedDates();
}
}
private static DateTime Now => DateTime.Now.Date;
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public new Padding Padding
{
get => base.Padding;
set => base.Padding = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? PaddingChanged
{
add => base.PaddingChanged += value;
remove => base.PaddingChanged -= value;
}
/// <summary>
/// This is used for international applications where the language is written from RightToLeft.
/// When this property is true, and the RightToLeft is true, mirroring will be turned on on
/// the form, and control placement and text will be from right to left.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Localizable(true)]
[DefaultValue(false)]
[SRDescription(nameof(SR.ControlRightToLeftLayoutDescr))]
public virtual bool RightToLeftLayout
{
get => _rightToLeftLayout;
set
{
if (value == _rightToLeftLayout)
{
return;
}
_rightToLeftLayout = value;
using (new LayoutTransaction(this, this, PropertyNames.RightToLeftLayout))
{
OnRightToLeftLayoutChanged(EventArgs.Empty);
}
}
}
/// <summary>
/// The scroll rate for a month calendar control. The scroll rate is the
/// number of months that the control moves its display when the user clicks
/// a scroll button. If this value is zero, the month delta is reset to the
/// default, which is the number of months displayed in the control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(DefaultScrollChange)]
[SRDescription(nameof(SR.MonthCalendarScrollChangeDescr))]
public int ScrollChange
{
get => _scrollChange;
set
{
if (_scrollChange == value)
{
return;
}
ArgumentOutOfRangeException.ThrowIfNegative(value);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MaxScrollChange);
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETMONTHDELTA, (WPARAM)value);
}
_scrollChange = value;
}
}
/// <summary>
/// Indicates the end date of the selected range of dates.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.MonthCalendarSelectionEndDescr))]
public DateTime SelectionEnd
{
get => _selectionEnd;
set
{
if (_selectionEnd == value)
{
return;
}
ArgumentOutOfRangeException.ThrowIfLessThan(value, MinDate);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, MaxDate);
// If we've moved SelectionEnd before SelectionStart, move SelectionStart back
if (_selectionStart > value)
{
_selectionStart = value;
}
// If we've moved SelectionEnd too far beyond SelectionStart, move SelectionStart forward
if ((value - _selectionStart).Days >= _maxSelectionCount)
{
_selectionStart = value.AddDays(1 - _maxSelectionCount);
}
// Set the new selection range
SetSelRange(_selectionStart, value);
}
}
/// <summary>
/// Indicates the start date of the selected range of dates.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.MonthCalendarSelectionStartDescr))]
public DateTime SelectionStart
{
get => _selectionStart;
set
{
if (_selectionStart == value)
{
return;
}
ArgumentOutOfRangeException.ThrowIfLessThan(value, _minDate);
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _maxDate);
// If we've moved SelectionStart beyond SelectionEnd, move SelectionEnd forward
if (_selectionEnd < value)
{
_selectionEnd = value;
}
// If we've moved SelectionStart too far back from SelectionEnd, move SelectionEnd back
if ((_selectionEnd - value).Days >= _maxSelectionCount)
{
_selectionEnd = value.AddDays(_maxSelectionCount - 1);
}
// Set the new selection range
SetSelRange(value, _selectionEnd);
}
}
/// <summary>
/// Retrieves the selection range for a month calendar control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.MonthCalendarSelectionRangeDescr))]
[Bindable(true)]
public SelectionRange SelectionRange
{
get => new(SelectionStart, SelectionEnd);
set => SetSelectionRange(value.Start, value.End);
}
/// <summary>
/// Indicates whether the month calendar control will display
/// the "today" date at the bottom of the control.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.MonthCalendarShowTodayDescr))]
public bool ShowToday
{
get => _showToday;
set
{
if (value == _showToday)
{
return;
}
_showToday = value;
UpdateStyles();
AdjustSize();
}
}
/// <summary>
/// Indicates whether the month calendar control will circle
/// the "today" date.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[DefaultValue(true)]
[SRDescription(nameof(SR.MonthCalendarShowTodayCircleDescr))]
public bool ShowTodayCircle
{
get => _showTodayCircle;
set
{
if (value == _showTodayCircle)
{
return;
}
_showTodayCircle = value;
UpdateStyles();
}
}
/// <summary>
/// Indicates whether the month calendar control will the display
/// week numbers (1-52) to the left of each row of days.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Localizable(true)]
[DefaultValue(false)]
[SRDescription(nameof(SR.MonthCalendarShowWeekNumbersDescr))]
public bool ShowWeekNumbers
{
get => _showWeekNumbers;
set
{
if (value == _showWeekNumbers)
{
return;
}
_showWeekNumbers = value;
UpdateStyles();
AdjustSize();
}
}
/// <summary>
/// The minimum size required to display a full month. The size information
/// is presented in the form of a Point, with the x and y members representing
/// the minimum width and height required for the control. The minimum
/// required window size for a month calendar control depends on the
/// currently selected font.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.MonthCalendarSingleMonthSizeDescr))]
public Size SingleMonthSize
{
get
{
if (IsHandleCreated)
{
RECT rect = default;
if (PInvokeCore.SendMessage(this, PInvoke.MCM_GETMINREQRECT, 0, ref rect) == 0)
{
throw new InvalidOperationException(SR.InvalidSingleMonthSize);
}
return new Size(rect.right, rect.bottom);
}
return s_defaultSingleMonthSize;
}
}
/// <summary>
/// Unlike most controls, serializing the MonthCalendar's Size is really bad:
/// when it's restored at runtime, it uses a a default SingleMonthSize, which
/// may not be right, especially for JPN/CHS machines.
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[Localizable(false)]
public new Size Size
{
get => base.Size;
set => base.Size = value;
}
internal override bool SupportsUiaProviders => true;
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Bindable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[AllowNull]
public override string Text
{
get => base.Text;
set => base.Text = value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? TextChanged
{
add => base.TextChanged += value;
remove => base.TextChanged -= value;
}
/// <summary>
/// The date shown as "Today" in the Month Calendar control. By default, "Today" is the current date at the
/// time the MonthCalendar control is created.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[SRDescription(nameof(SR.MonthCalendarTodayDateDescr))]
public DateTime TodayDate
{
get
{
if (_todayDateSet)
{
return _todaysDate;
}
if (IsHandleCreated)
{
SYSTEMTIME systemTime = default;
int result = (int)PInvokeCore.SendMessage(this, PInvoke.MCM_GETTODAY, 0, ref systemTime);
Debug.Assert(result != 0, "MCM_GETTODAY failed");
return ((DateTime)systemTime).Date;
}
return Now.Date;
}
set
{
if (!_todayDateSet || (DateTime.Compare(value, _todaysDate) != 0))
{
// Throw if trying to set the TodayDate to a value greater than MaxDate.
ArgumentOutOfRangeException.ThrowIfGreaterThan(value, _maxDate);
// Throw if trying to set the TodayDate to a value less than MinDate.
ArgumentOutOfRangeException.ThrowIfLessThan(value, _minDate);
_todaysDate = value.Date;
_todayDateSet = true;
UpdateTodayDate();
}
}
}
/// <summary>
/// Indicates whether or not the TodayDate property has been explicitly
/// set by the user. If TodayDateSet is true, TodayDate will return whatever
/// the user has set it to. If TodayDateSet is false, TodayDate will follow
/// wall-clock time; ie. TodayDate will always equal the current system date.
/// </summary>
[SRCategory(nameof(SR.CatBehavior))]
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
[SRDescription(nameof(SR.MonthCalendarTodayDateSetDescr))]
public bool TodayDateSet => _todayDateSet;
/// <summary>
/// The background color displayed in the month calendar's title.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.MonthCalendarTitleBackColorDescr))]
public Color TitleBackColor
{
get => _titleBackColor;
set
{
if (value.IsEmpty)
{
throw new ArgumentException(string.Format(SR.InvalidNullArgument, nameof(value)), nameof(value));
}
_titleBackColor = value;
SetControlColor(PInvoke.MCSC_TITLEBK, value);
}
}
/// <summary>
/// The foreground color used to display text within the month
/// calendar's title.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.MonthCalendarTitleForeColorDescr))]
public Color TitleForeColor
{
get => _titleForeColor;
set
{
if (value.IsEmpty)
{
throw new ArgumentException(string.Format(SR.InvalidNullArgument, nameof(value)), nameof(value));
}
_titleForeColor = value;
SetControlColor(PInvoke.MCSC_TITLETEXT, value);
}
}
/// <summary>
/// The color used to display the previous and following months that
/// appear on the current month calendar.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
[SRDescription(nameof(SR.MonthCalendarTrailingForeColorDescr))]
public Color TrailingForeColor
{
get => _trailingForeColor;
set
{
if (value.IsEmpty)
{
throw new ArgumentException(string.Format(SR.InvalidNullArgument, nameof(value)), nameof(value));
}
_trailingForeColor = value;
SetControlColor(PInvoke.MCSC_TRAILINGTEXT, value);
}
}
/// <summary>
/// Adds a day that will be bolded annually on the month calendar.
/// Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void AddAnnuallyBoldedDate(DateTime date)
{
_annualBoldDates.Add(date);
_monthsOfYear[date.Month - 1] |= 0x00000001 << (date.Day - 1);
}
/// <summary>
/// Adds a day that will be bolded on the month calendar.
/// Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void AddBoldedDate(DateTime date)
{
if (!_boldDates.Contains(date))
{
_boldDates.Add(date);
}
}
/// <summary>
/// Adds a day that will be bolded monthly on the month calendar.
/// Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void AddMonthlyBoldedDate(DateTime date)
{
_monthlyBoldDates.Add(date);
_datesToBoldMonthly |= 0x00000001 << (date.Day - 1);
}
private event EventHandler? CalendarViewChanged
{
add => _onCalendarViewChanged += value;
remove => _onCalendarViewChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? Click
{
add => base.Click += value;
remove => base.Click -= value;
}
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.MonthCalendarOnDateChangedDescr))]
public event DateRangeEventHandler? DateChanged
{
add => _onDateChanged += value;
remove => _onDateChanged -= value;
}
[SRCategory(nameof(SR.CatAction))]
[SRDescription(nameof(SR.MonthCalendarOnDateSelectedDescr))]
public event DateRangeEventHandler? DateSelected
{
add => _onDateSelected += value;
remove => _onDateSelected -= value;
}
private event EventHandler? DisplayRangeChanged
{
add => _onDisplayRangeChanged += value;
remove => _onDisplayRangeChanged -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event EventHandler? DoubleClick
{
add => base.DoubleClick += value;
remove => base.DoubleClick -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event MouseEventHandler? MouseClick
{
add => base.MouseClick += value;
remove => base.MouseClick -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event MouseEventHandler? MouseDoubleClick
{
add => base.MouseDoubleClick += value;
remove => base.MouseDoubleClick -= value;
}
[Browsable(false)]
[EditorBrowsable(EditorBrowsableState.Never)]
public new event PaintEventHandler? Paint
{
add => base.Paint += value;
remove => base.Paint -= value;
}
[SRCategory(nameof(SR.CatPropertyChanged))]
[SRDescription(nameof(SR.ControlOnRightToLeftLayoutChangedDescr))]
public event EventHandler? RightToLeftLayoutChanged
{
add => _onRightToLeftLayoutChanged += value;
remove => _onRightToLeftLayoutChanged -= value;
}
/// <summary>
/// Used to auto-size the control. The requested number of rows and columns are
/// restricted by the maximum size of the parent control, hence the requested number
/// of rows and columns may not be what you get.
/// </summary>
private void AdjustSize()
{
Size minSize = GetMinReqRect();
Size = minSize;
}
private void WriteBoldDates(Span<int> boldDates)
{
boldDates.Clear();
SelectionRange range = GetDisplayRange(visible: false);
DateTime start = range.Start;
int startMonth = start.Month;
int startYear = start.Year;
// Add the non-recurrent dates to bold.
foreach (DateTime date in _boldDates)
{
if (DateTime.Compare(date, start) >= 0 && DateTime.Compare(date, range.End) <= 0)
{
// Date is within the display range, convert it into month count starting from the first displayed month.
int monthIndex = (date.Year - startYear) * MonthsInYear + date.Month - startMonth;
boldDates[monthIndex] |= (0x00000001 << (date.Day - 1));
}
}
// Add monthly and annual dates to bold.
--startMonth;
for (int i = 0; i < boldDates.Length; ++i, ++startMonth)
{
boldDates[i] |= _monthsOfYear[startMonth % MonthsInYear] | _datesToBoldMonthly;
}
}
/// <summary>
/// Compares only the day and month of each time.
/// </summary>
private static bool CompareDayAndMonth(DateTime t1, DateTime t2)
=> (t1.Day == t2.Day && t1.Month == t2.Month);
protected override unsafe void CreateHandle()
{
if (!RecreatingHandle)
{
using ThemingScope scope = new(Application.UseVisualStyles);
PInvoke.InitCommonControlsEx(new INITCOMMONCONTROLSEX()
{
dwSize = (uint)sizeof(INITCOMMONCONTROLSEX),
dwICC = INITCOMMONCONTROLSEX_ICC.ICC_DATE_CLASSES
});
}
base.CreateHandle();
}
/// <summary>
/// Return a localized string representation of the given DateTime value.
/// Used for throwing exceptions, etc.
/// </summary>
private static string FormatDate(DateTime value) => value.ToString("d", CultureInfo.CurrentCulture);
/// <summary>
/// Retrieves date information that represents the low and high limits of the
/// control's display.
/// </summary>
public SelectionRange GetDisplayRange(bool visible)
{
if (visible)
{
return GetMonthRange(PInvoke.GMR_VISIBLE);
}
return GetMonthRange(PInvoke.GMR_DAYSTATE);
}
/// <summary>
/// Retrieves the enumeration value corresponding to the hit area.
/// </summary>
private static HitArea GetHitArea(MCHITTESTINFO_HIT_FLAGS hit) => hit switch
{
MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEBK => HitArea.TitleBackground,
MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEMONTH => HitArea.TitleMonth,
MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEYEAR => HitArea.TitleYear,
MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEBTNNEXT => HitArea.NextMonthButton,
MCHITTESTINFO_HIT_FLAGS.MCHT_TITLEBTNPREV => HitArea.PrevMonthButton,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARBK => HitArea.CalendarBackground,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATE => HitArea.Date,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATENEXT => HitArea.NextMonthDate,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDATEPREV => HitArea.PrevMonthDate,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARDAY => HitArea.DayOfWeek,
MCHITTESTINFO_HIT_FLAGS.MCHT_CALENDARWEEKNUM => HitArea.WeekNumbers,
MCHITTESTINFO_HIT_FLAGS.MCHT_TODAYLINK => HitArea.TodayLink,
_ => HitArea.Nowhere,
};
private static int GetIndexInMonths(DateTime startDate, DateTime currentDate)
=> (currentDate.Year - startDate.Year) * MonthsInYear + currentDate.Month - startDate.Month;
private Size GetMinReqRect() => GetMinReqRect(0, false, false);
/// <summary>
/// Used internally to get the minimum size needed to display the MonthCalendar.
/// This is needed because PInvoke.MCM_GETMINREQRECT returns an incorrect value if
/// showToday is set to false. If updateRows is true, then the number of
/// rows will be updated according to height.
/// </summary>
private Size GetMinReqRect(int newDimensionLength, bool updateRows, bool updateCols)
{
Size minSize = SingleMonthSize;
// Calculate calendar height
Size textExtent;
using (var hfont = GdiCache.GetHFONTScope(Font))
using (var screen = GdiCache.GetScreenHdc())
{
// this is the string that Windows uses to determine the extent of the today string
textExtent = screen.HDC.GetTextExtent(DateTime.Now.ToShortDateString(), hfont);
}
int todayHeight = textExtent.Height + 4; // The constant 4 is from the comctl32 MonthCalendar source code
int calendarHeight = minSize.Height;
if (ShowToday)
{
// If ShowToday is true, then minSize already includes the height of the today string.
// So we remove it to get the actual calendar height.
calendarHeight -= todayHeight;
}
if (updateRows)
{
if (calendarHeight + InsertHeightSize == 0)
{
_dimensions.Height = 1;
}
else
{
int nRows = (newDimensionLength - todayHeight + InsertHeightSize) / (calendarHeight + InsertHeightSize);
_dimensions.Height = (nRows < 1) ? 1 : nRows;
}
}
if (updateCols)
{
if (minSize.Width == 0)
{
_dimensions.Width = 1;
}
else
{
int nCols = (newDimensionLength - _extraPadding) / minSize.Width;
_dimensions.Width = (nCols < 1) ? 1 : nCols;
}
}
minSize.Width = (minSize.Width + InsertWidthSize) * _dimensions.Width - InsertWidthSize;
minSize.Height = (calendarHeight + InsertHeightSize) * _dimensions.Height - InsertHeightSize + todayHeight;
// If the width we've calculated is too small to fit the Today string, enlarge the width to fit
if (IsHandleCreated)
{
int maxTodayWidth = (int)PInvokeCore.SendMessage(this, PInvoke.MCM_GETMAXTODAYWIDTH);
if (maxTodayWidth > minSize.Width)
{
minSize.Width = maxTodayWidth;
}
}
// Fudge factor
minSize.Width += _extraPadding;
minSize.Height += _extraPadding;
return minSize;
}
private SelectionRange GetMonthRange(uint flag)
{
Span<SYSTEMTIME> times = stackalloc SYSTEMTIME[2];
PInvokeCore.SendMessage(this, PInvoke.MCM_GETMONTHRANGE, (WPARAM)(int)flag, ref times[0]);
return new SelectionRange
{
Start = (DateTime)times[0],
End = (DateTime)times[1]
};
}
/// <summary>
/// Calculate the number of visible months, even though they may be partially visible.
/// It is necessary to send to Windows correct info about all bolded dates that are visible.
/// Get an index of the last month, that starts from 0, and add 1 to get months count.
/// </summary>
private static int GetMonthsCountOfRange(SelectionRange displayRange)
=> GetIndexInMonths(displayRange.Start, displayRange.End) + 1;
/// <summary>
/// Called by SetBoundsCore. If updateRows is true, then the number of rows
/// will be updated according to height.
/// </summary>
private int GetPreferredHeight(int height, bool updateRows)
{
Size preferredSize = GetMinReqRect(height, updateRows, false);
return preferredSize.Height;
}
/// <summary>
/// Called by SetBoundsCore. If updateCols is true, then the number of columns
/// will be updated according to width.
/// </summary>
private int GetPreferredWidth(int width, bool updateCols)
{
Size preferredSize = GetMinReqRect(width, false, updateCols);
return preferredSize.Width;
}
/// <summary>
/// Determines which portion of a month calendar control is at a given point on the screen.
/// </summary>
public unsafe HitTestInfo HitTest(int x, int y)
{
MCHITTESTINFO mchi = new()
{
cbSize = (uint)sizeof(MCHITTESTINFO),
pt = new Point(x, y),
st = default
};
PInvokeCore.SendMessage(this, PInvoke.MCM_HITTEST, 0, ref mchi);
// If the hit area has an associated valid date, get it.
HitArea hitArea = GetHitArea(mchi.uHit);
if (HitTestInfo.HitAreaHasValidDateTime(hitArea))
{
SYSTEMTIME systemTime = new()
{
wYear = mchi.st.wYear,
wMonth = mchi.st.wMonth,
wDayOfWeek = mchi.st.wDayOfWeek,
wDay = mchi.st.wDay,
wHour = mchi.st.wHour,
wMinute = mchi.st.wMinute,
wSecond = mchi.st.wSecond,
wMilliseconds = mchi.st.wMilliseconds
};
return new HitTestInfo(mchi.pt, hitArea, (DateTime)systemTime);
}
return new HitTestInfo(mchi.pt, hitArea);
}
/// <summary>
/// Determines which portion of a month calendar control is at a given point on the screen.
/// </summary>
public HitTestInfo HitTest(Point point) => HitTest(point.X, point.Y);
/// <summary>
/// Handling special input keys, such as PageUp, PageDown, Home, End, etc.
/// </summary>
protected override bool IsInputKey(Keys keyData)
{
if ((keyData & Keys.Alt) == Keys.Alt)
{
return false;
}
return (keyData & Keys.KeyCode) switch
{
Keys.PageUp or Keys.PageDown or Keys.Home or Keys.End => true,
_ => base.IsInputKey(keyData),
};
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
if (!IsHandleCreated)
{
return;
}
SetSelRange(_selectionStart, _selectionEnd);
if (_maxSelectionCount != DefaultMaxSelectionCount)
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETMAXSELCOUNT, (WPARAM)_maxSelectionCount);
}
AdjustSize();
if (_todayDateSet)
{
SYSTEMTIME systemTime = (SYSTEMTIME)_todaysDate;
PInvokeCore.SendMessage(this, PInvoke.MCM_SETTODAY, (WPARAM)0, ref systemTime);
}
SetControlColor(PInvoke.MCSC_TEXT, ForeColor);
SetControlColor(PInvoke.MCSC_MONTHBK, BackColor);
SetControlColor(PInvoke.MCSC_TITLEBK, _titleBackColor);
SetControlColor(PInvoke.MCSC_TITLETEXT, _titleForeColor);
SetControlColor(PInvoke.MCSC_TRAILINGTEXT, _trailingForeColor);
int firstDay;
if (_firstDayOfWeek == Day.Default)
{
firstDay = (int)PInvoke.LCTYPE.IFIRSTDAYOFWEEK;
}
else
{
firstDay = (int)_firstDayOfWeek;
}
PInvokeCore.SendMessage(this, PInvoke.MCM_SETFIRSTDAYOFWEEK, (WPARAM)0, (LPARAM)firstDay);
SetRange();
if (_scrollChange != DefaultScrollChange)
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETMONTHDELTA, (WPARAM)_scrollChange);
}
SystemEvents.UserPreferenceChanged += MarshaledUserPreferenceChanged;
}
protected override void OnHandleDestroyed(EventArgs e)
{
SystemEvents.UserPreferenceChanged -= MarshaledUserPreferenceChanged;
base.OnHandleDestroyed(e);
}
/// <summary>
/// Fires the event indicating that the currently
/// calendar view has changed.
/// </summary>
private void OnCalendarViewChanged(EventArgs e)
=> _onCalendarViewChanged?.Invoke(this, e);
/// <summary>
/// Fires the event indicating that the currently selected date
/// or range of dates has changed.
/// </summary>
protected virtual void OnDateChanged(DateRangeEventArgs drevent)
=> _onDateChanged?.Invoke(this, drevent);
/// <summary>
/// Fires the event indicating that the user has changed the selection.
/// </summary>
protected virtual void OnDateSelected(DateRangeEventArgs drevent)
=> _onDateSelected?.Invoke(this, drevent);
protected override void OnGotFocus(EventArgs e)
{
base.OnGotFocus(e);
if (IsAccessibilityObjectCreated)
{
((MonthCalendarAccessibleObject)AccessibilityObject).FocusedCell?.RaiseAutomationEvent(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
}
/// <summary>
/// Fires the event indicating that the current display range is changed.
/// </summary>
private void OnDisplayRangeChanged(EventArgs e)
=> _onDisplayRangeChanged?.Invoke(this, e);
protected override void OnFontChanged(EventArgs e)
{
base.OnFontChanged(e);
AdjustSize();
}
protected override void OnForeColorChanged(EventArgs e)
{
base.OnForeColorChanged(e);
SetControlColor(PInvoke.MCSC_TEXT, ForeColor);
}
protected override void OnBackColorChanged(EventArgs e)
{
base.OnBackColorChanged(e);
SetControlColor(PInvoke.MCSC_MONTHBK, BackColor);
}
protected override void OnResize(EventArgs e)
{
base.OnResize(e);
UpdateDisplayRange();
}
[EditorBrowsable(EditorBrowsableState.Advanced)]
protected virtual void OnRightToLeftLayoutChanged(EventArgs e)
{
if (GetAnyDisposingInHierarchy())
{
return;
}
if (RightToLeft == RightToLeft.Yes)
{
RecreateHandle();
}
_onRightToLeftLayoutChanged?.Invoke(this, e);
}
internal override void ReleaseUiaProvider(HWND handle)
{
if (OsVersion.IsWindows8OrGreater() && IsAccessibilityObjectCreated)
{
(AccessibilityObject as MonthCalendarAccessibleObject)?.DisconnectChildren();
}
base.ReleaseUiaProvider(handle);
}
/// <summary>
/// Removes all annually bolded days. Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void RemoveAllAnnuallyBoldedDates()
{
_annualBoldDates.Clear();
for (int i = 0; i < MonthsInYear; ++i)
{
_monthsOfYear[i] = 0;
}
}
/// <summary>
/// Removes all the bolded days. Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void RemoveAllBoldedDates() => _boldDates.Clear();
/// <summary>
/// Removes all monthly bolded days. Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void RemoveAllMonthlyBoldedDates()
{
_monthlyBoldDates.Clear();
_datesToBoldMonthly = 0;
}
/// <summary>
/// Removes an annually bolded date. If the date is not found in the
/// bolded date list, then no action is taken. If date occurs more than
/// once in the bolded date list, then only the first date is removed. When
/// comparing dates, only the day and month are used. Be sure to call
/// UpdateBoldedDates afterwards.
/// </summary>
public void RemoveAnnuallyBoldedDate(DateTime date)
{
int length = _annualBoldDates.Count;
int i = 0;
for (; i < length; ++i)
{
if (CompareDayAndMonth(_annualBoldDates[i], date))
{
_annualBoldDates.RemoveAt(i);
break;
}
}
--length;
for (int j = i; j < length; ++j)
{
if (CompareDayAndMonth(_annualBoldDates[j], date))
{
return;
}
}
_monthsOfYear[date.Month - 1] &= ~(0x00000001 << (date.Day - 1));
}
/// <summary>
/// Removes a bolded date. If the date is not found in the bolded date list,
/// then no action is taken. If date occurs more than once in the bolded
/// date list, then only the first date is removed.
/// Be sure to call UpdateBoldedDates() afterwards.
/// </summary>
public void RemoveBoldedDate(DateTime date)
{
DateTime toRemove = date.Date;
for (int i = 0; i < _boldDates.Count; i++)
{
if (DateTime.Compare(_boldDates[i].Date, toRemove) == 0)
{
_boldDates.RemoveAt(i);
return;
}
}
}
/// <summary>
/// Removes a monthly bolded date. If the date is not found in the
/// bolded date list, then no action is taken. If date occurs more than
/// once in the bolded date list, then only the first date is removed. When
/// comparing dates, only the day and month are used. Be sure to call
/// UpdateBoldedDates afterwards.
/// </summary>
public void RemoveMonthlyBoldedDate(DateTime date)
{
int length = _monthlyBoldDates.Count;
int i = 0;
for (; i < length; ++i)
{
if (CompareDayAndMonth(_monthlyBoldDates[i], date))
{
_monthlyBoldDates.RemoveAt(i);
break;
}
}
--length;
for (int j = i; j < length; ++j)
{
if (CompareDayAndMonth(_monthlyBoldDates[j], date))
{
return;
}
}
_datesToBoldMonthly &= ~(0x00000001 << (date.Day - 1));
}
private void ResetAnnuallyBoldedDates() => _annualBoldDates.Clear();
private void ResetBoldedDates() => _boldDates.Clear();
private void ResetCalendarDimensions() => CalendarDimensions = new Size(1, 1);
/// <summary>
/// Resets the maximum selectable date. By default value, there is no
/// upper limit.
/// </summary>
private void ResetMaxDate() => MaxDate = DateTime.MaxValue;
/// <summary>
/// Resets the minimum selectable date. By default value, there is no
/// lower limit.
/// </summary>
private void ResetMinDate() => MinDate = DateTime.MinValue;
private void ResetMonthlyBoldedDates() => _monthlyBoldDates.Clear();
/// <summary>
/// Resets the limits of the selection range. By default value, the upper
/// and lower limit is the current date.
/// </summary>
private void ResetSelectionRange() => SetSelectionRange(Now, Now);
private void ResetTrailingForeColor() => TrailingForeColor = s_trailingForeColor;
private void ResetTitleForeColor() => TitleForeColor = s_defaultTitleForeColor;
private void ResetTitleBackColor() => TitleBackColor = s_defaultTitleBackColor;
/// <summary>
/// Resets the "today"'s date. By default value, "today" is the
/// current date (and is automatically updated when the clock crosses
/// over to the next day).
/// If you set the today date yourself (using the TodayDate property)
/// the control will no longer automatically update the current day
/// for you. To re-enable this behavior, ResetTodayDate() is used.
/// </summary>
private void ResetTodayDate()
{
_todayDateSet = false;
UpdateTodayDate();
}
/// <summary>
/// Overrides Control.SetBoundsCore to enforce auto-sizing.
/// </summary>
protected override void SetBoundsCore(int x, int y, int width, int height, BoundsSpecified specified)
{
Rectangle oldBounds = Bounds;
Size max = SystemInformation.MaxWindowTrackSize;
// Second argument to GetPreferredWidth and GetPreferredHeight is a boolean specifying
// if we should update the number of rows/columns.
// We only want to update the number of rows/columns if we are not currently being scaled.
bool updateRowsAndColumns = !ScaleHelper.IsScalingRequirementMet || !ScalingInProgress;
if (width != oldBounds.Width)
{
if (width > max.Width)
{
width = max.Width;
}
width = GetPreferredWidth(width, updateRowsAndColumns);
}
if (height != oldBounds.Height)
{
if (height > max.Height)
{
height = max.Height;
}
height = GetPreferredHeight(height, updateRowsAndColumns);
}
base.SetBoundsCore(x, y, width, height, specified);
}
/// <summary>
/// If the handle has been created, this applies the color to the control
/// </summary>
private void SetControlColor(uint colorIndex, Color value)
{
if (IsHandleCreated)
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETCOLOR, (WPARAM)(int)colorIndex, (LPARAM)value);
}
}
/// <summary>
/// Updates the window handle with the min/max ranges if it has been created.
/// </summary>
private void SetRange()
{
SetRange(DateTimePicker.EffectiveMinDate(_minDate), DateTimePicker.EffectiveMaxDate(_maxDate));
}
private void SetRange(DateTime minDate, DateTime maxDate)
{
// Keep selection range within passed in minDate and maxDate
if (_selectionStart < minDate)
{
_selectionStart = minDate;
}
if (_selectionStart > maxDate)
{
_selectionStart = maxDate;
}
if (_selectionEnd < minDate)
{
_selectionEnd = minDate;
}
if (_selectionEnd > maxDate)
{
_selectionEnd = maxDate;
}
if (_selectionStart > _focusedDate)
{
_focusedDate = _selectionStart.Date;
}
if (_selectionEnd < _focusedDate)
{
_focusedDate = _selectionEnd.Date;
}
SetSelRange(_selectionStart, _selectionEnd);
// Updated the calendar range
if (IsHandleCreated)
{
Span<SYSTEMTIME> times = [(SYSTEMTIME)minDate, (SYSTEMTIME)maxDate];
uint flags = PInvoke.GDTR_MIN | PInvoke.GDTR_MAX;
if (PInvokeCore.SendMessage(this, PInvoke.MCM_SETRANGE, (WPARAM)flags, ref times[0]) == 0)
{
throw new InvalidOperationException(
string.Format(SR.MonthCalendarRange, minDate.ToShortDateString(), maxDate.ToShortDateString()));
}
UpdateDisplayRange();
}
}
/// <summary>
/// Sets the number of columns and rows to display.
/// </summary>
public void SetCalendarDimensions(int x, int y)
{
if (x < 1)
{
throw new ArgumentOutOfRangeException(nameof(x), string.Format(SR.MonthCalendarInvalidDimensions, (x).ToString("D", CultureInfo.CurrentCulture), (y).ToString("D", CultureInfo.CurrentCulture)));
}
if (y < 1)
{
throw new ArgumentOutOfRangeException(nameof(y), string.Format(SR.MonthCalendarInvalidDimensions, (x).ToString("D", CultureInfo.CurrentCulture), (y).ToString("D", CultureInfo.CurrentCulture)));
}
// MonthCalendar limits the dimensions to x * y <= 12
// i.e. a maximum of twelve months can be displayed at a time
// The following code emulates what is done inside monthcalendar (in comctl32.dll):
// The dimensions are gradually reduced until the inequality above holds.
//
while (x * y > 12)
{
if (x > y)
{
x--;
}
else
{
y--;
}
}
if (_dimensions.Width != x || _dimensions.Height != y)
{
_dimensions.Width = x;
_dimensions.Height = y;
AdjustSize();
}
}
/// <summary>
/// Sets date as the current selected date. The start and begin of
/// the selection range will both be equal to date.
/// </summary>
public void SetDate(DateTime date)
{
ArgumentOutOfRangeException.ThrowIfLessThan(date, _minDate);
ArgumentOutOfRangeException.ThrowIfGreaterThan(date, _maxDate);
SetSelectionRange(date, date);
}
/// <summary>
/// Update states of displayed dates. Make the native control redraw bolded dates.
/// </summary>
private unsafe void SetMonthViewBoldedDates()
{
Debug.Assert(_mcCurView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH, "This logic should work only in the Month view.");
// Get the first and the last visible dates even they are in not fully displayed months
SelectionRange displayRange = GetDisplayRange(false);
int monthsCount = GetMonthsCountOfRange(displayRange);
// Create a special collection for storage states of dates of some displayed month.
// This collection will be send to Windows to update displayed dates states - bolded/unbolded.
Span<uint> monthDayStates = stackalloc uint[monthsCount];
// Run through all displayed bolded dates and fill the Span collection
FillMonthDayStates(monthDayStates, displayRange);
fixed (uint* arr = monthDayStates)
{
// Update display dates states.
// For more info see docs: https://docs.microsoft.com/windows/win32/controls/mcm-setdaystate
PInvokeCore.SendMessage(HWND, PInvoke.MCM_SETDAYSTATE, (WPARAM)monthsCount, (LPARAM)arr);
}
}
/// <summary>
/// Sets the selection for a month calendar control to a given date range.
/// The selection range will not be set if the selection range exceeds the
/// maximum selection count.
/// </summary>
public void SetSelectionRange(DateTime date1, DateTime date2)
{
// Keep the dates within the min and max dates
ArgumentOutOfRangeException.ThrowIfLessThan(date1, _minDate);
ArgumentOutOfRangeException.ThrowIfGreaterThan(date1, _maxDate);
ArgumentOutOfRangeException.ThrowIfLessThan(date2, _minDate);
ArgumentOutOfRangeException.ThrowIfGreaterThan(date2, _maxDate);
// If date1 > date2, we just select date2 (compat)
if (date1 > date2)
{
date2 = date1;
}
// If the range exceeds maxSelectionCount, compare with the previous range and adjust whichever
// limit hasn't changed.
if ((date2 - date1).Days >= _maxSelectionCount)
{
if (date1.Ticks == _selectionStart.Ticks)
{
// Bring start date forward.
date1 = date2.AddDays(1 - _maxSelectionCount);
}
else
{
// Bring end date back.
date2 = date1.AddDays(_maxSelectionCount - 1);
}
}
// Set the range.
SetSelRange(date1, date2);
}
/// <summary>
/// <paramref name="upper"/> must be greater than <paramref name="lower"/>.
/// </summary>
private void SetSelRange(DateTime lower, DateTime upper)
{
Debug.Assert(lower.Ticks <= upper.Ticks, "lower must be less than upper");
bool changed = false;
if (_selectionStart != lower || _selectionEnd != upper)
{
changed = true;
_selectionStart = lower;
_selectionEnd = upper;
}
// Always set the value on the control, to ensure that it is up to date.
if (IsHandleCreated)
{
Span<SYSTEMTIME> times = [(SYSTEMTIME)lower, (SYSTEMTIME)upper];
PInvokeCore.SendMessage(this, PInvoke.MCM_SETSELRANGE, 0, ref times[0]);
}
if (changed)
{
OnDateChanged(new DateRangeEventArgs(lower, upper));
}
}
private bool ShouldSerializeAnnuallyBoldedDates()
=> _annualBoldDates.Count > 0;
private bool ShouldSerializeBoldedDates()
=> _boldDates.Count > 0;
private bool ShouldSerializeCalendarDimensions()
=> !_dimensions.Equals(new Size(1, 1));
private bool ShouldSerializeTrailingForeColor()
=> !TrailingForeColor.Equals(s_trailingForeColor);
private bool ShouldSerializeTitleForeColor()
=> !TitleForeColor.Equals(s_defaultTitleForeColor);
private bool ShouldSerializeTitleBackColor()
=> !TitleBackColor.Equals(s_defaultTitleBackColor);
private bool ShouldSerializeMonthlyBoldedDates()
=> _monthlyBoldDates.Count > 0;
/// <summary>
/// Retrieves true if the maxDate should be persisted in code gen.
/// </summary>
private bool ShouldSerializeMaxDate()
=> _maxDate != DateTimePicker.MaximumDateTime && _maxDate != DateTime.MaxValue;
/// <summary>
/// Retrieves true if the minDate should be persisted in code gen.
/// </summary>
private bool ShouldSerializeMinDate()
=> _minDate != DateTimePicker.MinimumDateTime && _minDate != DateTime.MinValue;
/// <summary>
/// Retrieves true if the selectionRange should be persisted in code gen.
/// </summary>
private bool ShouldSerializeSelectionRange()
=> !DateTime.Equals(_selectionEnd, _selectionStart);
/// <summary>
/// Retrieves true if the todayDate should be persisted in code gen.
/// </summary>
private bool ShouldSerializeTodayDate() => _todayDateSet;
/// <summary>
/// Returns a string representation for this control.
/// </summary>
public override string ToString()
{
string s = base.ToString();
return $"{s}, {SelectionRange}";
}
/// <summary>
/// Forces month calendar to display the current set of bolded dates.
/// </summary>
public void UpdateBoldedDates()
{
if (IsHandleCreated && _mcCurView == MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH)
{
// Use a specific implementation in the Month view
// to avoid MonthCalendar display dates range resets
// and flickers when a MonthCalendar shows several months.
SetMonthViewBoldedDates();
}
}
/// <summary>
/// Updates the current display range of the calendar.
/// Includes gray dates of previous and next calendars.
/// This method is called when Size, MaxDate, MinDate, TodayDate,
/// the current calendar view are changed and
/// if Next and Previous buttons in the title is clicked.
/// </summary>
private void UpdateDisplayRange()
{
if (!IsHandleCreated)
{
return;
}
SelectionRange newRange = GetDisplayRange(false);
if (_currentDisplayRange is null)
{
_currentDisplayRange = newRange;
return;
}
if (_currentDisplayRange.Start != newRange.Start || _currentDisplayRange.End != newRange.End)
{
_currentDisplayRange = newRange;
OnDisplayRangeChanged(EventArgs.Empty);
}
}
/// <summary>
/// Updates the current setting for "TODAY" in the MonthCalendar control
/// If the today date is set, the control will be set to that. Otherwise,
/// it will be set to null (running clock mode - the today date will be
/// automatically updated).
/// </summary>
private void UpdateTodayDate()
{
if (IsHandleCreated)
{
if (_todayDateSet)
{
SYSTEMTIME systemTime = (SYSTEMTIME)_todaysDate;
PInvokeCore.SendMessage(this, PInvoke.MCM_SETTODAY, 0, ref systemTime);
}
else
{
PInvokeCore.SendMessage(this, PInvoke.MCM_SETTODAY, 0, 0);
}
}
}
private void MarshaledUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs pref)
{
try
{
// Use BeginInvoke instead of Invoke in case the destination thread is not processing messages.
BeginInvoke(new UserPreferenceChangedEventHandler(UserPreferenceChanged), [sender, pref]);
}
catch (InvalidOperationException)
{
// If the destination thread does not exist, don't send.
}
}
private void UserPreferenceChanged(object sender, UserPreferenceChangedEventArgs pref)
{
if (pref.Category == UserPreferenceCategory.Locale)
{
// We need to recreate the monthcalendar handle when the locale changes, because
// the day names etc. are only updated on a handle recreate (comctl32 limitation).
RecreateHandle();
}
}
/// <summary>
/// Handles the MCN_SELCHANGE notification
/// </summary>
private unsafe void WmDateChanged(ref Message m)
{
NMSELCHANGE* nmmcsc = (NMSELCHANGE*)(nint)m.LParamInternal;
DateTime start = (DateTime)nmmcsc->stSelStart;
DateTime end = (DateTime)nmmcsc->stSelEnd;
// Windows doesn't provide API to get the focused cell.
// To get the correct focused cell consider 3 cases:
// 1) One cell is selected:
// In this case, the previous _selectionStart value changes.
// Moreover the selected start equals the end. So, take the start value as focused.
// 2) Several cells are selected. Forward selection:
// It means that a user selects cells to the right (moves from early dates to late).
// In this case, the previous _selectionStart value doesn't change (start == _selectionStart)
// and _selectionEnd changes. So, the focused date is the end of the selection range.
// 3) Several cells are selected. Backward selection:
// It means that a user selects cells to the left (moves from late dates to early).
// In this case, the previous _selectionStart value changes (start != _selectionStart)
// and _selectionEnd doesn't change. So, the focused date is the start of the selection range.
_focusedDate = start == _selectionStart ? end.Date : start.Date;
_selectionStart = start;
_selectionEnd = end;
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
AccessibilityNotifyClients(AccessibleEvents.ValueChange, -1);
// We should use the Date for comparison in this case. The user can work in the calendar only with dates,
// while the minimum / maximum date can contain the date and custom time, which, when comparing Ticks,
// may lead to incorrect calculation.
if (start.Date < _minDate.Date || end.Date < _minDate.Date)
{
// When calendar control is switched from a date display mode to year, decade or century mode, displayed range
// is changed proportional to the scale change. Thus we need to enforce user-defined selection again.
SetSelRange(_minDate, _minDate);
}
else if (start.Date > _maxDate.Date || end.Date > _maxDate.Date)
{
SetSelRange(_maxDate, _maxDate);
}
if (IsHandleCreated)
{
UpdateDisplayRange();
}
if (IsAccessibilityObjectCreated)
{
MonthCalendarAccessibleObject calendarAccessibleObject = (MonthCalendarAccessibleObject)AccessibilityObject;
calendarAccessibleObject.RaiseAutomationEventForChild(UIA_EVENT_ID.UIA_AutomationFocusChangedEventId);
}
OnDateChanged(new DateRangeEventArgs(start, end));
}
/// <summary>
/// Handles the MCN_GETDAYSTATE notification by returning an array of bitmasks, one entry per month,
/// that specifies which dates to display in bold.
/// </summary>
private unsafe void WmDateBold(ref Message m)
{
NMDAYSTATE* nmmcds = (NMDAYSTATE*)(nint)m.LParamInternal;
Span<int> boldDates = new((int*)nmmcds->prgDayState, nmmcds->cDayState);
WriteBoldDates(boldDates);
}
/// <summary>
/// Handles the MCN_VIEWCHANGE notification
/// </summary>
private unsafe void WmCalViewChanged(ref Message m)
{
NMVIEWCHANGE* nmmcvm = (NMVIEWCHANGE*)(nint)m.LParamInternal;
Debug.Assert(_mcCurView == nmmcvm->dwOldView, "Calendar view mode is out of sync with native control");
if (_mcCurView != nmmcvm->dwNewView)
{
_mcOldView = _mcCurView;
_mcCurView = nmmcvm->dwNewView;
OnCalendarViewChanged(EventArgs.Empty);
AccessibilityNotifyClients(AccessibleEvents.ValueChange, -1);
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
}
}
/// <summary>
/// Handles the MCN_SELECT notification.
/// </summary>
private unsafe void WmDateSelected(ref Message m)
{
NMSELCHANGE* nmmcsc = (NMSELCHANGE*)(nint)m.LParamInternal;
DateTime start = _selectionStart = (DateTime)nmmcsc->stSelStart;
DateTime end = _selectionEnd = (DateTime)nmmcsc->stSelEnd;
AccessibilityNotifyClients(AccessibleEvents.NameChange, -1);
AccessibilityNotifyClients(AccessibleEvents.ValueChange, -1);
// We should use the Date for comparison in this case. The user can work in the calendar only with dates,
// while the minimum / maximum date can contain the date and custom time, which, when comparing Ticks,
// may lead to incorrect calculation.
if (start.Date < _minDate.Date || end.Date < _minDate.Date)
{
// When calendar control is switched from a date display mode to year, decade or century mode, displayed range
// is changed proportional to the scale change. Thus we need to enforce user-defined selection again.
SetSelRange(_minDate, _minDate);
}
else if (start.Date > _maxDate.Date || end.Date > _maxDate.Date)
{
SetSelRange(_maxDate, _maxDate);
}
OnDateSelected(new DateRangeEventArgs(start, end));
}
/// <summary>
/// Handles the WM_GETDLGCODE message.
/// </summary>
private static void WmGetDlgCode(ref Message m)
{
// The MonthCalendar does its own handling of arrow keys.
m.ResultInternal = (LRESULT)(nint)PInvoke.DLGC_WANTARROWS;
}
/// <summary>
/// Handles the WM_COMMAND messages reflected from the parent control.
/// </summary>
private unsafe void WmReflectCommand(ref Message m)
{
if (m.HWnd == Handle)
{
NMHDR* nmhdr = (NMHDR*)(nint)m.LParamInternal;
switch (nmhdr->code)
{
case PInvoke.MCN_SELECT:
WmDateSelected(ref m);
break;
case PInvoke.MCN_SELCHANGE:
WmDateChanged(ref m);
break;
case PInvoke.MCN_GETDAYSTATE:
WmDateBold(ref m);
UpdateDisplayRange();
break;
case PInvoke.MCN_VIEWCHANGE:
WmCalViewChanged(ref m);
break;
}
}
}
protected override void WndProc(ref Message m)
{
switch (m.MsgInternal)
{
case PInvokeCore.WM_LBUTTONDOWN:
Focus();
if (!ValidationCancelled)
{
base.WndProc(ref m);
}
break;
case PInvokeCore.WM_GETDLGCODE:
WmGetDlgCode(ref m);
break;
case MessageId.WM_REFLECT_NOTIFY:
WmReflectCommand(ref m);
base.WndProc(ref m);
break;
case PInvokeCore.WM_PAINT:
base.WndProc(ref m);
if (_mcCurView != MONTH_CALDENDAR_MESSAGES_VIEW.MCMV_MONTH)
{
// Check if the display range is changed and update it.
// Win32 doesn't provide a notification about the display range is changed,
// so we have to use PInvokeCore.WM_PAINT and check it manually in the Year, Decade and Century views.
// MCN.GETDAYSTATE handles the display range changes in the Month view.
UpdateDisplayRange();
}
break;
default:
base.WndProc(ref m);
break;
}
}
}
|