Print a calendar in C#


[calendar]

This example prints a calendar containing some text for each day. It’s mostly a matter of keeping track of where to draw things, but it also requires you to handle some confusing internationalization issues and there’s one potential gotcha.

The program uses uses several techniques including the method described in the post Get the name of the first day of the week in C#.

One of the bigger issues is that different countries begin the week with different days. For example, in the United States the week begins with Sunday but in Germany and Australia it begins with Monday.

The program uses the following two variables to store information about the calendar.

// The calendar data.
DateTime FirstOfMonth;
private string[] CalendarData;

The FirstOfMonth variable stores the first date of the month that should be printed. The CalendarData array holds text to display in each of the month’s dates. This example lets you select the month and year. The program initializes this array randomly, although in a real application you would want to use appointment reminders and other useful text.

When you click the Preview button, the program reads the date you selected and creates the random data. It then calls the ShowDialog method for the Print Preview Dialog that I created at design time. Also at design time I created a PrintDocument and set the dialog’s Document property equal to it.

When the program calls ShowDialog, the dialog uses the PrintDocument to generate the printout by raising some events. The first event used by this example is QueryPageSettings, which is handled by the following event handler.

// Print in landscape mode.
private void pdocCalendar_QueryPageSettings(object sender,
    QueryPageSettingsEventArgs e)
{
    e.PageSettings.Landscape = true;
}

This code simply makes the printout appear in landscape mode. If you want to print in portrait mode, just comment out this line.

After this event finishes, the PrintDocument raises its PrintPage event to do the drawing. This example’s PrintPage event handler simply calls the following DrawCalendar method.

// Draw the calendar as big as posisble.
private void DrawCalendar(Graphics gr, RectangleF bounds,
    DateTime first_of_month, string[] date_data)
{
    // Make the rows and columns as big as possible.
    float col_wid = bounds.Width / 7f;

    // See how many weeks we will need.
    int num_rows = NumberOfWeekRows(first_of_month);

    // Add an extra row for the month and year at the top.
    num_rows++;

    // Calculate the row height.
    float row_hgt = bounds.Height / (float)num_rows;

    // Draw the month and year.
    float x = bounds.X;
    float y = bounds.Y;
    RectangleF rectf = new RectangleF(
        x, y, bounds.Width, row_hgt / 2f);
    DrawMonthAndYear(gr, rectf, first_of_month);
    y += row_hgt / 2f;

    // Draw the day names.
    DrawWeekdayNames(gr, x, y, col_wid, row_hgt / 2f);
    y += row_hgt / 2f;

    // Draw the date cells.
    DrawDateData(first_of_month, date_data,
        gr, x, y, col_wid, row_hgt);

    // Outline the calendar.
    gr.DrawRectangle(Pens.Black,
        bounds.X, bounds.Y, bounds.Width, bounds.Height);
}

This method makes the calendar as large as possible while fitting within the rectangle it receives as a parameter. To do that, it divides the area’s width into seven even columns.

The method then calls the NumberOfWeekRows method described shortly to figure out how many rows the month will need to hold all of its days. It adds one to the number of weeks to allow room for the calendar’s title and then divides the available height by the resulting number.

The code then calls the DrawMonthAndYear and DrawWeekdayNames methods to draw the title and weekday names in their appropriate positions.

Next the method calls DrawDateData to draw the date cells. It finishes by drawing a box around the whole calendar.

The following code shows the NumberOfWeekRows method.

// Return the number of week rows needed by this month.
private int NumberOfWeekRows(DateTime first_of_month)
{
    // Get the number of days in the month.
    int num_days = DateTime.DaysInMonth(
        first_of_month.Year, first_of_month.Month);

    // Add the column number (numbered from 1)
    // for the first day of the month.
    num_days += DateColumn(first_of_month);

    // Divide by 7 and round up.
    return (int)Math.Ceiling(num_days / 7f);
}

This method uses DateTime.DaysInMonth to see how many days are in the month in question. It then uses the DateColumn method described next to get the column number for the first day of the month and adds that to the number of days in the month. Basically this adds a day to the month for each day that comes before the first day of the month.

For example, suppose the week starts with Sunday and the first day of the month is Tuesday. Then the code adds 2 days (the column numbers are Sunday = 0, Monday = 1, Tuesday = 2) to make room for the Sunday and Monday that come before the first day.

After adding any extra fake days, the method simply divides by 7, rounds up, and returns the result.

The following code shows the DateColumn method.

// Return the column number for this date in the current locale.
private int DateColumn(DateTime date)
{
    int col =
        (int)date.DayOfWeek -
        (int)CultureInfo.CurrentCulture.
            DateTimeFormat.FirstDayOfWeek;
    if (col < 0) col += 7;
    return col;
}

The DateColumn method returns the column number for a particular date. First it gets the date’s DayOfWeek number. This number is the date’s index in the culture’s array of day names and that array always starts with Sunday in array position 0. For example, if the day is a Tuesday, then this number is 2.

But the week doesn’t necessarily start with Sunday, depending on the locale, so the code subtracts the index of the first day of the week.

For example, suppose you’re in Australia so the first day of the week is Monday with index 1. Then if the date is a Tuesday, the column number is 2 – 1 = 1. That makes sense because in Australia the first column holds Monday and the second holds Tuesday.

Now if the date happened to be Sunday and the week starts on Monday, the column number is -1. In that case the code adds 7 to get column number 6, meaning this date belongs in the last column of the week.

To recap a bit, the DrawCalendar method calls NumberOfWeekRows which calls DateColumn. The DrawCalendar method then calls DrawMonthAndYear, DrawWeekdayNames, and DrawDateData. The following code shows the DrawMonthAndYear method.

// Draw the month and year.
private void DrawMonthAndYear(Graphics gr,
    RectangleF rectf, DateTime date)
{
    using (StringFormat sf = new StringFormat())
    {
        // Center the text.
        sf.Alignment = StringAlignment.Center;
        sf.LineAlignment = StringAlignment.Center;

        string[] month_names =
            CultureInfo.CurrentCulture.DateTimeFormat.MonthNames;
        string title = month_names[date.Month - 1] +
            " " + date.Year.ToString();

        // Find the biggest font that will fit.
        int font_size = FindFontSize(gr, rectf,
            "Times New Roman", title);

        // Draw the text.
        gr.FillRectangle(Brushes.LightBlue, rectf);
        using (Font font = new Font("Times New Roman", font_size))
        {
            gr.DrawString(title, font, Brushes.Blue, rectf, sf);
        }
    }
}

This method first makes a StringFormat object to center text.

Next it uses the current culture’s MonthNames array to get the name of the selected month. Here’s the potential gotcha. The DateTime structure’s Month property gives the month number between 1 and 12. Like almost every other array in C#, the month names array begins with index 0, so the code subtracts 1 from the month number to get the month’s correct name. It then uses the month name and the date’s year to build the calendar’s title string.

The code then calls the FindFontSize method to find the largest font it can use while making the title string fit inside the area allowed. The FindFontSize method, which isn’t shown here, simply tries bigger and bigger fonts until it finds one that won’t fit and then returns the last size that did fit.

After it has found the font size, the method draws the calendar’s title.

The following code shows how the DrawWeekdayNames method draws the weekday names over the calendar’s columns.

// Draw the weekday names.
private void DrawWeekdayNames(Graphics gr,
    float x, float y, float col_wid, float hgt)
{
    // Find the widest day name.
    float max_wid = 0;
    string[] day_names =
        CultureInfo.CurrentCulture.DateTimeFormat.DayNames;
    string widest_name = day_names[0];
    using (Font font = new Font("Times New Roman", 10))
    {
        foreach (string name in day_names)
        {
            SizeF size = gr.MeasureString(name,font);
            if (max_wid < size.Width)
            {
                max_wid = size.Width;
                widest_name = name;
            }
        }
    }

    // Find the biggest font size that will fit.
    RectangleF rectf = new RectangleF(x, y, col_wid, hgt);
    int font_size = FindFontSize(gr, rectf,
        "Times New Roman", widest_name);

    // Draw the day names.
    using (Font font = new Font("Times New Roman", font_size))
    {
        using (StringFormat sf = new StringFormat())
        {
            sf.Alignment = StringAlignment.Center;
            sf.LineAlignment = StringAlignment.Center;

            int index = (int)CultureInfo.CurrentCulture.
                DateTimeFormat.FirstDayOfWeek;
            for (int i = 0; i < 7; i++)
            {
                gr.FillRectangle(Brushes.LightBlue, rectf);
                gr.DrawString(day_names[index], font,
                    Brushes.Blue, rectf, sf);
                index = (index + 1) % 7;
                rectf.X += col_wid;
            }
        }
    }
}

The code first gets the current culture's DayNames array, which contains the names of the weekdays. It creates a font and then loops through the names, measuring each to see which is widest when drawn in the font.

The code then calls FindFontSize to see how big it can draw the largest weekday name so it fits within the space above a column.

Next the code loops through the names and draws each over its column. The culture's array of weekday names always holds the names in the order Sunday, Monday, Tuesday, ..., Saturday. The culture's DateTimeFormat.FirstDayOfWeek value gives you the index in that array of the first day of the week. For example, that value is 0 in the United States and 1 in Australia.

The program starts with variable index equal to the culture's FirstDayOfWeek and then displays 7 values, wrapping around to index 0 if necessary.

(Note that the program assumes there are seven days in a week. That seems to be the case for every culture supported by .NET. It's one of the very few things that every country seems to agree on.)

The following code shows the DrawDateData method.

// Draw the data for each date.
private void DrawDateData(DateTime first_of_month,
    string[] date_data, Graphics gr, float x, float y,
    float col_wid, float row_hgt)
{
    // Let date numbers occupy the upper quarter
    // and left third of the date box.
    RectangleF date_rectf =
        new RectangleF(x, y, col_wid / 3f, row_hgt / 4f);

    // The date data goes below the date rectangle.
    RectangleF data_rectf =
        new RectangleF(x, y, col_wid, row_hgt * 0.75f);

    // See how big we can make the font.
    int font_size = FindFontSize(gr, date_rectf,
        "Times New Roman", "30");

    // Get the column number for the first day of the month.
    int col = DateColumn(first_of_month);

    // Draw the dates.
    using (Font number_font =
        new Font("Times New Roman", font_size))
    {
        using (Font data_font =
            new Font("Times New Roman", font_size * 0.75f))
        {
            using (StringFormat ul_sf = new StringFormat())
            {
                ul_sf.Alignment = StringAlignment.Near;
                ul_sf.LineAlignment = StringAlignment.Near;
                ul_sf.Trimming = StringTrimming.EllipsisWord;
                ul_sf.FormatFlags = StringFormatFlags.LineLimit;

                int num_days = DateTime.DaysInMonth(
                    first_of_month.Year, first_of_month.Month);
                for (int day_num = 0; day_num < num_days; day_num++)
                {
                    // Outline the cell.
                    RectangleF cell_rectf = new RectangleF(
                        x + col * col_wid, y, col_wid, row_hgt);
                    gr.DrawRectangle(Pens.Black,
                        cell_rectf.X, cell_rectf.Y,
                        cell_rectf.Width, cell_rectf.Height);

                    // Draw the date.
                    date_rectf.X = cell_rectf.X;
                    date_rectf.Y = cell_rectf.Y;
                    gr.DrawString((day_num + 1).ToString(),
                        number_font, Brushes.Blue, date_rectf, ul_sf);

                    // Draw the data.
                    data_rectf.X = x + col * col_wid;
                    data_rectf.Y = y + row_hgt * 0.25f;
                    gr.DrawString(date_data[day_num],
                        data_font, Brushes.Black, data_rectf, ul_sf);

                    // Move to the next cell.
                    col = (col + 1) % 7;
                    if (col == 0) y += row_hgt;
                }
            }
        }
    }
}

The code starts by defining the date_rectf rectangle that occupies the upper quarter and left third of the area allowed for the upper leftmost date cell. This is the area where it will draw the date number.

The code then creates the data_rectf rectangle to hold the date's data below the date_rectf rectangle.

Next the code uses the FindFontSize method to find the biggest font it can use to fit the text "30" in the date number rectangle.

The code then calls DateColumn (described previously) to see which column should contain the first day of the month.

Next the code creates fonts to draw the date numbers and the date text. It creates a StringFormat object that places text in the upper left corner of a formatting rectangle. It sets the object's Trimming and FormatFlags properties so text ends with an ellipsis after the last word that can fit and a new line isn't started if it won't fit entirely within the formatting rectangle.

The code then gets the number of days in this month and is ready to start drawing the dates.

The variable day_num loops from 0 to one less than the number of days in the month. For each day, the code sets the cell_rectf rectangle's position. Its X coordinate is given by the column number col times the column width. Its Y position is stored in the variable y. After it sets the rectangle's position for the cell, the code outlines it.

Next the code positions the date number rectangle and draws the date in it. It then does the same for the date's data, positioning the appropriate rectangle and then drawing the data.

After it has finished drawing this date, the code adds 1 to the column number, wrapping back to 0 if the calendar row is full.

That's how the most complicated pieces of the program work. Download the example to see additional details such as the FindFontSize method and the way the program changes its locale to test for Australia and Germany.


Download Example   Follow me on Twitter   RSS feed   Donate




About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in internationalization, system and tagged , , , , , , , , , , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *