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)
{
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)
{
string title = dialog.AppointmentTitle;
DateTime dateStart = dialog.AppointmentDateStart;
DateTime dateEnd = dialog.AppointmentDateEnd;
e.Control.Appointments.Add(new ExtendedAppointment() {
Subject = title, DateStart = dateStart, DateEnd = dateEnd });
weekView1.RefreshAppointments();
monthView1.RefreshAppointments();
dayView1.RefreshAppointments();
dayView2.RefreshAppointments();
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
{
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)
{
}
protected override void OnPaint(PaintEventArgs e)
{
}
}
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