Introduction
I wanted to use a WPF Calendar and highlight and mark individual dates of interest. I searched the web, but couldn't find any working solutions.
I found "RedLetterDays" from Microsoft: http://msdn.microsoft.com/en-us/magazine/dd882520.aspx#id0430067,
but that only works with WPF Toolkit and it is not very configurable. This solution doesn't rely on modifying the calendar, instead it modifies the background.
Using the code
Using the code is quite easy and is done in four steps:
- Setting up the Calendar
- Setting up icons
- Setting dates
- Updating background when changing date
The XAML part is nothing more than a basic Calendar.
<Grid>
<Calendar Name="Kalender" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
</Grid>
The CalendarBackground
is declared in the class. The background class is initialized with a reference to the
Calendar
class. This is done to be able to access DisplayDate
.
private readonly CalenderBackground background;
background = new CalenderBackground(Kalender);
First you must configure the icons you whish to use in the background. AddOverlay
is called with an ID and a filename for the image.
The images are 21x16 pixels with transparent backgrounds. The spacing between the rows in the Calendar varies between 15 and 16 pixels.
background.AddOverlay("circle", "circle.png");
background.AddOverlay("tjek", "tjek.png");
background.AddOverlay("cross", "cross.png");
background.AddOverlay("box", "box.png");
background.AddOverlay("gray", "gray.png");
Next you can add dates and the ID for the image you want to show on that date. It is possible to add more than one icon to one date.
The overlay is done semi transparent so the icons are visible even if stacked.
background.AddDate(new DateTime(2013, 02, 20), "tjek");
background.AddDate(new DateTime(2013, 02, 17), "tjek");
background.AddDate(new DateTime(2013, 02, 12), "tjek");
background.AddDate(new DateTime(2013, 02, 13), "tjek");
background.AddDate(new DateTime(2013, 02, 14), "tjek");
background.AddDate(new DateTime(2013, 02, 15), "tjek");
background.AddDate(new DateTime(2013, 02, 15), "circle");
background.AddDate(new DateTime(2013, 03, 01), "circle");
background.AddDate(new DateTime(2013, 03, 02), "circle");
background.AddDate(new DateTime(2013, 02, 10), "cross");
If you want you can set an option to mark the weekends.
background.grayoutweekends = "gray";
Assign the output from the class as the background for the Calendar.
Create a DisplayDateChanged
eventhandler to handle the update of the background when changing dates in the Calendar.
Kalender.Background = background.GetBackground();
Kalender.DisplayDateChanged += KalenderOnDisplayDateChanged;
private void KalenderOnDisplayDateChanged(object sender, CalendarDateChangedEventArgs calendarDateChangedEventArgs)
{
Kalender.Background = background.GetBackground();
}
The real "magic" takes place in the CalenderBackground
class.
First I must calculate the first shown date (January 28. in the screenshot above). The code handles Monday and Sunday as first day of week.
As I know on which weekday the first day of the month is, I am able to calculate which days is the first shown.
DateTime displaydate = _calendar.DisplayDate;
var firstdayofmonth = new DateTime(displaydate.Year, displaydate.Month, 1);
var dayofweek = (int) firstdayofmonth.DayOfWeek;
if (dayofweek == 0) dayofweek = 7; if (dayofweek == (int)_calendar.FirstDayOfWeek) dayofweek = 8; if (_calendar.FirstDayOfWeek == DayOfWeek.Sunday) dayofweek += 1;
DateTime firstdate = firstdayofmonth.AddDays(-((Double) dayofweek) + 1);
I create a default background with shading behind the month/year.
var rtBitmap = new RenderTargetBitmap( 178 , 160 ,
96 , 96 , PixelFormats.Default);
var drawVisual = new DrawingVisual();
using (DrawingContext dc = drawVisual.RenderOpen())
{
var backGroundBrush = new LinearGradientBrush();
backGroundBrush.StartPoint = new Point(0.5, 0);
backGroundBrush.EndPoint = new Point(0.5, 1);
backGroundBrush.GradientStops.Add(new GradientStop((Color) ColorConverter.ConvertFromString ("#FFE4EAF0"), 0.0));
backGroundBrush.GradientStops.Add(new GradientStop((Color) ColorConverter.ConvertFromString ("#FFECF0F4"), 0.16));
backGroundBrush.GradientStops.Add(new GradientStop((Color) ColorConverter.ConvertFromString ("#FFFCFCFD"), 0.16));
backGroundBrush.GradientStops.Add(new GradientStop((Color) ColorConverter.ConvertFromString ("#FFFFFFFF"), 1));
dc.DrawRectangle(backGroundBrush, null, new Rect(0, 0, rtBitmap.Width, rtBitmap.Height));
}
rtBitmap.Render(drawVisual);
The final and most important part iterates through the 7 columns and 6 rows of the Calendar. As I know the first shown date,
I can traverse the dates and when I have a match in the "datelist" I can add the overlay at the calculated position.
This is done drawing a rectangle with the content from the overlay.
using (DrawingContext dc = drawVisual.RenderOpen())
{
for (int y = 0; y < 6; y++)
for (int x = 0; x < 7; x++)
{
int xpos = x*21 + 17;
int ypos = y*16 + 50;
foreach (string overlayid in datelist.Where(c => c.date == firstdate).Select(c => c.overlay))
{
if (overlayid != null)
{
overlay overlay = overlays.Where(c => c.id == overlayid).FirstOrDefault();
dc.DrawRectangle(overlay.Brush, null ,
new Rect(xpos, ypos, overlay.BitMap.Width, overlay.BitMap.Height));
}
}
firstdate = firstdate.AddDays(1);
}
}
rtBitmap.Render(drawVisual);
var brush = new ImageBrush(rtBitmap); return brush;
Voila. An ease way to make a custom calendar.
Future improvements
One nice feature to add would be a mouseover tooltip with an explanation on why the date is highlighted.
History