|
Note: This is an unedited contribution. If this article is inappropriate,
needs attention or copies someone else's work without reference then please
Report This Article
GridDrawer.Net
The DataGridView control is a very powerful tool to display data in tabular format. However, a printing method was not incorporated into the control which presents the developer to create or devise a method to easily print the contents of the DataGridView.
DataDrawer is a class DLL to easily provide DataGridView printing. The library is written in C# with a version for .NET 3.5 incorporating LINQ and a .NET 2.0 versions. The library supports the C++, C#, VB.NET, and F# development environments.
The Class Library DLL provides the developer with a seamless approach to achieve DataGridView printing.
The DLL implements the following features:
- Print level by level or column by column
- Print selected rows or columns
- Center partitions on printed sheet
- Page numbering
- Optimized DataGridViewTextBoxCell printing
- Word wrapping and cell size as presented in the DataGridView control
- Implement the power of Framework .NET 3.5. A version for .NET 2.0 is also available.
- Provide scale resizing to force columns to fit onto a single sheet
- Optional Header, Footer and Title blocks printing
- Self-contained library for developer ease of use
- Compatible with C++, C#, VB.NET, and F#
To make the print job flow smoothly, one basic idea has been to create a Coding Line within the project:
The DataGridView has a number of Levels which are cut into Partitions. A Level identifies a number of rows which can be printed on a sheet. A Partition is the number of columns which can be printed on a sheet.
With this concept, it is very easy to make some calculations and having a set of partitions which cover the DataGridView.
Class DataGridViewExtentions
 |
This class implements the print job, adds functionality to the DataGridView and its components.
The Font, ColumnWidth, HeaderHeight and RowHeight functions have some overloads which use a "scale" parameter. The “scale” parameter is the core of the "FitColumnsToPage" functionality.
The code was designed to provide a uniform or similar approach to the form of the Functions:
public static Color ForeColor(this DataGridViewCell cell)
{
if (cell.HasStyle && cell.Style.ForeColor != Color.Empty) return cell.Style.ForeColor;
else return cell.InheritedStyle.ForeColor;
}
public static Color BackColor(this DataGridViewCell cell)
{
if (cell.HasStyle && cell.Style.BackColor != Color.Empty) return cell.Style.BackColor;
else return cell.InheritedStyle.BackColor;
}
Utilizing this approach to coding the functions should minimize initialize a style when it is not required. See Cell Styles in the Windows Forms DataGridView Control on MSDN
|
Class DocumentMetrics
 |
Usefull to keep track of the printable coordinates of a document; in order to do that, the "FromPrintDocument" function builds a new PrintDocument based on the given PrintDocument parameters. This approach is used to decouple or separate the print properties from the library.
public static DocumentMetrics FromPrintDocument(PrintDocument printDocument)
{
PageSettings pageSettings = printDocument.DefaultPageSettings;
return new DocumentMetrics()
{
Width =
(pageSettings.Landscape)
?pageSettings.PaperSize.Height:pageSettings.PaperSize.Width,
Height =
(pageSettings.Landscape)
?pageSettings.PaperSize.Width:pageSettings.PaperSize.Height,
LeftMargin = pageSettings.Margins.Left,
TopMargin = pageSettings.Margins.Top,
RightMargin = pageSettings.Margins.Right,
BottomMargin = pageSettings.Margins.Bottom
};
}
|
PartitionBounds Class
|
The PartitionBounds Class contains the bounds of the number of partitions that were split from the DataGridView, i.e., the included rows and columns of a partition as well as the coordinate information of the bounds (size). This class could have been named PartitionMetrics but there might be some confusion with DocumentMetrics.
|
Partition Class
|
The Patition class represents the portion of a DataGridView which can be printed on a single sheet. This class encapsulate the PartitionBounds class and provides functions which give an easy way to retrieve the DataGridViewColumns and DataGridViewRows which are assigned to the partition which is to be printed.
public DataGridViewRow GetRow(int i)
{
return GridView.Rows[Bounds.StartRowIndex + i];
}
public IEnumerable GetRows()
{
for (int i = 0; i < Bounds.RowsCount; i++)
if (GetRow(i).Visible) yield return GetRow(i);
}
|
PrintBlock Abstract class
This class isolates the title, header, and footer printing from the library. 3-PrintBlock objects are defined in GridDrawer: TitlePrintBlock, SheetFooter, and SheetHeader. These PrintBlock objects are defined by extending the PrintBlock class.
Described below is the approach to implement title, header, and footer printing with the library. You call the GetSize method first which sets a Rectangle in which the Draw method will print. This allows us to define some blocks to be printed without modifying the library core.
The Draw function receives a Dictionary parameter containing information from the CodeEnum enumeration for the Page number, page count, date, and time.
Note. There is a reusable TitlePrintBlock class already implemented in Lib.GridDraw.Tools. See "Working with this library" section for more detail on how this works.
public class TitlePrintBlock : PrintBlock
{
public String Title { get; set; }
public Color ForeColor { get; set; }
public Font Font { get; set; }
public StringFormat Format { get; set; }
public override SizeF GetSize(Graphics g, DocumentMetrics metrics)
{
return g.MeasureString(Title, Font, metrics.PrintAbleWidth, Format);
}
public override void Draw(Graphics g, Dictionary codes)
{
g.DrawString(Title, Font, new SolidBrush(ForeColor), Rectangle, Format);
}
}
GridDrawer Class
The core of the library is the GridDrawer class. GridDrawer exposes the properties and methods of the class. Using this class should be straight forward and easily implemented into your code.
There are three main axes in the class.
Calculate Partition bounds
- The first step when printing the document is to calculate partitions bounds of the sheet to be printed. Care must be taken for the Title (first level), Header, and Footer heights.
- Calculate the scale if "MustFitColumnsToPage" is set to true and the Columns width is greater than Sheet PrintAbleWidth.
- Define the rows which must appear in the first position of a partition.
- Define the columns which must appear in the first position of a partition.
- Define a code Dictionary which sets the PrintBlock abstract class for the title, header, and footer printing, page number, page count, date, and time information.
Create Partitions
All partitions are calculated and created in a single instance. Partitions are set from information calculated during the Initialization process or step.
Drawing a sheet
From the Partition information, the sheet is drawn.
- Header (optional)
- Title (optional)
- Partition (column header and cells of the partition)
- Footer (optional)
Needing a Header?
Create a class extending the PrintBlock class similar to the one included in the demo project.
public class HeaderPrintBlock : PrintBlock
{
float imgHeight = 75;
public override SizeF GetSize(Graphics g, DocumentMetrics metrics)
{
return new SizeF(metrics.PrintAbleWidth, imgHeight + 2);
}
public override void Draw(System.Drawing.Graphics g, Dictionary codes)
{
GraphicsUnit units = GraphicsUnit.Pixel;
RectangleF rec = Properties.Resources.logo.GetBounds(ref units);
float scale = imgHeight / rec.Height;
g.DrawImage(Properties.Resources.logo, new RectangleF(Rectangle.X, Rectangle.Y, rec.Width * scale, imgHeight));
}
}
Needing a Footer?
Create a class extending the PrintBlock class similar to the one included in the demo project.
public class FooterPrintBlock : PrintBlock
{
Font font = new Font("Tahoma", 9, GraphicsUnit.Point);
public override SizeF GetSize(Graphics g, DocumentMetrics metrics)
{
return g.MeasureString("Page X Of Y", font);
}
public override void Draw(System.Drawing.Graphics g, Dictionary codes)
{
StringFormat format = new StringFormat();
format.Trimming = StringTrimming.Word;
format.FormatFlags = StringFormatFlags.NoWrap;
format.Alignment = StringAlignment.Far;
g.DrawString(
string.Format("Page {0} Of {1}", codes[CodeEnum.SheetNumber], codes[CodeEnum.SheetsCount]),
font,
new SolidBrush(Color.Black),
Rectangle,
format);
}
}
Needing a title?
Initialize the reusable class implemented in Lib.GridDraw.Tools.TitlePrintBlock.cs.
TitlePrintBlock titleBlock = new TitlePrintBlock(printDocument.DocumentName,Color.DarkBlue);
Last step: Print the DataGridView
The Lib.GridDraw.Tools.PrintingDataGridViewProvider class initializes the printProvider using the printDocument.PrintPage event to manage the printing of the DataGridview.
printProvider = Tools.PrintingDataGridViewProvider.Create(
printDocument,
GridView, chkCenter.Checked, chkFitColumns.Checked,
new TitlePrintBlock(printDocument.DocumentName,Color.DarkBlue),
new PrintBlocks.HeaderPrintBlock(),
new PrintBlocks.FooterPrintBlock());
I hope you enjoy GridDraw.NET. Do not hesitate providing your feedback. It has been a pleasure to create and contribute this project to the community.
This is the first project I have written with .NET 3.5, using Linq. Understanding and using Linq simplified the source code.
I whish to thank Salan Al-Ani for his CodeProject article and to codeplex.com for providing source control feature for the GridDrawer Library. I whish also to thank Marc Miller, he has reviewed whole parts of this article and corrected most of it grammar.
| You must Sign In to use this message board. |
|
| | Msgs 1 to 23 of 23 (Total in Forum: 23) (Refresh) | FirstPrevNext |
|
|
 |
|
|
Hi BlaiseBraye,
I saw your control and we all can use it, if you convert it to .NET 2.0. Can you please tell me when it will be available.
Regards, Venkat
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi Venkat,
It's done and you can find it in start of the article.
I have to tell you that the library is now written for .net 2 by default. There is a new release on codeplex, you will find the link at end of the article. A new release is under preparation, it could be ready for this week-end, I hope you will enjoy it.
Nice evening Blaise.
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Blaise,
I am a little off the mark on my attempt to view a Print Preview of the contents of a DataGridView control I have populated with a LINQ method I have written which returns an IQueryable resultset (query).
The DataGridView displays the contents correctly but when I started to try and print the rows etc I found this not an easy task to accomplish. When I found your code I was enlightened to say the least!! It seems when I pass my datagridview control (dgReportResults) I receive a message on the Print Preview dialog box saying 'Document does not contain any pages'. Can you or anyone else who has worked with this huge time saving component assist me?
//Print Preview Button Event Code: private void btnPrintPreview_Click(object sender, EventArgs e) { if (printDialog1.ShowDialog(this) == DialogResult.OK) { printDocument1.DefaultPageSettings.Margins = new System.Drawing.Printing.Margins(40, 40, 40, 40);
printProvider = PrintingDataGridViewProvider.Create( printDocument1, dgReportResults, chkLevelByLevel.Checked, chkCenter.Checked, chkFitColumns.Checked, new TitlePrintBlock() { ForeColor = Color.DarkBlue, Title = printDocument1.DocumentName }, new Printing.DataGridViewPrint.Sample.PrintBlocks.HeaderPrintBlock(), new Printing.DataGridViewPrint.Sample.PrintBlocks.FooterPrintBlock()); printPreviewDialog1.ShowDialog(this); } }
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I think I am having a problem creating a sheetHeader and sheetFooter PrintBlock with the latest code, can someone send me a quick snippit example please? Thank you for your help.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
did you try one of the samples? I think it contains everything you need to answer to your questions.
about first question, I think you didn't define the Document property of the Printdialog control, did you?
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Ok, I think I'm just plain stuck. I still cannot print preview the contents of my DataGridView control, very strange. I did double check my printDialog and printPreviewDialog control was set, and the Document Property is assigned to: printDocument control
So, I guess this means I am doing something wrong with the printDocument control?
I have determined that the only thing different between your method of populating the GridView and my way is you are using bindingsource, dataset (with internal adapter) etc. My approach is simply calling a LINQ method in my Data Access Layer which sends back an IQueryable resultset.
So when I do the following, the dataGrid displays the query contents. dataGridView1.DataSource = RunStatReport(dteBegin, dteEnd, sID);
I even went as far as to rewrite the dataGridView binding code as follows, and this displays the query contents also:
Here is what I have: //Form Load Event private void Sample_Load(object sender, EventArgs e) { DataSet ds = new DataSet(); ds = RunStatReport(StartDate.ToShortDateString(), StopDate.ToShortDateString(), sID); //This now returns DataSet, not IQueryable set // Get a DataView of the table contained in the dataset. DataTableCollection tables = ds.Tables; DataView view1 = new DataView(tables[0]); // Create a BindingSource and set its DataSource property to // the DataView. BindingSource source1 = new BindingSource(); source1.DataSource = view1; // Set the data source for the DataGridView. GridView.DataSource = source1; }
//Form Constructor Event public Sample() { InitializeComponent(); printDocument.DefaultPageSettings.Margins = new System.Drawing.Printing.Margins(40, 40, 40, 40); printProvider = PrintingDataGridViewProvider.Create( printDocument, GridView, chkLevelByLevel.Checked, chkCenter.Checked, chkFitColumns.Checked, new TitlePrintBlock() { Title = printDocument.DocumentName, ForeColor = Color.DarkBlue }, null, null); } //Notes: I found, that TitlePrintBlock() needed the Title String as //first param, not the second so I switched them around.
I am not really sure if the Print Preview is not being displayed correctly because, since I am using the latest downloadable code, the PrintBlocks code seems to have changed from: public class HeaderPrintBlock : PrintBlock to public abstract class PrintBlock and I honestly cannot figure out what to do with the PrintBlocks for Header and Footer, so I passed them into: Create() as null values.
//And Finally the Print Preview Button Click Event private void btnPreview_Click(object sender, EventArgs e) { if (printDialog.ShowDialog(this) == DialogResult.OK) printPreviewDialog.ShowDialog(this); }
Can you tell me what I am doing wrong? thanks!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I suppose you did it but I prefer to ensure of : did you subscribe a method to the PrintDocument.PrintPage event?
see Form1.cs
private void printDocument_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e) { if (!pgm.Draw(e) && e.Cancel) MessageBox.Show("Nothing to print"); //else if (!e.HasMorePages) // MessageBox.Show("Datas have been printed"); }
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Nice work, really useful for my needs. However, I had to do a little change in one class as it failed to print anything when the first column (with index number 0) in the DataGridView was set to invisible. I changed the following lines in GridDrawer.cs, starting at line 251:
float tmpWidth = 0; foreach (var column in GridView.Columns.OfType() .Where(colSelector)) { columnWidths[column.Index] *= scale;
tmpWidth += columnWidths[column.Index];
if (tmpWidth > metrics.PrintAbleWidth || column.Index == 0) { firstColumnsOnPartition.Insert(0, column.Index); tmpWidth = columnWidths[column.Index]; } } firstColumnsOnPartition.Reverse();
into the following:
float tmpWidth = 0; bool firstColumnAdded = false; foreach (var column in GridView.Columns.OfType() .Where(colSelector)) { columnWidths[column.Index] *= scale;
tmpWidth += columnWidths[column.Index];
if (tmpWidth > metrics.PrintAbleWidth || !firstColumnAdded) { firstColumnAdded = true; firstColumnsOnPartition.Insert(0, column.Index); tmpWidth = columnWidths[column.Index]; } } firstColumnsOnPartition.Reverse();
The problem was that the first column, with index number 0, was never enumerated in the foreach loop since it was invisible, making firstColumnsOnPartition empty (as all my columns would fit into one page width).
As a side note, I think a footer print block class adding the "Page X of Y" text (preferable culture-independent) should be added to the main project just like the title print block class. But it was a nice example to see in the sample project.
Keep up the good work!
/Tobias
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
hello TobiasP, thanks for your comment.
I think I have already corrected this behavior; there were other things which were not good with the used mechanism. But... I am not so sure of it, logically: the colselector should not check not visible columns, isn't it?
Any way, whole of the library has been rewritten, I will update the project as soon as possible; I want to ensure of all is ok before doing it and I will have to rewrite a complete article because it covers more features in a different way. I will contact you when it is done to ask you to test your use case if I can't reproduce it.
here you are the new algorithm about columns partition process, the one for rows is working in a similar way.
var colEnumerator = ColumnsToPrint.GetEnumerator();
notfinished = colEnumerator.MoveNext(); if (notfinished) { do { partitionsColIdList.Insert(0, colEnumerator.Current.Index); int width = 0; do { width += drawer.CalculateColumnWidth(g, colEnumerator.Current); notfinished = colEnumerator.MoveNext(); } while (notfinished && width + drawer.CalculateColumnWidth(g, colEnumerator.Current) < clipBounds.Width);
partitionsWidthList.Insert(0, width);
} while (notfinished); }
partitionsColIdList.Reverse();
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
ok, I have read again your comment. For sure, the problem is not still a problem with new algorithm 
If you want to keep your version,
you should just change the colum.index test in the line
if (tmpWidth > metrics.PrintAbleWidth || column.Index == 0)
in fact, it is not zero but the first id returned by GridView.Columns.OfType().Where(colSelector) so before looping you can say
int firstid = GridView.Columns.OfType().Where(colSelector).First()
and then change
if (tmpWidth > metrics.PrintAbleWidth || column.Index == 0)
with
if (tmpWidth &gt; metrics.PrintAbleWidth || column.Index == firstid )
I think this will also correct your problem
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Yes, though I haven't tested it I think both your new solution in your first reply and the code above in the post I reply to now fixes the problem I found. The firstid solution looks a bit nicer but the effect would be the same as the boolean variable I used. Thanks for your reply, I'm looking forward to your new article.
/Tobias
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
 |
|
|
Hi,
very useful!
My list contains bool values (shown as checkboxes) and some null values.
Testing this list, i get this error: "Sequence contains no elements"
Here:
/// Calculate the header height of a DataGridView in a drawing surface, /// it takes care only about visible columns /// </summary> /// <param name="dgv"></param> /// <param name="g"></param> /// <returns>DataGridView Columns Header Height</returns> public static float HeaderHeight(this DataGridView dgv, Graphics g) { return (from column in dgv.Columns.OfType<DataGridViewColumn>() where column.Visible select g.MeasureString( column.HeaderCell.EditedFormattedValue.ToString(), column.HeaderCell.Font(), column.HeaderCell.Size.Width).Height). Max(); }
Thanks Thomas
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
ok, what do you think we should do in this case? I want to say that we should not count when column.HeaderCell.EditedFormattedValue is null... Do you think it's what we should do?
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Changes:
public static float HeaderHeight(this DataGridView dgv, Graphics g) { if (dgv.RowCount == 0) return 0; ...
We must check this at more than one position:
Next position to change ist here:
GridDrawer.cs: // we have to set the first visible row outside of the loop // cause we want to take care about the possible set Title block int firstVisibleRowIndex = GridView.Rows.OfType<DataGridViewRow>() .Where(row => row.Visible).First().Index;
Changes:
public void Initialize(Graphics g, DocumentMetrics metrics) { if (GridView.Rows.Count == 0) return;
Or simple here:
public static PrintingDataGridViewProvider Create(PrintDocument printDocument, DataGridView dgv, bool mustCenterPartition, bool mustFitColumnsToPage, PrintBlock titlePrintBlock, PrintBlock sheetHeader, PrintBlock sheetFooter) { if (dgv.RowCount == 0) return null; // ? -> Check outside to open the printdialog
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
yes you are right, I will look forward it soon.
thanks for proposal
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
It is not a problem, installing the framework .Net 3.5 won't disturb .Net 2.0 projects it is juste some more packages added to the framework .Net 2.0 if I am right.
Am I wrong?
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I ask this because many people don't want to install .NET 3.5. It is too new and IT people are remaining with .NET 2.0 and .NET 3.0.
Also, if you require any help with English grammar, I will be happy to help you.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
mmm ok with your argument... But if I rewrite the project into a .NET 2.0 project, why maintaining a project based on .NET 3.5 framework? in other words, I wonder if it is useful to provide both versions... Do you think it is?
If I need some help with english grammar? For sure! Do you propose to me to review my article? I totally accept if it is the case. Then I could add my CodeProject folder under source control on codeplex in order you can check it out and commit your proposal. Do you think it's the good way?
Let's make code sharing our goal... Blaise Braye
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I work with a lot of people and they do not want to upgrade to .NET 3.5. For myself, I am doing all .NET applications in .NET 2.0 becuase I do not use LINQ.
So, if you can make two .NET versions, that would be nice.
I will pleased to do your grammar. Will try to send an email to you.
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
buster95 wrote: becuase I do not use LINQ.
About Linq, you should use it; I promise you that you will gain with flexibility and productivity.
buster95 wrote: I work with a lot of people and they do not want to upgrade to .NET 3.5. For myself
I think you are true and I am gona make a .Net 2.0 compliant project. This will have same functionalities.
buster95 wrote: I will pleased to do your grammar. Will try to send an email to you.
about grammar, why don't you want to work with source control on codeplex? it is very easy and it would be a pleasure to explain you the bases of this one (only the bases because I don't know more héhé).
You will find my code project template in Folder CodeProject under last source control branch on Change Set 10805
Let's make code sharing our goal... Blaise Braye
modified on Tuesday, April 29, 2008 5:42 AM
|
| Sign In·View Thread· | | | | | |