Ink and Stroke Objects

Ink and Stroke Objects

We know that digital ink is captured in the form of Stroke objects that are obtained through user input occurring on InkCollector and InkOverlay objects. Stroke objects are the fundamental building blocks of an ink document, with each object representing a stroke of digital ink. Because Stroke objects are essentially a collection of packets, where each packet contains an x,y location and optionally other packet properties such as pen pressure, it follows that a Stroke is really just a fancy polyline.

A Stroke object is contained by an instance of the Ink class. The Ink class is the outermost entry point into the Ink Data API, and it is analogous to a document class the root of an ownership hierarchy for ink data. Ink objects define a scope in which multiple Stroke objects can exist. An Ink object owns zero or more Stroke objects, and a Stroke can be contained by exactly one Ink object therefore, an Ink object owns a collection of Stroke objects. Although a Stroke may be transferred between different Ink objects, it cannot exist without an Ink object as its owner.

NOTE
Recall that InkCollector and InkOverlay each have a property on them called Ink. That s an instance of the Ink class that is being described here. An InkCollector or InkOverlay object creates the Stroke objects in the ownership scope of the Ink object currently set with that property.

The Ink class typically exposes its Stroke objects through a collection class called Strokes. A Strokes collection indeed lives up to its name it is a collection of Stroke objects, with some extra capabilities we ll find out about later. The Ink class has a read-only property named Strokes whose purpose is to return an instance of a Strokes collection containing the current list of Stroke objects owned by the Ink object.

Stroke Objects vs. Strokes

You might have noticed that in the last couple of paragraphs I ve referred to a plurality of Stroke objects explicitly as Stroke objects and not simply Strokes, specifically to avoid confusion with the Strokes collection. Although it may be a little more cumbersome to read, hopefully it will keep things clear.

The Strokes collection is actually a collection of references to Stroke objects adding or removing Stroke objects to or from a Strokes collection has no effect on the Stroke objects lifetimes or on their containment. The lifetime of a Stroke object is only tied to its owning Ink object, and the containment of a Stroke object is only changed by an Ink object hence, it s possible for a Strokes collection to refer to a Stroke object that has been destructed. Don t fear, though, as the Ink Data Management API takes this into account and deals with it nicely, as we ll soon see.

To reiterate the point we just made: a Strokes collection plays no role in the ownership or lifetime of Stroke objects; it only references them. To further explain this, consider the following code snippet:

// An attempt to remove all Stroke objects from an Ink object inkOverlay.Ink.Strokes.Clear();

This code has no observable effect. The Strokes collection returned from the Ink object s property Strokes is actually a copy of the current list of Stroke objects it owns, and the Strokes collection only references, not owns, Stroke objects anyway, so removing Stroke objects from the collection will have no effect on whether they are destroyed. Similarly, adding Stroke objects to a Strokes collection does not imply that their owning Ink objects will be changed.

A Strokes collection is created by an Ink object and can refer only to Stroke objects owned by that same Ink object. It is impossible for a Strokes collection to refer to Stroke objects contained in different Ink objects, and if an attempt is made to do so an exception will be thrown. It is perfectly allowable (and common) for multiple Strokes collections to be created from one Ink object, each referring to zero or more Stroke objects owned by the Ink object.

Introduction to the Ink, Stroke, and Strokes Classes

Figure 5-1 shows the relationship between Ink, Stroke, and Strokes objects using the HelloInkOverlay sample from Chapter 4. In a nutshell, an Ink object is a container for Stroke objects, and a Strokes collection references Stroke objects. This is one of the most fundamental and important concepts to understand about the Ink Data Management API.

figure 5-1 a diagram of the relationship between ink, stroke, and strokes objects.

Figure 5-1. A diagram of the relationship between Ink, Stroke, and Strokes objects.

The Ink Class

An instance of the Ink class is automatically created when an InkCollector or InkOverlay object is created, and it is made accessible through the Ink property. An Ink object can also be created explicitly via the C# new operator, and there are a couple of Ink Data Management APIs that return new Ink objects. Once it has been created, an Ink object requires no special setup for use.

All Ink objects and Stroke objects use the HIMETRIC coordinate system. A HIMETRIC unit represents 0.01mm, where the measurement is derived from the screen s current dpi. The coordinate space is the usual Microsoft Windows style fourth quadrant in which the origin (0,0) represents the upper-left corner of the space and positive x and y coordinates imply locations to the right and down, respectively. The Stroke objects contained in an Ink object all share the coordinate space, so each Stroke object s packet s x, y values are based from a common (0,0) origin.

The Ink class provides a number of functions that operate on Strokes collections and Stroke objects, performing such operations as creation and destruction, transformations, and persistence. As already discussed, the class most notably defines a read-only property called Strokes that returns a snapshot of the current collection of Stroke objects it contains.

The Stroke Class

Stroke objects are a series of packets comprised of (x, y) coordinates and optionally some other packet properties, defined at the time of the Stroke s creation. One stroke represents exactly one continuous stroke of ink it cannot have multiple start and end points in it.

Each Stroke object is assigned a unique ID within the scope of its owning Ink object. The ID is an integer that is assigned by the Ink object at the time of the Stroke s creation or addition to the Ink object, and it remains constant for the Stroke s lifetime even when the Stroke object is modified. If the Stroke is later transferred to another Ink object, the ID will be changed because the uniqueness of a Stroke s ID is only guaranteed within the scope of its owning Ink object. The ID of a Stroke object is obtained via the Stroke class s Id property and is of type int.

Stroke objects also possess the read-only property Deleted, indicating if the Stroke object exists anymore. Exists anymore? Huh? Let s find out what that means by first learning a little about how Strokes collections work.

The Strokes Class

As has already been mentioned, a Strokes collection is a collection of references to Stroke objects. Strokes collections provide a useful way to deal with multiple Stroke objects, first by acting as an aggregation of Stroke objects, and second by defining common operations on groups of Stroke objects, such as transformation and visual style formatting.

A Strokes collection is really just an encapsulation of an array of stroke IDs. When it needs to dereference one of its elements, a Strokes collection queries the owning Ink object for the Stroke object with the ID found in the element being dereferenced.

If a Strokes collection references a Stroke object that has been destroyed by the owning Ink object, the reference will still remain in the Strokes collection and will still yield a Stroke object when dereferenced. However, no operations can be executed on that stroke, and only two of its properties will be readable: Id and Deleted. An attempt to call a method or query any other properties on that Stroke object will cause an exception to occur. This is why the Deleted property was noted previously; a Strokes collection will always yield Stroke objects, though they may not necessarily be usable. Given a Strokes collection whose contents are not under your full control, it s always a good idea to check the Deleted property of Stroke objects once they re dereferenced before performing any operations on them.

Strokes collections are commonly found by querying the Ink class s Strokes property (which we ve learned always returns a copy of the current collection) and the InkOverlay class s Selection property (which returns a copy of the current collection of selected Stroke objects).

Sample Application StrokeIdViewer

To best illustrate the behavior of stroke ID values, we ll jump right into the StrokeIdViewer sample application, shown in Figure 5-2. Its purpose is to present an InkOverlay-based UI and draw each Stroke object s corresponding stroke ID at the start point of the Stroke. Then, as strokes are created, deleted, moved, resized, and so forth, we can observe the stroke ID values.

figure 5-2 the strokeidviewer sample application.

Figure 5-2. The StrokeIdViewer sample application.

The first block of code presented shows the implementation of the DrawStrokeIds method found in the RendererEx class, which is part of the BuildingTabletApps utility library. There are in fact two overloads of the DrawStrokeIds method, defined as:

public static void DrawStrokeIds(Graphics g, Font font, Ink ink) public static void DrawStrokeIds(Renderer renderer, Graphics g, Font font, Strokes strks)

These static functions render each Stroke object s stroke ID value to the Graphics object g using the Font font. The Stroke objects are obtained from the Ink object ink or referenced by the Strokes collection strokes. The meaning of the Renderer object renderer will be covered later in the chapter, but for now it s sufficient to say that it s used to convert ink coordinates into screen pixels.

namespace MSPress.BuildingTabletApps { public class RendererEx { // Draw the stroke IDs in the top-left corner of each stroke's // bounding rectangle for an Ink object public static void DrawStrokeIds(Graphics g, Font font, Ink ink) { DrawStrokeIds(new Renderer(), g, font, ink.Strokes); } // Draw the stroke IDs for a Strokes collection public static void DrawStrokeIds( Renderer renderer, Graphics g, Font font, Strokes strokes) { // Iterate through every stroke referenced by the collection foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { // Draw the stroke's ID at its starting point string str = s.Id.ToString(); Point pt = s.GetPoint(0); renderer.InkSpaceToPixel(g, ref pt); g.DrawString( str, font, Brushes.White, pt.X-1, pt.Y-1); g.DrawString( str, font, Brushes.White, pt.X+1, pt.Y+1); g.DrawString( str, font, Brushes.Black, pt.X, pt.Y); } } } } }

Notice that the implementation of DrawStrokeIds that accepts Ink as a parameter calls the other overload of DrawStrokeIds, passing the ink parameter s Strokes property and creating a new instance of the Renderer class thus the real meat of the functionality in this application is this code:

// Iterate through every stroke referenced by the collection foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { // Draw the stroke's ID at its starting point string str = s.Id.ToString(); Point pt = s.GetPoint(0); renderer.InkSpaceToPixel(g, ref pt); g.DrawString(str, font, Brushes.White, pt.X-1, pt.Y-1); g.DrawString(str, font, Brushes.White, pt.X+1, pt.Y+1); g.DrawString(str, font, Brushes.Black, pt.X, pt.Y); } }

Because the Strokes collection class implements the IEnumerator interface defined by the Microsoft .NET Framework, Strokes collections can be used with the foreach statement. The preceding snippet iterates over all Stroke objects referenced by the collection, and for every Stroke object obtained the function checks to see whether the Stroke is usable by looking at its Deleted property. Recall that Stroke objects are always returned when dereferencing from a Strokes collection, even if the Stroke doesn t exist anymore in the owning Ink object.

Upon determining whether the Stroke object is usable, the function obtains the Stroke object s ID via the Id property. The function then gets the starting point of the Stroke object (defined to be the zero point in the stroke), and because that value is in HIMETRIC units it must be converted to pixels using the Renderer class s InkSpaceToPixel method so the ID can be drawn to the graphics object. The stroke ID is then drawn it is actually rendered three times to help with readability of the value. If the ID is drawn over the top of any black strokes, it likely would not be readable; therefore, a white space is created around the value by rendering it in white and slightly offset from the final real rendering of the value in black.

Now let s take a look at the actual sample program listing that leverages the DrawStrokeIds method:

StrokeIdViewer.cs

//////////////////////////////////////////////////////////////////// // // StrokeIdViewer.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates the behavior of Stroke IDs. An // InkOverlaySharp control is created and as strokes are added, // removed, and modified, their IDs are drawn to show their // behavior. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl inkCtl; private StrokesEventHandler evtInkAdded; private StrokesEventHandler evtInkDeleted; // Entry point of the program [STAThread] static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // Create and place all of our controls inkCtl = new InkControl(); inkCtl.Location = new Point(8, 8); inkCtl.Size = new Size(352, 216); // Configure the form itself ClientSize = new Size(368, 232); Controls.AddRange(new Control[] { inkCtl }); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "StrokeIdViewer"; ResumeLayout(false); // Hook up to the InkOverlay's event handlers inkCtl.InkOverlay.Painted += new InkOverlayPaintedEventHandler(inkCtl_Painted); inkCtl.InkOverlay.Ink.InkAdded += new StrokesEventHandler(inkCtl_InkAdded); inkCtl.InkOverlay.Ink.InkDeleted += new StrokesEventHandler(inkCtl_InkDeleted); // Create event handlers so we can be called back on the correct // thread evtInkAdded = new StrokesEventHandler(inkCtl_InkAdded_Apt); evtInkDeleted = new StrokesEventHandler(inkCtl_InkDeleted_Apt); // Set the ink color to something other than black so it's // easier to see the stroke ID values inkCtl.InkOverlay.DefaultDrawingAttributes.Color = Color.DarkOrange; // We're now set to go, so turn on tablet input inkCtl.InkOverlay.Enabled = true; } // Handle the InkOverlay having been painted private void inkCtl_Painted(object sender, PaintEventArgs e) { RendererEx.DrawStrokeIds( e.Graphics, Font, inkCtl.InkOverlay.Ink); } // Handle ink having been added private void inkCtl_InkAdded(object sender, StrokesEventArgs e) { // Make sure the event fires on the correct thread this.Invoke(evtInkAdded, new object[] { sender, e }); } private void inkCtl_InkAdded_Apt(object sender, StrokesEventArgs e) { inkCtl.InkInputPanel.Invalidate(); } // Handle ink having been deleted private void inkCtl_InkDeleted(object sender, StrokesEventArgs e) { // Make sure the event fires on the correct thread this.Invoke(evtInkDeleted, new object[] { sender, e }); } private void inkCtl_InkDeleted_Apt(object sender, StrokesEventArgs e) { inkCtl.InkInputPanel.Invalidate(); } }

The sample application begins its execution by creating an instance of the InkControl class that was first mentioned in Chapter 4. The InkControl class is a control based on the HelloInkOverlay sample. The InkControl s InkOverlay then has a Paint event handler installed, which when triggered will call RendererEx.DrawStrokeIds, resulting in the stroke IDs being drawn. Next, two more event handlers are installed, this time on the InkOverlay s Ink property:

inkCtl.InkOverlay.Ink.InkAdded += new StrokesEventHandler(inkCtl_InkAdded); inkCtl.InkOverlay.Ink.InkDeleted += new StrokesEventHandler(inkCtl_InkDeleted);

The InkAdded and InkDeleted events are triggered when an Ink object has Stroke objects created and deleted from it as a result of either tablet input or programmatic means. The implementations of the StrokesEvent Handler event handlers merely cause a full redraw to occur on pnlInput, resulting in the latest stroke ID values to be displayed.

Notice that the implementations of both the InkAdded and InkDeleted events actually trigger another event handler using the .NET Framework s Control.Invoke method. This is because the InkAdded and InkDeleted events can be fired on a thread other than the application s main thread of execution. To avoid any multithreading problems, the application ensures that only Windows Forms data access occurs on the main thread.

Analysis of Stroke ID Behavior

When you first start the StrokeIdViewer application, try drawing a stroke using the mouse or pen. You ll see that once you complete the stroke, a stroke ID of 1 should appear at the stroke s starting point. Now try drawing another stroke and observe it is assigned a stroke ID of 2. Draw yet another stroke and it should be assigned a stroke ID of 3. Notice the pattern the stroke ID increments by 1 for every stroke drawn.

Now, switch to Select mode and select a stroke by either tapping or lassoing. Once you have a selection, drag it around the input area. Observe that the stroke IDs remain constant. Try resizing the selection, and again notice the stroke ID values don t change.

When you switch back to Ink mode and draw another stroke, the assigned stroke ID value isn t what you might expect. If the last stroke ID assigned before you switched to select mode was 3, you would think that the next stroke drawn would be assigned a stroke ID of 4 in keeping with the pattern we have observed. Instead, an ID of 8 gets assigned (your value might be different, but it won t be 4). What happened to the IDs in between? Well, each pen action you just performed in select mode was assigned an ID, even though no ink was drawn. So if the ink stroke you just created was assigned an ID of 8, switch back to select mode and tap the pen or click the mouse. Then switch to ink mode and draw an ink stroke the ID assigned should be 10 because ID 9 was assigned to the tap or click you just performed.

Finally we ll take a look at erase mode. Switch into Delete mode and make sure that StrokeErase is chosen. Now, try erasing a stroke or two, and observe that the other stroke IDs don t change. Any pen or mouse operation in this mode will increment the stroke ID as in select mode; you can easily confirm this by switching to ink mode, drawing a stroke, and observing its stroke ID.

Now choose PointErase and swipe the eraser through some ink. Notice that as you erase ink, the number of stroke IDs increases because the point-level erasing is splitting strokes into two separate pieces. The first half of the stroke is preserved from the original stroke, but the second half is created from scratch and this results in a new (and therefore larger valued than the first half) stroke ID being assigned.

NOTE
All pen actions actually result in a Stroke object being created and the InkCollectorStrokeEventHandler event being fired. In Select and Delete modes, the InkOverlay will automatically set the InkCollectorStrokeEventArgs Cancel property to true, resulting in no residual ink strokes left in the Ink object.

Using Strokes Collections

Apart from being obtained from the Ink class s Strokes property and InkOverlay s Selection property, Strokes collections can also be explicitly created from an Ink object. The Ink class s CreateStrokes method serves this purpose, and it has two overloads:

Strokes CreateStrokes(); Strokes CreateStrokes(int[] ids);

The first variant of the method constructs an empty Strokes collection, and the second variant constructs a Strokes collection filled with the set of Stroke object references as determined by the array of stroke IDs passed in via the ids parameter. If a stroke ID is encountered that doesn t exist in the ink object (for example, if the Stroke object has been deleted or it hasn t been created yet), CreateStrokes will throw an exception.

Collection Functionality

The Strokes collection provides all common list operations, such as adding and removing Stroke object references. The Strokes collection s list functionality is presented in Table 5-1.

Table 5-1. The List Management Methods and Properties of the Strokes Class

Method or Property Name

Description

void Add(Stroke s) void Add(Strokes strokes)

Appends a reference to a Stroke object or appends references to the Stroke objects in a Strokes collection to the end of the collection

void Clear()

Removes all Stroke object references from the collection

bool Contains(Stroke s)

Determines if a reference to the given Stroke object is present in the collection

int Count

Returns the number of elements in the collection

int IndexOf(Stroke s)

Returns the index of the reference to the given Stroke object in the collection

Ink Ink

Returns the Ink object whose Stroke objects the Strokes collection can hold references to

operator[]

Returns the Stroke object referenced at the specified index

void Remove(Stroke s) void Remove(Strokes strokes)

Removes the reference to the Stroke object or removes references to the Stroke objects in a Strokes collection

void RemoveAt(int index)

Removes the references of the Stroke object found at the specified index

Receiving Notification of Stroke Addition or Removal

Much like the Ink class provides event notification of strokes being added or deleted from an Ink object via InkAdded and InkDeleted, the Strokes collection class provides the StrokesAdded and StrokesRemoved events that are fired when Stroke object references are added or removed from the collection. Similar to InkAdded and InkDeleted events, the StrokesAdded and StrokesRemoved events can be fired on a thread other than the main thread of execution be aware of this when implementing their event handlers. The StrokesAdded and Strokes Removed events, like InkAdded and InkDeleted, also use Stroke EventHandler-based event handlers.

Sample Application InkSelector

This next sample application illustrates the usage of Strokes collections by implementing common menu operations on an InkOverlay s selection. The sample provides menu items to select all ink currently in the InkOverlay s Ink object, select the next stroke after the current selection in the Ink object, toggle the selection state of the strokes, and deselect all strokes.

InkSelector.cs

//////////////////////////////////////////////////////////////////// // // InkSelector.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates working with the Strokes collection by // showing various operations on the selection. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl inkCtl; private MenuItem miSelectAll; private MenuItem miSelectNext; private MenuItem miSelectToggle; private MenuItem miSelectNone; // Entry point of the program [STAThread] static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // Create the main menu Menu = new MainMenu(); MenuItem miFile = new MenuItem("&File"); Menu.MenuItems.Add(miFile); MenuItem miExit = new MenuItem("E&xit"); miExit.Click += new EventHandler(miExit_Click); Menu.MenuItems[0].MenuItems.Add(miExit); MenuItem miEdit = new MenuItem("&Edit"); miEdit.Popup += new EventHandler(miEdit_Popup); Menu.MenuItems.Add(miEdit); miSelectAll = new MenuItem("Select &All"); miSelectAll.Click += new EventHandler(miSelectAll_Click); Menu.MenuItems[1].MenuItems.Add(miSelectAll); miSelectNext = new MenuItem("Select &Next"); miSelectNext.Click += new EventHandler(miSelectNext_Click); Menu.MenuItems[1].MenuItems.Add(miSelectNext); miSelectToggle = new MenuItem("&Toggle Selection"); miSelectToggle.Click += new EventHandler(miSelectToggle_Click); Menu.MenuItems[1].MenuItems.Add(miSelectToggle); miSelectNone = new MenuItem("&Deselect All"); miSelectNone.Click += new EventHandler(miSelectNone_Click); Menu.MenuItems[1].MenuItems.Add(miSelectNone); // Create and place all of our controls inkCtl = new InkControl(); inkCtl.Location = new Point(8, 8); inkCtl.Size = new Size(352, 220); // Configure the form itself ClientSize = new Size(368, 236); Controls.AddRange(new Control[] { inkCtl }); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "InkSelector"; ResumeLayout(false); // We're now set to go, so turn on tablet input inkCtl.InkOverlay.Enabled = true; } // Handle the "Exit" menu item being clicked private void miExit_Click(object sender, EventArgs e) { Application.Exit(); } // Handle the "Edit" submenu popping up private void miEdit_Popup(object sender, EventArgs e) { bool fSelectMode = (inkCtl.InkOverlay.EditingMode == InkOverlayEditingMode.Select); // Enable or disable the various menu items miSelectAll.Enabled = fSelectMode && (inkCtl.InkOverlay.Ink.Strokes.Count > 0); miSelectNext.Enabled = fSelectMode && (inkCtl.InkOverlay.Selection.Count > 0); miSelectToggle.Enabled = fSelectMode && (inkCtl.InkOverlay.Ink.Strokes.Count > 0); miSelectNone.Enabled = fSelectMode && (inkCtl.InkOverlay.Selection.Count > 0); } // Handle the "Select All" menu item being clicked private void miSelectAll_Click(object sender, EventArgs e) { // Set the selection strokes collection to be all strokes inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.Strokes; } // Handle the "Select Next" menu item being clicked private void miSelectNext_Click(object sender, EventArgs e) { // Find the index of the last stroke in the selection int nIndex = inkCtl.InkOverlay.Ink.Strokes.IndexOf( inkCtl.InkOverlay.Selection[0]); // Increment the index's value, and wrap around to 0 if it goes // beyond the end of the number of strokes in the ink object nIndex++; if (nIndex >= inkCtl.InkOverlay.Ink.Strokes.Count) { nIndex = 0; } // Set the selection to be the stroke located at the new index inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(new int [] { inkCtl.InkOverlay.Ink.Strokes[nIndex].Id }); } // Handle the "Toggle Selection" menu item being clicked private void miSelectToggle_Click(object sender, EventArgs e) { // Add the strokes which are not in the selection to a new // strokes collection Strokes strks = inkCtl.InkOverlay.Ink.CreateStrokes(); foreach (Stroke s in inkCtl.InkOverlay.Ink.Strokes) { if (!inkCtl.InkOverlay.Selection.Contains(s)) { strks.Add(s); } } // Set the selection to the new strokes collection inkCtl.InkOverlay.Selection = strks; } // Handle the "Deselect All" menu item being clicked private void miSelectNone_Click(object sender, EventArgs e) { // Set the selection strokes collection to an empty collection inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); } }

Again, this sample makes use of the InkControl class found in the BuildingTabletApps library. A rather straightforward initialization in frmMain() results in the main menu being constructed along with menu item handlers, and an InkControl being created. The interesting part of the application comes in the implementation of the various menu item handlers.

To manipulate the selection, the Strokes collection of InkOverlay s Strokes property is reassigned to a different Strokes collection altogether. Consider the code to perform a select all operation:

// Set the selection strokes collection to be all strokes inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.Strokes;

The Selection property is assigned the Strokes collection that references all the Stroke objects contained in the Ink object. That seems easy enough, doesn t it?

The implementation of the select next operation is a bit more complex, however. The purpose of this operation is to shift the selection to the next item in the document using some logical order. In our case, we ll define the next item to be the next Stroke object ordered in the Ink object s collection of strokes:

// Find the index of the last stroke in the selection int nIndex = inkCtl.InkOverlay.Ink.Strokes.IndexOf( inkCtl.InkOverlay.Selection[0]); // Increment the index's value, and wrap around to 0 if it goes // beyond the end of the number of strokes in the ink object nIndex++; if (nIndex >= inkCtl.InkOverlay.Ink.Strokes.Count) { nIndex = 0; } // Set the selection to be the stroke located at the new index inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(new int [] { inkCtl.InkOverlay.Ink.Strokes[nIndex].Id });

The code first computes the index of the selected stroke in the collection of all Stroke objects contained in the Ink object via the Strokes class s IndexOf method. If multiple Stroke objects are selected (and hence multiple items are in the Strokes collection), the first one, the zeroth element, is used.

Next the index of the selected stroke is incremented and wrapped around to 0 if it exceeds the upper limit of allowable indexes in the Strokes collection. Finally the selection is altered by assigning a new Strokes collection to the Selection property of InkOverlay. The Strokes collection that s used is created via the Ink class s CreateStrokes method, passing in the stroke ID of the next logical Stroke object as the sole element of an array of IDs.

You might think that instead of reassigning the Selection property to a new Strokes collection, the existing Strokes collection used for Selection simply could be manipulated as thus:

// Set the selection to be the stroke located at the new index inkCtl.InkOverlay.Selection.Clear(); inkCtl.InkOverlay.Selection.Add( inkCtl.InkOverlay.Ink.Strokes[nIndex]);

That method won t work, however recall that InkOverlay s Selection property returns a copy of the current Strokes collection used, the same way Ink s Strokes property does.

Toggling the selection state of ink objects becomes easy work using the Strokes collection s methods. To toggle the selected ink strokes, we want to select only those strokes that are not currently found in the selection:

// Add the strokes which are not in the selection to a new // strokes collection Strokes strks = inkCtl.InkOverlay.Ink.CreateStrokes(); foreach (Stroke s in inkCtl.InkOverlay.Ink.Strokes) { if (!inkCtl.InkOverlay.Selection.Contains(s)) { strks.Add(s); } } // Set the selection to the new strokes collection inkCtl.InkOverlay.Selection = strks;

The code first creates an empty Strokes collection. Next, every Stroke object contained by the Ink object is checked to see if it is found in the current selection. Those Stroke objects that are not found in the current selection are added to the Strokes collection that was just created. Finally, the selection is changed to be those strokes that were not part of the original selection.

An alternative (and simpler!) method can actually be used to accomplish toggle selection :

// Shorter way of toggling the selection Strokes strks = inkCtl.InkOverlay.Ink.Strokes; strks.Remove(inkCtl.InkOverlay.Selection); inkCtl.InkOverlay.Selection = strks;

This method takes advantage of the Strokes collection s Remove method. The entire collection of Stroke objects contained by the Ink object has the current selection set removed from it, and the resulting Strokes collection is assigned to the selection. Pretty neat, huh? We decided to include the first longer method because it was a little more illustrative, but the second method is probably preferable because it requires less code and is less complex.

Lastly, the deselect all operation is a straightforward one to implement:

// Set the selection strokes collection to an empty collection inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes();

The Selection property is assigned an empty Strokes collection, very similar in principle to the preceding select all implementation.

Creation, Deletion, and Ownership of Stroke Objects

Instances of the InkCollector and InkOverlay classes create Stroke objects as a result of tablet input, but oftentimes it is desirable to copy, delete, remove, or construct new Stroke objects contained within an Ink object through programmatic means. This section covers the methods found in the Ink Data Management API that provide that functionality.

Table 5-2 lists the methods found in the Ink class that result in Stroke objects being either added to or removed from an Ink object.

Table 5-2. Object Creation and Deletion Functions of the Ink Class

Method Name

Description

void AddStrokesAtRectangle(Strokes strokes, Rectangle rect)

Copies the Stroke objects referenced by a Strokes collection into the Ink object at a specified location

Ink Clone()

Copies the entire contents of the Ink object and returns a new Ink object instance

Stroke CreateStroke(Point[] pts)

Creates a new Stroke object using an (x,y) coordinate array

void DeleteStroke(Stroke s)

Destroys a Stroke object

void DeleteStrokes(Strokes strokes)

Destroys all Stroke objects referenced by a Strokes collection

Ink ExtractStrokes() Ink ExtractStrokes(Strokes strokes) Ink ExtractStrokes(Strokes strokes, ExtractFlags f) Ink ExtractStrokes(Rectangle r) Ink ExtractStrokes(Rectangle r, ExtractFlags f)

Removes or copies Stroke objects from an Ink object, returning them in a new instance of Ink

Adding Ink

AddStrokesAtRectangle is a useful method for adding existing strokes into an Ink object. The owning Ink object of the source Strokes collection does not have to be the same as the destination, though it can be. The Rectangle parameter passed to the method defines the bounding box that the Stroke objects referenced by the Strokes collection will be fit into. Like most coordinate parameters to Ink Data Management API functions, it is in HIMETRIC units.

Unfortunately, the AddStrokesAtRectangle method of the Ink class does not return a Strokes collection referencing the newly added ink. However if this functionality is needed, the BuildingTabletApps utility library supplies an identical method InkEx.AddStrokesAtRectangle which does.

The CreateStroke method constructs a new Stroke object out of a series of points given in ink space coordinates. The newly created stroke is returned by the method so that it can be further manipulated if desired.

NOTE
Astute readers might notice that Stroke objects are actually made up of packets that may have other data besides x and y information. Unfortunately, the CreateStroke method allows Stroke objects to be created only from x and y data it is impossible to use the Ink Data Management API to programmatically generate Stroke objects with packet properties other than x and y.

The Clone method does exactly what its name purports it generates an exact copy of the Ink object that the method is called on.

Deleting Ink

The DeleteStroke and DeleteStrokes methods both do exactly the same thing: they destroy Stroke objects. The only difference between them is the way the Stroke objects to be deleted are specified DeleteStroke takes a Stroke object as a parameter, and DeleteStrokes takes a Strokes collection as a parameter. I mentioned earlier it is possible for a Strokes collection to contain a reference to a Stroke object that has been destroyed. The Stroke object that results from the dereference will be unusable but its Deleted and Id properties can be read (Deleted will equal true, Id will equal the expected stroke ID); any other property or method usage causes an exception. And let me reiterate that good programming practice is to check the Deleted property of Stroke objects when dereferencing from a Strokes collection if code other than your own maintains the collection.

ExtractStrokes is a method that copies or moves Stroke objects into a new Ink object. There are five variations of this function that can be put into three groups:

Ink ExtractStrokes()

This version of ExtractStrokes will remove all the Stroke objects from an Ink object, add them into a newly created Ink object, and return the new Ink object. There will be no ink left in the Ink object that has ExtractStrokes called on it.

The next two versions of ExtractStrokes will remove the Stroke objects from an Ink object as identified by either a Strokes collection or a rectangle, add them into a newly created Ink object, and return the new Ink object. The rectangle, specified in ink coordinates, will clip out the Stroke objects, hopping them into segments if needed.

Ink ExtractStrokes(Strokes strokes) Ink ExtractStrokes(Rectangle r)

These last two versions of ExtractStrokes allow you to specify whether the Stroke objects are removed or copied from the Ink object via the ExtractFlags parameter, whose values are listed in Table 5-3. The use of the Strokes collection and Rectangle parameters are the same as for the previous versions.

Ink ExtractStrokes(Strokes strokes, ExtractFlags f) Ink ExtractStrokes(Rectangle r, ExtractFlags f)

Table 5-3. The ExtractFlags Enumeration Used by the Ink Class s ExtractStrokes Method

ExtractFlags Values

Description

CopyFromOriginal

Preserve strokes in the source ink object

RemoveFromOriginal

Remove the strokes from the source ink object

Default

Use the default value, which is RemoveFromOriginal

You might be wondering why ExtractStrokes doesn t just return a Strokes collection from the method rather than a whole Ink object. The answer is this: a Stroke object cannot live outside an Ink object, and because the ExtractStrokes method removes Stroke objects from their owning Ink object, the Stroke objects no longer have an owning Ink object thus a new Ink object must be created as a place for the extracted Stroke objects to live.

Receiving Notification of Ink Addition and Deletion

As mentioned earlier, the Ink class provides two events that allow notification of Stroke objects being added and deleted: InkAdded and InkDeleted, respectively.

Implementing a Live Strokes Collection

We know that the Ink class s Strokes property returns a copy of a Strokes collection. However, if we hold onto that Strokes collection and Stroke objects are then added or removed on the Ink object, the Strokes collection we re holding a reference to will not be accurately reflecting the state of the Ink object anymore. Under most circumstances that s OK; we would just get the Strokes collection generated over again by querying the Strokes property. But if for some reason we wanted to keep a reference to a Strokes collection and not have the reference be updated, by using the InkAdded and InkDeleted handlers we can implement a live Strokes collection that always reflects the Stroke objects contained in an Ink object. When a Stroke object is added to the Ink object we simply add a reference to it in the Strokes collection, and similarly when a Stroke object is deleted from the Ink object we remove its reference from the Strokes collection.

Sample Application InkCopy

The InkCopy sample application demonstrates some of the functionality we ve just been talking about. Two InkControl objects are displayed on a form and each can have ink drawn, selected, moved, resized, and deleted. The illustrative functionality comes from menu items that allow copying the entire contents of the top InkControl to the bottom one, copying or moving the top InkControl s current selection to the bottom one, and deleting the top InkControl s current selection completely.

InkCopy.cs

//////////////////////////////////////////////////////////////////// // // InkCopy.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates how to copy, delete, and move Stroke // objects between two Ink objects. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl inkCtl1; private InkControl inkCtl2; private MenuItem miCopy; private MenuItem miMove; private MenuItem miDelete; // Entry point of the program [STAThread] static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // Create the main menu Menu = new MainMenu(); MenuItem miFile = new MenuItem("&File"); Menu.MenuItems.Add(miFile); MenuItem miExit = new MenuItem("E&xit"); miExit.Click += new EventHandler(miExit_Click); Menu.MenuItems[0].MenuItems.Add(miExit); MenuItem miEdit = new MenuItem("&Edit"); miEdit.Popup += new EventHandler(miEdit_Popup); Menu.MenuItems.Add(miEdit); MenuItem miDuplicate = new MenuItem("D&uplicate All"); miDuplicate.Click += new EventHandler(miDuplicate_Click); Menu.MenuItems[1].MenuItems.Add(miDuplicate); Menu.MenuItems[1].MenuItems.Add(new MenuItem("-")); miCopy = new MenuItem("&Copy"); miCopy.Click += new EventHandler(miCopy_Click); Menu.MenuItems[1].MenuItems.Add(miCopy); miMove = new MenuItem("&Move"); miMove.Click += new EventHandler(miMove_Click); Menu.MenuItems[1].MenuItems.Add(miMove); miDelete = new MenuItem("&Delete"); miDelete.Click += new EventHandler(miDelete_Click); Menu.MenuItems[1].MenuItems.Add(miDelete); // Create and place all of our controls inkCtl1 = new InkControl(); inkCtl1.Location = new Point(8, 8); inkCtl1.Size = new Size(352, 216); inkCtl2 = new InkControl(); inkCtl2.Location = new Point(8, 232); inkCtl2.Size = new Size(352, 216); // Configure the form itself ClientSize = new Size(368, 456); Controls.AddRange(new Control[] { inkCtl1, inkCtl2}); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "InkCopy"; ResumeLayout(false); // We're now set to go, so turn on tablet input inkCtl1.InkOverlay.Enabled = true; inkCtl2.InkOverlay.Enabled = true; } // Handle the "Exit" menu item being clicked private void miExit_Click(object sender, EventArgs e) { Application.Exit(); } // Handle the "Edit" submenu popping up private void miEdit_Popup(object sender, EventArgs e) { // Only enable the Copy, Move, and Delete menu items if there // is a current selection bool fSelection = (inkCtl1.InkOverlay.EditingMode == InkOverlayEditingMode.Select) && (inkCtl1.InkOverlay.Selection.Count > 0); miCopy.Enabled = fSelection; miMove.Enabled = fSelection; miDelete.Enabled = fSelection; } // Handle the "Duplicate" menu item being clicked private void miDuplicate_Click(object sender, EventArgs e) { // Turn off input in inkCtl2 since we're going to assign a new // Ink object to it inkCtl2.InkOverlay.Enabled = false; // Copy over the entire ink object from inkCtl1 to inkCtl2 inkCtl2.InkOverlay.Ink = inkCtl1.InkOverlay.Ink.Clone(); // Turn tablet input back on for inkCtl2 inkCtl2.InkOverlay.Enabled = true; // Draw the new ink in inkCtl2 inkCtl2.InkInputPanel.Invalidate(); } // Handle the "Copy" menu item being clicked private void miCopy_Click(object sender, EventArgs e) { // Copy over the selection from inkCtl1 to inkCtl2 Strokes strokes = inkCtl1.InkOverlay.Selection; inkCtl2.InkOverlay.Ink.AddStrokesAtRectangle( strokes, strokes.GetBoundingBox()); // Draw the new ink in inkCtl2 inkCtl2.InkInputPanel.Invalidate(); } // Handle the "Move" menu item being clicked private void miMove_Click(object sender, EventArgs e) { // Copy over the selection from inkCtl1 to inkCtl2 Strokes strokes = inkCtl1.InkOverlay.Selection; inkCtl2.InkOverlay.Ink.AddStrokesAtRectangle( strokes, strokes.GetBoundingBox()); // Delete the selection from inkCtl1 inkCtl1.InkOverlay.Ink.DeleteStrokes(strokes); // Clear the selection in inkCtl1 inkCtl1.InkOverlay.Selection = inkCtl1.InkOverlay.Ink.CreateStrokes(); // Redraw the ink in both inkCtl1 and inkCtl2 inkCtl1.InkInputPanel.Invalidate(); inkCtl2.InkInputPanel.Invalidate(); } // Handle the "Delete" menu item being clicked private void miDelete_Click(object sender, EventArgs e) { // Delete the selection from inkCtl1 Strokes strokes = inkCtl1.InkOverlay.Selection; inkCtl1.InkOverlay.Ink.DeleteStrokes(strokes); // Clear the selection in inkCtl1 inkCtl1.InkOverlay.Selection = inkCtl1.InkOverlay.Ink.CreateStrokes(); // Redraw the ink in inkCtl1 inkCtl1.InkInputPanel.Invalidate(); } }

The application creates its main menu, hooks up event handlers to the menu items, and then creates the two InkControl controls. inkCtl1 is the top InkControl and inkCtl2 is the bottom one. As with the InkSelector sample, the code we re most interested in lies within the menu item handlers.

The duplicate all menu item results in the entire Ink object in inkCtl1 s InkOverlay being copied to inkCtl2 s InkOverlay. This is accomplished by using the Ink class s Clone method and assigning the results to the Ink property of inkCtl2 s InkOverlay:

// Turn off input in inkCtl2 since we're going to assign a new // Ink object to it inkCtl2.InkOverlay.Enabled = false; // Copy over the entire ink object from inkCtl1 to inkCtl2 inkCtl2.InkOverlay.Ink = inkCtl1.InkOverlay.Ink.Clone(); // Turn tablet input back on for inkCtl2 inkCtl2.InkOverlay.Enabled = true;

Besides the use of Clone, something to note here is that an InkOverlay instance must disable tablet input to have its Ink property value changed; hence the bookending of setting the inkCtl2.InkOverlay.Enabled property.

The copy menu item utilizes the AddStrokesAtRectangle method to take the currently selected Stroke objects in inkCtl1 s InkOverlay and copy them into inkCtl2 s InkOverlay:

// Copy over the selection from inkCtl1 to inkCtl2 Strokes strokes = inkCtl1.InkOverlay.Selection; inkCtl2.InkOverlay.Ink.AddStrokesAtRectangle( strokes, strokes.GetBoundingBox());

Notice that the second argument to AddStrokesAtRectangle is something we haven t learned about yet GetBoundingBox. That function computes the smallest rectangle that fully encloses all the Stroke objects referenced by the Strokes collection. We ll learn more about that function in the next chapter. The result of the AddStrokesAtRectangle method is that the Stroke objects are copied into the Ink object found in the InkOverlay. Also note that because the Ink object s value in inkCtl2 s InkOverlay isn t changing we don t need to disable and re-enable tablet input in the InkOverlay object.

The menu item move is the same as copy except it also deletes the selected Stroke objects from inkCtl1 s InkOverlay:

// Copy over the selection from inkCtl1 to inkCtl2 Strokes strokes = inkCtl1.InkOverlay.Selection; inkCtl2.InkOverlay.Ink.AddStrokesAtRectangle( strokes, strokes.GetBoundingBox()); // Delete the selection from inkCtl1 inkCtl1.InkOverlay.Ink.DeleteStrokes(strokes); // Clear the selection in inkCtl1 inkCtl1.InkOverlay.Selection = inkCtl1.InkOverlay.Ink.CreateStrokes();

Once the selected Stroke objects have been copied over to inkCtl2 s InkOverlay, the DeleteStrokes method is used to destroy the selected Stroke objects in inkCtl1 s InkOverlay. Once deleted, the current selection in the InkOverlay is reset because otherwise the Strokes collection returned via the Selection property will reference deleted Stroke objects.

The delete functionality is almost the same as move it skips the step of copying the selected Stroke objects into inkCtl2 s InkOverlay but otherwise performs the same operations. It deletes the currently selected Stroke objects and then resets the current selection.

Sample Application InkFactory

This next sample application, InkFactory, shown in Figure 5-3, demonstrates how to programmatically create Stroke objects. It allows the user the choice of a circle or a square created in an InkControl via the application s main menu.

figure 5-3 the inkfactory sample application in action.

Figure 5-3. The InkFactory sample application in action.

InkFactory.cs

//////////////////////////////////////////////////////////////////// // // InkFactory.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates how to create new strokes // programmatically. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl inkCtl; // Entry point of the program [STAThread] static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // Create the main menu Menu = new MainMenu(); MenuItem miFile = new MenuItem("&File"); Menu.MenuItems.Add(miFile); MenuItem miExit = new MenuItem("E&xit"); miExit.Click += new EventHandler(miExit_Click); Menu.MenuItems[0].MenuItems.Add(miExit); MenuItem miShape = new MenuItem("&Shape"); Menu.MenuItems.Add(miShape); MenuItem miCircle = new MenuItem("&Circle"); miCircle.Click += new EventHandler(miCircle_Click); Menu.MenuItems[1].MenuItems.Add(miCircle); MenuItem miSquare = new MenuItem("&Square"); miSquare.Click += new EventHandler(miSquare_Click); Menu.MenuItems[1].MenuItems.Add(miSquare); // Create and place all of our controls inkCtl = new InkControl(); inkCtl.Location = new Point(8, 8); inkCtl.Size = new Size(352, 216); // Configure the form itself ClientSize = new Size(368, 232); Controls.AddRange(new Control[] { inkCtl }); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "InkFactory"; ResumeLayout(false); // We're now set to go, so turn on tablet input inkCtl.InkOverlay.Enabled = true; } // Handle the "Exit" menu item being clicked private void miExit_Click(object sender, EventArgs e) { Application.Exit(); } // Handle the "Circle" menu item being clicked private void miCircle_Click(object sender, EventArgs e) { // Get the size of the input area in ink coordinates Rectangle rcBounds = new Rectangle( new Point(0,0), inkCtl.InkInputPanel.ClientSize); Graphics g = inkCtl.InkInputPanel.CreateGraphics(); RendererEx.PixelToInkSpace( inkCtl.InkOverlay.Renderer, g, ref rcBounds); g.Dispose(); // Compute a random size and location for the circle Random r = new Random((int)DateTime.Now.Ticks); int nRadius = r.Next(1, rcBounds.Height/2); int X = r.Next(nRadius, rcBounds.Width - nRadius); int Y = r.Next(nRadius, rcBounds.Height - nRadius); // Compute the points of the circle Point[] pts = new Point[101]; double fRadian = 2f*Math.PI; for (int i = 0; i < 100; i++) { pts[i].X = X + (int)(nRadius * Math.Cos(fRadian * i/100f)); pts[i].Y = Y + (int)(nRadius * Math.Sin(fRadian * i/100f)); } // "Connect" the endpoint with the start point - note it doesn't // really get connected (strokes cannot be closed polygons), but // rather the endpoint is a copy of the start point pts[100].X = pts[0].X; pts[100].Y = pts[0].Y; // Create the new stroke using the points Stroke stroke = inkCtl.InkOverlay.Ink.CreateStroke(pts); // Give it the current drawing attributes of inkCtl1 stroke.DrawingAttributes = inkCtl.InkOverlay.DefaultDrawingAttributes; // Draw the new ink stroke inkCtl.InkInputPanel.Invalidate(); } // Handle the "Square" menu item being clicked private void miSquare_Click(object sender, EventArgs e) { // Get the size of the input area in ink coordinates Rectangle rcBounds = new Rectangle( new Point(0,0), inkCtl.InkInputPanel.ClientSize); Graphics g = inkCtl.InkInputPanel.CreateGraphics(); RendererEx.PixelToInkSpace( inkCtl.InkOverlay.Renderer, g, ref rcBounds); g.Dispose(); // Compute a random size and location for the square Random r = new Random((int)DateTime.Now.Ticks); int nSizeLength = r.Next(1, rcBounds.Height); int X = r.Next(0, rcBounds.Width - nSizeLength); int Y = r.Next(0, rcBounds.Height - nSizeLength); // Compute the points of the square Point[] pts = new Point[101]; for (int i = 0; i < 25; i++) { pts[i].X = X + (int)(nSizeLength * i / 25f); pts[i].Y = Y; pts[i+25].X = X + nSizeLength; pts[i+25].Y = Y + (int)(nSizeLength * i / 25f); pts[i+50].X = X + (nSizeLength - (pts[i].X - X)); pts[i+50].Y = Y + nSizeLength; pts[i+75].X = X; pts[i+75].Y = Y + (nSizeLength - (pts[i+25].Y - Y)); } // "Connect" the endpoint with the start point - note it doesn't // really get connected (strokes cannot be closed polygons), but // rather the endpoint is a copy of the start point pts[100].X = pts[0].X; pts[100].Y = pts[0].Y; // Create the new stroke using the points Stroke stroke = inkCtl.InkOverlay.Ink.CreateStroke(pts); // Give it the current drawing attributes of inkCtl1 stroke.DrawingAttributes = inkCtl.InkOverlay.DefaultDrawingAttributes; // Draw the new ink stroke inkCtl.InkInputPanel.Invalidate(); } }

The real guts of this application are found in the menu handlers, as they were in the last couple of samples. Let s take a look at the code snippet to create a circle (the code to create a square is the same, except for the algorithm used to compute the points):

// Get the size of the input area in ink coordinates Rectangle rcBounds = new Rectangle( new Point(0,0), inkCtl.InkInputPanel.ClientSize); Graphics g = inkCtl.InkInputPanel.CreateGraphics(); RendererEx.PixelToInkSpace( inkCtl.InkOverlay.Renderer, g, ref rcBounds); g.Dispose(); // Compute a random size and location for the circle Random r = new Random((int)DateTime.Now.Ticks); int nRadius = r.Next(1, rcBounds.Height/2); int X = r.Next(nRadius, rcBounds.Width - nRadius); int Y = r.Next(nRadius, rcBounds.Height - nRadius); // Compute the points of the circle Point[] pts = new Point[101]; double fRadian = 2f*Math.PI; for (int i = 0; i < 100; i++) { pts[i].X = X + (int)(nRadius * Math.Cos(fRadian * i/100f)); pts[i].Y = Y + (int)(nRadius * Math.Sin(fRadian * i/100f)); } // "Connect" the endpoint with the start point - note it doesn't // really get connected (strokes cannot be closed polygons), but // rather the endpoint is a copy of the start point pts[100].X = pts[0].X; pts[100].Y = pts[0].Y; // Create the new stroke using the points Stroke stroke = inkCtl.InkOverlay.Ink.CreateStroke(pts); // Give it the current drawing attributes of inkCtl1 stroke.DrawingAttributes = inkCtl.InkOverlay.DefaultDrawingAttributes;

The Ink class s CreateStroke method is the key enabler of this sample s functionality. As described earlier, this method constructs a Stroke object given an array of points that are specified in ink coordinates (HIMETRIC units). Most of the functionality in the code snippet is to compute the array of points that are passed into CreateStroke.

The first section of code figures out how big the input area is in ink coordinates, also known as ink space. A method in the BuildingTabletApps utility library named RendererEx.PixelToInkSpace is utilized that converts a rectangle of screen pixels into ink coordinates. It will be discussed in further detail in the next section.

Next, the circle s radius and location are determined by randomly picking some numbers, ensuring the circle won t be created outside the input area. An array of 101 points is allocated to hold all the points making up the circle, and the values are then filled in. Lastly the circle shape is closed by setting the 101st point to be equal to the first. The polyline for the stroke doesn t actually get closed it s just a polyline whose start and end points perfectly overlap.

Polylines vs. Curves

The Tablet PC Platform implements ink strokes as polylines that is, as a set of discrete (x,y) values that connect. This is largely a result of the method by which ink strokes are captured tablet digitizers are only capable of providing an (x,y) location of the stylus at a regular sampling interval. Because physical ink by nature is curved, the points in an electronic ink stroke should be sufficiently close together to give the illusion of curvature, and/or B zier curve fitting should be employed. We ll learn more about curve fitting in the next section.

Once the point array has been computed, the Stroke object is created via the CreateStrokes method. The style of the ink stroke (its color, width, and so forth) is set to the current style used by inkCtl s InkOverlay, so the stroke is displayed in all its glory.

And speaking of ink style, it s time to move on to the next section, which is all about rendering ink.



Building Tablet PC Applications
Building Tablet PC Applications (Pro-Developer)
ISBN: 0735617236
EAN: 2147483647
Year: 2001
Pages: 73

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net