Click here to Skip to main content
Click here to Skip to main content

Day, Week, and Month Calendar Controls

By , 9 May 2011
 
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, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)

About the Author

A. Sydney
Australia Australia
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionSelectedAppointment Fixmembercjb11011 Feb '13 - 23:19 
I just tracked down a minor bug, where the SelectedAppointment was being set without any user interaction.
 
Change the RefreshAppointments method to:
        
public void RefreshAppointments()
{
    //sort the appointments by date
    Appointments.SortAppointments();
 
    BoundsValidAppointment = false;
    IsUpdating = true;  //stop the Grid SelectionChanged setting a SelectedAppointment
    AppointmentGrid.DataSource = Appointments as List<Appointment>;
}
When the DataSource was set the grd_SelectionChanged event was firing and setting the SelectedAppointment to the first in the DataSource D'Oh! | :doh:
Generalopening the projectmembervjkmr575718 Dec '12 - 20:08 
While am running the project am getting the errors.that i could not open the outside class library..can u please make it clear to me
GeneralMy vote of 3membershareque27 Jun '12 - 20:51 
nice
Questionselected appointmentmemberhozone3 Apr '12 - 2:04 
5+
is there a way to get the reference of the selected appointment?
QuestionAppointmentRegion Boundsmembercjb11015 Feb '12 - 2:27 
Although it was probably my data at fault (I had 0 length appointments), the Bounds code will return 0 height rectangles, which then causes exceptions in the other drawing code.
 
I added a crude + 1 to the height terms...but there's probly a more elegant fix Big Grin | :-D
GeneralMy vote of 5membercjb11015 Feb '12 - 2:22 
Excellent control and article.
QuestionWhy am I getting index out of bounds errorsmemberJim Wilkie8 Feb '12 - 10:07 
When I inhibited the create random appointments inorder to create my own I get index out of bounds errors from the OnMouseClick event of the base schedule control. I thought I was doing something wrong in my use of the controls, but when you stop the est app from creating the appointments it happens too. Add an appointment to the day tab, then switch to week and click on the appointment. most of the time it will throw an unhandled excpetion.
 
Pity. Really good looking controls.
GeneralMy vote of 5memberenxx19847 Feb '12 - 20:35 
nice job!
QuestionHow to use this control with DBmemberMostafaHamdy29 Jan '12 - 3:38 
Hi
really it's so interesting control and really amzing me , but I have question , how can I add appointment to DB and get them again from DB to display to the control
regards
GeneralMy vote of 5memberclprogrammer1 Dec '11 - 6:34 
Great work! Was easy to customize too.

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web01 | 2.6.130523.1 | Last Updated 9 May 2011
Article Copyright 2011 by A. Sydney
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid