Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Day, Week, and Month Calendar Controls

0.00/5 (No votes)
9 May 2011 1  
Windows forms calendar controls for showing a series of appointments similar to Outlook
screen1.png - Click to enlarge image

Introduction

This control suite is a prototype set of calendar controls for a Windows Forms .NET application. They were created as a proof-of-concept that an Outlook style calendar could be integrated into a large Windows Forms application without the overhead of using a third party library.
Three controls are included - DayScheduleControl, WeekScheduleControl and MonthScheduleControl. The controls render a series of appointments in different layouts. I am unlikely to have time to enhance it in the near future, so I will release them in the hope that they will be useful to others.

DayScheduleControl is the closest in appearance to Outlook. It supports one-day and five-day views.

WeekScheduleControl displays a 7-day week. Unlike DayScheduleControl, it does not show hour slots.

MonthScheduleControl displays a whole month of appointments.

Using the Code

The class Form1 in the included test project contains sample usage of the controls. First, it has code to create a list of random appointments, and set the controls to show the current date. The CreateRandomAppointments method loads up a couple of months worth of random dummy data.

DateTime weekstart = DateTime.Now;
AppointmentList appts = CreateRandomAppointments(weekstart);
weekView1.Date = weekstart;
weekView1.Appointments = appts;
monthView1.Date = weekstart;
monthView1.Appointments = appts;
dayView1.Date = weekstart;
dayView1.Appointments = appts;
dayView2.Date = weekstart;
dayView2.Appointments = appts;

Rather than CreateRandomAppointments, in a normal app, you would build up an AppointmentList and add Appointment objects something like the following, using your own data as a source.

var appts = new AppointmentList(); 
ExtendedAppointment app = new ExtendedAppointment();
app.ColorBlockBrush = Brushes.Red;
app.Subject = "A sample appointment";
app.DateStart = DateTime.Now.AddMinutes(30);
app.DateEnd = DateTime.Now.AddMinutes(60);  
appts.Add(app); 

Second, the create, move and edit events are wired up to demo methods.

weekView1.AppointmentCreate += calendar_AppointmentAdd;
monthView1.AppointmentCreate += calendar_AppointmentAdd;
dayView1.AppointmentCreate += calendar_AppointmentAdd;
dayView2.AppointmentCreate += calendar_AppointmentAdd;

weekView1.AppointmentMove += calendar_AppointmentMove;
monthView1.AppointmentMove += calendar_AppointmentMove;
dayView1.AppointmentMove += calendar_AppointmentMove;
dayView2.AppointmentMove += calendar_AppointmentMove;

weekView1.AppointmentEdit += calendar_AppointmentEdit;
monthView1.AppointmentEdit += calendar_AppointmentEdit;
dayView1.AppointmentEdit += calendar_AppointmentEdit;
dayView2.AppointmentEdit += calendar_AppointmentEdit;

When the user interacts with a calendar appointment, the event is fired so we pop up a custom dialog to deal with it. NewAppointment in this case is a dialog for entering the title, start and end dates of an appointment. The MoveAppointment and EditAppointment dialogs are pretty similar in what they do.

private void calendar_AppointmentAdd(object sender, AppointmentCreateEventArgs e)
{
    //show a dialog to add an appointment
    using (NewAppointment dialog = new NewAppointment())
    {
        if (e.Date != null)
        {
            dialog.AppointmentDateStart = e.Date.Value;
            dialog.AppointmentDateEnd = e.Date.Value.AddMinutes(15);
        }
        DialogResult result = dialog.ShowDialog();
        if (result == DialogResult.OK)
        {
            //if the user clicked 'save', save the new appointment 
            string title = dialog.AppointmentTitle;
            DateTime dateStart = dialog.AppointmentDateStart;
            DateTime dateEnd = dialog.AppointmentDateEnd;
            e.Control.Appointments.Add(new ExtendedAppointment() { 
            Subject = title, DateStart = dateStart, DateEnd = dateEnd });

            //have to tell the controls to refresh appointment display
            weekView1.RefreshAppointments();
            monthView1.RefreshAppointments();
            dayView1.RefreshAppointments();
            dayView2.RefreshAppointments();

            //get the controls to repaint 
            weekView1.Invalidate();
            monthView1.Invalidate();
            dayView1.Invalidate();
            dayView2.Invalidate();
        }
    }
}

The ExtendedAppointment class is for adding additional properties to appointments. Because this is located in the test application along with the dialogs for create/move/edit, you can add more fields to the dialogs without having to go into the guts of the SheduleControls assembly.

How It Works

The requirements for these controls were:

  • Support Windows 7 styles
  • Support keyboard access
  • Be accessible to the disabled
  • Don't use much memory

As much of the code for the three controls is similar, they all inherit from BaseScheduleControl. This base control handles drag and drop, the hidden DataGridView of appointments, and mouse click events. Much of the code is for linking keyboard operations in the hidden DataGridView to the UI display (e.g., a selected appointment), and vice versa with mouse operations.

namespace Syd.ScheduleControls
{
    /// <summary>
    /// The BaseScheduleControl defines properties common to 
    /// the three schedule controls. 
    /// </summary>
    public  partial class BaseScheduleControl : Control, 
    System.ComponentModel.ISupportInitialize
    { 

BaseScheduleControl has a DataGridView control in order to save time setting up the keyboard access and the accessibility features. The grid object is of type HiddenGrid, which extends DataGridView but overrides the OnPaint and OnPaintBackground events so that the control is not visible. The grid is exposed to child controls as the AppointmentGrid property.

internal class HiddenGrid : DataGridView
{
    protected override void OnPaintBackground(PaintEventArgs pevent)
    {
        //Don't paint anything
    }
    protected override void OnPaint(PaintEventArgs e)
    {
        //Don't paint anything
    }
}

The VisualStyleRenderer is used to pick up the current Windows theme and display with the colours from that. If visual styles are disabled, the regular windows colours are used. All rendering operations are wrapped up in the RendererCache class, which paints everything including the days, their titles to the appointments. RendererCache holds a series of IRenderer objects which are objects that can draw boxes with text or borders (everything painted on the controls is basically a box).

internal class RendererCache
{
    //...
    private readonly IRenderer bigHeaderRenderWrap = null;
    private readonly IRenderer headerRenderWrap = null;
    private readonly IRenderer headerRenderSelWrap = null;
    private readonly IRenderer appointmentRenderWrap = null;
    private readonly IRenderer appointmentRenderSelWrap = null;
    private readonly IRenderer controlRenderWrap = null;
    private readonly IRenderer bodyRenderWrap = null;
    private readonly IRenderer bodyLightRenderWrap = null;

There are different implementations of IRenderer depending on whether visual styles are enabled. If the code was running with visual styles turned on, the IRenderer for a header would be initialised as follows (VisualStyleWrapper implements IRenderer):

        private readonly VisualStyleRenderer headerRender = null;
        private readonly VisualStyleElement headerElement = 
		VisualStyleElement.ExplorerBar.NormalGroupHead.Normal;
 //...
        headerRender = new VisualStyleRenderer(headerElement);
        headerRenderWrap = new VisualStyleWrapper
		(headerRender, SystemPens.ControlDarkDark);

However if visual styles were turned off, a simpler implementation of IRenderer called NonVisualStyleWrapper would be used. The NonVisualStyleWrapper just uses system colour brushes and maybe a bit of gradient fill to render days and appointments on the control.

headerRenderWrap = new NonVisualStyleWrapper(SystemColors.ControlText, 
        SystemBrushes.ControlText, 
        SystemColors.Control, 
        SystemBrushes.Control, 
        SystemColors.ControlLightLight, 
        SystemBrushes.ControlLightLight, 
        SystemPens.ControlText);
       ((NonVisualStyleWrapper)headerRenderWrap).NoGradientBlend=true;

RendererCache is a singleton and is used by all three controls in the OnPaint event. The following is a sample of how it is used to draw a header box for a day.

RendererCache.Current.Header.DrawBox
(e.Graphics, Font, day.TitleBounds, day.FormattedName);

Speed was the primary consideration in the design of the app, so there isn't much usage of events. Likewise, day and appointment items are not controls themselves - that would be slow to render.

Instead of using lots of controls, the lists of days/hours and appointments have their screen real estate calculated in one hit, and they are all painted by the parent cont. Days are wrapped up in DayRegion objects, which contain the name of the day and its bounds. Appointments are wrapped up in AppointmentRegion objects, which contain the Appointment and its bounds. The IRegion interface defines the common property to all these regions - the bounds.

internal interface IRegion
{
    Rectangle Bounds { get; set; }
}

The size and shape of the day and hour regions is figured out by the CalculateTimeSlotBounds method. This method is called from OnPaint only when the property BoundsValidTimeSlot is set to false (this property is set to false in cases such as when the control has been resized or the date shown has changed). It is overridden in all three controls, as all three have different layouts of days.

protected override void CalculateTimeSlotBounds(Graphics g)

The size and shape of the appointments is figured out by the CalculateAppointmentBounds method. This ensures that all the appointments fit into their owning day or hour, if possible, and handles any other calculations such as overlaps. This method is called from OnPaint only when the property BoundsValidAppointment is set to false (this property is set to false in cases such as when the control has been resized or the appointment list has changed).

protected override void CalculateAppointmentBounds(Graphics g)

The controls don't include default dialogs for creating/moving/editing appointments, but the demo project includes sample dialogs wired up to those three events.

Future Enhancements

Missing features in the current version:

  • Lots more stuff on the controls should be configurable in properties
  • Support for full-day or multiple-day appointments
  • Tooltips when you hover over an appointment (to display the full subject)
  • XP, high contrast mode, high DPI support
  • DayScheduleControl doesn't support weekends, out-of-hours appointments, or scrolling
  • Controls haven't been tested under a screen reader, the DataGridView may not have the properties set for it to be readable
  • The DayScheduleControl handles overlapping appointments, but the maths it uses isn't very good
  • Time slot navigation/selection with keyboard or mouse
  • Better highlight of the current day

History

  • Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here