DataGridView Printing

Introduction

Well it is that time of the year again. Time to get in shape for the summer months, and what better way to do it then through a rigid exercise program. I was never very good at tracking my progress in the gym, but thanks to .NET (and my wife),  I have a way to do just that. This program uses the DataGridView bound to a Microsoft Access Database to create a printed sheet with your latest work out plans. The workout chart includes the exercise, number of sets, number of reps, amount of weight, and most importantly, whether or not you completed the exercise. The best part of this program is that you can print out your exercise program and bring it with you to the weight room. 

 

Figure 1 - Exercise Program

New Form Capture Feature

.NET comes with a new form capture feature: the ability to copy the form as you see it on the screen into a bitmap. Although not useful for controls that you need to scroll, New Form Capture Feature works if you can fit all the contents you need to see on the screen into a bitmap. In this application, we stretched the DataGridView to allow for the full contents of the exercises, and we enabled AutoScrolling on the Form. This way the DataGridView will be much larger than the form, and the screen capture will print the entire DataGridView contents to the printer. The exercise application gives you two options: turning on the form capture feature to print out the form bitmap, or printing using a customized DataGridView printing. The customized printing is the default mode of the exercise program and it's more powerful because you can print the data in the grid view beyond any scrolling limitations of the DataGridView. Below is the code for capturing the form in a bitmap and drawing it to a graphics surface:
 

Listing 1 - Printing the Form Capture to a Graphics Surface

private void DrawFormSurface(System.Drawing.Printing.PrintPageEventArgs e)
        {

            // create a bitmap the size of the form
            Bitmap bmp = new Bitmap(gridExercise.Width, gridExercise.Height);

            // draw the form image to the bitmap
            gridExercise.DrawToBitmap(bmp, new Rectangle(0, 0, gridExercise.Width, gridExercise.Height));

            // draw the bitmap image of the form onto the graphics surface
            e.Graphics.DrawImage(bmp, new Point(0, 0));
        }

 

Using a Filename to Map an Image in the DataGridView

We want to place an image of the exercise into the DataGridView without inserting the image in the database.  We prefer to place all the images in an images directory. Then we will read the images file names from the database, retrieve the images from the images directory, and put the images into the DataGridView. In order to accomplish all this, we'll need to create a custom grid view column and grid view cell that inherits from DataGridViewImageColumn and DataGridViewImageCell.  We then override the GetFormattedValue method in the custom DataGridViewImageCell class. In the GetFormatted value method, we create a mapping between the file names and the actual images. Below is the code for accomplishing the filename to image mapping in a DataGridView:

Listing 2 - Mapping Images into the DataGridView from file name Strings

using System;
using
System.IO;
using
System.Collections.Generic;
using
System.Text;
using
System.Drawing;
using
System.Windows.Forms;
using
System.ComponentModel;
using
System.Reflection;
 

namespace ExerciseProgram
{

 /// <summary>
 /// Create a Custom DataGridViewImageColumn
 /// </summary>

public class StringImageColumn : DataGridViewImageColumn
{

    public StringImageColumn()
    {
        this.CellTemplate = new CustomImageCell();
        this.ValueType = typeof(string);  // value of this column is a string, but it shows  images in the cells after formatting
    }

}

 

/// <summary>
///
Create a Custom DataGridViewImageCell
///
</summary>

public class CustomImageCell : DataGridViewImageCell
{

    // mapping between filename and image
    static System.Collections.Generic.Dictionary<string, Image> dotImages = new Dictionary<string, Image>(); // load up custom dot images

    static public System.Collections.Generic.Dictionary<string, Image> Images
    {
        get
        {
            return dotImages;
        }
    }

    public CustomImageCell()
    {
        this.ValueType = typeof(int);
    }

 

    protected override object GetFormattedValue(
        object value, int rowIndex, ref DataGridViewCellStyle cellStyle,
        TypeConverter valueTypeConverter,
        TypeConverter formattedValueTypeConverter,
        DataGridViewDataErrorContexts context)
    {

        if (value.GetType() != typeof(string))
            return null;

        // get the image from the string and return it
        LoadImage(value);

        // return the mapped image
        return dotImages[(string)value];
    }

    public static void LoadImage(object value)
    {

       
//  load the image from the images directory if it does not exist
        if (dotImages.ContainsKey((string)value) == false)
        {
            string path = Path.GetDirectoryName(Application.ExecutablePath) + "\\images\\" + (string)value;

            // read the image file
            Image theImage = Image.FromFile(path);

             //  assign the image mapping
            dotImages[(string)value] = theImage;
        }
    }

    public override object DefaultNewRowValue
    {
        get
        {

           
return 0;
       
}
    }

}

Updating the Grid

The DataGridView is bound to the Microsoft Access Database via a DataGridAdapter. We can edit the sets, reps, and weight values inside the grid while increasing body strength and the program will persist these values to the database. Sometimes it is useful to override the behavior of the DataGridView for updates and inserts in order to customize how the grid is updated. Below is the ADO.NET code that updates the grid in Microsoft Access without using the OleDb Adapter:
 

Listing 3 - Updating the Microsoft  Access Database after editing the GridView

/// <summary>

        /// Event Handler called when Cell is finished editing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void gridExercise_CellEndEdit(object sender, DataGridViewCellEventArgs e)
        {
            UpdateCurrentRow(e);
        }

        private void UpdateCurrentRow(DataGridViewCellEventArgs e)
        {
            int index = e.RowIndex;
            DataRow dr = scheduleDataSet.Tables[0].Rows[e.RowIndex];
            string val = dr[gridExercise.Columns[e.ColumnIndex].DataPropertyName].ToString();
            OleDbCommand updateCommand = new OleDbCommand();

            // construct update command and update from the data set
            updateCommand.CommandText = "UPDATE `Schedule` SET `Completed` = {0},  `Reps` = {1},
                                             `Weight` = {2}, `Sets` = {3} WHERE `ID` = {4}"
;

            updateCommand.CommandText = String.Format(updateCommand.CommandText, dr["Completed"],
                                             dr["Reps"], dr["Weight"],
                                             dr["Sets"], dr["ID"]);

            updateCommand.CommandType = System.Data.CommandType.Text;
            updateCommand.Connection = scheduleTableAdapter.Connection;
            updateCommand.Connection.ConnectionString = string.Format(
                               @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={0}\Schedule.mdb"
,
                               Path.GetDirectoryName(Application.ExecutablePath));

            updateCommand.Connection.Open();

            // execute the update on the database
            updateCommand.ExecuteNonQuery();
            updateCommand.Connection.Close();
        }

Printing the DataGridView by Brute Force.

Once we are satisfied with our exercise parameters, it is time to print out the exercise schedule and head to the gym. Printing is accomplished using the PrintDocument in conjuction with GDI+. Every row and column is painstakingly drawn to the print graphics surface. Listing 4 shows the code that draws the exercise rows below the header. The method loops through each row in the DataSet and draws the text corresponding to the column in the data row.  If the row contains a picture, rather than text, the image that is mapped to the text in the cell is printed instead. We also need to track the row and column position after placing each object onto the graphics surface so we know where the next object goes. We do this by incrementing the local variables: columnPosition and rowPosition.
 

Listing 4 - Printing out the Exercise Rows

        private void DrawGridBody(Graphics g, ref int columnPosition, ref int rowPosition)
        {
            // loop through each row and draw the data to the graphics
            // surface.

            foreach (DataRow dr in scheduleDataSet.Tables[0].Rows)
            {
                columnPosition = 0;
 

                // draw a line to separate the rows 

                g.DrawLine(Pens.Black, new Point(0, rowPosition), new Point(this.Width, rowPosition));

 

                // loop through each column in the row, and
                // draw the individual data item
                foreach (DataGridViewColumn dc in gridExercise.Columns)
                {
                    // if its a picture, draw a bitmap
                    if (dc.DataPropertyName == "Picture")
                    {
                        if (dr[dc.DataPropertyName].ToString().Length != 0)
                        {
                            if (CustomImageCell.Images.ContainsKey(dr[dc.DataPropertyName].ToString()))
                            {
                                g.DrawImage(CustomImageCell.Images[dr[dc.DataPropertyName].ToString()],
                                         new
Point(columnPosition, rowPosition));
                            }
                        }
                    }
                    else if (dc.ValueType == typeof(bool))
                    {
                        // draw a check box in the column

                        g.DrawRectangle(Pens.Black, new Rectangle(columnPosition, rowPosition + 20, 10, 10));
                    }
                    else
                    {
                        // just draw string in the column
                        string text = dr[dc.DataPropertyName].ToString();

                        if (dc.DefaultCellStyle.Font != null)
                            g.DrawString(text, dc.DefaultCellStyle.Font, Brushes.Black,
                                (float)columnPosition, (float)rowPosition + 20f); 
                       else
                            g.DrawString(text, this.Font, Brushes.Black, (float)columnPosition, (float)rowPosition + 20f);
                    }

 

                    // go to the next column position
                    columnPosition += dc.Width + 5;  

                } 

                // go to the next row position
                rowPosition = rowPosition + 65;
            }

        } 

Conclusion

The DataGridView is even more full featured than the DataGrid and provides a good interface for manipulating DataSets. This article explained two ways to print data inside the DataGridView. Hopefully this exercise will give you some insight into printing one of the more powerful controls inside the Windows Form in C# and .NET.
 



--
Alain Lompo
Excelta - Conseils et services informatiques
MCT
MCSD For Microsoft .Net
MVP Windows Systems Server / Biztalk Server
Certifié ITIL et Microsoft Biztalk Server

Commentaires

Posts les plus consultés de ce blog

Printing and Graphics

Solution Items

Printing in C#