/* Copyright 2012 Justin LeCheminant This file is part of WindowsFormsCalendar. indowsFormsCalendar is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. indowsFormsCalendar is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with indowsFormsCalendar. If not, see . */ using System; using System.Collections.Generic; using System.Text; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace WindowsFormsCalendar { /// /// Hosts a month-level calendar where user can select day-based dates /// [DefaultEvent("SelectionChanged")] public class MonthView : ContainerControl { #region Fields private int _forwardMonthIndex; private MonthViewDay _lastHitted; private bool _mouseDown; private Size _daySize; private DateTime _selectionStart; private DateTime _selectionEnd; private string _monthTitleFormat; private DayOfWeek _weekStart; private DayOfWeek _workWeekStart; private DayOfWeek _workWeekEnd; private MonthViewSelection _selectionMode; private string _dayNamesFormat; private bool _dayNamesVisible; private int _dayNamesLength; private DateTime _viewStart; private Size _monthSize; private MonthViewMonth[] _months; private Padding _itemPadding; private Color _monthTitleColor; private Color _monthTitleColorInactive; private Color _monthTitleTextColor; private Color _monthTitleTextColorInactive; private Color _dayBackgroundColor; private Color _daySelectedBackgroundColor; private Color _dayTextColor; private Color _daySelectedTextColor; private Color _arrowsColor; private Color _arrowsSelectedColor; private Color _dayGrayedText; private Color _todayBorderColor; private int _maxSelectionCount; private Rectangle _forwardButtonBounds; private bool _forwardButtonSelected; private Rectangle _backwardButtonBounds; private bool _backwardButtonSelected; #endregion #region Events /// /// Occurs when selection has changed. /// public event EventHandler SelectionChanged; #endregion #region Properties /// /// Gets the size of days rectangles /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public Size DaySize { get { return _daySize; } } /// /// Gets or sets the format of day names /// [DefaultValue( "ddd" )] public string DayNamesFormat { get { return _dayNamesFormat; } set { _dayNamesFormat = value; } } /// /// Gets or sets a value indicating if day names should be visible /// [DefaultValue( true )] public bool DayNamesVisible { get { return _dayNamesVisible; } set { _dayNamesVisible = value; } } /// /// Gets or sets how many characters of day names should be displayed /// [DefaultValue( 2 )] public int DayNamesLength { get { return _dayNamesLength; } set { _dayNamesLength = value; UpdateMonths(); } } /// /// Gets or sets what the first day of weeks should be /// [DefaultValue( DayOfWeek.Sunday )] public DayOfWeek FirstDayOfWeek { get { return _weekStart; } set { _weekStart = value; } } /// /// Gets a value indicating if the backward button is selected /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public bool BackwardButtonSelected { get { return _backwardButtonSelected; } } /// /// Gets the bounds of the backward button /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public Rectangle BackwardButtonBounds { get { return _backwardButtonBounds; } } /// /// Gets a value indicating if the forward button is selected /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public bool ForwardButtonSelected { get { return _forwardButtonSelected; } } /// /// Gets the bounds of the forward button /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public Rectangle ForwardButtonBounds { get { return _forwardButtonBounds; } } /// /// Gets or sets the Font of the Control /// public override Font Font { get { return base.Font; } set { base.Font = value; UpdateMonthSize(); UpdateMonths(); } } /// /// Gets or sets the internal padding of items (Days, day names, month names) /// public Padding ItemPadding { get { return _itemPadding; } set { _itemPadding = value; } } /// /// Gets or sets the maximum selection count of days /// [DefaultValue( 0 )] public int MaxSelectionCount { get { return _maxSelectionCount; } set { _maxSelectionCount = value; } } /// /// Gets the Months currently displayed on the calendar /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public MonthViewMonth[] Months { get { return _months; } } /// /// Gets the size of an entire month inside the /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public Size MonthSize { get { return _monthSize; } } /// /// Gets or sets the format of month titles /// [DefaultValue( "MMMM yyyy" )] public string MonthTitleFormat { get { return _monthTitleFormat; } set { _monthTitleFormat = value; UpdateMonths(); } } /// /// Gets or sets the start of selection /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public DateTime SelectionStart { get { return _selectionStart; } set { if( MaxSelectionCount > 0 ) { if( Math.Abs( value.Subtract( SelectionEnd ).TotalDays ) >= MaxSelectionCount ) { return; } } _selectionStart = value; Invalidate(); OnSelectionChanged( EventArgs.Empty ); } } /// /// Gets or sets the end of selection /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public DateTime SelectionEnd { get { return _selectionEnd; } set { if( MaxSelectionCount > 0 ) { if( Math.Abs( value.Subtract( SelectionStart ).TotalDays ) >= MaxSelectionCount ) { return; } } _selectionEnd = value.Date.Add( new TimeSpan( 23, 59, 59 ) ); Invalidate(); OnSelectionChanged( EventArgs.Empty ); } } /// /// Gets or sets the selection mode of /// [DefaultValue( MonthViewSelection.Manual )] public MonthViewSelection SelectionMode { get { return _selectionMode; } set { _selectionMode = value; } } /// /// Gets or sets the date of the first displayed month /// [DesignerSerializationVisibility( DesignerSerializationVisibility.Hidden )] public DateTime ViewStart { get { return _viewStart; } set { _viewStart = value; UpdateMonths(); Invalidate(); } } /// /// Gets the last day of the last month showed on the view. /// public DateTime ViewEnd { get { DateTime month = Months[Months.Length - 1].Date; return month.Date.AddDays( DateTime.DaysInMonth( month.Year, month.Month ) ); } } /// /// Gets or sets the day that starts a work-week /// [DefaultValue( DayOfWeek.Monday )] public DayOfWeek WorkWeekStart { get { return _workWeekStart; } set { _workWeekStart = value; } } /// /// Gets or sets the day that ends a work-week /// [DefaultValue( DayOfWeek.Friday )] public DayOfWeek WorkWeekEnd { get { return _workWeekEnd; } set { _workWeekEnd = value; } } #endregion #region Color Properties /// /// Gets or sets the color of the arrows selected. /// /// /// The color of the arrows selected. /// public Color ArrowsSelectedColor { get { return _arrowsSelectedColor; } set { _arrowsSelectedColor = value; } } /// /// Gets or sets the color of the arrows. /// /// /// The color of the arrows. /// public Color ArrowsColor { get { return _arrowsColor; } set { _arrowsColor = value; } } /// /// Gets or sets the color of the day selected text. /// /// /// The color of the day selected text. /// public Color DaySelectedTextColor { get { return _daySelectedTextColor; } set { _daySelectedTextColor = value; } } /// /// Gets or sets the color of the day selected. /// /// /// The color of the day selected. /// public Color DaySelectedColor { get { return _dayTextColor; } set { _dayTextColor = value; } } /// /// Gets or sets the color of the day selected background. /// /// /// The color of the day selected background. /// public Color DaySelectedBackgroundColor { get { return _daySelectedBackgroundColor; } set { _daySelectedBackgroundColor = value; } } /// /// Gets or sets the color of the day background. /// /// /// The color of the day background. /// public Color DayBackgroundColor { get { return _dayBackgroundColor; } set { _dayBackgroundColor = value; } } /// /// Gets or sets the day grayed text. /// /// /// The day grayed text. /// public Color DayGrayedText { get { return _dayGrayedText; } set { _dayGrayedText = value; } } /// /// Gets or sets the color of the month title. /// /// /// The color of the month title. /// public Color MonthTitleColor { get { return _monthTitleColor; } set { _monthTitleColor = value; } } /// /// Gets or sets the month title text color inactive. /// /// /// The month title text color inactive. /// public Color MonthTitleTextColorInactive { get { return _monthTitleTextColorInactive; } set { _monthTitleTextColorInactive = value; } } /// /// Gets or sets the color of the month title text. /// /// /// The color of the month title text. /// public Color MonthTitleTextColor { get { return _monthTitleTextColor; } set { _monthTitleTextColor = value; } } /// /// Gets or sets the month title color inactive. /// /// /// The month title color inactive. /// public Color MonthTitleColorInactive { get { return _monthTitleColorInactive; } set { _monthTitleColorInactive = value; } } /// /// Gets or sets the color of the today day border color /// public Color TodayBorderColor { get { return _todayBorderColor; } set { _todayBorderColor = value; } } #endregion /// /// Initializes a new instance of the class. /// public MonthView() { SetStyle(ControlStyles.Opaque, true); DoubleBuffered = true; _dayNamesFormat = "ddd"; _monthTitleFormat = "MMMM yyyy"; _selectionMode = MonthViewSelection.Manual; _workWeekStart = DayOfWeek.Monday; _workWeekEnd = DayOfWeek.Friday; _weekStart = DayOfWeek.Sunday; _dayNamesVisible = true; _dayNamesLength = 2; _viewStart = DateTime.Now; _itemPadding = new Padding(2); _monthTitleColor = SystemColors.ActiveCaption; _monthTitleColorInactive = SystemColors.InactiveCaption; _monthTitleTextColor = SystemColors.ActiveCaptionText; _monthTitleTextColorInactive = SystemColors.InactiveCaptionText; _dayBackgroundColor = Color.Empty; _daySelectedBackgroundColor = SystemColors.Highlight; _dayTextColor = SystemColors.WindowText; _daySelectedTextColor = SystemColors.HighlightText; _arrowsColor = SystemColors.Window; _arrowsSelectedColor = Color.Gold; _dayGrayedText = SystemColors.GrayText; _todayBorderColor = Color.Maroon; UpdateMonthSize(); UpdateMonths(); } #region Public Methods /// /// Checks if a day is hitted on the specified point /// /// /// public MonthViewDay HitTest(Point p) { for (int i = 0; i < Months.Length; i++) { if (Months[i].Bounds.Contains(p)) { for (int j = 0; j < Months[i].Days.Length; j++) { if (/*Months[i].Days[j].Visible && */Months[i].Days[j].Bounds.Contains(p)) { return Months[i].Days[j]; } } } } return null; } /// /// Moves the view one month forward /// public void GoForward() { ViewStart = ViewStart.AddMonths(1); } /// /// Moves the view one month backward /// public void GoBackward() { ViewStart = ViewStart.AddMonths(-1); } #endregion #region Private Methods /// /// Sets the forward button bounds. /// /// The bounds. private void SetForwardButtonBounds(Rectangle bounds) { _forwardButtonBounds = bounds; } /// /// Sets the backward button bounds. /// /// The bounds. private void SetBackwardButtonBounds(Rectangle bounds) { _backwardButtonBounds = bounds; } /// /// Sets the forward button selected. /// /// if set to true [selected]. private void SetForwardButtonSelected(bool selected) { _forwardButtonSelected = selected; Invalidate(ForwardButtonBounds); } /// /// Sets the backward button selected. /// /// if set to true [selected]. private void SetBackwardButtonSelected(bool selected) { _backwardButtonSelected = selected; Invalidate(BackwardButtonBounds); } /// /// Selects the week where the hit is contained /// /// private void SelectWeek(DateTime hit) { int preDays = (new int[] { 0, 1, 2, 3, 4, 5, 6 })[(int)hit.DayOfWeek] - (int)FirstDayOfWeek; _selectionStart = hit.AddDays(-preDays); SelectionEnd = SelectionStart.AddDays(6); } /// /// Selecs the work-week where the hit is contanied /// /// private void SelectWorkWeek(DateTime hit) { int preDays = (new int[] { 0, 1, 2, 3, 4, 5, 6 })[(int)hit.DayOfWeek] - (int)WorkWeekStart; _selectionStart = hit.AddDays(-preDays); SelectionEnd = SelectionStart.AddDays(Math.Abs(WorkWeekStart - WorkWeekEnd)); } /// /// Selecs the month where the hit is contanied /// /// private void SelectMonth(DateTime hit) { _selectionStart = new DateTime(hit.Year, hit.Month, 1); SelectionEnd = new DateTime(hit.Year, hit.Month, DateTime.DaysInMonth(hit.Year, hit.Month)); } /// /// Draws a box of text /// /// private void DrawBox(MonthViewBoxEventArgs e) { if (!e.BackgroundColor.IsEmpty) { using (SolidBrush b = new SolidBrush(e.BackgroundColor)) { e.Graphics.FillRectangle(b, e.Bounds); } } if (!e.TextColor.IsEmpty && !string.IsNullOrEmpty(e.Text)) { TextRenderer.DrawText(e.Graphics, e.Text, e.Font != null ? e.Font : Font, e.Bounds, e.TextColor, e.TextFlags); } if (!e.BorderColor.IsEmpty) { using (Pen p = new Pen(e.BorderColor)) { Rectangle r = e.Bounds; r.Width--; r.Height--; e.Graphics.DrawRectangle(p, r); } } } /// /// Updates the size of the month. /// private void UpdateMonthSize() { //One row of day names plus 31 possible numbers string[] strs = new string[7 + 31]; int maxWidth = 0; int maxHeight = 0; for (int i = 0; i < 7; i++) { strs[i] = ViewStart.AddDays(i).ToString(DayNamesFormat).Substring(0, DayNamesLength); } for (int i = 7; i < strs.Length; i++) { strs[i] = (i - 6).ToString(); } Font f = new Font(Font, FontStyle.Bold); for (int i = 0; i < strs.Length; i++) { Size s = TextRenderer.MeasureText(strs[i], f); maxWidth = Math.Max(s.Width, maxWidth); maxHeight = Math.Max(s.Height, maxHeight); } maxWidth += ItemPadding.Horizontal; maxHeight += ItemPadding.Vertical; _daySize = new Size(maxWidth, maxHeight); _monthSize = new Size(maxWidth * 7, maxHeight * 7 + maxHeight * (DayNamesVisible ? 1 : 0)); } /// /// Updates the months. /// private void UpdateMonths() { int gapping = 2; int calendarsX = Convert.ToInt32(Math.Max(Math.Floor((double)ClientSize.Width / (double)(MonthSize.Width + gapping)), 1.0)); int calendarsY = Convert.ToInt32(Math.Max(Math.Floor((double)ClientSize.Height / (double)(MonthSize.Height + gapping)), 1.0)); int calendars = calendarsX * calendarsY; int monthsWidth = (calendarsX * MonthSize.Width) + (calendarsX - 1) * gapping; int monthsHeight = (calendarsY * MonthSize.Height) + (calendarsY - 1) * gapping; int startX = (ClientSize.Width - monthsWidth) / 2; int startY = (ClientSize.Height - monthsHeight) / 2; int curX = startX; int curY = startY; _forwardMonthIndex = calendarsX - 1; _months = new MonthViewMonth[calendars]; for (int i = 0; i < Months.Length; i++) { Months[i] = new MonthViewMonth(this, ViewStart.AddMonths(i)); Months[i].SetLocation(new Point(curX, curY)); curX += gapping + MonthSize.Width; if ((i + 1) % calendarsX == 0) { curX = startX; curY += gapping + MonthSize.Height; } } MonthViewMonth first = Months[0]; MonthViewMonth last = Months[_forwardMonthIndex]; SetBackwardButtonBounds(new Rectangle(first.Bounds.Left + ItemPadding.Left, first.Bounds.Top + ItemPadding.Top, DaySize.Height - ItemPadding.Horizontal, DaySize.Height - ItemPadding.Vertical)); SetForwardButtonBounds(new Rectangle(first.Bounds.Right - ItemPadding.Right - BackwardButtonBounds.Width, first.Bounds.Top + ItemPadding.Top, BackwardButtonBounds.Width, BackwardButtonBounds.Height )); } #endregion #region Overrides /// /// Raises the event. /// /// An that contains the event data. protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); Invalidate(); } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); Focus(); _mouseDown = true; MonthViewDay day = HitTest(e.Location); if (day != null) { switch (SelectionMode) { case MonthViewSelection.Manual: case MonthViewSelection.Day: SelectionEnd = _selectionStart = day.Date; break; case MonthViewSelection.WorkWeek: SelectWorkWeek(day.Date); break; case MonthViewSelection.Week: SelectWeek(day.Date); break; case MonthViewSelection.Month: SelectMonth(day.Date); break; } } if (ForwardButtonSelected) { GoForward(); } else if (BackwardButtonSelected) { GoBackward(); } } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); if (_mouseDown) { MonthViewDay day = HitTest(e.Location); if (day != null && day != _lastHitted) { switch (SelectionMode) { case MonthViewSelection.Manual: if (day.Date > SelectionStart) { SelectionEnd = day.Date; } else { SelectionStart = day.Date; } break; case MonthViewSelection.Day: SelectionEnd = _selectionStart = day.Date; break; case MonthViewSelection.WorkWeek: SelectWorkWeek(day.Date); break; case MonthViewSelection.Week: SelectWeek(day.Date); break; case MonthViewSelection.Month: SelectMonth(day.Date); break; } _lastHitted = day; } } if (ForwardButtonBounds.Contains(e.Location)) { SetForwardButtonSelected(true); } else if (ForwardButtonSelected) { SetForwardButtonSelected(false); } if (BackwardButtonBounds.Contains(e.Location)) { SetBackwardButtonSelected(true); } else if (BackwardButtonSelected) { SetBackwardButtonSelected(false); } } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); _mouseDown = false; } /// /// Raises the event. /// /// A that contains the event data. protected override void OnMouseWheel(MouseEventArgs e) { base.OnMouseWheel(e); if (e.Delta < 0) { GoForward(); } else { GoBackward(); } } /// /// Raises the event. /// /// An that contains the event data. protected override void OnLostFocus(EventArgs e) { base.OnLostFocus(e); Invalidate(); } /// /// Raises the event. /// /// A that contains the event data. protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); e.Graphics.Clear(SystemColors.Window); for (int i = 0; i < Months.Length; i++) { if (Months[i].Bounds.IntersectsWith(e.ClipRectangle)) { #region MonthTitle string title = Months[i].Date.ToString(MonthTitleFormat); MonthViewBoxEventArgs evtTitle = new MonthViewBoxEventArgs(e.Graphics, Months[i].MonthNameBounds, title, Focused ? MonthTitleTextColor : MonthTitleTextColorInactive, Focused ? MonthTitleColor : MonthTitleColorInactive); DrawBox(evtTitle); #endregion #region DayNames for (int j = 0; j < Months[i].DayNamesBounds.Length; j++) { MonthViewBoxEventArgs evtDay = new MonthViewBoxEventArgs(e.Graphics, Months[i].DayNamesBounds[j], Months[i].DayHeaders[j], StringAlignment.Far, ForeColor, DayBackgroundColor); DrawBox(evtDay); } if (Months[i].DayNamesBounds != null && Months[i].DayNamesBounds.Length != 0) { using (Pen p = new Pen(MonthTitleColor)) { int y = Months[i].DayNamesBounds[0].Bottom; e.Graphics.DrawLine(p, new Point(Months[i].Bounds.X, y), new Point(Months[i].Bounds.Right, y)); } } #endregion #region Days foreach (MonthViewDay day in Months[i].Days) { if (!day.Visible) continue; MonthViewBoxEventArgs evtDay = new MonthViewBoxEventArgs(e.Graphics, day.Bounds, day.Date.Day.ToString(), StringAlignment.Far, day.Grayed ? DayGrayedText : (day.Selected ? DaySelectedTextColor : ForeColor), day.Selected ? DaySelectedBackgroundColor : DayBackgroundColor); if (day.Date.Equals(DateTime.Now.Date)) { evtDay.BorderColor = TodayBorderColor; } DrawBox(evtDay); } #endregion #region Arrows if (i == 0) { Rectangle r = BackwardButtonBounds; using (Brush b = new SolidBrush(BackwardButtonSelected ? ArrowsSelectedColor : ArrowsColor)) { e.Graphics.FillPolygon(b, new Point[] { new Point(r.Right, r.Top), new Point(r.Right, r.Bottom - 1), new Point(r.Left + r.Width / 2, r.Top + r.Height / 2), }); } } if (i == _forwardMonthIndex) { Rectangle r = ForwardButtonBounds; using (Brush b = new SolidBrush(ForwardButtonSelected ? ArrowsSelectedColor : ArrowsColor)) { e.Graphics.FillPolygon(b, new Point[] { new Point(r.X, r.Top), new Point(r.X, r.Bottom - 1), new Point(r.Left + r.Width / 2, r.Top + r.Height / 2), }); } } #endregion } } } /// /// Raises the event. /// /// An that contains the event data. protected override void OnResize(EventArgs e) { UpdateMonths(); Invalidate(); } /// /// Raises the event. /// /// The instance containing the event data. protected void OnSelectionChanged(EventArgs e) { if (SelectionChanged != null) { SelectionChanged(this, e); } } #endregion } }