Printing Ink

 

Peter Gruenbaum
Microsoft Corporation

January 2004

Applies to:
   Microsoft® Tablet PC Platform SDK
   Ink
   Printing

Summary: This article describes how to print ink from an ink collector, how to adjust for printer margins, and how to print antialiased and transparent ink. In addition, the article discusses how to print ink stored in an InkEdit control. These descriptions and examples—in C#, Microsoft Visual Basic® .NET, and C++ — use the Microsoft Tablet PC Platform SDK version 1.5 API. Readers should be familiar with the Microsoft Tablet PC Platform SDK and managed code. (12 printed pages)

Download PrintInk.exe.

Contents

Introduction
How to Draw Ink to the Printer Graphics
How to Adjust for Printer Margins
Antialiasing and Transparency
How to Print an InkPicture
Printing Ink from an InkEdit Control

Introduction

Printing ink is mostly a straightforward process, but in some cases it does require making Windows API calls. To print ink using managed code, you need to draw to a printer Graphics object, which means that for a given form, you need to create aPrintDocument object and handle its PrintPage event. The PrintPageEventArgs object has a Graphics property, which you can draw on. Use the Renderer.Draw method to draw either directly on this Graphics, or, in the case of antialiased or transparent strokes, indirectly on temporary Graphics with your Stroke objects. This can be used with all ink collectors: InkOverlay, InkCollector, and InkPicture. This article describes the most basic way to print ink, but also explains how to work with ink and pixel coordinate spaces when dealing with page margins, as well as covering antialiasing, transparency, and printing ink in InkPicture and InkEdit controls.

This article discusses how to:

  • Print ink.
  • Adjust ink for printer margins.
  • Print ink with antialiasing and transparency
  • Print an InkPicture
  • Print the contents of an InkEdit control where ink has been inserted

This article does not discuss:

  • General information on how to print in .NET.
  • How to print multiple pages.

For more information about printing in general, see Preview and Print your Windows Forms App with the .NET Printing Namespace in the MSDN Library.

How to Draw Ink to the Printer Graphics

With Managed Code

With managed code, start by dragging a PrintDocument from the Designer toolbox onto your form. When the user decides to print, you simply call the Print method on the PrintDocument object. In order to do a print preview, create a PrintPreviewDialog object and then set its Document property to your PrintDocument.

Create an event handler for the PrintDocument's PrintPage event. Using the Renderer object, call the Draw method using the event's Graphics property and the ink's Strokes. This will place ink so that the origin is in the upper left corner of the page; the following section will discuss how to adjust for printer margins.

C#

The following C# example uses an InkOverlay object called theInkOverlay and a PrintDocument object called thePrintDocument.

// Event handler for PrintDocument.PrintPage
private void thePrintDocument_PrintPage(object sender, 
  System.Drawing.Printing.PrintPageEventArgs e)
{
    // Draw strokes to the printer graphics
    theInkOverlay.Renderer.Draw(e.Graphics, theInkOverlay.Ink.Strokes);
}

Visual Basic .NET

The following Visual Basic .NET example uses an InkOverlay object called theInkOverlay and a PrintDocument object called thePrintDocument.

' Event handler for PrintDocument.PrintPage
Private Sub thePrintDocument_PrintPage(ByVal sender As Object, 
  ByVal e As System.Drawing.Printing.PrintPageEventArgs) _
    Handles thePrintDocument.PrintPage
{
    ' Draw strokes to the printer graphics
    theInkOverlay.Renderer.Draw(e.Graphics, theInkOverlay.Ink.Strokes)
}

In C++

In C++, you can use the HDC from the printer and the InkRenderer object's Draw method to print ink. In the example below, the MFC Wizard was used to create Document and View classes. In the View class, the OnPrint method of a class called CPrintingInkView (that inherits from CView) is overridden and calls the IInkRenderer::Draw method. The spInkOverlay object is of type CComPtr <IInkOverlay>. Note that in a real application, you would want to handle errors instead of simply returning, which will result in nothing being printed.

void CPrintingInkView::OnPrint(CDC* pDC, CPrintInfo* pInfo) 
{
    if (spInkOverlay != NULL)
    {
        HRESULT result;
        IInkRenderer* pRenderer;
        result = spInkOverlay->get_Renderer(&pRenderer);
        if (SUCCEEDED(result))
        {
            IInkDisp* pInk;
            result = spInkOverlay->get_Ink(&pInk);
            if (SUCCEEDED(result))
            {
                IInkStrokes* pStrokes;
                result = pInk->get_Strokes(&pStrokes);
                if (SUCCEEDED(result))
                {
                    pRenderer->Draw((long)pDC->GetSafeHdc(), pStrokes);
                }
            }
        }
    }
}

How to Adjust for Printer Margins

The code in the previous section is simple, but it places the origin at the top left corner and doesn't take into account the printer margins. At a minimum you should translate your strokes by the left and top margins. In most cases, you could apply a translation to the Graphics Transform, but unfortunately the Renderer's Draw method only uses the HDC of the Graphics and does not make use of its transformation. (This may changes in future releases of the SDK.) Therefore, you must use the Renderer's object transform to translate the strokes, remembering that the translation must happen in ink coordinate space. The margins will be in printer units, which are 1/100th of an inch by default. You can turn those values into pixels by multiplying them by the Graphics' DpiX and DpiY properties, and then dividing by 100. Then you can use the Renderer's PixelToInkSpace method to convert the margins to ink space.

Note that the technique in the following section about printing antialiased and transparent strokes does not require the transformation from printer units. However, that technique does require unsafe code, whereas this technique does not.

C#

The following C# example uses the same variable names as the previous C# example. Note that this assumes that the Graphics' PageUnit property is set to Display, and that the Graphics' PageScale property is 1 in order to have the default printer units of 1/100th of an inch.

using System.Windows.Forms;
// Event handler for PrintDocument.PrintPage
private void thePrintDocument_PrintPage(object sender, 
  System.Drawing.Printing.PrintPageEventArgs e)
{
    // First translate it so that it's within the print margins
    // Create a new transformation matrix
    System.Drawing.Drawing2D.Matrix transformation = 
  new System.Drawing.Drawing2D.Matrix();

    // Create an offset based on the margins. 
    // IMPORTANT: Default printer units are 1/100 of an inch, 
    // so divide by 100 and multiply by Dpi to get
    // true pixel dimensions.
    System.Drawing.Point offset = 
        new System.Drawing.Point((int)(e.MarginBounds.Left * 
          e.Graphics.DpiX / 100), 
         (int)(e.MarginBounds.Top * e.Graphics.DpiY / 100));

    // Convert to ink coordinates
    theInkOverlay.Renderer.PixelToInkSpace(e.Graphics, ref offset);

    // Apply the translation to the transformation
    transformation.Translate(offset.X, offset.Y);

    // Apply the transformation to the Renderer
    theInkOverlay.Renderer.SetObjectTransform(transformation);

    // Draw strokes to the printer graphics
    theInkOverlay.Renderer.Draw(e.Graphics, theInkOverlay.Ink.Strokes);

    // Undo overlay's transformation
    theInkOverlay.Renderer.SetObjectTransform(new 
      System.Drawing.Drawing2D.Matrix());
}

Visual Basic .NET

The following VB.NET example uses the same variable names as the previous VB.NET example. Note that this assumes that the Graphics' PageUnit property is set to Display, and that the Graphics' PageScale property is 1 in order to have the default printer units of 1/100th of an inch.

' Event handler for PrintDocument.PrintPage
Private Sub thePrintDocument_PrintPage(ByVal sender As Object,
  ByVal e As System.Drawing.Printing.PrintPageEventArgs) _
    Handles thePrintDocument.PrintPage
{
    ' First translate it so that it's within the print margins
    ' Create a new transformation matrix
    Dim transformation As new System.Drawing.Drawing2D.Matrix()

    ' Create an offset based on the margins. 
    ' IMPORTANT: Default printer units are 1/100 of an inch,
    ' so divide by 100 and multiply by Dpi to get
    ' true pixel dimensions.
    Dim offset As new System.Drawing.Point(CInt(e.MarginBounds.Left *
      e.Graphics.DpiX / 100), _
        CInt(e.MarginBounds.Top * e.Graphics.DpiY / 100))

    ' Convert to ink coordinates
    theInkOverlay.Renderer.PixelToInkSpace(e.Graphics, offset)

    ' Apply the translation to the transformation
    transformation.Translate(offset.X, offset.Y)

    ' Apply the transformation to the Renderer
    theInkOverlay.Renderer.SetObjectTransform(transformation)

    ' Draw strokes to the printer graphics
    theInkOverlay.Renderer.Draw(e.Graphics, theInkOverlay.Ink.Strokes)

    ' Undo overlay's transformation
    theInkOverlay.Renderer.SetObjectTransform(new 
      System.Drawing.Drawing2D.Matrix())
}

Antialiasing and Transparency

When you use the techniques described above to draw, the strokes are not antialiased, and transparent strokes don't look as blended as they should. If you require the printed strokes to look as good as they appear on the screen, you have to use a more sophisticated technique that involves unsafe code. Note that this technique requires full trust and will not work in partial trust scenarios. For example, Web deployment often results in applications being run under partial trust.

C#

This article contains sample code in C# that shows how to print with both the simple method and the sophisticated antialiased method. In the sample code there is a file called DibGraphicsBuffer.cs. DibGraphicsBuffer.cs contains a class called DibGraphicsBuffer that can create a Graphic using a Device Independent Bitmap. Include this in your project and add using Microsoft.NET.Samples to your Form. Go to the project properties and enable unsafe code. Then use the code below for your PrintPage event handler. This code uses a DibGraphicsBuffer object to create an image that you can draw your strokes on. Then you can draw that image into the event's Graphics object, adjusted by the print margins. In the example, panelInk is a Panel that has an InkOverlay named theInkOverlay attached to it. Note that when you use this technique, you do not have to convert to printer units when adjusting for the margins.

// Event handler for PrintDocument.PrintPage
private void thePrintDocument_PrintPage(object sender,
  System.Drawing.Printing.PrintPageEventArgs e)
{
    // Create an image using temporary graphics and graphics buffer object
    Bitmap imageForPrinting = new Bitmap(this.panelInk.Width,
      this.panelInk.Height);
    using (Graphics graphicsImage = Graphics.FromImage(imageForPrinting))
    using (DibGraphicsBuffer dib = new DibGraphicsBuffer())
    // Create temporary screen Graphics
    using (Graphics graphicsPanel = panelInk.CreateGraphics())
    // Create temporary Graphics from the Device Independent Bitmap
    using (Graphics graphicsTemp = 
        dib.RequestBuffer(graphicsPanel, this.panelInk.Width,
          this.panelInk.Height))
    {
        graphicsTemp.Clear(this.panelInk.BackColor);

        // Draw the text onto the graphics for the temporary image
        PointF labelLocation = new PointF(labelA.Left, labelA.Top);    
        graphicsTemp.DrawString(labelA.Text, labelA.Font,
          new SolidBrush(Color.Black), labelLocation);

        // Draw the strokes onto the temporary image
        theInkOverlay.Renderer.Draw(graphicsTemp,
          theInkOverlay.Ink.Strokes);

        // Use the buffer to paint onto the final image
        dib.PaintBuffer(graphicsImage, 0, 0);

        // Draw this image onto the printer graphics,
        // adjusting for printer margins
        e.Graphics.DrawImage(imageForPrinting,
          e.MarginBounds.Left, e.MarginBounds.Top);
    }
}

VB.NET

Although we do not provide a VB.NET sample for antialiasing and transparency, it is straightforward to convert the C# example. The DibGraphicsBuffer.cs file needs to be in its own class library C# project, but then you can write code that references this project to use the DibGraphicsBuffer class. Go to the project properties for this C# project and enable unsafe code. Then use the code below for your PrintPage event handler. This code uses a DibGraphicsBuffer to create an image that you can draw your strokes on. Then you can draw that image into the event's Graphics object, adjusted by the print margins. In the example, PanelInk is a Panel that has an InkOverlay named theInkOverlay attached to it. Note that when you use this technique, you do not have to convert to printer units when adjusting for the margins.

Private Sub thePrintDocument_PrintPage(ByVal sender As Object, _
    ByVal e As System.Drawing.Printing.PrintPageEventArgs) _
Handles thePrintDocument.PrintPage
        ' Create an image using temporary graphics
        ' and graphics buffer object
        Dim imageForPrinting As New Bitmap(PanelInk.Width,
          PanelInk.Height)
        Dim graphicsImage As Graphics
        Dim dib As DibGraphicsBuffer
        Dim graphicsPanel As Graphics
        Dim graphicsTemp As Graphics 

        Try
            graphicsImage = Graphics.FromImage(imageForPrinting)
            dib = New DibGraphicsBuffer
            ' Create temporary screen graphics
            graphicsPanel = PanelInk.CreateGraphics()
            ' Create temporary graphics from the Device Independent Bitmap
            graphicsTemp = _
                dib.RequestBuffer(graphicsPanel, PanelInk.Width, _
                  PanelInk.Height)

            graphicsTemp.Clear(PanelInk.BackColor)

            'Draw the strokes onto the temporary image
            theInkOverlay.Renderer.Draw(graphicsTemp, _
              theInkOverlay.Ink.Strokes)

            'Use the buffer to paint onto the final image
            dib.PaintBuffer(graphicsImage, 0, 0)

            'Draw this image onto the printer graphics, adjusting for
            'printer margins
            e.Graphics.DrawImage(imageForPrinting, e.MarginBounds.Left,
              e.MarginBounds.Top)
        Catch ex As Exception
            'For this simple example, just display the exception
            MessageBox.Show(ex.ToString())
        Finally
            'Clean up from temporary graphics and bitmaps
            If Not graphicsImage Is Nothing Then graphicsImage.Dispose()
            If Not dib Is Nothing Then dib.Dispose()
            If Not graphicsPanel Is Nothing Then graphicsPanel.Dispose()
            If Not graphicsTemp Is Nothing Then graphicsTemp.Dispose()
        End Try
    End Sub

How to Print an InkPicture

A common task is to print an InkPicture, a control that easily lets you ink on top of a picture. In the PrintPage event handler, you can use a variation of the technique described above for printing antialiased ink. Again, include the DibGraphicsBuffer.cs file and change the project's properties to allow for unsafe code. Draw the InkPicture's image on the temporary Graphics before drawing the ink. How the picture is drawn will depend on the SizeMode property of the InkPicture. Note that for most SizeModes, you need to specify a GraphicsUnit of Pixel in order to get the correct image size. The following C# code shows the PrintPage event handler using an InkPicture named inkPicture. You can use similar code in VB.NET, but you will have to replace the using statement with a TryCatch block similar to the antialiasing and transparency example.

private void thePrintDocument_PrintPage(object sender,
  System.Drawing.Printing.PrintPageEventArgs e)
{
    // Create an image using graphics buffer object
    // and the form's width and height
    Bitmap imageForPrinting = new Bitmap(this.Width, this.Height);
    using (Graphics graphicsImage = Graphics.FromImage(imageForPrinting))
    using (DibGraphicsBuffer dib = new DibGraphicsBuffer())
    // Create temporary screen graphics
    using (Graphics graphicsForm = this.CreateGraphics())
    // Create temporary graphics from the Device Independent Bitmap
    using (Graphics graphicsTemp = 
        dib.RequestBuffer(graphicsForm, Width, Height))
    {
        graphicsTemp.Clear(this.BackColor);

        // Draw the InkPicture image, adjusting for size mode
        switch (inkPicture.SizeMode)
        {
            case PictureBoxSizeMode.Normal:
            case PictureBoxSizeMode.AutoSize:
                // Draw the image at the top left corner
                graphicsTemp.DrawImage(inkPicture.Image,
                  inkPicture.Bounds,
                    0, 0, inkPicture.Width, inkPicture.Height,
                     GraphicsUnit.Pixel);
                break;
            case PictureBoxSizeMode.CenterImage:
                // Calculate the location that centers the picture
                Point locationForCenter = 
                 new Point((inkPicture.Image.Width- inkPicture.Width) / 2, 
                    (inkPicture.Image.Height - inkPicture.Height) / 2);
                graphicsTemp.DrawImage(inkPicture.Image,
                 inkPicture.Bounds,
                    locationForCenter.X, locationForCenter.Y, 
                    inkPicture.Width, inkPicture.Height,
                      GraphicsUnit.Pixel);
                break;
            case PictureBoxSizeMode.StretchImage:
                // Draw the image so that it fits in the InkPicture bounds
                graphicsTemp.DrawImage(inkPicture.Image,
                 inkPicture.Bounds);
                break;
        }

        // Draw the strokes onto the temporary graphics,
        // first translating the Renderer
        // by the InkPicture location
        Matrix inkTransform = new Matrix();
        Point pictureLocation = inkPicture.Location;
        inkPicture.Renderer.PixelToInkSpace(graphicsTemp,
          ref pictureLocation);
        inkTransform.Translate(pictureLocation.X, pictureLocation.Y);
        inkPicture.Renderer.SetObjectTransform(inkTransform);
        inkPicture.Renderer.Draw(graphicsTemp, inkPicture.Ink.Strokes);
        inkPicture.Renderer.SetObjectTransform(new Matrix());

        // Use the buffer to paint onto the final image
        dib.PaintBuffer(graphicsImage, 0, 0);
        // Draw this image onto the printer graphics,
        // adjusting for margins
                e.Graphics.DrawImage(imageForPrinting,
                  e.MarginBounds.Left, e.MarginBounds.Top);
    }        
}

In a real application, you may have to clip the image so that it does not run over into the right or bottom margins.

Printing Ink from an InkEdit

There is one more situation that involves ink that you may want to print. InkEdit controls provide the user with a way to input text using a pen. The user writes anywhere in the control, and when enough time has passed after the user stops adding strokes, the ink is converted into text. (This length of time is defined by the RecoTimeout property.)

You also have to have the option to allow you to insert ink into the InkEdit box as ink, and not have it converted. You can accomplish this by setting the InkInsertMode property to InkInsertMode.InsertAsInk. The control is able to do this because InkEdit inherits from RichTextBox, and it can store and display not only text, but formatting and graphics as well; in this case, ink will be stored as part of the rich text. If you want to print this ink, you will need to print the rich text.

Printing rich text from a RichTextBox can be accomplished by the SendMessage Windows API call with the appropriate arguments. It's best to create a special class to handle the Windows API call, and an example of this is in the code download section. Look for a C# project called PrintInkEdit, which contains a file called InkEditWithPrinting.cs. This file contains a class called InkEditWithPrinting which inherits from InkEdit, and contains a Print method which makes the necessary calls to print using a PrintPageEventArgs object that is passed in.

In PrintInkEditForm.cs, you will notice that the InkInsertMode property of the InkEdit named inkEdit is set to InsertAsInk. Then, in the PrintInkEditForm's PrintPageEventHandler, you will find the following line:

    this.inkEdit.Print(0, inkEdit.TextLength, e);

where e is the PrintPageEventArgs object, and inkEdit is the InkEditWithPrinting object. This will result in drawing the rich text onto the printer Graphics.

Note that in this simple example, all printing is done on the first page. In theory, if your ink requires multiple pages, you can do some analysis on the ink to figure out where the page breaks should go, and then keep track of the number of characters printed (which is returned from the InkEditWithPrinting.Print method) in order to print multiple pages. However, if you are collecting that much ink, it makes more sense to use an InkOverlay to create a special control that will work for your purposes.