Applies To:
FlexGrid for WinForms
Author:
John Juback
Published On:
3/16/2006
This article demonstrates some advanced features of the C1FlexGrid control, including owner-drawn cells, merged cell ranges, formatting with styles, custom cell selection, and tool tips. The accompanying sample project implements a derived control, C1FlexCalendar, that can switch between monthly and weekly views of the same data. By exposing a generic interface for appointments, the calendar can function without knowledge of the underlying data source.
The following illustration shows the C1FlexCalendar control in monthly view. The title row contains two navigation buttons for moving backward or forward one month at a time. A bold data cell indicates today's date, and a khaki background color indicates that one or more appointments have been scheduled for that date. When the mouse hovers over one of these cells, that day's appointments and their start times are displayed in a tooltip.
The appointments themselves are entered in weekly view. The sample project contains a context menu for toggling the control's View property. In weekly view, the user can select a cell or range of cells, then type the text of the appointment. For ranges that span multiple rows in the same column (day), the appointment is rendered as a single cell with a white background. Entering an empty string deletes the appointment(s) that span the selected range.
Note that the same control instance is used to render both views. Changing the value of the View property merely triggers different owner-draw logic when the control is repainted.
The C1FlexCalendar control class derives from C1FlexGrid (the LicenseProvider attribute is needed to suppress the licensing dialog at run time):
using C1.Win.C1FlexGrid;
namespace C1FlexCalendar
{
[LicenseProvider(typeof(System.ComponentModel.LicenseProvider))]
public partial class C1FlexCalendar : C1FlexGrid
{
...
}
}
In addition to the properties inherited from C1FlexGrid, the C1FlexCalendar control implements the following public properties, which are available at design time:
View
A ViewEnum value, either Monthly or Weekly.
FirstDayOfWeek
A Day value representing the first day of the week.
ShowTitle
A boolean that controls whether the title/navigation row should be shown.
ShowDayNames
A boolean that controls whether the names of the days of the week should be shown.
SelectionRange
A SelectionRange object representing the DateTime range corresponding to the selected cell(s) in the calendar.
The C1FlexCalendar control also implements the following public properties, which are available at runtime only:
SelectionStart
Sets or gets a DateTime value representing the starting point of the selection, normalized to 12:00 AM.
SelectionEnd
Sets or gets a DateTime value representing the ending point of the selection, normalized to 12:00 AM.
TimeStart
Gets a DateTime value representing the starting point of the selection, including the time.
TimeEnd
Gets a DateTime value representing the ending point of the selection, including the time.
TaskList
Sets or gets an ArrayList of objects that implement the ITaskItem interface.
SelectedTaskList
Gets an ArrayList of objects contained in TaskList that are within the current SelectionRange.
The ITaskItem interface represents task/appointment data entered by the user in weekly view. Applications that use C1FlexCalendar should implement a class that supports this interface. For an example, see the Appointment class in the sample project.
public interface ITaskItem
{
string Task { get; }
DateTime Start { get; }
DateTime Finish { get; }
}
The C1FlexCalendar control also provides the following public method:
GetSelectionSize
Returns a Size object that specifies the width and height of the selected cells, in pixels.
The constructor for C1FlexCalendar disables resizing and sorting, enables owner draw, and adds two navigation buttons (previous and next) to the grid's Controls collection. The buttons are actually Label controls that display a single character from a symbol font, Wingdings 3.
public C1FlexCalendar()
{
// disable resizing and sorting
AllowResizing = AllowResizingEnum.None;
AllowSorting = AllowSortingEnum.None;
// enable owner draw
DrawMode = DrawModeEnum.OwnerDraw;
// add navigation buttons
\_btnPrev.ImageAlign = \_btnNext.ImageAlign = ContentAlignment.MiddleCenter;
\_btnPrev.BackColor = \_btnNext.BackColor = Color.Transparent;
\_btnPrev.Click = new EventHandler(\_btnPrevNext_Click);
\_btnNext.Click = new EventHandler(\_btnPrevNext_Click);
\_btnPrev.DoubleClick = new EventHandler(\_btnPrevNext_Click);
\_btnNext.DoubleClick = new EventHandler(\_btnPrevNext_Click);
_btnPrev.Font = new Font("Wingdings 3", 16);
\_btnNext.Font = \_btnPrev.Font;
_btnPrev.Text = "|"; // left triangle
_btnNext.Text = "}"; // right triangle
Controls.Add(_btnPrev);
Controls.Add(_btnNext);
// create styles for owner-drawn cells
SetupStyles();
// finish initialization
InitializeComponent();
}
The SetupStyles method creates several CellStyle objects that are used to apply formatting to different parts of the calendar. For example, the Title style is applied to row 0, which contains the title string and the navigation buttons. Other styles are applied to CellRange objects returned by the grid's GetCellRange method. The following named styles are created:
Title
Title string and navigation buttons (row 0)
DayNames
Days of the week in monthly view (row 1)
WeekDayNames
Days of the week in weekly view (row 1)
Hours
Hourly time slots in weekly view (column 0)
GrayDays
Days outside of the current month in monthly view
Weekends
Saturdays and Sundays in monthly view
Today
The cell representing today's date in monthly view
FreeTime
Empty cells before 8:00 AM and after 5:00 PM in weekly view
WorkTime
Empty cells between 8:00 AM and 5:00 PM in weekly view
The code for the SetupStyles method is as follows:
protected void SetupStyles()
{
// change alignment and default format for normal style
CellStyle cs = Styles.Normal;
cs.TextAlign = TextAlignEnum.CenterCenter;
cs.Format = "%d";
// title/navigation bar (row 0)
cs = Styles.Add("Title");
cs.BackColor = SystemColors.ActiveCaption;
cs.ForeColor = SystemColors.ActiveCaptionText;
cs.Font = new Font(cs.Font.FontFamily.Name, 14, FontStyle.Bold);
cs.Border.Style = BorderStyleEnum.None;
cs.Border.Color = cs.BackColor;
cs.TextAlign = TextAlignEnum.CenterCenter;
// weekdays (monthly view)
cs = Styles.Add("DayNames");
cs.Font = new Font(cs.Font, FontStyle.Regular);
cs.Border.Style = BorderStyleEnum.Flat;
cs.Border.Direction = BorderDirEnum.Horizontal;
cs.Border.Color = cs.ForeColor;
cs.Border.Width = 1;
cs.Format = "ddd";
cs.TextAlign = TextAlignEnum.CenterCenter;
// weekdays (weekly view)
cs = Styles.Add("WeekDayNames");
cs.Font = new Font(cs.Font, FontStyle.Regular);
cs.Border.Style = BorderStyleEnum.Flat;
cs.Border.Direction = BorderDirEnum.Both;
cs.Border.Color = SystemColors.ControlDark;
cs.Border.Width = 1;
cs.Format = "ddd\\nM/d";
cs.TextAlign = TextAlignEnum.CenterCenter;
// hours (weekly view, column 0)
cs = Styles.Add("Hours");
cs.Font = new Font(cs.Font, FontStyle.Regular);
cs.Border.Style = BorderStyleEnum.Flat;
cs.Border.Direction = BorderDirEnum.Both;
cs.Border.Color = SystemColors.ControlDark;
cs.Border.Width = 1;
cs.Format = "t";
cs.TextAlign = TextAlignEnum.RightCenter;
// gray dates
cs = Styles.Add("GrayDays");
cs.ForeColor = SystemColors.GrayText;
// weekends
cs = Styles.Add("Weekends");
cs.ForeColor = SystemColors.WindowText;
// today
cs = Styles.Add("Today");
cs.Font = new Font(cs.Font, FontStyle.Bold);
// free time (weekly view)
cs = Styles.Add("FreeTime");
cs.WordWrap = true;
cs.BackColor = Color.Khaki;
cs.Border.Style = BorderStyleEnum.Flat;
cs.Border.Color = SystemColors.Control;
cs.Border.Direction = BorderDirEnum.Both;
cs.Border.Width = 1;
// work time (weekly view)
cs = Styles.Add("WorkTime");
cs.WordWrap = true;
cs.BackColor = Color.LightGoldenrodYellow;
cs.Border.Style = BorderStyleEnum.Flat;
cs.Border.Color = SystemColors.Control;
cs.Border.Direction = BorderDirEnum.Both;
cs.Border.Width = 1;
}
C1FlexCalendar overrides the InitLayout method of System.Windows.Forms.Control, which is called after the control has been added to another container.
override protected void InitLayout()
{
base.InitLayout();
// adjust row/column sizes based on control size
UpdateLayout(true);
// set cell values and redraw the calendar
BuildCalendar(true);
}
The UpdateLayout method is used to initialize the grid's rows, columns, and scroll bars according to the current view (monthly or weekly). If the boolean parameter viewChanged is true, the grid is cleared and the row/column counts are reset. If viewChanged is false, the row/column counts are not changed. In either case, the default row and column sizes are calculated based on the size of the control and the values of the ShowTitles and ShowDayNames properties.
The UpdateLayout method is also called when the control is resized (via the OnSizeChanged override) or when the ShowTitle, ShowDayNames, or View properties are changed.
protected void UpdateLayout(bool viewChanged)
{
// reset grid rows/cols and scroll bars if view changed
if (viewChanged)
{
Clear(C1.Win.C1FlexGrid.ClearFlags.All);
_calendarStart = DateTime.MinValue;
if (_view == ViewEnum.Monthly)
{
// set number of rows: current/nav, weekday names, 6 weeks
Rows.Count = 8;
Rows.Fixed = 2;
// set number of columns: 7 days
Cols.Count = 7;
Cols.Fixed = 0;
// no scroll bars, single cell selection, fixed-only merging
ScrollBars = ScrollBars.None;
SelectionMode = SelectionModeEnum.Cell;
AllowMerging = AllowMergingEnum.FixedOnly;
AllowEditing = false;
}
else if (_view == ViewEnum.Weekly)
{
// set number of rows: current/nav, weekday names, 24 hours
Rows.Count = 26;
Rows.Fixed = 2;
// set number of columns: 7 days, time slots
Cols.Count = 8;
Cols.Fixed = 1;
// apply styles
ClearWeeklyView();
foreach (C1.Win.C1FlexGrid.Column c in Cols)
{
c.AllowMerging = true;
c.ComboList = "|...";
}
// vertical scroll bars, cell range selection, free merging
ScrollBars = ScrollBars.Vertical;
SelectionMode = SelectionModeEnum.CellRange;
AllowMerging = AllowMergingEnum.Free;
AllowEditing = true;
}
}
// show/hide title, days of the week
Rows[0].Visible = \_btnPrev.Visible = \_btnNext.Visible = _showTitle;
Rows[1].Visible = _showDayNames;
// get adjusted row count to calculate average row heights
double cnt = (_view == ViewEnum.Monthly) ? Rows.Count - 2 : Rows.Count / 2;
if (_showTitle) cnt ;
if (_showDayNames) cnt ;
// adjust average row/col sizes
Rows[0].Height = Cols[0].Width = -1;
Rows.DefaultSize = (int)(ClientSize.Height / cnt);
Cols.DefaultSize = ClientSize.Width / Cols.Count;
// fix round-off
int delta = ClientSize.Height - Rows[Rows.Count - 1].Bottom;
Rows[0].Height = Rows[0].HeightDisplay delta;
if (Rows[0].Height < 0) Rows[0].Height = 2 * Rows[0].HeightDisplay;
delta = ClientSize.Width - Cols[Cols.Count - 1].Right;
Cols[0].Width = Cols[0].WidthDisplay delta;
// set the first visible row to 6:00 am
if (viewChanged && _view == ViewEnum.Weekly)
{
int top = 6;
if (_showTitle) top ;
if (_showDayNames) top ;
TopRow = top;
}
// adjust button positions
Rectangle rc = GetCellRect(0, 0, false);
_btnPrev.Bounds = rc;
_btnPrev.TextAlign = ContentAlignment.MiddleLeft;
rc = GetCellRect(0, Cols.Count - 1, false);
_btnNext.Bounds = rc;
_btnNext.TextAlign = ContentAlignment.MiddleRight;
}
Note that the following C1FlexGrid properties receive different values depending on whether the current view is monthly or weekly:
Also, in weekly view, note the use of the ComboList property of the Column object:
foreach (C1.Win.C1FlexGrid.Column c in Cols)
{
c.AllowMerging = true;
c.ComboList = "|...";
}
This notation marks the affected cells as editable by clicking an in-cell button or by typing directly into the cell. C1FlexCalendar itself does not handle the CellButtonClick event. This is the responsibility of the containing form, which can display a custom input dialog or simply call the grid's StartEditing method.
The InitLayout override ends with a call to the BuildCalendar method, which assigns data values to the grid's cells and initiates repainting. This method begins by computing the first visible date, which may be in the preceding month. It then creates a CellRange object representing the entire first row (index 0) and assigns the appropriate title string for the current view. For each cell in the second row (index 1), it assigns a DateTime value corresponding to each day of the first displayed week. The method also applies a different named style to the second row based on the current view (DayNames for monthly view, WeekDayNames for weekly view).
For monthly view, no other data values are required, as the owner draw routines determine the appropriate day number based on the cell position. For weekly view, the leftmost column (index 0) is populated with DateTime values in one hour increments. A named style (Hours) is applied to all non-title cells in the leftmost column. This style has its Format property set to "t" so that only time values are displayed.
Finally, the method invalidates the control to force all cells to be repainted. The code for the BuildCalendar method is as follows:
protected void BuildCalendar(bool force)
{
// not while the mouse is down...
if (!force && (Control.MouseButtons & MouseButtons.Left) != 0)
return;
// backtrack to first of the month
DateTime date = SelectionStart;
if (_view == ViewEnum.Monthly)
{
while (date.Day > 1)
date = date.AddDays(-1);
date = date.AddDays(-1);
}
// backtrack to previous FirstDayOfWeek
DayOfWeek first = GetFirstDayOfWeek();
while (date.DayOfWeek != first)
date = date.AddDays(-1);
// same start date? no need to rebuild calendar
if (!force && date == _calendarStart)
{
if (_view == ViewEnum.Weekly)
{
SetWeeklyData(false); // user data needs to be reset
}
else
{
Invalidate(); // repainting cells is sufficient
SyncSelection();
}
return;
}
_calendarStart = date;
_calendarMonth = SelectionRange.Start;
// column offset
int offset = (_view == ViewEnum.Monthly) ? 0 : 1;
// month on row 0
Rows[0].Style = Styles["Title"];
Rows[0].AllowMerging = true;
CellRange rg = GetCellRange(0, 1, 0, 5 offset);
if (offset > 0)
{
rg.Data = "Week of " _calendarStart.ToString("MMMM dd, yyyy");
}
else
{
rg.Data = _calendarMonth.ToString("MMMM, yyyy");
}
// DayNames on first row
Rows[1].Style = (_view == ViewEnum.Monthly) ? Styles["DayNames"] : Styles["WeekDayNames"];
DateTime weekDay = date;
for (int c = 0; c < 7; c )
{
this[1, c offset] = weekDay;
weekDay = weekDay.AddDays(1);
}
if (offset > 0)
{
this[1, 0] = null;
weekDay = date;
for (int i = 0; i < 24; i )
{
this[2 i, 0] = weekDay;
weekDay = weekDay.AddHours(1);
}
CellRange cr = GetCellRange(1, 0, 25, 0);
cr.Style = Styles["Hours"];
cr = GetCellRange(0, 0);
cr.Style = Styles["Title"];
SetWeeklyData(true);
}
// invalidate all rows
Invalidate();
// synchronize grid cell with SelectionRange
SyncSelection();
}
The boolean parameter force is an optimization for cases where the calendar needs to be redrawn but the displayed date range has not changed. If it is false, the BuildCalendar method skips the initialization of fixed rows and columns.
The C1FlexCalendar constructor specifies owner-draw mode by setting the grid's DrawMode property to DrawModeEnum.OwnerDraw. To handle the actual drawing, the derived class overrides the OnOwnerDrawCell member, which delegates the bulk of the work to a private method corresponding to the current view:
override protected void OnOwnerDrawCell(OwnerDrawCellEventArgs e)
{
base.OnOwnerDrawCell(e);
e.DrawCell();
if (_view == ViewEnum.Monthly)
{
DrawItemDay(e);
}
else if (_view == ViewEnum.Weekly)
{
DrawItemHour(e);
}
}
For monthly view, the DrawItemDay member determines the DateTime value corresponding to the cell being rendered, then applies a different style if the cell represents today's date, a date outside the current month, or a date that falls on a weekend. If the date has appointments associated with it, the cell is rendered with a different background color. Finally, the day portion of the date is converted to a string and rendered along with the cell's border.
private void DrawItemDay(OwnerDrawCellEventArgs e)
{
if (e.Row >= 2)
{
DateTime date = GetCellDate(e.Row, e.Col);
if (date == DateTime.Today)
{
e.Style = Styles["Today"];
}
else if (date.Month != _calendarMonth.Month)
{
e.Style = Styles["GrayDays"];
}
else if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday)
{
e.Style = Styles["Weekends"];
}
if (HasOwnerDrawTasks(date))
{
e.Graphics.FillRectangle(_khakiBrush, e.Bounds);
}
e.Text = date.Day.ToString();
e.DrawCell(DrawCellFlags.Content | DrawCellFlags.Border);
}
}
For weekly view, the DrawItemHour method is only concerned with data cells containing objects derived from ITaskItem. The grid's GetMergedRange method is used to determine the contiguous range of cells that share the same ITaskItem instance. If the selection intersects any part of the range, the cell is drawn normally. Otherwise, a white background is drawn, then the ITaskItem.Task string is rendered along with the cell's border.
private void DrawItemHour(OwnerDrawCellEventArgs e)
{
if (e.Row >= 2 && e.Col > 0)
{
object data = GetData(e.Row, e.Col);
if (data is ITaskItem)
{
ITaskItem ti = (ITaskItem)data;
e.Text = ti.Task;
CellRange cr = GetMergedRange(e.Row, e.Col);
for (int i = cr.r1; i <= cr.r2; i )
{
if (Selection.Contains(i, e.Col))
{
e.DrawCell();
return;
}
}
e.Graphics.FillRectangle(_whiteBrush, e.Bounds);
e.DrawCell(DrawCellFlags.Border | DrawCellFlags.Content);
}
}
}
Click the following link to download the source code for C1FlexCalendar. This Visual Studio 2005 project (in C#) also includes a sample form that hosts the calendar control and demonstrates how to persist appointment data using XML serialization.
[C1FlexCalendar.zip](//gccontent.blob.core.windows.net/gccontent/blogs/legacy/c1/2006/3/C1FlexCalendar.zip)
This project requires C1FlexGrid version 2.0.20061.250 or greater.