Greetings,
I have a problem with printing in WPF.
I am creating a flow document and add some controls to that flow document.
Print Preview works ok and i have no problem with printing from a print preview window.
The problem exists when I print directly to the printer without a print preview. But what is more surprisingly – when I use XPS Document Writer as a printer
everyting is ok, when i use some physical printer, some controls on my flow document are not displayed.
Thanks in advance
WPF Printing Flow Document
flowdocumentprintingwpf
Related Solutions
yes, make a copy of the FlowDocument before printing it. This is because the pagination and margins will be different. This works for me.
private void DoThePrint(System.Windows.Documents.FlowDocument document)
{
// Clone the source document's content into a new FlowDocument.
// This is because the pagination for the printer needs to be
// done differently than the pagination for the displayed page.
// We print the copy, rather that the original FlowDocument.
System.IO.MemoryStream s = new System.IO.MemoryStream();
TextRange source = new TextRange(document.ContentStart, document.ContentEnd);
source.Save(s, DataFormats.Xaml);
FlowDocument copy = new FlowDocument();
TextRange dest = new TextRange(copy.ContentStart, copy.ContentEnd);
dest.Load(s, DataFormats.Xaml);
// Create a XpsDocumentWriter object, implicitly opening a Windows common print dialog,
// and allowing the user to select a printer.
// get information about the dimensions of the seleted printer+media.
System.Printing.PrintDocumentImageableArea ia = null;
System.Windows.Xps.XpsDocumentWriter docWriter = System.Printing.PrintQueue.CreateXpsDocumentWriter(ref ia);
if (docWriter != null && ia != null)
{
DocumentPaginator paginator = ((IDocumentPaginatorSource)copy).DocumentPaginator;
// Change the PageSize and PagePadding for the document to match the CanvasSize for the printer device.
paginator.PageSize = new Size(ia.MediaSizeWidth, ia.MediaSizeHeight);
Thickness t = new Thickness(72); // copy.PagePadding;
copy.PagePadding = new Thickness(
Math.Max(ia.OriginWidth, t.Left),
Math.Max(ia.OriginHeight, t.Top),
Math.Max(ia.MediaSizeWidth - (ia.OriginWidth + ia.ExtentWidth), t.Right),
Math.Max(ia.MediaSizeHeight - (ia.OriginHeight + ia.ExtentHeight), t.Bottom));
copy.ColumnWidth = double.PositiveInfinity;
//copy.PageWidth = 528; // allow the page to be the natural with of the output device
// Send content to the printer.
docWriter.Write(paginator);
}
}
Take a look at this tutorial. Buried in the comments is a DocumentPaginator
which was explicitly created for the printing of a DataGrid
.
public class DocPaginator : DocumentPaginator
{
#region Fields
private int _rows;
private int _columns;
private int _rowsPerPage;
private int _columnsPerPage;
private int _firstColumnWidth;
private int _horizontalPageCount;
private int _verticalPageCount;
private System.Windows.Size _pageSize;
private DataGrid _dataGrid;
private List<string> _columnsList;
private bool _isFirstColumnLarger;
private List<Dictionary<int, Pair<int, int>>> _columnSlotsList;
private string _strTitle;
private string _strComment;
#endregion Fields
#region Constructor
public DocPaginator(DataGrid dataGrid, System.Windows.Size pageSize, List<string> columnsList,
bool isFirstColumnLarger = false, string strTitle = null, string strComment = null)
{
_rows = dataGrid.Items.Count;
_columns = dataGrid.Columns.Count;
_dataGrid = dataGrid;
_columnsList = columnsList;
_isFirstColumnLarger = isFirstColumnLarger;
_strTitle = strTitle;
_strComment = strComment;
CalculateFirstColumnWidth();
PageSize = pageSize;
_horizontalPageCount = HorizontalPageCount;
_verticalPageCount = VerticalPageCount;
GenerateColumnSlots();
}
#endregion Constructor
#region Public Methods
public override DocumentPage GetPage(int pageNumber)
{
double pgNumber = Math.IEEERemainder(pageNumber, _verticalPageCount) >= 0 ?
Math.IEEERemainder(pageNumber, _verticalPageCount) :
Math.IEEERemainder(pageNumber, _verticalPageCount) + _verticalPageCount;
int currentRow = Convert.ToInt32(_rowsPerPage * pgNumber);
var page = new PageElement(currentRow, Math.Min(_rowsPerPage, _rows - currentRow), _dataGrid,
_columnsList, _firstColumnWidth, _isFirstColumnLarger,
GetColumnSlot(pageNumber), _strTitle, _strComment, GetPageNumber(pageNumber))
{
Width = PageSize.Width,
Height = PageSize.Height,
};
page.Measure(PageSize);
page.Arrange(new Rect(new System.Windows.Point(0, 0), PageSize));
return new DocumentPage(page);
}
#endregion Public Methods
#region Public Properties
public override bool IsPageCountValid
{ get { return true; } }
public override int PageCount
{ get { return _horizontalPageCount * _verticalPageCount; } }
public override System.Windows.Size PageSize
{
get { return _pageSize; }
set
{
_pageSize = value;
_rowsPerPage = PageElement.RowsPerPage(PageSize.Height);
_columnsPerPage = PageElement.ColumnsPerPage(PageSize.Width, _firstColumnWidth);
//Can't print anything if you can't fit a row on a page
Debug.Assert(_rowsPerPage > 0);
}
}
public override IDocumentPaginatorSource Source
{ get { return null; } }
public int HorizontalPageCount
{
get { return (int)Math.Ceiling((_columns - 1) / (double)_columnsPerPage); }
}
public int VerticalPageCount
{
get { return (int)Math.Ceiling(_rows / (double)_rowsPerPage); }
}
#endregion Public Properties
#region Private Methods
private void CalculateFirstColumnWidth()
{
int maxDataLen = 0;
for (int i = 0; i < _dataGrid.Items.Count; i++)
{
List<Object> icol = (List<Object>)_dataGrid.Items[i];
var largestDataItem = (from d in icol
select d != null ? d.ToString().Length : 0).Max();
maxDataLen = maxDataLen < largestDataItem ? largestDataItem : maxDataLen;
}
string strDataLen = string.Join("a", new string[maxDataLen + 1]);
_firstColumnWidth = PageElement.CalculateBitLength(strDataLen,
new Font("Tahoma", 8, System.Drawing.FontStyle.Regular, GraphicsUnit.Point));
}
private void GenerateColumnSlots()
{
_columnSlotsList = new List<Dictionary<int, Pair<int, int>>>();
for (int i = 0; i < _horizontalPageCount; i++)
{
Dictionary<int, Pair<int, int>> columnSlot = new Dictionary<int, Pair<int, int>>();
columnSlot.Add(1, new Pair<int, int>((_columnsPerPage * i) + 1,
Math.Min(_columnsPerPage * (i + 1), _columns - 1)));
_columnSlotsList.Add(columnSlot);
}
}
private Dictionary<int, Pair<int, int>> GetColumnSlot(int pageNumber)
{
for (int i = 0; i <= _columnSlotsList.Count; i++)
{
if (i == Math.Ceiling(Convert.ToDouble(pageNumber / _verticalPageCount)))
return _columnSlotsList[i];
}
return new Dictionary<int, Pair<int, int>>();
}
private string GetPageNumber(int intPageNumber)
{
string strPageNumber = String.Empty;
if (_horizontalPageCount == 1)
strPageNumber = (intPageNumber + 1).ToString();
else
{ }
return strPageNumber;
}
#endregion Private Methods
}
#region Pair Class
public class Pair<TStart, TEnd>
{
public Pair(TStart start, TEnd end)
{
Start = start;
End = end;
}
public TStart Start { get; set; }
public TEnd End { get; set; }
}
#endregion
And this is the enhanced version of PageElement:
public class PageElement : UserControl
{
#region Constants
private const int PAGE_MARGIN = 40;
private const int HEADER_HEIGHT = 50;
private const int LINE_HEIGHT = 20;
private const int COLUMN_WIDTH = 60;
private const int HEADER_CHR_WIDTH = 9;
private const int HEADER_LINE_HEIGHT = 12;
private const string EXCAPE_CHAR = "\r\n";
private const string NOT_APPLICAPLE = "N/A";
#endregion Constants
#region Fields
private int _currentRow;
private int _rows;
private DataGrid _dataGrid;
private List<string> _columns;
private int _firstColumnWidth;
private bool _isFirstColumnLarger;
private static int _columnsPerPage;
private Dictionary<int, Pair<int, int>> _columnSlot;
private string _strTitle;
private string _strComment;
private string _strPageNumber;
#endregion Fields
#region Constructor
public PageElement(int currentRow, int rows, DataGrid dataGrid, List<string> columns,
int firstColumnWidth, bool isFirstColumnLarger, Dictionary<int,
Pair<int, int>> columnSlot, string strTitle, string strComment,
string strPageNumber)
{
Margin = new Thickness(PAGE_MARGIN);
_currentRow = currentRow;
_rows = rows;
_dataGrid = dataGrid;
_columns = columns;
_firstColumnWidth = firstColumnWidth;
_isFirstColumnLarger = isFirstColumnLarger;
_columnSlot = columnSlot;
_strTitle = strTitle;
_strComment = strComment;
_strPageNumber = strPageNumber;
}
#endregion Constructor
#region Public Static Functions
public static int RowsPerPage(double height)
{
//5 times Line Height deducted for: 1 for Title and Comments each; 2 for Page Number and 1 for Date
return (int)Math.Floor((height - (2 * PAGE_MARGIN) - HEADER_HEIGHT - (5 * LINE_HEIGHT)) / LINE_HEIGHT);
}
public static int ColumnsPerPage(double width, int firstColumnWidth)
{
_columnsPerPage = (int)Math.Floor((width - (2 * PAGE_MARGIN) - firstColumnWidth) / COLUMN_WIDTH);
return _columnsPerPage;
}
public static int CalculateBitLength(string strData, d.Font font)
{
using (d.Graphics graphics = d.Graphics.FromImage(new d.Bitmap(1, 1)))
{
d.SizeF dtsize = graphics.MeasureString(strData, font);
return Convert.ToInt32(dtsize.Width);
}
}
#endregion Public Static Functions
#region Private Functions
private static FormattedText MakeText(string text, int fontSize)
{
return new FormattedText(text, CultureInfo.CurrentCulture,
FlowDirection.LeftToRight, new Typeface("Tahoma"), fontSize, Brushes.Black);
}
#endregion Private Functions
#region Protected Functions
protected override void OnRender(DrawingContext dc)
{
Point curPoint = new Point(0, 0);
int beginCounter = 0;
double intYAxisTracker = 0;
double TitleHeight = 0;
//Print Title.
if (_strTitle != null)
{
int intTitleLength = CalculateBitLength(_strTitle, new d.Font("Tahoma", 9, d.FontStyle.Regular));
curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intTitleLength / 2);
dc.DrawText(MakeText(_strTitle, 9), curPoint);
curPoint.Y += LINE_HEIGHT;
curPoint.X = 0;
}
//Print Comment.
if (_strTitle != null)
{
int intCommentLength = CalculateBitLength(_strComment, new d.Font("Tahoma", 9, d.FontStyle.Regular));
curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intCommentLength / 2);
dc.DrawText(MakeText(_strComment, 9), curPoint);
curPoint.Y += LINE_HEIGHT;
curPoint.X = 0;
}
//Print current Date.
int intDatLength = CalculateBitLength(String.Format("{0:MMMM dd, yyyy}", DateTime.Now), new d.Font("Tahoma", 9, d.FontStyle.Regular));
curPoint.X = ((Width - (2 * PAGE_MARGIN)) / 2) - (intDatLength / 2);
dc.DrawText(MakeText(String.Format("{0:MMMM dd, yyyy}", DateTime.Now), 9), curPoint);
curPoint.Y += LINE_HEIGHT;
curPoint.X = 0;
TitleHeight = curPoint.Y;
//Print First column of header row.
dc.DrawText(MakeText(_columns[0], 9), curPoint);
curPoint.X += _firstColumnWidth;
beginCounter = _columnSlot[1].Start;
//Print other columns of header row
for (int i = beginCounter; i <= _columnSlot[1].End; i++)
{
//Remove unwanted characters
_columns[i] = _columns[i].Replace(EXCAPE_CHAR, " ");
if (_columns[i].Length > HEADER_CHR_WIDTH)
{
//Loop through to wrap the header text
for (int k = 0; k < _columns[i].Length; k += HEADER_CHR_WIDTH)
{
int subsLength = k > _columns[i].Length - HEADER_CHR_WIDTH ? _columns[i].Length - k : HEADER_CHR_WIDTH;
dc.DrawText(MakeText(_columns[i].Substring(k, subsLength), 9), curPoint);
curPoint.Y += HEADER_LINE_HEIGHT;
}
}
else
dc.DrawText(MakeText(_columns[i], 9), curPoint);
//YAxisTracker keeps track of maximum lines used to print the headers.
intYAxisTracker = intYAxisTracker < curPoint.Y ? curPoint.Y : intYAxisTracker;
curPoint.X += COLUMN_WIDTH;
curPoint.Y = TitleHeight;
}
//Reset X and Y pointers
curPoint.X = 0;
curPoint.Y += intYAxisTracker - TitleHeight;
//Draw a solid line
dc.DrawRectangle(Brushes.Black, null, new Rect(curPoint, new Size(Width, 2)));
curPoint.Y += HEADER_HEIGHT - (2 * LINE_HEIGHT);
//Loop through each collection in dataGrid to print the data
for (int i = _currentRow; i < _currentRow + _rows; i++)
{
List<Object> icol = (List<Object>)_dataGrid.Items[i];
//Print first column data
dc.DrawText(MakeText(icol[0].ToString(), 10), curPoint);
curPoint.X += _firstColumnWidth;
beginCounter = _columnSlot[1].Start;
//Loop through items in the collection; Loop only the items for currect column slot.
for (int j = beginCounter; j <= _columnSlot[1].End; j++)
{
dc.DrawText(MakeText(icol[j] == null ? NOT_APPLICAPLE : icol[j].ToString(), 10), curPoint);
curPoint.X += COLUMN_WIDTH;
}
curPoint.Y += LINE_HEIGHT;
curPoint.X = 0;
}
//Print Page numbers
curPoint.Y = Height - (2 * PAGE_MARGIN) - LINE_HEIGHT;
curPoint.X = Width - (2 * PAGE_MARGIN) - COLUMN_WIDTH;
dc.DrawText(MakeText(_strPageNumber, 9), curPoint);
}
#endregion Protected Functions
}
Best Answer
Important thing to note : You can use XpsDocumentWriter even when printing directly to a physical printer. Don't make the mistake I did of avoiding it just because you're not creating an .xps file!
Anyway - I had this same problem, and none of the
DoEvents()
hacks seemed to work. I also wasn't particularly happy about having to use them in the first place. In my situation some of the databound controls printed fine, but some others (nested UserControls) didnt. It was as if only one 'level' was being databound and the rest wouldn't bind even with a 'DoEvents()' hack.The solution was simple though. Use XpsDocumentWriter like this. it will open a dialog where you can choose whichever installed physical printer you want.
I found the OReilly book on 'Programming WPF' quite useful with its chapter on Printing - found through Google Books.
If you don't want a print dialog to appear, but want to print directly to the default printer you can do the following. (For me the application is to print packing slips in a warehouse environment - and I don't want a dialog popping up every time).