How can I restrict the Month Calendar (C# WinForms) to just the month view?

193 Views Asked by At

I have a form where my user will pick a year from a numeric up-down, and that will restrict the month calendar to December of that year. This makes the year, decade etc views of the calendar pointless, and I want to disable clicking on the month title to get to this view.

I have tried using the min and max values for the calendar, and while this disabled the month left and right buttons, I can still click on the month title, and it ends up looking like this: result of clicking on month title.

There also does not seem to be a calendar mode property/attribute to handle or a title click event.

Apologies if I have missed something obvious, I am pretty inexperienced with WinForms.

2

There are 2 best solutions below

2
Olivier Jacot-Descombes On

You create you own calendar control deriving from MonthCalendar and then intercept the low-level messages sent to the control to suppress mouse events.

More specifically, you can suppress the WM_LBUTTONDOWN event from happening when title background is being clicked.

public class RestrictedMonthCalendar : MonthCalendar
{
    protected override void WndProc(ref Message m)
    {
        const int WM_LBUTTONDOWN = 0x0201;

        if (m.Msg == WM_LBUTTONDOWN &&
            HitTest(GetPoint(m.LParam)).HitArea == HitArea.TitleBackground)
        {
            return;
        }
        base.WndProc(ref m);
    }

    private static Point GetPoint(nint _xy)
    {
        unchecked {
            uint xy = IntPtr.Size == 8 ? (uint)_xy.ToInt64() : (uint)_xy.ToInt32();
            int x = (short)xy;
            int y = (short)(xy >> 16);
            return new Point(x, y);
        }
    }
}

Note that once you have compiled this code, this control will appear in the controls toolbox and you can drag and drop it to your form.

Alternatively, you can close the form designer and save the form. Then open yourForm.Designer.cs where yourForm is the name of your form. In there replace MonthCalendar by RestrictedMonthCalendar (it appears twice) and save.

0
IV. On

The behavior that you're describing is a significant departure from the normal functioning of a MonthCalendar, and Olivier's answer does a terrific job of explaining how to intercept Win32 messages if your strategy is to disable the functionality that you understandably deem "pointless" using this approach.

The reason I'm posting another answer is to offer a perspective that it's fairly straightforward to make your own control in the first place. This way you can make it do exactly what you want in terms of style and function.

If you're open to 'not' basing your solution on MonthControl, you might want to experiment with using TableLayoutPanel controls to simplify making the grids you need.

designer

The grid will need to be provisioned with blank labels as a canvas, but it's way easier to do this programmatically in the constructor of the UserControl than to do it by hand in the designer.

public DecemberOnlyCalendar()
{
    InitializeComponent();
    buttonYearDown.Click += (sender, e) => Year--;
    buttonYearUp.Click += (sender, e) => Year++;
    labelYear.DataBindings.Add("Text", this, "FormattedYear", true, DataSourceUpdateMode.OnPropertyChanged);
    for (int col = 0; col < 7; col++) for (int row = 3; row < 9; row++)
        {
            var label = new Label
            {
                TextAlign = ContentAlignment.MiddleCenter,
                Padding = new Padding(0),
                Margin = new Padding(0),
                BackColor = Color.White,
                Dock = DockStyle.Fill,
            };
            label.Click += LabelClicked;
            label.Paint += (sender, e) =>
            {
                if(
                    SelectedDay is DateTime date &&     
                    sender is Label label && 
                    int.TryParse(label.Text, out int day) &&
                    date.Day == day)
                {
                    var rect = new Rectangle(new Point(), e.ClipRectangle.Size);
                    rect.Inflate(-5, -5);
                    e.Graphics.DrawRectangle(Pens.Red, rect);
                }
            };
            tableLayoutPanel.Controls.Add(label, col, row);
        }
    Year = DateTime.Today.Year;
}

In this minimal proof of concept, the left and right arrows decrement or increment the Year property, showing the previous/next December only.

runtime

Changes are handled by first clearing all the numbers from all the days and then rebuilding the month of December based on the day of the week that theFirst falls on.

private void OnYearChanged()
{
    Clear();
    var theFirst = new DateTime(Year, 12, 1);
    var dayOfWeek = theFirst.DayOfWeek;
    var dayIterator = theFirst;
    int row = 3;
    int col = (int)dayOfWeek;
    while (dayIterator.Month == 12)
    {
        if (tableLayoutPanel.GetControlFromPosition(col, row) is Label label)
        {
            label.Text = $"{dayIterator.Day}";
            col = (col + 1) % 7;
            if (col == 0) row++;
        }
        dayIterator = dayIterator + TimeSpan.FromDays(1);
    };
}
private void Clear()
{
    SelectedDay = null;
    for (int col = 0; col < 7; col++) for (int row = 2; row < 9; row++)
        {
            if (tableLayoutPanel.GetControlFromPosition(col, row) is Label label)
                label.Text = string.Empty;
        }
}

I made a GitHub repo while testing the answer. You can browse or clone for full details, and it also shows how to make a picker version that utilizes the same UserControl.

picker version