Correcting the printed margin calculation of the Windows Forms ReportViewer control
[Update 02/04/2008 - It has recently been brought to my attention that this issue has been corrected with the ReportViewer control shipping with Visual Studio 2008.]
In certain situations when printing a report from the Windows Forms ReportViewer control, the printed report's margins are different from the margins specified in the report. This seems to vary from printer to printer, but as a general rule of thumb, if the top margin or left margin are specified to be less than 0.5" then the printed output will appear to have larger-than-specified margins. Until such time as this issue is corrected, there are two supported workarounds: either export the report to an intermediate format (such as PDF) and print that, or override the print functionality of the Windows Forms ReportViewer control. Yet another possibility would be to host a web browser control and use the Web Forms ReportViewer control.
The first workaround is obvious, but the second is a little less straightforward, so I will walk through all of the code necessary to provide your own printing logic for the control. Note that the code differs slightly depending on whether you are using a ServerReport or a LocalReport in your ReportViewer. I will first present the common elements and then explain the differences between LocalReport processing and ServerReport processing.
To begin with, start with the MSDN Walkthrough: Printing a Local Report without Preview article. This is a console application, so you will most likely want to modify it to a class that you can use with your application. Your first step is to add a handler for the ReportViewer control's Print event, similar to the following:
public void HandlePrint(object sender, CancelEventArgs e)
{
Run();
e.Cancel = true;
}
The Run method invokes the custom printing code from the article, and by setting e.Cancel to true we prevent the internal ReportViewer printing mechanism from firing.
Because we're replacing the print functionality of the ReportViewer, we probably don't want the print to occur silently. As a result, you can make changes similar to the following to display a print dialog:
private void Print(PrinterSettings printerSettings)
{
if (m_streams == null || m_streams.Count == 0)
return;
PrintDocument printDoc = new PrintDocument();
printDoc.PrinterSettings = printerSettings;
printDoc.PrintPage += new PrintPageEventHandler(PrintPage);
printDoc.Print();
}
private void Run()
{
PrintDialog printDialog = new PrintDialog();
if (printDialog.ShowDialog() == DialogResult.OK)
{
Export(m_viewer.ServerReport);
m_currentPageIndex = 0;
Print(printDialog.PrinterSettings);
}
}
This brings us to the crux of the issue, correcting the margin offset:
private void PrintPage(object sender, PrintPageEventArgs ev)
{
Metafile pageImage = new
Metafile(m_streams[m_currentPageIndex]);
// Note: Coordinate (0,0) does not coincide with the top left corner of
// the page; it coincides with the top left corner of the printable area
// of the page. To account for this we have to subtract the hard margin.
RectangleF adjustedRect = new RectangleF(
ev.PageBounds.Left - ev.PageSettings.HardMarginX,
ev.PageBounds.Top - ev.PageSettings.HardMarginY,
ev.PageBounds.Width, ev.PageBounds.Height);
ev.Graphics.DrawImage(pageImage, adjustedRect);
m_currentPageIndex++;
ev.HasMorePages = (m_currentPageIndex < m_streams.Count);
}
So ends the common functionality to both the ServerReport and LocalReport cases. In the LocalReport case, you are actually done here; the rendering code in the walkthrough sample is all that you need. However, in the case of the ServerReport, the Render method has no overload that accepts a CreateStreamCallback delegate, so you have to do the heavy lifting yourself. There are two basic approaches: call the Render method, get the array of stream identifiers and process each with a call to the RenderStream method. This works, but it is much slower than the other option, which is to utilize the rs:PersistStreams option of the ReportServer web service by accessing the ReportServer web service via URL access. As an example, the updated Export method might look like the following:
private void Export(ServerReport report)
{
string deviceInfo =
"<DeviceInfo>" +
" <OutputFormat>EMF</OutputFormat>" +
" <PageWidth>8.5in</PageWidth>" +
" <PageHeight>11in</PageHeight>" +
" <MarginTop>0.25in</MarginTop>" +
" <MarginLeft>0.25in</MarginLeft>" +
" <MarginRight>0.25in</MarginRight>" +
" <MarginBottom>0.25in</MarginBottom>" +
"</DeviceInfo>";
m_streams = new List<Stream>();
string mimeType;
string extension;
System.Collections.Specialized.NameValueCollection urlAccessParameters = new System.Collections.Specialized.NameValueCollection();
urlAccessParameters.Add("rs:PersistStreams", "True");
Stream reportStream =
report.Render("Image", deviceInfo, urlAccessParameters, out mimeType, out extension);
m_streams.Add(reportStream);
urlAccessParameters.Remove("rs:PersistStreams");
urlAccessParameters.Add("rs:GetNextStream", "True");
while (reportStream.Length != 0)
{
reportStream =
report.Render("Image", deviceInfo, urlAccessParameters, out mimeType, out extension);
m_streams.Add(reportStream);
}
m_streams.RemoveAt(m_streams.Count - 1);
foreach (Stream stream in m_streams)
stream.Position = 0;
}
Note that this code has hard coded the page dimensions and margin information; you would likely want to parameterize this in the real world, and undoubtedly there are other changes you would want to consider to overcome some of the naive aspects of the sample code displayed here.