Thursday, March 24, 2011

Silverlight4 - Printing fit to page

RAVAGE

In my project one of the feature we need to provide is the ability to print a page which holds multiple custom widgets. Printing ability need to be provided in two stages. One for printing the individual widget and the other one for printing the entire page with multiple widgets. Initially I started with developing the provision for printing individual widgets and achieved it simply than what I expected, using the print API in Silverlight 4.

SIMPLE PRINT

SNIPPET 1

public void Print(UIElement elementToBePrinted, string documentName)
{
    PrintDocument docToPrint = new PrintDocument();

    docToPrint.PrintPage += (s, args) =>
    {
        args.PageVisual = elementToBePrinted;
    };

    docToPrint.Print(documentName);
}

I tried printing widgets of normal sizes using the above code which worked and tried the same for printing the entire page with multiple widgets where I got caught. Upon printing entire page, some part will exceed the print area based on the paper size resulting in partial area got print. Simply speaking it won't fit into print area.

We can tranform the FrameworkElement to fit the paper size but upon doing so the element will be scaled visually while printing and the user experience will not be good. Upon searching the web I found that people who experienced similar issues have done workarounds something similar to the one specified in a blog by Jacek Jura.

Workaround is hiding the scaled screen and showing a custom text like "Priniting in progress". I implemented similar workaround and still my tech lead is not satisfied. Hence, I pushed out to find a better alternate for this.

After trying with several options I compromised with one workaround. Idea is getting a snapshot of the element / page and showing the snapshot by hiding the scaled / transformed element while printing. Yes as most of might guess, its what our actors do to trick out video surveillance (in movies like MissionImpossible, Speed, etc....).

I altered the code to fit my needs. Until the completion of the printing process, the UI thread will be blocked and hence the user can't perform actions on UI till then. Hence showing snapshot at that time won't deviate the user experience.

Pros and Cons

The advantage is, user will experience the original view(visually) even when the element to be printed is scaled.
Disadvantage is, since we're showing the snapshot, it'll be slightly blur, but compromisable.

Snippet 2

public class PrintUtility
{
    #region Class Level Variables

    private const string NullReferenceExceptionMessage = "PrintElement is null : Please specify the element to be printed.";
    private Grid parentGrid;
    private Border borderItem;
    private WriteableBitmap printElementImage;
    private ImageBrush printElementImageBrush;

    #endregion

    #region Properties

    public FrameworkElement PrintElement { get; private set; }

    public string DocumentName { get; private set; }

    public FrameworkElement TabletUIElement { get; set; }

    #endregion

    #region Constructors

    public PrintUtility(FrameworkElement elementToBePrinted, string documentName)
    {
        this.PrintElement = elementToBePrinted;
        this.DocumentName = documentName;
    }

    #endregion

    #region Event Handlers

    void DocToPrint_EndPrint(object sender, EndPrintEventArgs e)
    {
        this.PrintElement.RenderTransform = null;

        if (this.borderItem != null)
        {
            if (parentGrid.Children.Contains(this.borderItem))
            {
                parentGrid.Children.Remove(this.borderItem);
            }
            this.borderItem = null;
        }

        this.parentGrid = null;
        this.printElementImage = null;
        this.printElementImageBrush = null;
    }

    void DocToPrint_BeginPrint(object sender, BeginPrintEventArgs e)
    {
        FrameworkElement maskElement = this.TabletUIElement ?? this.PrintElement;

        if (maskElement.Parent is Grid)
        {
            parentGrid = (Grid)maskElement.Parent;
            parentGrid.Children.Add(BuildBorderWithImageFill());
        }
    }

    void DocToPrint_PrintPage(object sender, PrintPageEventArgs e)
    {
        double scale = 1;

        if (e.PrintableArea.Height < this.PrintElement.ActualHeight)
        {
            scale = e.PrintableArea.Height / this.PrintElement.ActualHeight;
        }

        if (e.PrintableArea.Width < this.PrintElement.ActualWidth && e.PrintableArea.Width / this.PrintElement.ActualWidth < scale)
        {
            scale = e.PrintableArea.Width / this.PrintElement.ActualWidth;
        }

        if (scale < 1)
        {
            ScaleTransform scaleTransform = new ScaleTransform();
            scaleTransform.ScaleX = scale;
            scaleTransform.ScaleY = scale;

            this.PrintElement.RenderTransform = scaleTransform;
        }

        e.PageVisual = this.PrintElement;
    }

    #endregion

    #region Public Methods

    public void Print()
    {
        if (this.PrintElement != null)
        {
            PrintDocument docToPrint = new PrintDocument();

            docToPrint.PrintPage += new EventHandler<PrintPageEventArgs>(DocToPrint_PrintPage);
            docToPrint.BeginPrint += new EventHandler<BeginPrintEventArgs>(DocToPrint_BeginPrint);
            docToPrint.EndPrint += new EventHandler<EndPrintEventArgs>(DocToPrint_EndPrint);

            docToPrint.Print(this.DocumentName);
        }
        else
        {
            throw new NullReferenceException(NullReferenceExceptionMessage);
        }

    }

    #endregion

    #region Private Methods

    private Border BuildBorderWithImageFill()
    {
        printElementImageBrush = new ImageBrush();
        borderItem = new Border();

        printElementImage = new WriteableBitmap(this.TabletUIElement ?? this.PrintElement, null);
        printElementImageBrush.ImageSource = printElementImage;

        borderItem.Background = printElementImageBrush;
        borderItem.Child = new TextBlock() { Text = "Printing...", HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center };

        return borderItem;
    }

    #endregion

}

As a prerequisite, we need a simple Grid immediately surrounding the printable element so as to place the snapshot in the Grid.

SUMMARY

Also I tried with creating an image from the printable element and scale the image to print. The resultant quality is good but not as good as printing the element directly. You can also try that if it suits for you. Snippet for creating an image of the printable element, scale the created image and then print the scaled image is specified below.

Snippet 3

public class PrintUtilityImageScaling
{
    #region Class Level Variables

    private const string NullReferenceExceptionMessage = "PrintElement is null : Please specify the element to be printed.";
    
    #endregion

    #region Properties

    public FrameworkElement PrintElement { get; private set; }

    public string DocumentName { get; private set; }

    #endregion

    #region Constructors

    public PrintUtilityImageScaling(FrameworkElement elementToBePrinted, string documentName)
    {
        this.PrintElement = elementToBePrinted;
        this.DocumentName = documentName;
    }

    #endregion

    #region Event Handlers

    void DocToPrint_PrintPage(object sender, PrintPageEventArgs e)
    {
        double scale = 1;
        WriteableBitmap writeableBitmap;

        if (e.PrintableArea.Height < this.PrintElement.ActualHeight)
        {
            scale = e.PrintableArea.Height / this.PrintElement.ActualHeight;
        }

        if (e.PrintableArea.Width < this.PrintElement.ActualWidth && e.PrintableArea.Width / this.PrintElement.ActualWidth < scale)
        {
            scale = e.PrintableArea.Width / this.PrintElement.ActualWidth;
        }

        if (scale < 1)
        {
            ScaleTransform scaleTransform = new ScaleTransform();
            scaleTransform.ScaleX = scale;
            scaleTransform.ScaleY = scale;

            writeableBitmap = new WriteableBitmap(this.PrintElement, scaleTransform);
            writeableBitmap.Invalidate();
        }
        else
        {
            writeableBitmap = new WriteableBitmap(this.PrintElement, null);
        }

        Image image = new Image();
        image.Source = writeableBitmap;

        Grid imageGrid = new Grid();
        imageGrid.Children.Add(image);

        e.PageVisual = imageGrid;
    }

    #endregion

    #region Public Methods

    public void Print()
    {
        if (this.PrintElement != null)
        {
            PrintDocument docToPrint = new PrintDocument();

            docToPrint.PrintPage += new EventHandler<PrintPageEventArgs>(DocToPrint_PrintPage);
            
            docToPrint.Print(this.DocumentName);
        }
        else
        {
            throw new NullReferenceException(NullReferenceExceptionMessage);
        }

    }

    #endregion

}

I prefer Snippet 2 over Snippet 3. But again its upto you to decide based on the scenario.

REFERENCES

http://jacekjura.blogspot.com/2010/11/silverlight-printing-fit-to-page.html

No comments:

Post a Comment

Creative Commons License
This work by Tito is licensed under a Creative Commons Attribution 3.0 Unported License.