Creating a Derived Calendar Control with FlexGrid

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.

C1FlexCalendar (Montly View)

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.

C1FlexCalendar (Weekly View)

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:

  • ScrollBars
  • SelectionMode
  • AllowMerging
  • AllowEditing

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.

GrapeCity

GrapeCity Developer Tools
comments powered by Disqus