VB and Tablet PC: Inking on Irregular Surfaces

So I was playing around with my tablet PC application this evening because someone in a Visual Basic list had posted a question about how to create an irregular drawing surface for a medical application they were working on. They wanted users to be able to draw on a picture of a body, but did not want to collect the ink anywhere outside of the body shape. It actually turned out to be relatively easy. But some background first...

The Tablet PC SDK comes with a couple designable controls called InkPicture and InkEdit. You can drop these controls onto your form and they will immediately start capturing ink. The InkEdit will also convert your ink to text.

These controls make it extremely easy to work with ink. For instance, here’s how we can save just the ink we draw on the InkPicture into a file:

Private Sub SaveInk(ByVal filename As String, ByVal format As Microsoft.Ink.PersistenceFormat)

    Try

        Dim inkData As Byte() = Me.InkPicture1.Ink.Save(format, Microsoft.Ink.CompressionMode.Default)

        

        With My.Computer.FileSystem

            If .FileExists(filename) Then

                .DeleteFile(filename)

            End If

            .WriteAllBytes(filename, inkData, False)

        End With

        MsgBox(String.Format("Saved ink data to {0}", filename), MsgBoxStyle.Information)

       

    Catch ex As Exception

        MsgBox(String.Format("Unable to save ink data to {0}", filename))

    End Try

End Sub

The persistence format can even be specified as a Gif format so you can create a completely new picture of just your annotations. To load the annotations, we just read in the bytes and then load them into a new Ink.Ink object and set it to the Ink property of our InkPicture:

Private Sub LoadInk(ByVal filename As String)

   Dim inkData As Byte() = Nothing

   Try

   With My.Computer.FileSystem

   If .FileExists(filename) Then

   inkData = .ReadAllBytes(filename)

   End If

   End With

   Catch ex As Exception

   MsgBox(String.Format("Unable to load ink data from {0}", filename))

   End Try

   If inkData IsNot Nothing Then

   Dim newink As New Microsoft.Ink.Ink

   newink.Load(inkData)

   Me.InkPicture1.InkEnabled = False

   Me.InkPicture1.Ink = newink

   Me.InkPicture1.InkEnabled = True

   End If

End Sub

When we want to save both the picture and the ink we just have to create a new picture and then render the ink on the picture using the Ink.Renderer object:

Private Sub SaveInkOnPicture(ByVal filename As String)

    Try

        'Create graphics object

        Dim bm As New Bitmap(Me.InkPicture1.Image)

        Dim g As Graphics = Graphics.FromImage(bm)

        'Create a renderer to draw ink on the graphics surface

        Dim r As New Microsoft.Ink.Renderer()

        r.Draw(g, Me.InkPicture1.Ink.Strokes)

      'Save the new image

        bm.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp)

        MsgBox(String.Format("Saved picture to {0}", filename), MsgBoxStyle.Information)

    Catch ex As Exception

        MsgBox(String.Format("Unable to save picture to {0}", filename))

    End Try

End Sub

These designable controls are really easy to use and will get you up and running quickly, however what if you want to draw on something other than a picture? That’s where the InkOverlay object comes in. You can use this object to wrap any windows Control to enable inking. For instance, to create a blank “drawing surface” you can use a Panel with the InkOverlay:

Private WithEvents overlay As InkOverlay

Private Sub Form1_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load

    overlay = New InkOverlay(Me.Panel1)

    overlay.Enabled = True

End Sub

 

So now back to the original question about inking on an irregular surface. Well since the InkOverlay can be used to wrap any control, and we can draw any control any way we want, it’s actually pretty easy to achieve this. Since this was a medical application I first found I picture of a human body. I then threw a panel on a form and set its background image to the picture of the body. Next, I added an event handler to the Paint event in order to draw the panel how I wanted to. The key is setting up a GraphicsPath and then creating a new Region along that path for the control.

Public Class Form2

    Private WithEvents overlay As Microsoft.Ink.InkOverlay

    Private Sub Form2_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        overlay = New Microsoft.Ink.InkOverlay(Me.Panel1)

        overlay.DefaultDrawingAttributes.Color = Color.Red

        overlay.Enabled = True

    End Sub

    Private Sub Panel1_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Panel1.Paint

        Dim Person As New System.Drawing.Drawing2D.GraphicsPath()

        Person.AddEllipse(82, 1, 74, 74)

        Person.AddLine(65, 75, 175, 75)

        Person.AddLine(175, 75, 250, 240)

        Person.AddLine(250, 240, 210, 260)

        Person.AddLine(210, 260, 170, 180)

      Person.AddLine(170, 180, 175, 440)

        Person.AddLine(175, 440, 145, 440)

        Person.AddLine(145, 440, 127, 245)

        Person.AddLine(127, 245, 105, 440)

        Person.AddLine(105, 440, 70, 440)

        Person.AddLine(70, 440, 80, 180)

       Person.AddLine(80, 180, 35, 260)

        Person.AddLine(35, 260, 1, 240)

        Person.AddLine(1, 240, 65, 75)

        Me.Panel1.Region = New Region(Person)

    End Sub

End Class

The graphics path I created here is pretty basic but it illustrates the idea. Now when users draw on the panel, it will only allow ink on the graphics path!