Month Calendar
Month Calendar
I decided to write this control because I needed a month calendar that's much more
customizable and flexible than the standard Visual Studio Calendar, in one of my projects.
My primary focus was the ability to add information (color, text, image etc.) to each day,
and for this to work reasonably well, it must also be resizable.
This is my second article for CodeProject, and it shares some of the implementation
techniques of the previous article, so if you want some detail on the implementation of
themes, nested properties, and TypeEditors, check out my MozBar article. For information
about how to edit and persist collections, check out Daniel Zaharia's excellent article.
Like any other .NET control, for using in the IDE, you should add the MonthCalendar
control to the Toolbox panel. This can be accomplished by right-clicking on a Toolbox tab
and selecting "Add/Remove items...", browsing to the MonthCalendar assembly, and
selecting it. This will add the MonthCalendar control to the Toolbox so that it can be
dragged/dropped to a Windows Form.
Once an instance of MonthCalendar is added to the form, select it and view its properties.
The basic design for the control is pretty much like the standard Visual Studio Calendar,
but it is a lot more flexible. To make it more portable and to reduce the overhead,
everything is drawn directly with GDI. MonthCalendar basically consists of five different
regions, each with its own set of properties.
The only region that must be visible is the month region, all the others can be switched
on or off.
Properties
The following properties control the most significant behavior and the look and feel of the
MonthCalendar control:
Month
BackgroundImage: Image used as background.
Transparency: Text and background transparency (used when painting colors).
FormatTrailing: Indicates whether formatting should be shown for trailing dates
(default=true).
Padding: Vertical and horizontal spacing.
DateAlign: Date alignment within a day.
ShowMonthInDay: Indicates whether the first and the last day of a month should
have the month displayed in the date (default=false).
TextAlign: Text alignment within a day.
EnableImageClick: Enables the ImageClick event (default=false).
ImageAlign: Image alignment within a day.
DateFont: Font used for the date.
TextFont: Font used for the text.
Colors: Collection of colors used in the calendar.
BorderStyles: Collection of border styles used in the calendar.
Header
Align: Alignment of the month/text in the header.
MonthSelectors: Indicates whether the next/previous month button should be
visible (default=true).
YearSelectors: Indicates whether the next/previous year button should be visible
(default=false).
ShowMonth: Indicates if the month name should be visible in the header (default =
true).
Text: The text to be displayed in the header (if ShowMonth is false).
BackColor1: The color used for the header background.
BackColor2: The second color used for background when using a gradient.
GradientMode: Type of gradient used.
Font: The font used for the month/text.
TextColor: The color used for the month/text.
MonthContextMenu: Indicates whether a month selection menu should be
displayed while right-clicking the header (default = true).
Footer
Align: The text alignment for the footer.
Text: The text to be displayed in the footer, if ShowToday is false.
ShowToday: Indicates whether today's date should be displayed in the footer
(default=true).
Format: The format used for today's date.
BackColor1: The color used for the header background.
BackColor2: The second color used for background when using a gradient.
GradientMode: Type of gradient used.
Font: The font used for the date/text.
TextColor: The color used for the date/text.
Weekdays
BorderColor: The color used for the border.
BackColor1: The color used for the header background.
BackColor2: The second color used for background when using a gradient.
GradientMode: Type of gradient used.
Font: The font used for weekdays.
TextColor: The color used for weekdays.
Format: The format used to display weekdays.
Align: Text alignment for weekdays.
Weeknumbers
BorderColor: The color used for the border.
BackColor1: The color used for the header background.
BackColor2: The second color used for background when using a gradient.
GradientMode: Type of gradient used.
Font: The font used for week numbers.
TextColor: The text color used for week numbers.
Align: Text alignment for week numbers.
Events
Month
MonthChanged: Indicates that the active month was changed.
BeforeMonthChanged: Raised before a month is displayed. Use this event to
prevent the selection of certain months.
DayDragDrop: Indicates that data was dropped on a day, AllowDrop must be true
for this event to be raised.
ImageClick: Indicates that an image was clicked.
DayClick: Indicates that a day was clicked.
DayRender: Occurs before a date is updated.
DayQueryInfo: Occurs before a date is updated.
DayDoubleClick: Indicates that a day was double clicked.
DaySelected: Indicates that one or more days were selected.
BeforeDaySelected: Raised before a day is selected. Use this event to prevent
selection of days.
DayDeselected: Indicates that one or more days were deselected.
BeforeDayDeselected: Raised before a day is deselected. Use this event to
prevent deselection of days.
DayGotFocus: Indicates that a day received focus.
DayLostFocus: Indicates that a day lost focus.
DayMouseMove: Indicates that the mouse is moved inside a day.
Header
HeaderClick: Indicates that the header was clicked.
HeaderDoubleClick: Indicates that the header was double clicked.
HeaderMouseEnter: Indicates that the mouse entered the header region.
HeaderMouseLeave: Indicates that the mouse left the header region.
Footer
FooterClick: Indicates that the footer was clicked.
FooterDoubleClick: Indicates that the footer was double clicked.
FooterMouseEnter: Indicates that the mouse entered the footer region.
FooterMouseLeave: Indicates that the mouse left the footer region.
Weekday
WeekdayClick: Indicates that a weekday was clicked.
WeekdayDoubleClick: Indicates that a weekday was double clicked.
WeekdayMouseEnter: Indicates that the mouse entered the weekday region.
WeekdayMouseLeave: Indicates that the mouse left the weekday region.
Weeknumber
WeeknumberClick: Indicates that a week was clicked.
WeeknumberDoubleClick: Indicates that a week was double clicked.
WeeknumberMouseEnter: Indicates that the mouse entered the week number
region.
WeeknumberMouseLeave: Indicates that the mouse left the week number region.
As usual, when using events, the supplied eventArgs parameter contains the properties
useful for that particular event.
Methods
Date formatting
void AddDateInfo(DateItem[] info): Adds an array of formatted dates.
void RemoveDateInfo(DateTime dt): Removes the DateItem associated with the
supplied date.
void RemoveDateInfo(DateItem item): Removes the supplied DateItem.
void AddDateInfo(DateItem info): Adds a formatted date.
void ResetDateInfo(): Removes all date formatting.
DateItem[] GetDateInfo(): Returns all the formatted dates.
DateItem[] GetDateInfo(DateTime dt): Returns an array of DateItems
matching the supplied date.
Selection
void SelectDate(DateTime d): Selects the supplied date. The date must be
between MinDate and MaxDate.
void ClearSelection(): Clears the existing selection.
bool IsSelected(DateTime dt): Returns true if the supplied date is selected,
otherwise false.
void SelectArea(DateItem topleft, DateItem bottomright): Selects the
area described by the two dates.
void DeselectArea(DateItem topleft, DateItem bottomright): Deselects the
area described by the two dates.
void SelectRange(DateItem from, DateItem to): Selects the range described
by the two dates.
void DeselectRange(DateItem from, DateItem to): Deselects the range
described by the two dates.
void SelectWeek(int week): Selects every day in the supplied week.
void DeselectWeek(int week): Deselects every selected day in the supplied
week.
void SelectWeekday(DayOfWeek day): Selects every DayOfWeek in the current
month.
void DeselectWeekday(DayOfWeek day): Deselects every selected DayOfWeek in
the current month.
Misc.
void Print(): Prints the current calendar on the default printer.
Bitmap Snapshot(): Returns a bitmap of the current calendar.
void SaveAsImage(string filename, ImageFormat format): Saves the
calendar to a file.
void Copy(): Copies the calendar to the clipboard.
You can add formatted dates either during runtime by using one of the DateInfo
methods, or in design mode by using the built-in DateItemCollection.
Adding date formatting in design mode is, of course, not very practical for real
applications, your info will most likely be stored in some kind of database, so the best
way to add date formatting is to respond to the MonthChanged event and add the dates
during runtime:
To add formatted dates during runtime, you need to create DateItem objects. A DateItem
has several properties that define the appearance of the date:
monthCalendar1.AddDateInfo(d);
}
If you don't want to add a lot of DateItems at once, you can use the DayQueryInfo event
and add formatting depending on the date. The DayQueryInfo event is raised before
each date is rendered, and makes it possible to supply date formatting using the
DateItem properties.
Note that you must set OwnerDraw=true to add formatting, and the formatting added by
this event is overridden by the DayRender event if that is used.
Recurring dates
None: No recurring pattern is applied (default), the formatting is only valid for the
day specified by the Date property.
Daily: The formatting will be applied to every day within the Range.
Weekly: The formatting will be applied to every same weekday within the Range.
Monthly: The formatting will be applied to the same day each month within the
Range.
Yearly: The formatting will be applied to the same day every year within the
Range.
Note that there is no "collision detection", so overlapping patterns will be drawn on top of
each other. You can use functions like GetDateInfo to check which DateItems are
applied to a certain date. The Range and Pattern properties do not have any impact
when set through the DayQueryInfo event.
Owner-drawn dates
If the built-in formatting doesn't give you the appearance you want, then you can provide
your own by using the DayRender event. The DayRender event is raised before each date
in the calendar is painted. You can control the contents and formatting of a date by
providing code in the event handler for the DayRender event. To make the date owner-
drawn, set DayRenderEventArs.OwnerDraw=true:
Collapse
private void monthCalendar1_DayRender(object sender,
Pabo.Calendar.DayRenderEventArgs e)
{
Brush bgBrush = new SolidBrush(Color.White);
Brush dateBrush = new SolidBrush(Color.Black);
Font dateFont = new Font("Microsoft Sans Serif",(float)8.25);
StringFormat dateAlign = new StringFormat();
dateAlign.Alignment = StringAlignment.Far;
dateAlign.LineAlignment = StringAlignment.Near;
// Clean up
bgBrush.Dispose();
dateBrush.Dispose();
dateAlign.Dispose();
dateFont.Dispose();
}
Note that using the DayRender event to draw the appearance of a date completely
overrides all other formatting done by the control for the specific date.
One thing I wanted to add to the control was multi-select. The problem with this is that
when selected days have different colors for border and background, then it doesn't look
very nice drawing all the selected days individually, and drawing the days as selected will
also cover the previous backcolor; so what you have to do is draw the entire selected
area with one transparent background and one border ala Excel. So, how can we do this?
It's pretty simple. The month region basically consists of an array of days, each with its
own rectangle, so what we need is the start and the end day for the selection so that we
can construct our "selection" rectangle. When the user clicks on a day, we set the start
day (m_selStart). And when the mouse is pressed and moved over a new day (that can
be selected), we set the end day (m_selEnd). When drawing the selection, we only need
to get the coordinates for the selection area and draw it with the desired transparent
color.
Collapse
// Check if an selection exist
if (m_selectedDays.Length>0)
{
Brush selBrush = new SolidBrush(Color.FromArgb(125,
Colors.SelectedBackground));
// Draw selection
Rectangle selRect = new Rectangle(m_selLeft,m_selTop,
m_selRight-m_selLeft,m_selBottom-m_selTop);
e.FillRectangle(selBrush,selRect);
ControlPaint.DrawBorder(e,selRect,Colors.SelectedBorder,
BorderStyles.Selected);
selBrush.Dispose();
}
// Using SelectedDates
SelectedDatesCollection m_dates = monthCalendar1.SelectedDates;
}
Sometimes, you want to provide the user with a list of possible choices for a property.
One way to do this is to use enumerations which automatically provide you with a
dropdown with the possible choices. But this solution is static, the list will always have the
same content. What if you want the choices to change depending on other choices the
user makes?
The solution is to create a custom TypeConverter and override some functions that will
let us supply our own values through the StandardValuesCollection.
Now, all we have to do is override GetStandardValuesCollection and return the list that
we want to use:
You will also need to override OnConvertFrom and OnConvertTo to validate the input, and
if you don't inherit from StringConverter, you must override CanConvertFrom and
CanConvertTo as well.
Week number
Calculating week number is somewhat problematic since different countries in the world
have different rules associated with their calendars. In most parts of Europe, we calculate
week number according to the ISO8601 calendar, which means we use the
FirstFourDayWeek rule and Monday as the first day of the week. Using these settings and
the Swedish culture with the GetWeekOfYear function found in
System.Globalization.Calendar doesn't always give you the correct week (according
to ISO8601), so it seems there are bugs in the .NET week calculation routines.
Looking around, I found a webpage that describes this problem in more detail, and even
better, provides a function that works:
Collapse
private int GetISO8601WeekNumber(DateTime date)
{
// Calculates the ISO 8601 Weeknumber
// In this scenario the first day of the week is monday,
// and the week rule states that:
// [...] the first calendar week of a year is the one
// that includes the first Thursday of that year and
// [...] the last calendar week of a calendar year is
// the week immediately preceding the first
// calendar week of the next year.
// The first week of the year may thus start in the
// preceding year
if (m_calendar.m_dateTimeFormat.FirstDayOfWeek!=0)
{
//if( StartWeekDayOfYear == 0)
StartWeekDayOfYear = 8 - StartWeekDayOfYear;
//if( EndWeekDayOfYear == 0)
EndWeekDayOfYear = 8 - EndWeekDayOfYear;
}
If you use the ISO8601 calendar (most of Europe), MonthCalendar will give you a correct
week, if not, you will get the Microsoft week that might be right or wrong depending on
your culture setting.
If the above function is our callback, setting it up could look something like this:
Known problems/issues
Since this control uses an ImageList, it suffers from the infamous ImageList bug
that destroys the alpha channel for 32 bit images when you add images to the
ImageList in design mode. Workarounds for this could be to replace the
ImageList with a control like the ImageSet by Tom Guinter (that is available
here), or to add the images at run time. This is, hopefully, fixed in Visual Studio
2005 so it might not be worth the hassle.
Conclusion
I hope this control can be of use to you. I'm sure there is plenty of room for improvements
and added functionality, so if you have any ideas or suggestions, please post a comment.