This Custom Control tweaks a little the standard DateTimePicker to get a Year or Month selection style only.
▶ The standard DateTimePicker's CustomFormat
and Format
properties are disabled, setting internally the former to yyyy
or MMMM
(a simple modification can add different formats) and the latter to DateTimePickerFormat.Custom
. These properties are hidden from the PropertyGrid an cannot be changed.
▶ The browsing functionality is maintained, but limited to the Decade/Year or Decade/Year/Month selection.
Clicking the DTP's Title Area, brings on the Decade selector and the previous and next Buttons are of course functional (these can only show years).
▶ The DTP is closed and the current value set when a MCN_VIEWCHANGE
notification reveals, passing the current selection level in a NMVIEWCHANGE
structure, that the current selection has reached the View Mode set by the SelectionMode
property.
This property value is an enumerator which, in turn, reflects the MonthCalendar's MCM_SETCURRENTVIEW
message values.
▶ The current View is set sending a MCM_SETCURRENTVIEW
message to the MonthCalendar control, changing the default View to MCMV_DECADE
or MCMV_YEAR
(depending on the current SelectionMode
) each time the MonthCalendar control is shown. The opening animation is then preserved.
▶ The only style changed is MCS_NOTODAY
, set in the OnHandleCreated
method. It can be switched on/off at any time, calling the ShowMonCalToday()
method.
This style shows the Today
date, at the bottom of the DateTimerPicker. It sets the current Year or Month value when clicked.
This is how it works:
Tested on VisualStudio 2017.
.Net Framework 4.8 (only).
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[DesignerCategory("Code")]
public class MonthYearPicker : DateTimePicker
{
private string m_CustomFormat = "yyyy";
private DateTimePickerFormat m_Format = DateTimePickerFormat.Custom;
private SelectionViewMode m_SelectionMode = SelectionViewMode.Year;
private bool m_ShowToday = false;
private IntPtr hWndCal = IntPtr.Zero;
public MonthYearPicker() {
base.CustomFormat = m_CustomFormat;
base.Format = m_Format;
}
[DefaultValue(SelectionViewMode.Year)]
[Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
[Category("Appearance"), Description("Set the current selection mode to either Month or Year")]
public SelectionViewMode SelectionMode {
get => m_SelectionMode;
set {
if (value != m_SelectionMode) {
m_SelectionMode = value;
m_CustomFormat = m_SelectionMode == SelectionViewMode.Year ? "yyyy" : "MMMM";
base.CustomFormat = m_CustomFormat;
}
}
}
[DefaultValue(false)]
[Browsable(true), EditorBrowsable(EditorBrowsableState.Always)]
[Category("Appearance"), Description("Shows or hides \"Today\" date at the bottom of the Calendar Control")]
public bool ShowToday {
get => m_ShowToday;
set {
if (value != m_ShowToday) {
m_ShowToday = value;
ShowMonCalToday(m_ShowToday);
}
}
}
[DefaultValue("yyyy")]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new string CustomFormat {
get => base.CustomFormat;
set => base.CustomFormat = m_CustomFormat;
}
[DefaultValue(DateTimePickerFormat.Custom)]
[Browsable(false), EditorBrowsable(EditorBrowsableState.Never)]
public new DateTimePickerFormat Format {
get => base.Format;
set => base.Format = m_Format;
}
protected override void OnHandleCreated(EventArgs e)
{
base.OnHandleCreated(e);
ShowMonCalToday(m_ShowToday);
}
protected override void OnDropDown(EventArgs e)
{
hWndCal = SendMessage(this.Handle, DTM_GETMONTHCAL, 0, 0);
if (hWndCal != IntPtr.Zero) {
SendMessage(hWndCal, MCM_SETCURRENTVIEW, 0, (int)(MonCalStyles)m_SelectionMode);
}
base.OnDropDown(e);
}
private void ShowMonCalToday(bool show)
{
int styles = SendMessage(this.Handle, DTM_GETMCSTYLE, 0, 0).ToInt32();
styles = show ? styles &~(int)MonCalStyles.MCS_NOTODAY : styles | (int)MonCalStyles.MCS_NOTODAY;
SendMessage(this.Handle, DTM_SETMCSTYLE, 0, styles);
}
protected override void WndProc(ref Message m)
{
switch (m.Msg) {
case WM_NOTIFY:
var nmh = (NMHDR)m.GetLParam(typeof(NMHDR));
switch (nmh.code) {
case MCN_VIEWCHANGE:
var nmView = (NMVIEWCHANGE)m.GetLParam(typeof(NMVIEWCHANGE));
if (nmView.dwNewView < (MonCalView)m_SelectionMode) {
SendMessage(this.Handle, DTM_CLOSEMONTHCAL, 0, 0);
}
break;
default:
// NOP: Add more notifications handlers...
break;
}
break;
default:
// NOP: Add more message handlers...
break;
}
base.WndProc(ref m);
}
public enum SelectionViewMode : int
{
Month = MonCalView.MCMV_YEAR,
Year = MonCalView.MCMV_DECADE,
}
// Move to a NativeMethods class, eventually
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
internal static extern IntPtr SendMessage(IntPtr hWnd, int Msg, int wParam, int lParam);
internal const int WM_NOTIFY = 0x004E;
internal const int MCN_VIEWCHANGE = -750;
internal const int DTM_FIRST = 0x1000;
internal const int DTM_GETMONTHCAL = DTM_FIRST + 8;
internal const int DTM_SETMCSTYLE = DTM_FIRST + 11;
internal const int DTM_GETMCSTYLE = DTM_FIRST + 12;
internal const int DTM_CLOSEMONTHCAL = DTM_FIRST + 13;
internal const int MCM_FIRST = 0x1000;
internal const int MCM_GETCURRENTVIEW = MCM_FIRST + 22;
internal const int MCM_SETCURRENTVIEW = MCM_FIRST + 32;
[StructLayout(LayoutKind.Sequential)]
internal struct NMHDR
{
public IntPtr hwndFrom;
public UIntPtr idFrom;
public int code;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NMVIEWCHANGE
{
public NMHDR nmhdr;
public MonCalView dwOldView;
public MonCalView dwNewView;
}
internal enum MonCalView : int
{
MCMV_MONTH = 0,
MCMV_YEAR = 1,
MCMV_DECADE = 2,
MCMV_CENTURY = 3
}
internal enum MonCalStyles : int
{
MCS_DAYSTATE = 0x0001,
MCS_MULTISELECT = 0x0002,
MCS_WEEKNUMBERS = 0x0004,
MCS_NOTODAYCIRCLE = 0x0008,
MCS_NOTODAY = 0x0010,
MCS_NOTRAILINGDATES = 0x0040,
MCS_SHORTDAYSOFWEEK = 0x0080,
MCS_NOSELCHANGEONNAV = 0x0100
}
}