Introduction
We are moving towards the future with our bright eyes keenly focused on the next wave of technology. We have left behind the technology of yesterday,
casting it aside for the new and 'better' mousetraps. The purpose of this article is, for a brief moment, gaze backwards a bit and rediscover our old,
and for the most part, forgotten technologies. These technologies still play a role however small we are led to believe.
Background
In today's technology landscape if we were to build a report generator for the UI, the first technology that would come to mind for most
.NET developers is WPF. In keeping in step with the theme of this article, I'm going to build this component with GDI+
and WinForms instead. That's right. I'm going to demonstrate how a seemingly old UI technology can do the job just fine. Not only that but my
solution will also be fast and efficient and have all the features of any comparable .NET reporting component.
I will call the report component Star Report.
Here are the requirements of the UI report generator component:
- Support Multiple Report Types (GeneralList, CardList, Hierarchy, FreeForm, MultipleTables)
- Paging Support
- Customize Filters, Multiple Filters
- Customize Sort Descriptors
- Display
- Page Number
- Date
- Optional Header: Company Name, Address, Contact Information
- Auto Grid Lines
- Support Customizable Indentation
- Dates Support
- Summaries
- Nested Summaries, Group By Summaries
- Support Sum(), Avg(), Min(), Max(), Count() functions
- Support Summary Fonts and Pens
- Total Columns
- Grand Summary Cells
- Drill Down Architecture
- Customizable Cells
- Support Images
- Support Alignments
- Support Different Formats
- Wrap, No Wrap, FitBlackBox, NoClip, DirectionVertical, LineLimit
- Customizable Rows
- Alternate Colors
- Support Null Values and Null Value data
- Support multiple fonts
- Support Look up Tables
- Full Print Preview Support
- Configurable Graphic Units (Pixel, Point, Inch, Document, Milliliter)
- Interact with
DataTable
- Completely configurable through code
- First class support in Visual Studio. This means that I can configure the entire report using only the Visual Studio 2012 IDE using custom Type Descriptors!
- Serializable: Ability to serialize its all settings
- Intuitive User Interface
- High Quality Picture with High Quality Interpolation Mode
- Anti-Alias Grid Fit Technology
Pretty awesome, huh?
Pre-Implementation Decisions
The algorithm for building StarReport is deceptively simple:
- Draw the headers
- Draw Company Information
- Draw Report Name
- Draw Company Name
- Draw Company Address
- Draw Date (optional)
- Draw Page Number (optional)
- Draw the records
- Draw Columns
- Draw Rows
- Draw Summaries (optional)
- Draw the footer (optional)
- Repeat for each page of the report
- Draw a grand summary on the last page (optional)
Having those steps in mind the first questions that should come to mind are:
- Where do I get the data to generate the report?
- Where do I print the data? On what surface or control?
Let's answer the first question.
I decided to use the most ubiquitous data control in .NET, namely DataTable
. Every developer is familiar with this object and it has tremendous support in .NET.
Many other .NET libraries return DataTable
objects as data containers. I felt that it would be the simplest vehicle to use to pass data into StarReport.
It also has other useful properties which I can use. For example:
- I can filter the
DataTable
using pseudo SQL syntax which I can dynamically generate.
- I can easily iterate over its columns and rows. I can use its column and row indexers to access columns and rows relatively quickly
- I can create a default view over the records, modify it, and still have the original records
So there you go, I chose the DataTable
to get data into StarReport.
Now let's answer the second question ( Where do I print the data? On what surface
or control?)
We need a drawing surface to draw our report on. The heart of GDI+ (Graphics Device Interface) is the
Graphics
object.
We use this object to draw our report items, namely text, cells, rows, images, shapes, grids, and so on. I could have used the device context of a Windows Form as a drawing surface
on however I thought that this was a bit clumsy. I wanted to heavily customize my drawing surface and have the following capabilities:
- To be able to set a high quality smoothing mode
- To be able to set a high quality interpolation mode
- To set the width and height of the report 'inside' of the UI report component
- To be able to add other features easily later on. For example, saving a picture of the report.
I decided to use a Bitmap
object as my drawing surface. I would get the
Graphics
object I needed from the same Bitmap
as well.
The Bitmap
object gives me all the functionality I mentioned above and a lot more. However we can't create a
Bitmap
for each page of the report since the memory cost
would be too high. Therefore I cached my bitmaps and only display the current visible page. Of course state information has to be stored for this to work.
OK! We've got the main two things, a DataTable to get the data from and a drawing surface on which to draw the actual report on.
Drawing The First Page of the Report
In order to draw the first page of the report, we first need data. As we discussed it comes from a
DataTable
. The DataTable
contains columns and rows which
I assume are pre-populated before calling the GenerateReport
method of StarReport. In order to set up our report, we create
the corresponding ColumnCell
and RowCellInfo
objects to match the rows and columns of our data table. These objects contain all
the information required to draw the data on the screen.
Let's first set up the header information for the report:
StarReport sr = new StarReport();
sr.Report.Settings.ReportType = ReportType.GeneralList;
sr.Report.Settings.Header.ReportTitle = "Patient Master List";
sr.Report.Settings.OutlineTable = true;
sr.Report.Settings.PrintTableGrid = true;
sr.Report.Settings.PrintTotalRecords = true;
sr.Report.Settings.TotalText = "Total Records";
sr.PageSettings.Landscape = true;
sr.Report.Settings.PrintPageNumbers = true;
sr.Report.Settings.PrintRowSeparator = true;
sr.Report.Settings.ReportDate = PrintReportDateLocation.LeftTop;
sr.Report.Settings.PrintHeading = PrintHeading.OnAllPages;
sr.Report.Settings.ColorAlternateRows = true;
The OutlineTable
and PrintTableGrid
properties allow us to automatically draw a grid (like Excel) for the report.
There is no need to deal with 'line' objects and spend hours positioning them just correctly. This used to really annoy me with Crystal Reports.
Also notice that the report type is set to GeneralList
. This means that this is a basic report with columns and rows like Excel.
Later in the article I will show show to create hierarchical reports with groupings and totals.
Now we create a ColumnCell
and a corresponding
RowCellInfo
object for each column that will be displayed in the report.
ColumnCell name = new ColumnCell("Name", 200);
RowCellInfo nameInfo = new RowCellInfo("Name", 200);
nameInfo.RowCell.CellFormat.FormatFlags = StringFormatFlags.NoClip;
nameInfo.RowCell.CellFormat.Alignment = Alignment.Far;
nameInfo.RowCell.Font = new Font(nameInfo.RowCell.Font, FontStyle.Bold);
name.RowCellInfoCollection.Add(nameInfo);
The first column of the report will be displayed as 'Name' in the report. The row cells for this column will get
its data from the 'Name'
column of the underlying data table. As you see from the code example above, I can set other properties as well. For example, formatting, font, just to name a few.
I can even add an image in the row cell if I desire.
We continue along this path and add another column, Chart No. to the report.
Notice that it maps to column ID in the underlying data table.
ColumnCell id = new ColumnCell("Chart No.", 70);
RowCellInfo idInfo = new RowCellInfo("ID", 70);
idInfo.RowCell.DataType = DataType.Int32;
idInfo.RowCell.FormatString = "D8"
Let's assume that these are the only two columns I need for the report. The next step is to generate the report on the surface of our
Graphics
object and display it on the screen.
sr.GenerateReport(table, PrintPageDisplay.PREVIEW);
The 'table' is the data table containing the actual data for the report. That's it! Now we have a fully functioning UI report component. We can now print, print preview,
sort, and filter the report.
Internally I use the Graphics
object that I created from the
Bitmap
to draw the columns:
private void PrintColumns(ColumnCellCollection columns, int position,
ref int yPos, ref Rectangle rect, SolidBrush myBrush, Pen linePen, PrintPageEventArgs e)
{
foreach (ColumnCell column in columns)
{
column.Height = report.Settings.HeaderFont.Height;
DrawColumn(column, position, yPos, ref rect, myBrush, linePen, e);
position += column.Width;
}
}
private void PrintColumns(SolidBrush myBrush, Pen linePen, Rectangle rect, float yPos, PrintPageEventArgs e)
{
int x = e.MarginBounds.Left;
foreach (ColumnCell columnCell in this.report.Data.ColumnCellCollection)
{
columnCell.Height = report.Settings.HeaderFont.Height;
DrawColumn(columnCell, x, (int)yPos, ref rect, myBrush, linePen, e);
x += columnCell.Width;
}
}
private void DrawColumn(ColumnCell columnCell, int x, int y,
ref Rectangle rect, SolidBrush brush, Pen linePen, PrintPageEventArgs e)
{
if (columnCell.ExtendToMargin)
{
columnCell.Width = e.MarginBounds.Right - x;
rect.Width = e.MarginBounds.Right - x;
}
else
rect.Width = columnCell.Width;
rect.Height = columnCell.Height;
rect.X = x;
rect.Y = y;
brush.Color = columnCell.BackgroundColor;
if (columnCell.BackgroundColor != Color.White)
e.Graphics.FillRectangle(brush, rect);
if (columnCell.PrintSeparator)
e.Graphics.DrawRectangle(Pens.Black, rect.X, rect.Y, rect.Width, rect.Height);
defaultStringFormat.Alignment = (StringAlignment)((int)columnCell.CellFormat.Alignment);
defaultStringFormat.LineAlignment = (StringAlignment)((int)columnCell.CellFormat.LineAlignment);
defaultStringFormat.FormatFlags = (StringFormatFlags)((int)columnCell.CellFormat.FormatFlags);
defaultStringFormat.Trimming = (StringTrimming)((int)columnCell.CellFormat.Trimming);
brush.Color = columnCell.ForeColor;
e.Graphics.DrawString(" " + columnCell.Text,
columnCell.Font, brush, rect, defaultStringFormat);
if (columnCell.OutlineColumn)
{
linePen.Color = report.Settings.RowSeparatorLine.Color;
e.Graphics.DrawRectangle(linePen, rect);
}
}
We use a similar procedure to draw each cell of the report:
private void DrawCell(RowCell cell, int x, int y, ref Rectangle rect, SolidBrush brush, Graphics g)
{
if (cell.Text.Trim() == String.Empty && cell.DontPrintIfBlank) return;
if (cell.Width == 0) return;
if (cell.ExtendToMargin)
rect.Width = marginBounds_Right - x;
else
rect.Width = cell.Width;
rect.Height = report.Settings.HeaderFont.Height;
rect.X = x;
rect.Y = y;
brush.Color = cell.BackgroundColor;
if (cell.BackgroundColor != Color.White)
{
g.FillRectangle(brush, rect);
}
brush.Color = cell.ForeColor;
defaultStringFormat.Alignment = (StringAlignment)((int)cell.CellFormat.Alignment);
defaultStringFormat.LineAlignment = (StringAlignment)((int)cell.CellFormat.LineAlignment);
defaultStringFormat.FormatFlags = cell.CellFormat.FormatFlags;
defaultStringFormat.Trimming = (StringTrimming)((int)cell.CellFormat.Trimming);
if (cell.FormatString != String.Empty && cell.Text != report.Settings.NullValue)
{
string formattedString = String.Empty;
switch (cell.DataType)
{
case DataType.Int32:
formattedString = String.Format("{0:" +
cell.FormatString.Trim() + "}", Int32.Parse(cell.Text));
break;
case DataType.Double:
formattedString = String.Format("{0:" +
cell.FormatString.Trim() + "}", Double.Parse(cell.Text));
break;
case DataType.DateTime:
formattedString = String.Format("{0:" +
cell.FormatString.Trim() + "}", DateTime.Parse(cell.Text));
break;
case DataType.String:
formattedString = String.Format("{0:" +
cell.FormatString.Trim() + "}", cell.Text);
break;
}
cell.Text = formattedString;
}
if (cell.Image != null)
{
g.DrawImage(cell.Image, rect.X, rect.Y, cell.Image.Width, cell.Image.Height);
}
if (cell.LeftPadding > 0)
{
cell.Text = PadText(cell.Text.Trim(), cell.PaddingCharacter, cell.LeftPadding);
}
if (cell.Outline)
g.DrawRectangle(Pens.Black, rect);
g.DrawString(" " + cell.Text, cell.Font, brush, rect, defaultStringFormat);
}
What we can gather from the procedures above is that there are a lot of things taken into consideration to properly draw the report to the Graphics
object. For example:
- The available screen dimensions
- What coordinates to start drawing from
- How many records will fit in one page
- The actual width and height of each column and row font. Incorrectly measuring the fonts can wreak havoc on your report!
Some of these considerations as well as the report drawing algorithm mentioned above are outlined in the following procedure:
private int Print(DataTable table, int recordIndex)
{
int pageSize_Width = 816;
int pageSize_Height = 1056;
if (PageSettings.Landscape)
{
pageSize_Width = 1056;
pageSize_Height = 816;
marginBounds_Left = 48;
marginBounds_Width = 960;
marginBounds_Right = 1008;
marginBounds_Top = 48;
marginBounds_Bottom = 768;
marginBounds_Height = 720;
marginBounds_X = 48;
marginBounds_Y = 48;
pageBounds_Width = 1056;
pageBounds_Height = 816;
leftMargin = 48;
}
else
{
pageSize_Width = 1056;
pageSize_Height = 816;
}
Bitmap image = new Bitmap(pageSize_Width, pageSize_Height);
Graphics g = Graphics.FromImage(image);
g.SmoothingMode = SmoothingMode.HighQuality;
g.InterpolationMode = InterpolationMode.HighQualityBilinear;
g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
int yPos = 0;
int numLines = marginBounds_Height / report.Settings.HeaderFont.Height;
numLines--;
Debug.WriteLine("Number of Lines:" + numLines.ToString());
Debug.WriteLine("Top Margins:" + yPos.ToString());
Debug.WriteLine("Font Height:" + report.Settings.HeaderFont.Height);
if (recordIndex == 0 && report.Settings.PrintHeading == PrintHeading.OnFirstPage)
{
PrintHeader2(ref yPos, ref numLines, g);
}
else if (report.Settings.PrintHeading == PrintHeading.OnAllPages)
{
PrintHeader2(ref yPos, ref numLines, g);
}
if (report.Settings.ReportType == ReportType.FreeForm)
{
return PrintFreeForm2(table, numLines, recordIndex, yPos, g);
}
if (table != null && table.DefaultView.Count == 0)
{
g.DrawString("NO RECORDS", new Font(report.Settings.SubscriptFont,
FontStyle.Bold), myBrush, leftMargin, yPos);
hasMorePages = false;
PictureBox dd = new PictureBox();
dd.Size = new Size(pageSize_Width, pageSize_Height);
dd.Image = image;
display.PageImages.Add(dd);
return 0;
}
Debug.WriteLine("Number of Lines:" + numLines.ToString());
Debug.WriteLine("Top Margins:" + yPos.ToString());
if (report.Settings.ReportDate != PrintReportDateLocation.None)
{
PrintDate2(this.report.Settings.ReportDate, g);
if (report.Settings.ReportDate == PrintReportDateLocation.LeftTop ||
report.Settings.ReportDate == PrintReportDateLocation.RightTop)
SkipLine(ref yPos, ref numLines);
}
if (report.Settings.PrintTotalRecords)
{
SizeF size = g.MeasureString(report.Settings.TotalText + ":" +
table.Rows.Count.ToString(), report.Settings.SubHeaderFont);
g.DrawString(report.Settings.TotalText + ":" + table.Rows.Count.ToString(),
report.Settings.SubHeaderFont, Brushes.Black, marginBounds_Right - (int)size.Width, yPos);
}
foreach (TextSection ts in report.Data.TextSections)
{
DrawTextSection2(ts, leftMargin, yPos, ref rect, myBrush, g);
SkipLine(ref yPos, ref numLines);
}
SkipLine(ref yPos, ref numLines);
if (report.Settings.ReportType == ReportType.Hierachy)
{
recordIndex = PrintGroup2(table, numLines, recordIndex, ref yPos, g);
}
else if (report.Settings.ReportType == ReportType.GeneralList)
{
recordIndex = PrintGeneralListReport2(table, numLines, recordIndex, yPos, g);
}
if (report.Settings.PrintPageNumbers)
{
g.DrawString("[ " + currentPage.ToString() + " ]",
report.Settings.SubscriptFont, Brushes.Black,
pageBounds_Width / 2, (float)marginBounds_Bottom);
}
int end = table.DefaultView.Count;
if (recordIndex >= end)
{
SkipLine(ref yPos, ref numLines);
deltaText = String.Empty;
deltaText2 = String.Empty;
PrintGrandTotals2(myBrush, linePen, leftMargin, ref yPos, g);
hasMorePages = false;
recordIndex = 0;
numPages = currentPage;
PictureBox dd = new PictureBox();
dd.Size = new Size(pageSize_Width, pageSize_Height);
dd.Image = image;
display.PageImages.Add(dd);
AddPageButtons(numPages, image);
currentPage = 1;
}
else
{
currentPage++;
hasMorePages = true;
PictureBox dd = new PictureBox();
dd.Size = new Size(pageSize_Width, pageSize_Height);
dd.Image = image;
display.PageImages.Add(dd);
}
UpdateStatusbar();
return recordIndex;
}
Summary Columns
StarReport also support the following aggregate functions:
-
Sum
Maximum
Miniumum
-
Average
Count
For example, let's say I wanted to count how many case records I had, I would configure my column as follows:
ColumnCell ccase = new ColumnCell("Case ID", 100);
sr.Report.Data.SubGroup.Columns.Add(ccase);
RowCellInfo ccaseIDInfo = new RowCellInfo("Case ID", 100);
ccaseIDInfo.FunctionType = FunctionType.Count;
ccaseIDInfo.PrintSummaryLine = true;
ccaseIDInfo.RowCell.FormatString = "D8";
ccaseIDInfo.RowCell.DataType = DataType.Int32;
Internally StarReport performs all the calculations required and then places the results at the bottom of the report.
There is a NumberCollection
object that is responsible for performing all aggregations. There is a
PrintTotals
procedure that prints out the totals.
Notice that we pass in the Graphics
objects from the Bitmap
.
private void PrintTotals(RowCellInfo cellInfo, SolidBrush myBrush, int x, float y, Graphics g)
{
SizeF size = g.MeasureString(cellInfo.SummaryText + cellInfo.NumberCollection.GetSum().ToString(
cellInfo.SummaryFormatString.Trim()), cellInfo.RowCell.Font);
if (cellInfo.RowCell.ExtendToMargin)
{
int rightMargin = marginBounds_Right;
x = rightMargin - (int)size.Width;
}
else
x = x - (int)size.Width;
Debug.WriteLine("Printing Totals");
switch (cellInfo.FunctionType)
{
case FunctionType.Average:
g.DrawString(cellInfo.AverageText + cellInfo.NumberCollection.GetAverage().ToString(
cellInfo.SummaryFormatString.Trim()), report.Settings.SummaryLineFont, myBrush, x, y);
break;
case FunctionType.Summary:
g.DrawString(cellInfo.SummaryText + cellInfo.NumberCollection.GetSum().ToString(
cellInfo.SummaryFormatString.Trim()), report.Settings.SummaryLineFont, myBrush, x, y);
break;
case FunctionType.Maximum:
g.DrawString(cellInfo.MaximumText + cellInfo.NumberCollection.GetMaximum().ToString(
cellInfo.SummaryFormatString.Trim()), report.Settings.SummaryLineFont, myBrush, x, y);
break;
case FunctionType.Minimum:
g.DrawString(cellInfo.MinimumText + cellInfo.NumberCollection.GetMinimum().ToString(
cellInfo.SummaryFormatString.Trim()), report.Settings.SummaryLineFont, myBrush, x, y);
break;
case FunctionType.Count:
g.DrawString(cellInfo.CountText + cellInfo.NumberCollection.GetCount().ToString(
cellInfo.SummaryFormatString.Trim()), report.Settings.SummaryLineFont, myBrush, x, y);
break;
}
}
Hierarchy Reports
StarReport support Hierarchy report types as well. For example, suppose we have a data table
with the following columns:
- Provider
- Case ID
- Date Created
- Description
Let's say we wanted to group the report by Provider and then count how many cases each provider has.
The following code (also included in the demo download) will accomplish this.
private void PrintReferringProviderCases()
{
DataTable table = GetPopulatedFromSomeWhere();
StarReport sr = new StarReport();
sr.Report.Settings.ReportType = ReportType.Hierachy;
sr.Report.Settings.Header.ReportTitle = "Referring Provider's Cases";
sr.Report.Settings.PrintRowSeparator = true;
sr.Report.Settings.GroupIndentSpace = 20;
ColumnCell providerName = new ColumnCell();
providerName.Width = 200;
providerName.Text = "Provider";
RowCellInfo providerNameInfo = new RowCellInfo();
providerNameInfo.DatabaseField = "Provider";
providerNameInfo.Delta = true; This is the pivot column
providerNameInfo.RowCell.Width = 200;
providerNameInfo.RowCell.Font = new Font("Tahoma", 10, FontStyle.Bold);
sr.Report.Data.Group.RowCellInfos.Add(providerNameInfo);
ColumnCell ccase = new ColumnCell("Case ID", 100);
sr.Report.Data.SubGroup.Columns.Add(ccase);
RowCellInfo ccaseIDInfo = new RowCellInfo("CaseID", 100);
ccaseIDInfo.FunctionType = FunctionType.Count;
ccaseIDInfo.PrintSummaryLine = true;
ccaseIDInfo.RowCell.FormatString = "D8";
ccaseIDInfo.RowCell.DataType = DataType.Int32;
sr.Report.Data.SubGroup.RowCellInfos.Add(ccaseIDInfo);
ColumnCell dateCreated = new ColumnCell("Date Created", 150);
RowCellInfo dateCreatedInfo = new RowCellInfo("DateCreated", 150);
sr.Report.Data.SubGroup.Columns.Add(dateCreated);
ColumnCell description = new ColumnCell("Description");
description.ExtendToMargin = true;
sr.Report.Data.SubGroup.Columns.Add(description);
RowCellInfo descriptionInfo = new RowCellInfo("Description");
descriptionInfo.RowCell.ExtendToMargin = true;
sr.Report.Data.SubGroup.RowCellInfos.Add(dateCreatedInfo);
sr.Report.Data.SubGroup.RowCellInfos.Add(descriptionInfo);
sr.GenerateReport(table, PrintPageDisplay.PREVIEW);
}
Notice that there is a Group
and SubGroup
collection. These collections are used to determine which group columns would go into. Also note that there is a
Delta
property.
This property indicates the 'Group By' field.
Previews
General Report UI
On the left side of the report we have a list of clickable page icons. They contain miniature views of the actual report displayed. By clicking on them, we jump
to the specific page.
In this specific report there are six pages. From this main screen you can also filter the report, perform Print Preview, Print, or move forward or backwards through the pages of the report.
Filter View
Sort View
Example Aging Report
Example Cases Report
Using the code
I have included many report examples with the download and source code. As you can see from the below code example, almost anything is configurable.
private void PrintPatientList()
{
DataTable table = new DataTable();
DataSet ds = new DataSet();
ds.ReadXml("Patients.xml");
table = ds.Tables[0];
sr.Report.Settings.ReportType = ReportType.GeneralList;
sr.Report.Settings.Header.ReportTitle = "Patient Master List";
sr.Report.Settings.OutlineTable = true;
sr.Report.Settings.PrintTableGrid = true;
sr.Report.Settings.PrintTotalRecords = true;
sr.Report.Settings.TotalText = "Total Records";
sr.PageSettings.Landscape = true;
ColumnCell name = new ColumnCell("Name", 200);
RowCellInfo nameInfo = new RowCellInfo("Name", 200);
nameInfo.RowCell.CellFormat.FormatFlags = StringFormatFlags.NoClip;
nameInfo.RowCell.Font = new Font(nameInfo.RowCell.Font, FontStyle.Bold);
name.RowCellInfoCollection.Add(nameInfo);
sr.Report.Data.ColumnCellCollection.Add(name);
ColumnCell id = new ColumnCell("Chart No.", 70);
RowCellInfo idInfo = new RowCellInfo("ID", 70);
idInfo.RowCell.DataType = DataType.Int32;
idInfo.RowCell.FormatString = "D8";
id.RowCellInfoCollection.Add(idInfo);
sr.Report.Data.ColumnCellCollection.Add(id);
ColumnCell dob = new ColumnCell("DOB", 70);
RowCellInfo dobInfo = new RowCellInfo("DOB", 70);
dob.RowCellInfoCollection.Add(dobInfo);
sr.Report.Data.ColumnCellCollection.Add(dob);
ColumnCell ss = new ColumnCell("SS#");
RowCellInfo ssInfo = new RowCellInfo("SocialSecurity");
ss.RowCellInfoCollection.Add(ssInfo);
ColumnCell genderCol = new ColumnCell("Gender", 60);
RowCellInfo genderInfo = new RowCellInfo("Gender", 60);
genderCol.RowCellInfoCollection.Add(genderInfo);
sr.Report.Data.ColumnCellCollection.Add(genderCol);
ColumnCell phoneCol = new ColumnCell("Phone", 150);
RowCellInfo phoneInfo = new RowCellInfo("Phone", 150);
phoneCol.RowCellInfoCollection.Add(phoneInfo);
sr.Report.Data.ColumnCellCollection.Add(phoneCol);
ColumnCell addressCol = new ColumnCell("Address", 200);
RowCellInfo addressInfo = new RowCellInfo("Address", 200);
addressCol.RowCellInfoCollection.Add(addressInfo);
sr.Report.Data.ColumnCellCollection.Add(addressCol);
ColumnCell cityCol = new ColumnCell("City", 150);
RowCellInfo cityInfo = new RowCellInfo("City", 150);
cityCol.RowCellInfoCollection.Add(cityInfo);
sr.Report.Data.ColumnCellCollection.Add(cityCol);
ColumnCell stateCol = new ColumnCell("State", 40);
RowCellInfo stateInfo = new RowCellInfo("State", 40);
stateCol.RowCellInfoCollection.Add(stateInfo);
sr.Report.Data.ColumnCellCollection.Add(stateCol);
ColumnCell zipCol = new ColumnCell("Zip", 50);
zipCol.ExtendToMargin = true;
RowCellInfo zipInfo = new RowCellInfo("Zip", 50);
zipCol.RowCellInfoCollection.Add(zipInfo);
sr.Report.Data.ColumnCellCollection.Add(zipCol);
zipInfo.RowCell.ExtendToMargin = true;
sr.GenerateReport(table, PrintPageDisplay.PREVIEW);
Points of Interest
StarReport also supports free form reports. You can place anything anywhere on the report and print it. This functionality can be used to create your own forms
to print using texts and rectangles for textboxes. I will add a few more examples on how to do this a bit later. I also plan on making the UI a little nicer in the future.