// -----------------------------------------------------------------------------
// CalendarControl.cs
//
// A reusable WinForms control that renders a grid of month calendars with GDI+.
// From bohemianbuddha.com — Steven Koch's workshop.
//
// Copyright (c) 2026 Steven Koch
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
// -----------------------------------------------------------------------------
using System;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
namespace BohemianBuddha.Controls
{
///
/// A reusable control that renders one or more month calendars in a grid.
/// Owns its GDI resources and disposes them properly.
///
public class CalendarControl : Control
{
private int _calendarsWide = 1;
private int _calendarsHigh = 3;
private int _squareSize = 30;
private DateTime _startMonth = DateTime.Now;
private const int BoxesWide = 7; // days of week
private const int BoxesHigh = 8; // 2 header rows + 6 week rows
private const int Padding = 10;
private const int Margin = 5;
private readonly string[] _dayNames = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" };
// Owned GDI resources
private Pen _blackPen;
private Pen _whitePen;
private Brush _blackBrush;
private Brush _weekendBrush;
private Font _cellFont;
private StringFormat _centered;
public CalendarControl()
{
// Reduce flicker on repaint.
SetStyle(ControlStyles.AllPaintingInWmPaint
| ControlStyles.OptimizedDoubleBuffer
| ControlStyles.UserPaint
| ControlStyles.ResizeRedraw, true);
_blackPen = new Pen(Color.Black);
_whitePen = new Pen(Color.White);
_blackBrush = new SolidBrush(Color.Black);
_weekendBrush = new SolidBrush(Color.LightBlue);
_cellFont = new Font("Courier", 8, FontStyle.Regular);
_centered = new StringFormat
{
LineAlignment = StringAlignment.Center,
Alignment = StringAlignment.Center
};
BackColor = SystemColors.Control;
}
[Category("Calendar"), DefaultValue(1)]
public int CalendarsWide
{
get => _calendarsWide;
set { _calendarsWide = Math.Max(1, value); Invalidate(); }
}
[Category("Calendar"), DefaultValue(3)]
public int CalendarsHigh
{
get => _calendarsHigh;
set { _calendarsHigh = Math.Max(1, value); Invalidate(); }
}
[Category("Calendar"), DefaultValue(30)]
public int SquareSize
{
get => _squareSize;
set { _squareSize = Math.Max(10, value); Invalidate(); }
}
[Category("Calendar")]
public DateTime StartMonth
{
get => _startMonth;
set { _startMonth = value; Invalidate(); }
}
/// Preferred size to fit all calendars without clipping.
public Size PreferredCalendarSize
{
get
{
int w = Margin + _calendarsWide * (BoxesWide * _squareSize + Padding);
int h = Margin + _calendarsHigh * (BoxesHigh * _squareSize + Padding);
return new Size(w, h);
}
}
protected override void OnPaint(PaintEventArgs e)
{
base.OnPaint(e);
Graphics g = e.Graphics; // owned by WinForms — do NOT dispose
int startX = Margin;
int startY = Margin;
int counter = 0;
for (int row = 0; row < _calendarsHigh; row++)
{
int rowStartX = startX;
for (int col = 0; col < _calendarsWide; col++)
{
DateTime month = _startMonth.AddMonths(counter);
DrawGrid(g, startX, startY);
WriteMonthHeader(g, startX, startY, month);
WriteDayHeader(g, startX, startY);
WriteDays(g, startX, startY, month);
startX += BoxesWide * _squareSize + Padding;
counter++;
}
startX = rowStartX;
startY += BoxesHigh * _squareSize + Padding;
}
}
private void WriteDays(Graphics g, int startX, int startY, DateTime d)
{
int daysInMonth = DateTime.DaysInMonth(d.Year, d.Month);
var rect = new Rectangle
{
Y = startY + _squareSize * 2, // account for the two header rows
Width = _squareSize,
Height = _squareSize
};
for (int day = 1; day <= daysInMonth; day++)
{
var date = new DateTime(d.Year, d.Month, day);
int dayOfWeek = (int)date.DayOfWeek;
rect.X = startX + dayOfWeek * _squareSize;
g.DrawString(day.ToString(), _cellFont, _blackBrush, rect, _centered);
if (dayOfWeek == 6) // Saturday — wrap to next week row
rect.Y += _squareSize;
}
}
private void WriteMonthHeader(Graphics g, int startX, int startY, DateTime month)
{
var rect = new Rectangle(startX, startY, _squareSize * BoxesWide, _squareSize);
g.DrawString(month.ToString("MMMM yyyy"), _cellFont, _blackBrush, rect, _centered);
}
private void WriteDayHeader(Graphics g, int startX, int startY)
{
var rect = new Rectangle(startX, startY + _squareSize, _squareSize, _squareSize);
foreach (var day in _dayNames)
{
g.DrawString(day, _cellFont, _blackBrush, rect, _centered);
rect.X += _squareSize;
}
}
private void DrawGrid(Graphics g, int startX, int startY)
{
int size = _squareSize;
int lenX = BoxesWide * size + startX;
int lenY = BoxesHigh * size + startY;
// Weekend columns (below the two header rows).
g.FillRectangle(_weekendBrush,
new Rectangle(startX, startY + 2 * size, size, lenY - 2 * size - startY)); // Sunday
g.FillRectangle(_weekendBrush,
new Rectangle(startX + 6 * size, startY + 2 * size, size, lenY - 2 * size - startY)); // Saturday
// Dark grid lines (with a 1px offset for the raised bevel look).
int x = startX, y = startY;
for (int i = 0; i < BoxesHigh + 1; i++)
{
g.DrawLine(_blackPen, x - 1, y - 1, lenX - 1, y - 1);
y += size;
}
x = startX; y = startY;
for (int i = 0; i < BoxesWide + 1; i++)
{
if (i == 0 || i == BoxesWide)
g.DrawLine(_blackPen, x - 1, y - 1, x - 1, lenY - 1);
else
g.DrawLine(_blackPen, x - 1, y + size - 1, x - 1, lenY - 1);
x += size;
}
// White highlight lines.
x = startX; y = startY;
for (int i = 0; i < BoxesHigh + 1; i++)
{
g.DrawLine(_whitePen, x, y, lenX, y);
y += size;
}
x = startX; y = startY;
for (int i = 0; i < BoxesWide + 1; i++)
{
g.DrawLine(_whitePen, x, y + size, x, lenY);
x += size;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_blackPen?.Dispose();
_whitePen?.Dispose();
_blackBrush?.Dispose();
_weekendBrush?.Dispose();
_cellFont?.Dispose();
_centered?.Dispose();
}
base.Dispose(disposing);
}
}
}