Microsoft has a rich history of development environments and
platforms built around the creation of reusable objects and
components to maximize developer productivity. These environments are
designed to mask some of the underlying complexities of the platform.
The addition of the Tablet PC SDK to Visual Studio .NET follows this
pattern.
In previous articles I touched on several of the basic
features of the Tablet PC object model. In this article I take a look
at a few of the basic steps needed to create an ink- enabled custom
control. Additionally, I look at some of the technologies discussed
in previous articles with an eye toward creating a reusable component
and object.
As the Tablet PC space matures, independent component
publishers are creating new and exciting ink-enabled controls.
However, the platform still enables developers to create their own,
if desired.
Windows XP Tablet Edition is a superset of the Windows XP
operating system; because of this developers can use all of the
features of Windows Forms and controls to create ink-enabled
applications. Start by creating a new C# Windows Control project
named InkInputControl. In order to access the ink libraries you need
to set a reference to the Microsoft.Ink assembly, located by default
at C:\Program Files\Microsoft Tablet PC Platform SDK\Include\Microsoft.Ink.dll.
At the heart of ink enablement is the InkCollector object,
which handles all of the ink capture for the application. In order to
make a surface of an application inkable, simply create a new
InkCollector and pass it a handle to the object you would like to
ink-enable. In the following code snippet, the local InkCollector is
initialized and passes a handle to the control container. This will
allow the entire container to be inkable. After initializing the
InkCollector, the object is enabled to begin receiving ink.
this.inkCollector = new InkCollector(this.Handle);
this.inkCollector.Enabled = true;
Once the InkCollector is associated with the control
container the control moves on to initialize the pen characteristics.
While the InkCollector initializes all of these attributes to
standard defaults, the InkInputControl adjusts these to its own
internal default settings. The default drawing characteristics of the
ink are controlled by the DefaultDrawingAttributes property of the
InkCollector.
The InkInputControl allows the consumer of the control to set
three pen-related attributes from publicly exposed properties, the
pen color, pen width, and the type of tip on the pen (see Listing 1).
These three properties then configure the InkCollector.
The first of these pen-related properties is PenColor. The
PenColor property works by taking in a standard .NET color object and
passing it through to the Color attribute of the DefaultDrawingAttributes property.
The PenTip property uses the PenTip enumeration that is found
in the Microsoft.Ink namespace. There are currently two supported pen
tips, ball and rectangle.
The size of these tips is adjusted by the PenWidth property.
This property is a floating-point number that describes the width of
the pen tip. Since pressure sensitivity is enabled on the
InkCollector, by default this width is relative. If a user applies
maximum pressure with the pen, the actual width of the ink will be
one and half times the width of the tip. If the user applies the
minimal pressure on the pen, this value will be one-half of the width
of the tip. A developer is able to turn off pressure sensitivity by
setting the IgnorePressure attribute on the DefaultDrawingAttributes
property to true.
Now that the default properties are initialized, the
InkInputControl is ready to accept ink. However, in order to make
this a particle control, some events and methods need to be added.
The InkInputControl requires an event to communicate with an
application notifying it when a user completes writing and the text
is recognized.
public event StripEventHandler InkRecognized;
public delegate void StripEventHandler(object sender, InkInputEventArgs e);
The InkInputControl will raise an Inkrecognized event based on
a preset timer when the user has completed writing. The event will
pass an InkInputEventArgs parameter to the event containing the
recognized text and the strokes collection.
In order to figure out exactly when the user finishes, the
InkInputControl uses a timer set to a predetermined default. Using
the CursorButtonDown event of the InkCollector we can determine when
the user starts to write on the control. The CursorButtonDown event
is fired when the user has the pen on the digitizer and is actively
writing a stroke. The timer is stopped when the InkInputControl
receives this event from the InkCollector.
After a user completes a stroke and lifts the pen from the
screen, the CursorButtonUp event is raised by the InkCollector. Upon
receiving this event the InkInputControl resets the timer and starts
it running.
If no new strokes are detected before the timer expires, the
control's InkRecognized event is raised. This notifies the
application that the user has completed writing and the InkInputPanel
has recognized the text.
The conversion of the strokes into text (see Figure 1) is
handled by the handwriting recognizers installed with the Windows XP
Tablet PC Edition operating system. Developers can access the
properties and methods of the recognizer object to convert the
handwriting into text, but the InkInputControl uses a well-placed
shortcut to access the default recognizer. The InkInputControl calls
the ToString method of the Strokes collection. When called, the
ToString method will invoke the default recognizer for the machine
and return the text equivalent for the strokes.
For maximum flexibility the control also returns the entire
strokes collection as part of the event argument passed to the
InkRecognized event.
Now the InkInputControl has a fully inkable surface,
properties that allow the user to change common pen characteristics,
and an event that notifies the consumer of the control that the user
has finished inking and that the text is recognized. One last method
is needed to add basic functionality to the control: the ability to
clear the ink from the writing surface and start over.
To implement the clear functionality the InkInputControl uses
the DeleteStrokes method of the Ink object to clear all of the
strokes. The DeleteStrokes method clears all of the strokes from the
collection if a specific collection of strokes to delete is not
passed to the method.
public void Clear()
{
if (! this.inkCollector.CollectingInk)
{
this.inkCollector.Ink.DeleteStrokes();
Refresh();
}
}
After checking if the InkCollector is actively collecting ink
from the user, the Clear method deletes all of the strokes and
redraws the control.
Now we will make a change to our object. Instead of the
InkCollector, we will change to the InkOverlay object for the
foundation of the control.
The InkOverlay object is a superset of the InkCollector
object, providing editing support. It is useful for applications in
which one wishes to work with ink that is neither handwriting nor
gestures.
Because the InkOverlay is a drop-in replacement, all of the
references to InkCollector can globally be replaced with InkOverlay
and the control can be recompiled.
this.inkOverlay = new InkOverlay(this.Handle);
this.inkOverlay.Enabled = true;
The local InkOverlay is initialized and passes a handle to
the control container. This will allow the entire container to be
inkable. After initializing the InkCollector the object is enabled to
begin receiving ink.
One of the differences with the InkOverlay control is that it
provides better support for erasing. Take advantage of the
InkInputControl by creating and exposing two new properties to
control the editing mode and type of eraser to be used (see Listing
2). The EditingMode and EraserMode offer consumers of the
InkInputControl the ability to erase and modify any text.
The InkOverlayEditingMode enumeration allows us to toggle the
control between ink modes, in which users can write with the pen tip;
erase mode, in which users can correct mistakes; and select mode,
which allows users to lasso sections of ink to perform various
operations.
Erasing is completed in various ways based on the form factor
of the Tablet PC. Some tablets, such as the Toshiba Portege, have an
"eraser" button on the back end of the pen, like a real pencil. Other
tablet manufacturers do not include this additional button on their
machines.
To allow for users without the "eraser" on the top of their
pen, consumers of this InkInputControl will need to implement a
button in their user interface that sets the EditingMode property of
the control. This allows the end user to toggle between erase and ink
mode.
Both the InkOverlay and the InkCollector raise a
NewInAirPacket event. This event, along with the NewPacket event,
allows developers to track the movement of the mouse or pen across
the screen. The difference between the NewPacket event and the InAir
event is that the NewPacket event is raised when the pen tip is
pressed on the screen, but the NewInAirPacket event is raised as the
user moves the pen above the screen.
The InkInputControl captures the NewInAirPackets event and
evaluates the InkCollectorNewInAirPacketsEventArgs object passed to
it. This object contains a reference to the Cursor object. The Cursor
object exposes an Inverted property that will tell the application if
the pen is flipped or right side up (see Listing 3).
The NewInAirPackets event is used to capture the movement of
the pen across the screen. If it detects that the pen is inverted the
control is put into erase mode, which enables a user to simply press
the top of the pen to the screen and erase a section of line. The
control defaults to point-erase mode, mimicking the standard action
of Microsoft Journal. This can be changed by the developer, but care
should be taken if the end users are familiar and comfortable with
the Journal interface.
If the user flips the pen back so that the tip is down, the
control will automatically revert to ink mode. For controls or an
application in which a user can toggle between ink and erase modes
with a button or by flipping the pen, developers should implement
code to make sure these two methods do not interfere with each other.
Now that users of the InkInputControl have the ability to
control the pen settings, utilize text recognition, erase with either
a button or with the pen tip, and clear the ink, the next thing to
add is the ability to persist the ink to a file.
The Tablet PC managed libraries provide the ability to
persist ink to either a fortified GIF or to the native Ink Serialized
Format (ISF).
Ink Serialized Format is the most compact of the various
formats. In addition, ISF can be embedded within a binary document or
moved directly to and from the Clipboard. Developers may use a
Base-64 encoded ISF format to encode ISF for use directly in XML or
HTML files.
The other option is a new GIF format called a fortified GIF.
A fortified GIF is a GIF file that contains ISF information as
additional metadata with the file. This additional metadata is
ignored by applications that do not understand ink, so it is the
prime choice for sharing ink-annotated images and ink files. When the
file is returned to an application that does understand ink, all of
the ink data is maintained and restored.
The InkInputControl implements a Save method that allows
consuming applications to persist the ink to a file. Because the
image can be used by any application, (for example, mailed by a user
to a co-worker with Outlook), the InkInputControl persists the file
as a fortified GIF (see Listing 4).
Conclusion
Because of the robust nature of the managed libraries for the
Tablet PC, the InkInputControl is able to add support for ink, erase,
and selection modes with a few lines of code and by using the drop-in
replacement for the InkCollector, the InkOverlay. In addition, by
using the NewInAirPackets event the control is able to easily tell if
the user is using the pen with the point down or the top down in an
erase mode. Utilizing the Save method of the ink collection
developers can easily persist ink to files in the universally
readable format of a GIF or the native ISF.
With very little code we now have the makings of a
full-featured Microsoft Journal-like control. The control can be
farther enhanced to add toolbar support, thus providing a built-in
interface and other more advanced features such as zooming and cut
and paste, by extending the public properties and methods. This
additional functionality is easy to implement with the Tablet PC
SDK.
About The Author
Brad McCabe is a technology evangelist for .NET, ASP.NET, and .NETCF for Infragistics
(www.infragistics.com), a leader in providing a broad infrastructure of reusable presentation-layer components essential for the creation of next-generation
Web-based applications and XML Web services utilizing .NET, COM, and Java.
Brad@infragistics.com
Listing 1
public Color PenColor
{
get {return
this.inkCollector.DefaultDrawingAttributes.Color;}
set {this.inkCollector.DefaultDrawingAttributes.Color = value;}
}
public float PenWidth
{
get {return
this.inkCollector.DefaultDrawingAttributes.Width;}
set {this.inkCollector.DefaultDrawingAttributes.Width = value;}
}
public PenTip PenTip
{
get {return this.inkCollector.DefaultDrawingAttributes.PenTip;}
set {this.inkCollector.DefaultDrawingAttributes.PenTip = value;}
}
Listing 2
public InkOverlayEditingMode EditingMode
{
get {return this.inkOverlay.EditingMode;}
set {this.inkOverlay.EditingMode = value;}
}
public InkOverlayEraserMode EraserMode
{
get {return this.inkOverlay.EraserMode;}
set {this.inkOverlay.EraserMode = value;}
}
Listing 3
private void OnInkOverlayNewInAirPackets(object sender,
InkCollectorNewInAirPacketsEventArgs e)
{
if (e.Cursor.Inverted)
{
this.EditingMode =
Microsoft.Ink.InkOverlayEditingMode.Delete;
this.EraserMode =
Microsoft.Ink.InkOverlayEraserMode.PointErase;
} else {
this.EditingMode = Microsoft.Ink.InkOverlayEditingMode.Ink;
}
}
Listing 4
public void Save()
{
SaveFileDialog saveDialog = new SaveFileDialog();
saveDialog.Filter = "Image file (*.gif)|*.gif";
if(saveDialog.ShowDialog() == DialogResult.OK)
{
byte[] fortifiedGif = null;
UTF8Encoding utf8 = new UTF8Encoding();
using (FileStream File =
File.OpenWrite(saveDialog.FileName))
{
fortifiedGif =
inkOverlay.Ink.Save(PersistenceFormat.Gif);
File.Write(fortifiedGif, 0, fortifiedGif.Length);
}
}
}
All Rights Reserved
Copyright © 2004 SYS-CON Media, Inc.
E-mail:
info@sys-con.com