Navigation


Markus on Development and Publishing

This is Markus Egger's professional blog, which covers topics such as development, publishing, and business in general. As the publisher of CoDe and CoDe Focus magazines, and as the President and Chief Software Architect of EPS Software Corp., Markus shares his insights and opinions on this blog.

Content Area Footer

Wednesday, July 07, 2010
Printing in Silverlight 4

Silverlight 4 can print. This was big news when it was first announced. While it may be true that printing isn’t as important anymore as it used to be. Lots of stuff is now just handled electronically rather than in paper. However, this doesn’t eliminate the need for printing. Here are a few scenarios where print is useful:

  • Sometimes you just need a piece of paper. Some countries still require printed paper invoices for instance. Or another example is our Tower48 Digital Escrow service, where legal documents and contracts just have to be printed, whether we want it or not.
  • Sometimes certain forms have to be filed, and they need to look exactly right. (Or maybe you need to print a check?). Just plain HTML output and relying on the browser to print that HTML doesn’t even get close to getting the job done.
  • Sometimes, it isn’t all that easy to convert whatever you have to print into HTML for print purposes. What if you have a vector drawing or some graph in Silverlight and want to print that? No easy conversion to HTML there.
  • Sometimes, you may not want a piece of paper, but you may need a file such as XPS or PDF that you can file away or email to someone. Printing these days doesn’t necessarily just mean “printing to paper”. Sending a document through the printer spooler and then through an XPS or PDF creator can be an easy way to accomplish this. (Creating PDF straight out of Silverlight low-level is not for the faint of heart… you’d need some kind of third party tool for this).

So printing is good to have. I just spend a whole bunch of time on a print algorithm for our escrow service company. I am happy with the result I was able to achieve with Silverlight 4. However, it must also be said that printing in SL4 is not a no-brainer. You now have basic print ability through an API, but it isn’t something that “just works”. You will have to do a lot of stuff yourself.

The basic idea of printing in SL4 is pretty simple: You create a print object, trigger a print job through it, and then receive various events, such as “printing a page now”. You simply react to this event and create some sort of Silverlight UI as the print “visual” (the thing you want to send to the printer) and let SL4 print that out. This process continues as long as you indicate there are more pages to be printed.

So let’s say you have a Silverlight control and you want to print the control exactly as it is on screen. You could simply put the following code in a button to have it print:

var doc = new PrintDocument();
doc.PrintPage += (sender, e) =>
                 {
                     e.PageVisual = this;
                     e.HasMorePages = false;
                 };
doc.Print("Example Document");

In this example, I simply use a Lambda Expression to handle the “PrintPage” event (you could have certainly also used a standard event handler). This event handler kicks in as soon as the print job is triggered via the Print method (the parameter is the document title, which is used for things such as the printer spooler). The event handler then fires when the first page is to be printed. It uses the current element (presumably the whole control) as the visual. It then indicates that no further content is to be printed, so this will result in a single page printout.

So far so good (and simple). However, while this is what is usually used as a sample, it is just about useless in real-world scenarios. After all, print-outs rarely look exactly like their on-screen counter parts. Plus, this has some issues such as the printouts really being exactly like the on-screen version. For instance, if you have a list of data, the printout will only show the same content that is on the screen. It won’t show stuff you’d have to scroll for, and it certainly won’t flow over to the next page.

So the question arises: How does one do something for real?

Real-World Printing

Well, that is a bit more difficult to answer. In my scenario, where I had tons of text to print, I had to first figure out how many pages I wanted/needed to print. To do this, I created completely new visuals, rather than using anything that was already on the screen. Unfortunately, there currently is no easy way to do any of this automatically. So your first step is to figure out how large the sheet of paper is you want to print. You really do not know that at this point, since the user hasn’t picked a printer yet, so you have to make some assumptions. In my scenario, I had to support Letter, Legal, and A4 paper formats. To do so, I simply created in-memory Canvas objects of appropriate sizes. In Silverlight, we use “logical pixels” as our measurement. One logical pixel is 1/96th of an inch. Armed with this information, we can create Canvas objects of appropriate sizes. For instance, this creates a Canvas that is of the same size as a Letter sheet of paper:

var letter = new Canvas();
letter.Height = 1056;
letter.Width = 816;

Note: Appropriate sizes are: Letter = 816x1056, Legal = 816x1248, A4 = 797.8x1123.2 pixels.

Theoretically, you can now put stuff on that canvas and use it to print:

var doc = new PrintDocument();
doc.PrintPage += (sender, e) =>
                 {
                     var letter = new Canvas();
                     letter.Height = 1056;
                     letter.Width = 816;
                     letter.Children.Add(
                        new TextBlock() { Text = "Hello World!" });

                     e.PageVisual = letter;
                     e.HasMorePages = false;
                 };
doc.Print("Example Document");

Of course now you have a whole “sheet of paper”, but most printers can’t print all the way to the edge. So you probably do not want to put elements at position 0,0 as in this example. The printer would be somewhat likely to cut it off. (Or more likely, it may print position 0,0 just fine, but will cut off at the bottom right end of the “page”). The way I like to take here is I like to handle the entire page as if it was exactly a physical page, so I support a margin within the document. Typically, a margin would be something like 1 inch all around. So I may want to position the TextBlock at position 96,96:

This gives you a single page. However, in most business cases, you have more data to print than one page. In my case I had a flexible amount of text stored in TextBlock elements. Silverlight does not automatically split that sort of content into multiple pages (a process known as “pagination”). Therefore, I had to do this myself. Not a trivial task really, and a good pagination algorithm is not for the faint of heard (it includes handling concepts such as “orphans” and requires advanced layout concepts). In my case, I decided it was good enough to figure out whether an entire paragraph, or at least a “run” within a paragraph, fit on the page. If not, it goes to the next page. (A “run” is a segment of text, such as an entire paragraph, or a single section with the same formatting, such as a bold sentence). In my case, this worked OK, since I had relatively short paragraphs with relatively simple formatting. For other scenarios, a more advanced approach is likely needed.

My basic idea is relatively simple: I put a TextBlock on my page canvas at the position I want it. Then, I start pulling “inlines” (these are the runs within a text block) from my source data, and add them to the canvas. Then, I measure how much space was taken up. If it didn’t all fit on the page, I remove the last inline again, create a new page, and start the process over, until I have all my pages with all the text. Here is the critical segment of code:

var canvas = new Canvas();
canvas.Height = 1056;
canvas.Width = 816;

var contentArea = new TextBlock();
contentArea.TextWrapping = TextWrapping.Wrap;
contentArea.Width = canvas.Width - (96 * 2);
contentArea.Height = canvas.Height - (96 * 2);
Canvas.SetTop(contentArea, 96);
Canvas.SetLeft(contentArea, 96);
canvas.Children.Add(contentArea);

int originalInlineCount = originalText.Inlines.Count;
for (int counter = 0; counter < originalInlineCount; counter++)
{
    var inline = originalText.Inlines[0];
    originalText.Inlines.RemoveAt(0);
    contentArea.Inlines.Add(inline);
    contentArea.InvalidateArrange();
    contentArea.InvalidateMeasure();
    contentArea.Measure(new Size(contentArea.Width, double.MaxValue));
    if (contentArea.ActualHeight > canvas.Height - (96 * 2);) // too large to fit on page!!!
    {
        contentArea.Inlines.Remove(inline);
        originalText.Inlines.Insert(0, inline); // Back into the original source
        contentArea.InvalidateArrange();
        contentArea.InvalidateMeasure();
    }
}
pages.Add(canvas);

This code continues on until the original source of my text (called “originalText” in this example) has no more inlines left. I add page after page to an in-memory List<Canvas>, so when the algorithm goes through, I have all my pages available to me.

This also allows me to do things like go through all my data to create all the pages, and then iterate over the list once again and add more information to each page, such as “Page 1 of X”. There are many such scenarios where you need to taker this “two-pass” approach of first creating all the content, and then adding more information to each page, once you know how many pages you have. There are so many useful scenarios in fact, that I would consider this the standard approach.

Now that we have all the pages, we can simply send them to the printer:

var doc = new PrintDocument();
var pages = GetPagesLikeAbove();
// Pseudo code…
int currentPage = -1;
doc.PrintPage += (sender, e) =>
                 {
                     currentPage++;
                     e.PageVisual = pages[currentPage];
                     e.HasMorePages = currentPage + 1 < pages.Count;
                 };
doc.Print("Example Document");

This works fine in theory,but there is one more tricky part here: In our page generation algorithm above, we assumed letter-size paper. However, once the print job starts, the user might choose a printer with different paper size. The same goes for margins. You can check these things via the event arguments (“e”) passed to you (there are PageMargins and PrintableArea properties on that object). But what do you really do with that information?

For the margins, a way to go is to set a negative margin on the actual page that is being printed:

var currentVisual = pages[currentPage];
currentVisual.Margin =
    new Thickness(e.PageMargins.Left * -1,
                  e.PageMargins.Top * -1, 0, 0);
e.PageVisual = currentVisual;

This shifts the visual up and to the left to be outside the printable range. However, since our physical page already has margins accounted, things should be back to “normal” (assuming the margins you have actually chosen aren’t smaller than the printer can actually handle… a detail you can check by looking at the margin information sent in the event args). If things get worse, such as the paper size being different from what you expect, or the margins being incompatible… well… that’s not good. You have to re-initiate your whole printing algorithm before the first page gets printed, to create an appropriate size and margin setup (which can really throw off a lot).

Print Preview

As you are reading this, you may say “fine… but why even assume certain sizes and margins? Couldn’t I just do all the page generation when the first page prints or when the print job starts?”. And the answer is “yes”. You may have to do that anyway, truth be told. However, I like having a print algorithm that can create the pages ahead of time. This way, I can allow the user to do things such as “print only page 4 and 7”. But even more useful to me, is the ability to show a preview. Since you already have all your pages available as Silverlight objects in memory, you can simply choose to show them on the screen as well. Stick them in a Viewbox, and you even have a thumbnail preview:

// Show 4th page in preview:
previewViewbox.Child = pages[3];

I find that very useful and extremely simple. Here is a dialog I created for Tower48 using this exact approach. Pretty nice for a web app, if you ask me :-)

Print Preview Dialog in Silverlight 4

So all things considered, printing works reasonably well in Silverlight 4. However, you have to handle a lot of things yourself. Creating a bullet-proof pagination algorithm that can handle all kinds of content is difficult. You can find some code for this on places like Codeplex. Check it out. Maybe it will fit your needs. But it would be nice if Microsoft made this a bit easier in future versions of Silverlight…



Posted @ 6:36 AM by Egger, Markus (markus@code-magazine.com) -
Comments (695)


 

 

 

 

 

 

 

Syndication RSS 2.0 RSS 2.0

All My Blogs:
My personal blogs:
Dev and Publishing Dev and Publishing
Travel and Internat. Living Travel and Internat. Living
Other blogs I contribute to:
Milos Blog (US) Milos Blog (US)
VFPConv. Dev Blog (US) VFPConv. Dev Blog (US)
VFPConv. Dev Blog (DE) VFPConv. Dev Blog (DE)

 

Blog Archives
All Blog Posts

2015
    September (1)
2012
    September (1)
    April (1)
    March (1)
2011
    October (1)
    June (3)
    May (1)
    March (2)
    February (2)
    January (2)
2010
    December (3)
    November (2)
    October (2)
    September (1)
    August (2)
    July (1)
    June (1)
    April (3)
    March (1)
    February (5)
    January (1)
2009
    October (4)
    September (2)
    August (1)
    July (1)
    May (4)
    April (6)
    February (1)
    January (1)
2008
    December (3)
    November (11)
    October (8)
    September (1)
    July (1)
    June (3)
    May (3)
    April (6)
    March (6)
    February (4)
2007
    December (1)
    November (1)
    October (5)
    September (1)
    August (1)
    July (6)
    June (3)
    May (3)
    April (1)
    March (2)
    January (2)
2006
    December (3)
    November (4)
    October (1)
    September (2)
    August (2)
    July (4)
    June (1)
    May (2)
    April (10)
    March (2)
    February (3)
    January (1)
2005
    December (6)
    November (7)
    October (6)
    September (8)
    August (10)
    July (6)
    June (9)

 

 

 

This Blog is powered by MilosTM Collaboration Components.