Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF-Less GDI+.NET Report Component: Star Report

0.00/5 (No votes)
25 Jan 2013 4  
StarReport: WPF-less GDI+.NET report component.

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:

  1. Where do I get the data to generate the report?
  2. 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: 

  1. I can filter the DataTable using pseudo SQL syntax which I can dynamically generate.
  2. I can easily iterate over its columns and rows. I can use its column and row indexers to access columns and rows relatively quickly
  3. 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:

  1. To be able to set a high quality smoothing mode
  2. To be able to set a high quality interpolation mode
  3. To set the width and height of the report 'inside' of the UI report component
  4. 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)
{
    //Draw Columns from left margins
    int x = e.MarginBounds.Left;
    foreach (ColumnCell columnCell in this.report.Data.ColumnCellCollection)
    {
        //Draw the column
        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)
{
  //Check if to draw this column all the way to the end margin
   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;

   //Draw column background
   brush.Color = columnCell.BackgroundColor;
   if (columnCell.BackgroundColor != Color.White)
       e.Graphics.FillRectangle(brush, rect);


   //Print a separator line at the end of the column
   if (columnCell.PrintSeparator)
        e.Graphics.DrawRectangle(Pens.Black, rect.X, rect.Y, rect.Width, rect.Height);

   //Align the text in the columns
   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;
   //Draw the column
   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)
{

   //Make sure that there is a value to print
   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;

   //Format the cell data
    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:

  1. The available screen dimensions
  2. What coordinates to start drawing from
  3. How many records will fit in one page
  4. 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)
{
    //Set up our margins
    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;
    }

    //Create our Graphics object
    Bitmap image = new Bitmap(pageSize_Width, pageSize_Height);
    Graphics g = Graphics.FromImage(image);

    //Make our report look good
    g.SmoothingMode = SmoothingMode.HighQuality;
    g.InterpolationMode = InterpolationMode.HighQualityBilinear;
    g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
  
    int yPos = 0;// e.MarginBounds.Top;  //Top margin position to start printing

    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);

    //Print report report.Settings.Header on first page of report
    if (recordIndex == 0 && report.Settings.PrintHeading == PrintHeading.OnFirstPage)
    {
        PrintHeader2(ref yPos, ref numLines, g);
    }
    else if (report.Settings.PrintHeading == PrintHeading.OnAllPages)
    {
        //Print report report.Settings.Header on all pages of report
        PrintHeader2(ref yPos, ref numLines, g);
    }

    //If this is a free form report then PrintFreeForm
    if (report.Settings.ReportType == ReportType.FreeForm)
    {
        return PrintFreeForm2(table, numLines, recordIndex, yPos, g);
    }

    //No records to print
    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());

    //Print date on report
    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);
    }

    //Print the total amount of records in this table
    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);
    }

    //Print any text section for this report
    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);

    //Print Hierarchy Report
    if (report.Settings.ReportType == ReportType.Hierachy)
    {
        recordIndex = PrintGroup2(table, numLines, recordIndex, ref yPos, g);
    }

    //Print General List Report
    else if (report.Settings.ReportType == ReportType.GeneralList)
    {
        //MessageBox.Show("About to PrintGeneralListReport2");
        recordIndex = PrintGeneralListReport2(table, numLines, recordIndex, yPos, g);
    }

    if (report.Settings.PrintPageNumbers)
    {
        //Print page number at the bottom of the page
        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);

        //Reset for group reports
        deltaText = String.Empty;
        deltaText2 = String.Empty;

        //Print Grand Totals
        PrintGrandTotals2(myBrush, linePen, leftMargin, ref yPos, g);

        hasMorePages = false;

        //Reset variables
        recordIndex = 0;
        numPages = currentPage;

        PictureBox dd = new PictureBox();
        dd.Size = new Size(pageSize_Width, pageSize_Height);
        dd.Image = image;
        display.PageImages.Add(dd);

       //Add the report buttons on the left hand side
        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: 

  1. Sum
  2. Maximum
  3. Miniumum
  4. Average
  5. 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;

    //Print Totals
    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: 

  1. Provider
  2. Case ID
  3. Date Created
  4. 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;


    //Column Provider Name
    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);

    //Display column in the second group
    sr.Report.Data.Group.RowCellInfos.Add(providerNameInfo);


    //Column Case
    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;

    // Display column in the second group
    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;

    //NAME
    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);

    //ID
    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);

    //DOB
    ColumnCell dob = new ColumnCell("DOB", 70);
    RowCellInfo dobInfo = new RowCellInfo("DOB", 70);
    dob.RowCellInfoCollection.Add(dobInfo);
    sr.Report.Data.ColumnCellCollection.Add(dob);

    //SOCIAL
    ColumnCell ss = new ColumnCell("SS#");
    RowCellInfo ssInfo = new RowCellInfo("SocialSecurity");
    ss.RowCellInfoCollection.Add(ssInfo);

    //GENDER
    ColumnCell genderCol = new ColumnCell("Gender", 60);
    RowCellInfo genderInfo = new RowCellInfo("Gender", 60);
    genderCol.RowCellInfoCollection.Add(genderInfo);
    sr.Report.Data.ColumnCellCollection.Add(genderCol);

    //PHONE
    ColumnCell phoneCol = new ColumnCell("Phone", 150);
    RowCellInfo phoneInfo = new RowCellInfo("Phone", 150);
    phoneCol.RowCellInfoCollection.Add(phoneInfo);
    sr.Report.Data.ColumnCellCollection.Add(phoneCol);

    //ADDRESS
    ColumnCell addressCol = new ColumnCell("Address", 200);
    RowCellInfo addressInfo = new RowCellInfo("Address", 200);
    addressCol.RowCellInfoCollection.Add(addressInfo);
    sr.Report.Data.ColumnCellCollection.Add(addressCol);

    //CITY
    ColumnCell cityCol = new ColumnCell("City", 150);
    RowCellInfo cityInfo = new RowCellInfo("City", 150);
    cityCol.RowCellInfoCollection.Add(cityInfo);

    sr.Report.Data.ColumnCellCollection.Add(cityCol);

    //STATE
    ColumnCell stateCol = new ColumnCell("State", 40);
    RowCellInfo stateInfo = new RowCellInfo("State", 40);
    stateCol.RowCellInfoCollection.Add(stateInfo);
    sr.Report.Data.ColumnCellCollection.Add(stateCol);

    //Zip
    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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here