// ----------------------------------------------------------------------------- // 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); } } }