Stroke Geometry

Stroke Geometry

This (somewhat ambiguously titled) section is devoted to working with ink strokes. Specifically, we ll discuss the aspects that apply to their measurement and shape.

Computing the Bounding Box of a Stroke

The bounding box of ink strokes is the smallest rectangle that completely encloses the ink. Often, it is useful to compute the bounds of this box for an Ink object, a Stroke object, or a Strokes collection. If, for example, one or more attributes of a Stroke object s DrawingAttributes property are altered, we might want to trigger a redraw for the changes to be reflected; it is much more efficient to invalidate the rectangular area surrounding the ink rather than the entire viewing window. Of course, this notion of determining boundaries begs the following questions, What is the edge of an ink stroke? Are the points in a stroke the edge? Does the thickness of the ink stroke matter? What if curve fitting is turned on, and the curve doesn t quite intersect points on the edge?

To address these questions, Ink objects supply two types of bounding boxes. The first type uses only the ink stroke s polyline points when calculating the bounding box, ignoring the rendered appearance of the ink entirely. This means factors like stroke thickness and curve-fitting are ignored. The second type of bounding box accounts for the rendered appearance of the ink.

The GetBoundingBox Method

The Ink, Strokes, and Stroke classes all contain a GetBoundingBox method, which computes the bounding box (or bounds ) of ink strokes. The object type calling the function determines which strokes are included when computing the bounds. For an Ink object, the bounds of all its ink strokes are used; for a Strokes collection, the Stroke objects the collection references are used; and for a Stroke object, the Stroke object is used.

The GetBoundingBox method comes in two overloaded forms, and both return a Rectangle structure of the bounds in ink coordinates:

Rectangle Ink.GetBoundingBox() Rectangle Ink.GetBoundingBox(BoundingBoxMode mode) Rectangle Strokes.GetBoundingBox() Rectangle Strokes.GetBoundingBox(BoundingBoxMode mode) Rectangle Stroke.GetBoundingBox() Rectangle Stroke.GetBoundingBox(BoundingBoxMode mode)

The first overload takes no parameters and computes the bounds of the ink according to its drawing attributes (how it looks on the screen). The second overload takes a member from the BoundingBoxMode enumeration as a parameter and returns the bounds as described by the values presented in Table 6-1.

Table 6-1. BoundingBoxMode Values

BoundingBoxMode Value

Description

CurveFit

Bounding box of the stroke taking into account its Drawing Attributes property when FitToCurve is true

Default

Bounding box of the stroke taking into account its DrawingAttributes property

NoCurveFit

Bounding box of the stroke taking into account its DrawingAttributes property when FitToCurve is false

PointsOnly

Bounding box of the stroke without taking into account its Drawing Attributes property (using only its points)

Union

Union of the results from BoundingBoxMode.CurveFit and Bounding BoxMode.NoCurveFit

Figure 6-1 defines members in the BoundingBoxMode enumeration. Notice that the version of GetBoundingBox without parameters gives the same results as calling GetBoundingBox(BoundingBoxMode.Default).

figure 6-1 types of bounding box modes.

Figure 6-1. Types of bounding box modes.

The Renderer Class s Measure Method

There is another method for obtaining the bounds of ink strokes in addition to the GetBoundingBox method. The Renderer class s Measure method is able to take into account any view or object transformation in calculating the bounding box:

Rectangle Renderer.Measure(Stroke s) Rectangle Renderer.Measure(Stroke s, DrawingAttributes da) Rectangle Renderer.Measure(Strokes strokes)

Notice that the Measure method doesn t use the BoundingBoxMode enumeration the only method it employs is using drawing attributes, obtained either from the DrawingAttributes property of the strokes being measured or from those that are explicitly provided. This method takes into account any view or object transforms set in the renderer.

Code Snippet DrawBoundingBoxes

The following function renders the bounding box of Stroke objects contained in an Ink object or referenced by a Strokes collection. This code can be found in the RendererEx class in the BuildingTabletApps helper library.

// Draw the bounding rect of each stroke in an Ink object public static void DrawBoundingBoxes(Graphics g, Ink ink) { DrawBoundingBoxes(g, ink.Strokes, new Renderer(), Pens.LightGreen, BoundingBoxMode.Default); } // Draw the various bounding rects of each stroke in a strokes // collection public static void DrawBoundingBoxes(Graphics g, Strokes strokes, Renderer renderer, Pen pen, BoundingBoxMode mode) { foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { Rectangle rcBounds = s.GetBoundingBox(mode); RendererEx.InkSpaceToPixel(renderer, g, ref rcBounds); g.DrawRectangle(pen, rcBounds); } } }

The RendererEx.InkSpaceToPixel method is a utility function, also found in the BuildingTabletApps library. It does what the Renderer class s InkSpaceToPixel method does, except it uses a Rectangle structure.

Retrieving the Points of a Stroke

It can sometimes be useful to get at the (X,Y) values that form an ink stroke, for example, to implement custom drawing of ink or for some custom recognition capability. Two kinds of points can be obtained from a Stroke object polyline points and B zier points.

Polyline Points

The Stroke class natively stores data for ink strokes in a polyline format, which is essentially an array of Point structures. The following functions are provided to get and set the polyline points of a Stroke object:

Point Stroke.GetPoint(int index) Point[] Stroke.GetPoints() Point[] Stroke.GetPoints(int index, int count) int Stroke.SetPoint(int index, Point pt) int Stroke.SetPoints(Point[] pts) int Stroke.SetPoints(int index, Point[] pts)

The GetPoint and GetPoints methods are straightforward they simply return a Stroke object s points in ink coordinates. The overload of GetPoints, which accepts two parameters, allows for a specified range of points to be queried. The first parameter is the start index into the array of points, and the second is the count of how many points should be returned.

The SetPoint method provides a means to adjust one point in a Stroke object. The index parameter s value must lie within the range of the Stroke object s point array, or else an exception will be thrown. The method returns the number of points changed in the ink stroke, that is, 1.

SetPoints similarly sets multiple points in a Stroke object. An array of points is passed, with an optional starting index, and the total number of points set is returned. This method cannot alter the total number of points making up an ink stroke; therefore, the array of points used should not exceed the range of the Stroke object s point array.

NOTE
Any extra packet property data contained in a Stroke object (for example, pressure data) will be left unaltered when either the SetPoint or SetPoints method is used.

B zier Control Points

The Stroke class s BezierPoints property provides the control points of the B zier curve that is drawn when its DrawingAttributes CurveFit property is true. These control points can then be used to compute the actual (X,Y) points that make up the curved line, or they can be passed along to a B zier curve drawing function such as the Graphics class s DrawBeziers method.

Alternatively, the Stroke class s GetFlattenedBezierPoints method will compute the actual (X,Y) points that approximate the B zier curve, optionally accepting an error value specified in ink units as its parameter:

Point[] Stroke.GetFlattenedBezierPoints() Point[] Stroke.GetFlattenedBezierPoints(int fittingError)

The array of points returned can then be used in conjunction with a call to Graphics DrawLines method to draw an approximation of the B zier curve. The default value for the fittingError parameter is 0. This results in the smoothest line but yields the greatest number of points returned. It could therefore adversely affect the performance of subsequent operations with the line (such as rendering).

Code Snippet DrawPoints (Draws B zier or Polyline Points of Ink)

This function, found in the BuildingTabletApps helper library, will render the (X,Y) points of a Stroke object. The type of data used for the points is determined by the custom StrokePointType enumeration, whose values are Polyline, Bezier, and FlattenedBezier. Overloads of the DrawPoints method are also provided for a Strokes collection and an entire Ink object.

public enum StrokePointType { Polyline, Bezier, FlattenedBezier } // Draw the polyline points each stroke has in an Ink object public static void DrawPoints(Graphics g, Ink ink) { DrawPoints(g, ink.Strokes, new Renderer(), Brushes.Red, StrokePointType.Polyline); } // Draw the various kinds of points each stroke has in a // strokes collection public static void DrawPoints(Renderer renderer, Graphics g, Strokes strokes, Brush brush, StrokePointType type) { foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { DrawPoints(g, s, renderer, brush, type); } } } // Draw the various kinds of points for a stroke public static void DrawPoints(Renderer renderer, Graphics g, Stroke stroke, Brush brush, StrokePointType type) { // Get the array of points according to the type desired Point [] pts = null; switch (type) { case StrokePointType.Bezier: pts = stroke.BezierPoints; break; case StrokePointType.FlattenedBezier: pts = stroke.GetFlattenedBezierPoints(); break; case StrokePointType.Polyline: pts = stroke.GetPoints(); break; } // Render the points if they were retrieved if (pts != null) { renderer.InkSpaceToPixel(g, ref pts); foreach (Point pt in pts) { g.FillEllipse(brush, pt.X-1, pt.Y-1, 4, 4); } } }

The preceding code is fairly self-explanatory based on the value of StrokePointType, the appropriate property or method of the Stroke object is used to retrieve an array of points to draw. The FillEllipse method is used to perform the actual rendering of each point.

Computing Intersections of a Stroke

Computing the intersections of ink strokes can be useful for a number of tasks performing stroke erasing or recognition, for example. Figure 6-2 shows the three kinds of intersections that the Tablet PC Platform can compute:

  • Self-intersection, which occurs when a stroke crosses itself

  • Stroke intersection, which occurs when a stroke crosses another stroke

  • Rectangle intersection, which occurs when a stroke crosses the bounds of a rectangle

    figure 6-2 the three kinds of intersections self, stroke, and rectangle.

    Figure 6-2. The three kinds of intersections self, stroke, and rectangle.

The Stroke class s SelfIntersections property returns an array of floating-point index values that specify where along the stroke any self-intersections occur. A floating-point index is a value that defines an arbitrary position along the length of an ink stroke. You can think of the value in the same way as an integer index value, except that the fractional part of the number represents where between adjacent point indexes the specified point actually is. Figure 6-3 shows an example for a floating-point index value of 2.4; this means that the point defined is 40 percent of the way along the line segment between points at indexes 2 and 3. Similarly, a floating-point index value of 6.8 defines a point that is 80 percent of the way along the line segment between points at indexes 6 and 7.

figure 6-3 partial index values allow for interpolation between points.

Figure 6-3. Partial index values allow for interpolation between points.

The Stroke class s FindIntersections method computes intersections between Stroke objects. This method returns an array of floating-point indexes that define the intersections between the Stroke object that is calling the method and the Stroke objects that are referenced by the method s Strokes collection parameter:

float[] FindIntersections(Strokes strokes)

This is the only function in the Tablet PC Platform in which the Stroke objects referenced by the Strokes collection must belong to the same Ink object as the Stroke object that s calling the method. An exception will be thrown otherwise. If null is passed for the strokes parameter, the method uses all ink strokes in the Ink object.

The GetRectangleIntersections method is a little more complex. This method computes the intersections between a rectangle and a Stroke object, but instead of returning an array of floating-point indexes it returns an array of StrokeIntersection structures. StrokeIntersection primarily encapsulates two properties, BeginIndex and EndIndex, that provide information about how the stroke intersects the rectangle.

The BeginIndex property is the floating-point index for where the stroke enters the rectangle, and the EndIndex property is the floating-point index for where the stroke leaves the rectangle. A special case occurs when BeginIndex equals -1.0, meaning the stroke begins inside the rectangle (there is no entry point), or if EndIndex equals -1.0, meaning that the stroke ends inside the rectangle. Figure 6-4 illustrates the various forms that the values of BeginIndex and EndIndex can take.

figure 6-4 given an ink stroke of n points, these are the possible values of strokeintersection s beginindex and endindex properties when returned from getrectangleintersections.

Figure 6-4. Given an ink stroke of n points, these are the possible values of StrokeIntersection s BeginIndex and EndIndex properties when returned from GetRectangleIntersections.

Code Snippet DrawIntersections

This function in the RendererEx class draws all three types of intersections we ve just covered stroke intersections, self-intersections, and rectangle intersections (using the bounding boxes of the other strokes). The custom enum StrokeIntersection Type can be provided to DrawIntersections to specify the type of intersection to render.

public enum StrokeIntersectionType { Self, Stroke, BoundingBox } // Draw the stroke intersections for the strokes in an Ink object public static void DrawIntersections(Graphics g, Ink ink) { DrawIntersections(g, ink.Strokes, new Renderer(), Pens.DarkRed, StrokeIntersectionType.Stroke); } // Draw the various intersections for the strokes in a Strokes // collection public static void DrawIntersections( Graphics g, Strokes strokes, Renderer renderer, Pen pen, StrokeIntersectionType type) { foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { if (type == StrokeIntersectionType.BoundingBox) { // Draw bounding box intersections DrawIntersections(g, s, renderer, pen); } else { // Compute either self or stroke intersections float[] fResults = null; switch (type) { case StrokeIntersectionType.Self: fResults = s.SelfIntersections; break; case StrokeIntersectionType.Stroke: fResults = s.FindIntersections(null); break; } // Draw the intersections (if any) if (fResults != null) { DrawIntersection( g, s, renderer, fResults, pen); } } } } } // Helper function to draw intersections between each stroke's // bounding rect and all other strokes private static void DrawIntersections(Graphics g, Stroke s, Renderer renderer, Pen pen) { Rectangle rcBounds = renderer.Measure(s); // Generate a strokes collection containing all strokes // except the one being used for its bounding rect Strokes strksToCheck = s.Ink.CreateStrokes(); strksToCheck.Add(s.Ink.Strokes); strksToCheck.Remove(s); foreach (Stroke s2 in strksToCheck) { if (!s2.Deleted) { StrokeIntersection[] siResults = s2.GetRectangleIntersections(rcBounds); ArrayList arrResults = new ArrayList(); foreach (StrokeIntersection si in siResults) { if (si.BeginIndex >= 0) { arrResults.Add(si.BeginIndex); } if (si.EndIndex > 0) { arrResults.Add(si.EndIndex); } } if (arrResults.Count > 0) { // Fill up a float array with the intersection // indicies float[] fResults = (float[])arrResults.ToArray(typeof(float)); // Use a handy helper to draw the intersections DrawIntersection(g, s2, renderer, fResults, pen); } } } } // Helper function to draw intersections along a stroke private static void DrawIntersection(Graphics g, Stroke s, Renderer renderer, float[] fIndicies, Pen pen) { foreach (float fIndex in fIndicies) { // Get the whole-number point of the intersection int nIndex = (int)fIndex; Point pt = s.GetPoint(nIndex); // Compute the fractional point of the intersection if // possible if (nIndex < s.GetPoints().Length - 1) { float fFraction = fIndex - nIndex; Point pt2 = s.GetPoint(nIndex+1); pt.X += (int)Math.Round((pt2.X - pt.X) * fFraction); pt.Y += (int)Math.Round((pt2.Y - pt.Y) * fFraction); } renderer.InkSpaceToPixel(g, ref pt); g.DrawEllipse(pen, pt.X-3, pt.Y-3, 7, 7); } }

For handling self-intersection and stroke intersection cases, the same code path is used because the SelfIntersections property and the GetStrokeIntersections method return an array of floating-point indexes. For rectangle intersection, the collective values of the StrokeIntersection properties BeginIndex and EndIndex are appended to an ArrayList, which is then converted to a floating-point array to leverage the drawing code used for self-intersections and stroke intersections.

Consider the code to compute the point given a floating-point index value:

// Get the whole-number point of the intersection int nIndex = (int)fIndex; Point pt = s.GetPoint(nIndex); // Compute the fractional point of the intersection if // possible if (nIndex < s.GetPoints().Length - 1) { float fFraction = fIndex - nIndex; Point pt2 = s.GetPoint(nIndex+1); pt.X += (int)Math.Round((pt2.X - pt.X) * fFraction); pt.Y += (int)Math.Round((pt2.Y - pt.Y) * fFraction); }

The index is separated into two parts: a whole number part and a fractional part. The whole number is used to obtain the two adjacent polyline points for the purpose of interpolation, and the fractional part is used to compute the final point along their corresponding line segment.

Retrieving and Setting the Packet Data of a Stroke

As we first learned in Chapter 4, there is often more to ink strokes than just (X,Y) data. Packet properties such as pressure and tilt angle can literally add an extra dimension of realism to digital ink. This section explains how to get and set packet property data on Stroke objects, which can be handy when custom rendering your own ink type, among other uses.

Every Stroke object has its own PacketDescription property, which is an array of PacketProperty Guid values the property acts as the template for the layout of packet data. PacketCount returns the number of packets making up the stroke, not coincidentally the same value as the length of the array returned by GetPoints.

NOTE
The PacketSize property returns the size in bytes of a packet, although this is actually unnecessary information because the length of the Packet Description array multiplied by the size in bytes of an individual packet value gives the same result.

Packet data is retrieved via the Stroke class s GetPacketData method, which takes on a few forms:

int[] Stroke.GetPacketData() int[] Stroke.GetPacketData(int index) int[] Stroke.GetPacketData(int index, int count)

If no parameters are given, GetPacketData will return a Stroke object s packet data in one big array of ints. Alternatively, if GetPacketData is supplied with an index, a single packet will be returned or, if both an index and a count are supplied, multiple packets are returned. It is your responsibility to parse the values and interpret the data as the appropriate packet property.

The allowable range and units of packet property values are obtained with the GetPacketDescriptionPropertyMetrics method. This function returns a TabletPropertyMetrics structure (which we saw in Chapter 4), given a packet property Guid value.

You ll find that working with packet data will often be on the basis of packet property type, making manually parsing through packets a bit of a pain. The GetPacketValuesByProperty and SetPacketValuesByProperty methods solve this problem:

int[] Stroke.GetPacketValuesByProperty(Guid id) int[] Stroke.GetPacketValuesByProperty(Guid id, int index) int[] Stroke.GetPacketValuesByProperty(Guid id, int index, int count) int Stroke.SetPacketValuesByProperty(Guid id, int[] packetValues) int Stroke.SetPacketValuesByProperty(Guid id, int index, int[] packetValues) int Stroke.SetPacketValuesByProperty(Guid id, int index, int count, int[] packetValues)

GetPacketValuesByProperty returns the values of a given packet property type in an ink stroke it can return all values, a single value, or a range of values, depending on the overloaded version used. Similarly, the SetPacketValuesByProperty method can set all values, a single value, or a range of values for a packet property type. It returns the number of values that were set as a result of calling the method.

Code Snippet PressureAdjust

The following snippet of code shows an example of how a Stroke object s pressure data can be adjusted using the packet data APIs of the Stroke class:

// First, check to see if the stroke has PacketProperty.NormalPressure // data in it bool fFound = false; for (int i = 0; i < stroke.PacketDescription.Length; i++) { if (stroke.PacketDescription[i] == PacketProperty.NormalPressure) { fFound = true; break; } } if (!fFound) { // ERROR: stroke does not contain any pressure information return; } // Get the range of allowable values for pressure TabletPropertyMetrics tpm = stroke.GetPacketDescriptionPropertyMetrics( PacketProperty.NormalPressure); // Figure out how much to increase the pressure by we'll arbitrarily // make it 25% of the allowable range int incAmount = (tpm.Maximum tpm.Minimum) / 4; // Get the current pressure values int[] values = stroke.GetPacketValuesByProperty(PacketProperty.NormalPressure); // Adjust all of the pressure values, clipping at the maximum value for (int i = 0; i < stroke.PacketCount; i++) { values[i] += incAmount; if (values[i] > tpm.Maximum) { values[i] = tpm.Maximum; } } // Set the changed values back on the ink stroke stroke.SetPacketValuesByProperty( PacketProperty.NormalPressure, values);

Retrieving the Cusps of a Stroke

A cusp in an ink stroke is defined as a point at which the direction of the ink changes in a discontinuous fashion. In other words, a cusp is a point along a stroke where the direction changes quickly for example, the elbow of a capital L or the point of a v. Cusps are useful for logically dividing a stroke into segments. They can aid in performing gesture recognition or partial stroke erasing.

The Tablet PC Platform can compute two kinds of cusps: polyline cusps and B zier cusps. The difference between the two is merely the set of points that are analyzed polyline cusps are computed from polyline points, and B zier cusps are computed from B zier points. Note that for both kinds of cusps, illustrated in Figure 6-5, the first and last point of a stroke are considered to be cusps because the direction of ink is changed in a discontinuous fashion (it starts or stops).

figure 6-5 the two kinds of cusps b zier and polyline.

Figure 6-5. The two kinds of cusps B zier and polyline.

Both the Stroke class s BezierCusps and PolylineCusps properties return an array of integers that identify the point indexes at which a cusp was determined.

Code Snippet DrawCusps

This code snippet will render the location of either the polyline or B zier cusps for an ink stroke:

public enum StrokeCuspType { Bezier, Polyline } // Draw the polyline cusps for the strokes in an Ink object public static void DrawCusps(Graphics g, Ink ink) { DrawCusps(g, ink.Strokes, new Renderer(), Pens.Blue, StrokeCuspType.Polyline); } // Draw the various cusps for the strokes in a strokes // collection public static void DrawCusps(Graphics g, Strokes strokes, Renderer renderer, Pen pen, StrokeCuspType type) { foreach (Stroke s in strokes) { // Make sure each stroke has not been deleted if (!s.Deleted) { DrawCusps(g, s, renderer, pen, type); } } } // Draw the various cusps for a stroke public static void DrawCusps(Graphics g, Stroke stroke, Renderer renderer, Pen pen, StrokeCuspType type) { // Get the cusp indexes and point array according to the // type desired int [] cusps = null; Point [] pts = null; switch (type) { case StrokeCuspType.Bezier: cusps = stroke.BezierCusps; pts = stroke.BezierPoints; break; case StrokeCuspType.Polyline: cusps = stroke.PolylineCusps; pts = stroke.GetPoints(); break; } // Render the points if they were retrieved if (cusps != null && pts != null) { foreach (int n in cusps) { Point pt = pts[n]; renderer.InkSpaceToPixel(g, ref pt); g.DrawEllipse(pen, pt.X-3, pt.Y-3, 7, 7); } } }

Probably the most important information to note here is that the results obtained from the PolylineCusps property apply to the Stroke object s polyline points, just as the results from the BezierCusps property apply to the B zier points.

Putting It Together the StrokeDataViewer Example

To better understand the stroke geometry data we ve been discussing, this next sample application shows the data in visual form. The BuildingTabletApps helper library contains a class, RendererEx, whose methods can render the various stroke data and is therefore heavily leveraged by the sample. The code snippets that have been presented here were taken from the RendererEx class.

Sample Application StrokeDataViewer

Loosely based on the StrokeIdViewer sample application from the last chapter, this sample, shown in Figure 6-6, can show all sorts of information about ink strokes stroke ID, bounding box, points, intersections, and cusps. The main form s menu lets the user choose the data to view.

figure 6-6 the strokedataviewer sample application displays all the stroke data you can handle.

Figure 6-6. The StrokeDataViewer sample application displays all the stroke data you can handle.

StrokeDataViewer.cs

//////////////////////////////////////////////////////////////////// // // StrokeDataViewer.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program displays lots of different information for // Stroke objects. An InkOverlaySharp control is created and the // user can choose various data attributes from the app's menu // to be drawn, such as points, bounding box, cusps, and // intersections. The data is kept up to date as strokes are added, // removed, and modified. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl2 inkCtl; private MenuItem miStrokeId; private MenuItem miNoPoints; private MenuItem miPolylinePoints; private MenuItem miBezierPoints; private MenuItem miFlattenedBezierPoints; private MenuItem miNoCusps; private MenuItem miBezierCusps; private MenuItem miPolylineCusps; private MenuItem miNoBoundingBox; private MenuItem miDefaultBoundingBox; private MenuItem miCurveFitBoundingBox; private MenuItem miNoCurveFitBoundingBox; private MenuItem miPointsOnlyBoundingBox; private MenuItem miUnionBoundingBox; private MenuItem miNoIntersections; private MenuItem miSelfIntersections; private MenuItem miStrokeIntersections; private MenuItem miBoundingBoxIntersections; private bool fDrawStrokeId = false; private bool fDrawPoints = false; private RendererEx.StrokePointType strokePointType = RendererEx.StrokePointType.Polyline; private bool fDrawCusps = false; private RendererEx.StrokeCuspType strokeCuspType = RendererEx.StrokeCuspType.Polyline; private bool fDrawBoundingBox = false; private BoundingBoxMode bboxMode = BoundingBoxMode.Default; private bool fDrawIntersections = false; private RendererEx.StrokeIntersectionType strokeIntersectionType = RendererEx.StrokeIntersectionType.Stroke; // 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 miView = new MenuItem("&View"); miView.Popup += new EventHandler(miView_Popup); Menu.MenuItems.Add(miView); miStrokeId = new MenuItem("Stroke Id"); miStrokeId.Click += new EventHandler(miStrokeId_Click); Menu.MenuItems[1].MenuItems.Add(miStrokeId); Menu.MenuItems[1].MenuItems.Add(new MenuItem("-")); MenuItem miPoints = new MenuItem("Points"); miPoints.Popup += new EventHandler(miPoints_Popup); Menu.MenuItems[1].MenuItems.Add(miPoints); miNoPoints = new MenuItem("None"); miNoPoints.Click += new EventHandler(miNoPoints_Click); Menu.MenuItems[1].MenuItems[2].MenuItems.Add(miNoPoints); Menu.MenuItems[1].MenuItems[2].MenuItems.Add(new MenuItem("-")); miPolylinePoints = new MenuItem("Polyline"); miPolylinePoints.Click += new EventHandler(miPolylinePoints_Click); Menu.MenuItems[1].MenuItems[2].MenuItems.Add(miPolylinePoints); miBezierPoints = new MenuItem("Bezier"); miBezierPoints.Click += new EventHandler(miBezierPoints_Click); Menu.MenuItems[1].MenuItems[2].MenuItems.Add(miBezierPoints); miFlattenedBezierPoints = new MenuItem("Flattened Bezier"); miFlattenedBezierPoints.Click += new EventHandler(miFlattenedBezierPoints_Click); Menu.MenuItems[1].MenuItems[2].MenuItems.Add( miFlattenedBezierPoints); MenuItem miCusps = new MenuItem("Cusps"); miCusps.Popup += new EventHandler(miCusps_Popup); Menu.MenuItems[1].MenuItems.Add(miCusps); miNoCusps = new MenuItem("None"); miNoCusps.Click += new EventHandler(miNoCusps_Click); Menu.MenuItems[1].MenuItems[3].MenuItems.Add(miNoCusps); Menu.MenuItems[1].MenuItems[3].MenuItems.Add(new MenuItem("-")); miPolylineCusps = new MenuItem("Polyline"); miPolylineCusps.Click += new EventHandler(miPolylineCusps_Click); Menu.MenuItems[1].MenuItems[3].MenuItems.Add(miPolylineCusps); miBezierCusps = new MenuItem("Bezier"); miBezierCusps.Click += new EventHandler(miBezierCusps_Click); Menu.MenuItems[1].MenuItems[3].MenuItems.Add(miBezierCusps); MenuItem miBoundingBox = new MenuItem("Bounding Box"); miBoundingBox.Popup += new EventHandler(miBoundingBox_Popup); Menu.MenuItems[1].MenuItems.Add(miBoundingBox); miNoBoundingBox = new MenuItem("None"); miNoBoundingBox.Click += new EventHandler(miNoBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add(miNoBoundingBox); Menu.MenuItems[1].MenuItems[4].MenuItems.Add(new MenuItem("-")); miDefaultBoundingBox = new MenuItem("Default"); miDefaultBoundingBox.Click += new EventHandler(miDefaultBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add( miDefaultBoundingBox); miCurveFitBoundingBox = new MenuItem("CurveFit"); miCurveFitBoundingBox.Click += new EventHandler(miCurveFitBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add( miCurveFitBoundingBox); miNoCurveFitBoundingBox = new MenuItem("NoCurveFit"); miNoCurveFitBoundingBox.Click += new EventHandler(miNoCurveFitBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add( miNoCurveFitBoundingBox); miPointsOnlyBoundingBox = new MenuItem("PointsOnly"); miPointsOnlyBoundingBox.Click += new EventHandler(miPointsOnlyBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add( miPointsOnlyBoundingBox); miUnionBoundingBox = new MenuItem("Union"); miUnionBoundingBox.Click += new EventHandler(miUnionBoundingBox_Click); Menu.MenuItems[1].MenuItems[4].MenuItems.Add( miUnionBoundingBox); MenuItem miIntersections = new MenuItem("Intersections"); miIntersections.Popup += new EventHandler(miIntersections_Popup); Menu.MenuItems[1].MenuItems.Add(miIntersections); miNoIntersections = new MenuItem("None"); miNoIntersections.Click += new EventHandler(miNoIntersections_Click); Menu.MenuItems[1].MenuItems[5].MenuItems.Add( miNoIntersections); Menu.MenuItems[1].MenuItems[5].MenuItems.Add(new MenuItem("-")); miSelfIntersections = new MenuItem("Self"); miSelfIntersections.Click += new EventHandler(miSelfIntersections_Click); Menu.MenuItems[1].MenuItems[5].MenuItems.Add( miSelfIntersections); miStrokeIntersections = new MenuItem("Stroke"); miStrokeIntersections.Click += new EventHandler(miStrokeIntersections_Click); Menu.MenuItems[1].MenuItems[5].MenuItems.Add( miStrokeIntersections); miBoundingBoxIntersections = new MenuItem("BoundingBox"); miBoundingBoxIntersections.Click += new EventHandler(miBoundingBoxIntersections_Click); Menu.MenuItems[1].MenuItems[5].MenuItems.Add( miBoundingBoxIntersections); // Create and place all of our controls inkCtl = new InkControl2(); 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 = "StrokeDataViewer"; 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); // Set the ink color to something other than black so it's // easier to see the data inkCtl.InkOverlay.DefaultDrawingAttributes.Color = Color.DarkGray; // 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 "View" submenu popping up private void miView_Popup(object sender, EventArgs e) { miStrokeId.Checked = fDrawStrokeId; } // Handle the "Stroke Id" menu being clicked private void miStrokeId_Click(object sender, EventArgs e) { fDrawStrokeId = !fDrawStrokeId; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Points" submenu popping up private void miPoints_Popup(object sender, EventArgs e) { miNoPoints.Checked = !fDrawPoints; miPolylinePoints.Checked = fDrawPoints && strokePointType == RendererEx.StrokePointType.Polyline; miBezierPoints.Checked = fDrawPoints && strokePointType == RendererEx.StrokePointType.Bezier; miFlattenedBezierPoints.Checked = fDrawPoints && strokePointType == RendererEx.StrokePointType.FlattenedBezier; } // Handle the "NoPoints" menu item being clicked private void miNoPoints_Click(object sender, EventArgs e) { fDrawPoints = false; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Polyline" menu item being clicked private void miPolylinePoints_Click(object sender, EventArgs e) { fDrawPoints = true; strokePointType = RendererEx.StrokePointType.Polyline; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Bezier" menu item being clicked private void miBezierPoints_Click(object sender, EventArgs e) { fDrawPoints = true; strokePointType = RendererEx.StrokePointType.Bezier; inkCtl.InkInputPanel.Invalidate(); } // Handle the "FlattenedBezier" menu item being clicked private void miFlattenedBezierPoints_Click( object sender, EventArgs e) { fDrawPoints = true; strokePointType = RendererEx.StrokePointType.FlattenedBezier; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Cusps" submenu popping up private void miCusps_Popup(object sender, EventArgs e) { miNoCusps.Checked = !fDrawCusps; miPolylineCusps.Checked = fDrawCusps && strokeCuspType == RendererEx.StrokeCuspType.Polyline; miBezierCusps.Checked = fDrawCusps && strokeCuspType == RendererEx.StrokeCuspType.Bezier; } // Handle the "None" menu item being clicked private void miNoCusps_Click(object sender, EventArgs e) { fDrawCusps = false; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Polyline" menu item being clicked private void miPolylineCusps_Click(object sender, EventArgs e) { fDrawCusps = true; strokeCuspType = RendererEx.StrokeCuspType.Polyline; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Bezier" menu item being clicked private void miBezierCusps_Click(object sender, EventArgs e) { fDrawCusps = true; strokeCuspType = RendererEx.StrokeCuspType.Bezier; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Bounding Box" submenu popping up private void miBoundingBox_Popup(object sender, EventArgs e) { miNoBoundingBox.Checked = !fDrawBoundingBox; miDefaultBoundingBox.Checked = fDrawBoundingBox && bboxMode == BoundingBoxMode.Default; miCurveFitBoundingBox.Checked = fDrawBoundingBox && bboxMode == BoundingBoxMode.CurveFit; miNoCurveFitBoundingBox.Checked = fDrawBoundingBox && bboxMode == BoundingBoxMode.NoCurveFit; miPointsOnlyBoundingBox.Checked = fDrawBoundingBox && bboxMode == BoundingBoxMode.PointsOnly; miUnionBoundingBox.Checked = fDrawBoundingBox && bboxMode == BoundingBoxMode.Union; } // Handle the "None" menu item being clicked private void miNoBoundingBox_Click(object sender, EventArgs e) { fDrawBoundingBox = false; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Default" menu item being clicked private void miDefaultBoundingBox_Click(object sender, EventArgs e) { fDrawBoundingBox = true; bboxMode = BoundingBoxMode.Default; inkCtl.InkInputPanel.Invalidate(); } // Handle the "CurveFit" menu item being clicked private void miCurveFitBoundingBox_Click(object sender, EventArgs e) { fDrawBoundingBox = true; bboxMode = BoundingBoxMode.CurveFit; inkCtl.InkInputPanel.Invalidate(); } // Handle the "NoCurveFit" menu item being clicked private void miNoCurveFitBoundingBox_Click( object sender, EventArgs e) { fDrawBoundingBox = true; bboxMode = BoundingBoxMode.NoCurveFit; inkCtl.InkInputPanel.Invalidate(); } // Handle the "PointsOnly" menu item being clicked private void miPointsOnlyBoundingBox_Click( object sender, EventArgs e) { fDrawBoundingBox = true; bboxMode = BoundingBoxMode.PointsOnly; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Union" menu item being clicked private void miUnionBoundingBox_Click(object sender, EventArgs e) { fDrawBoundingBox = true; bboxMode = BoundingBoxMode.Union; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Intersections" submenu popping up private void miIntersections_Popup(object sender, EventArgs e) { miNoIntersections.Checked = !fDrawIntersections; miSelfIntersections.Checked = fDrawIntersections && strokeIntersectionType == RendererEx.StrokeIntersectionType.Self; miStrokeIntersections.Checked = fDrawIntersections && strokeIntersectionType == RendererEx.StrokeIntersectionType.Stroke; miBoundingBoxIntersections.Checked = fDrawIntersections && strokeIntersectionType == RendererEx.StrokeIntersectionType.BoundingBox; } // Handle the "None" menu item being clicked private void miNoIntersections_Click(object sender, EventArgs e) { fDrawIntersections = false; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Self" menu item being clicked private void miSelfIntersections_Click(object sender, EventArgs e) { fDrawIntersections = true; strokeIntersectionType = RendererEx.StrokeIntersectionType.Self; inkCtl.InkInputPanel.Invalidate(); } // Handle the "Stroke" menu item being clicked private void miStrokeIntersections_Click(object sender, EventArgs e) { fDrawIntersections = true; strokeIntersectionType = RendererEx.StrokeIntersectionType.Stroke; inkCtl.InkInputPanel.Invalidate(); } // Handle the "BoundingBox" menu item being clicked private void miBoundingBoxIntersections_Click( object sender, EventArgs e) { fDrawIntersections = true; strokeIntersectionType = RendererEx.StrokeIntersectionType.BoundingBox; inkCtl.InkInputPanel.Invalidate(); } // Handle the InkOverlay having been painted private void inkCtl_Painted(object sender, PaintEventArgs e) { // Draw stroke IDs if needed if (fDrawStrokeId) { RendererEx.DrawStrokeIds( e.Graphics, Font, inkCtl.InkOverlay.Ink); } // Draw the stroke points if needed if (fDrawPoints) { RendererEx.DrawPoints(e.Graphics, inkCtl.InkOverlay.Ink.Strokes, inkCtl.InkOverlay.Renderer, Brushes.Red, strokePointType); } // Draw the stroke cusps if needed if (fDrawCusps) { RendererEx.DrawCusps(e.Graphics, inkCtl.InkOverlay.Renderer, inkCtl.InkOverlay.Ink.Strokes, Pens.Blue, strokeCuspType); } // Draw the stroke bounding box if needed if (fDrawBoundingBox) { RendererEx.DrawBoundingBoxes(e.Graphics, inkCtl.InkOverlay.Renderer, inkCtl.InkOverlay.Ink.Strokes, Pens.LightGreen, bboxMode); } // Draw the stroke intersections if needed if (fDrawIntersections) { RendererEx.DrawIntersections(e.Graphics, inkCtl.InkOverlay.Renderer, inkCtl.InkOverlay.Ink.Strokes, Pens.DarkRed, strokeIntersectionType); } } // Handle ink having been added private void inkCtl_InkAdded(object sender, StrokesEventArgs e) { inkCtl.InkInputPanel.Invalidate(); } // Handle ink having been deleted private void inkCtl_InkDeleted(object sender, StrokesEventArgs e) { inkCtl.InkInputPanel.Invalidate(); } }

Wow, that s quite the long listing! Fortunately, most of the code in this sample deals with all the pop-up menu items used to choose from among the different options. The key piece of code we re most interested in is the inkCtl_Painted event handler:

// Handle the InkOverlay having been painted private void inkCtl_Painted(object sender, PaintEventArgs e) { // Draw stroke IDs if needed if (fDrawStrokeId) { RendererEx.DrawStrokeIds( e.Graphics, Font, inkCtl.InkOverlay.Ink); } // Draw the stroke points if needed if (fDrawPoints) { RendererEx.DrawPoints(e.Graphics, inkCtl.InkOverlay.Renderer, inkCtl.InkOverlay.Ink.Strokes, Brushes.Red, strokePointType); } // Draw the stroke cusps if needed if (fDrawCusps) { RendererEx.DrawCusps(inkCtl.InkOverlay.Renderer. e.Graphics, inkCtl.InkOverlay.Ink.Strokes, Pens.Blue, strokeCuspType); } // Draw the stroke bounding box if needed if (fDrawBoundingBox) { RendererEx.DrawBoundingBoxes(inkCtl.InkOverlay.Renderer, e.Graphics, inkCtl.InkOverlay.Ink.Strokes, Pens.LightGreen, bboxMode); } // Draw the stroke intersections if needed if (fDrawIntersections) { RendererEx.DrawIntersections(inkCtl.InkOverlay.Renderer, e.Graphics, inkCtl.InkOverlay.Ink.Strokes, Pens.DarkRed, strokeIntersectionType); } }

Depending on the stroke data chosen to be viewed, different methods in the StrokeDataViewer example are called to display data for every stroke in inkCtl s Ink object. An improvement to this sample that you may find useful is to display the stroke data only for the current selection in inkCtl s InkOverlay. All that is required to achieve this is to replace inkCtl.InkOverlay.Ink.Strokes with inkCtl.InkOverlay.Selection.

Transforming Strokes

In the last chapter, we saw how the Renderer class supports view and object transforms that help with functions such as scrolling and zooming. The transforms that a Renderer uses don t modify stroke data, and they are applied to all Stroke objects contained in an Ink object. Sometimes, though, we ll want to apply transforms on a per-stroke basis and retain the result for example, to accomplish resizing or moving ink strokes. It s perfectly valid to modify the actual (X,Y) points of a Stroke object to apply a transformation, but the Tablet PC Platform provides a much more elegant mechanism: each ink stroke owns a transformation matrix.

A Stroke object s transformation matrix is automatically applied to any ink coordinates and measurements going into or coming out of its properties and methods. Both the Strokes and Stroke classes supply the following methods to modify the transformation matrix:

void Move(float offsetX, float offsetY) void Rotate(float degrees, Point ptAbout) void Scale(float scaleX, float scaleY) void ScaleToRectangle(Rectangle rectangle) void Shear(float shearX, float shearY) void Transform(Matrix m) void Transform(Matrix m, bool applyOnPenWidth)

You ll probably notice that these are all similar to the methods found in the Renderer class, and they accomplish pretty much the same thing when it comes to modifying the transformation matrix of a Stroke object. With the exception of the Transform method, they behave in an additive manner, meaning that any changes to the transformation are performed on the current transformation matrix.

The Transform method allows for the direct setting of the desired transformation matrix, optionally applying the matrix s changes to the stroke s width. This is akin to the reason the Renderer class has separate view and object transforms sometimes you might want transformation applied to ink width, but sometimes not. Presumably to save memory, the Stroke class contains only one transform and uses a flag to indicate the transform type.

It should be made clear that when altering the transformation matrix of an ink stroke, its (X,Y) point data (or any other packet data, for that matter) will remain unchanged. Only the matrix values are ever altered, thereby avoiding any loss of precision in the Stroke object s point data. Resetting the transformation matrix to the identity matrix restores the ink stroke to its original created size and orientation.

Sample Application StrokeWarper

This next sample application provides the user with the ability to rotate, resize, translate, and shear ink strokes. You first select some ink to perform a transformation on and then choose the desired transformation function from the application s main menu.

StrokeWarper.cs

//////////////////////////////////////////////////////////////////// // // StrokeWarper.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates how to apply various transformations // on ink strokes. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl2 inkCtl; private MenuItem miTranslate; private MenuItem miScale; private MenuItem miRotate; private MenuItem miShear; // 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 miTransform = new MenuItem("&Transform"); miTransform.Popup += new EventHandler(miTransform_Popup); Menu.MenuItems.Add(miTransform); miTranslate = new MenuItem("&Translate"); miTranslate.Click += new EventHandler(miTranslate_Click); Menu.MenuItems[1].MenuItems.Add(miTranslate); miScale = new MenuItem("&Scale"); miScale.Click += new EventHandler(miScale_Click); Menu.MenuItems[1].MenuItems.Add(miScale); miRotate = new MenuItem("&Rotate"); miRotate.Click += new EventHandler(miRotate_Click); Menu.MenuItems[1].MenuItems.Add(miRotate); miShear = new MenuItem("S&hear"); miShear.Click += new EventHandler(miShear_Click); Menu.MenuItems[1].MenuItems.Add(miShear); // Create and place all of our controls inkCtl = new InkControl2(); 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 = "StrokeWarper"; 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 "Transform" submenu popping up private void miTransform_Popup(object sender, EventArgs e) { // Only enable the Translate, Scale, Rotate, and Shear menu // items if there is a current selection bool fSelection = (inkCtl.InkOverlay.EditingMode == InkOverlayEditingMode.Select) && (inkCtl.InkOverlay.Selection.Count > 0); miTranslate.Enabled = fSelection; miScale.Enabled = fSelection; miRotate.Enabled = fSelection; miShear.Enabled = fSelection; } // Handle the "Translate" menu item being clicked private void miTranslate_Click(object sender, EventArgs e) { // Move the current selection 200 ink units in the X and Y // direction Strokes strokes = inkCtl.InkOverlay.Selection; strokes.Move(200, 200); // "Refresh" the selection bounds by first clearing it then // resetting it inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); inkCtl.InkOverlay.Selection = strokes; // Draw the transformed ink inkCtl.InkInputPanel.Invalidate(); } // Handle the "Scale" menu item being clicked private void miScale_Click(object sender, EventArgs e) { // Scale the current selection 115% along the X axis and 90% // along the Y axis. We'll preserve its location by first // translating the selection to (0,0), performing the scale, // and then translating back to its original location. Strokes strokes = inkCtl.InkOverlay.Selection; Rectangle rcBounds = strokes.GetBoundingBox(); strokes.Move(-rcBounds.X, -rcBounds.Y); strokes.Scale(1.15f, 0.9f); strokes.Move(rcBounds.X, rcBounds.Y); // "Refresh" the selection bounds by first clearing it then // resetting it inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); inkCtl.InkOverlay.Selection = strokes; // Draw the transformed ink inkCtl.InkInputPanel.Invalidate(); } // Handle the "Rotate" menu item being clicked private void miRotate_Click(object sender, EventArgs e) { // Rotate the current selection 10 degrees about its center // point Strokes strokes = inkCtl.InkOverlay.Selection; Rectangle rcBounds = inkCtl.InkOverlay.Selection.GetBoundingBox(); Point ptCenter = new Point(rcBounds.X + rcBounds.Width/2, rcBounds.Y + rcBounds.Height/2); strokes.Rotate(10, ptCenter); // "Refresh" the selection bounds by first clearing it then // resetting it inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); inkCtl.InkOverlay.Selection = strokes; // Draw the transformed ink inkCtl.InkInputPanel.Invalidate(); } // Handle the "Shear" menu item being clicked private void miShear_Click(object sender, EventArgs e) { // Shear the current selection 1.1 along the Y axis. We'll // preserve its location by first translating the selection to // (0,0), performing the scale, and then translating back to // its original location. Strokes strokes = inkCtl.InkOverlay.Selection; Rectangle rcBounds = strokes.GetBoundingBox(); strokes.Move(-rcBounds.X, -rcBounds.Y); strokes.Shear(0f, 1.1f); strokes.Move(rcBounds.X, rcBounds.Y); // "Refresh" the selection bounds by first clearing it then // resetting it inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); inkCtl.InkOverlay.Selection = strokes; // Draw the transformed ink inkCtl.InkInputPanel.Invalidate(); } }

The handlers for the menu items that perform the transformation matrix alternation all have a similar structure: they apply the transform adjustment and then reset the current selection.

// "Refresh" the selection bounds by first clearing it then // resetting it inkCtl.InkOverlay.Selection = inkCtl.InkOverlay.Ink.CreateStrokes(); inkCtl.InkOverlay.Selection = strokes;

You may be wondering why the current selection gets reset. It s because the InkOverlay doesn t know when the bounds of the Stroke objects referenced by its selection Strokes collection have changed. If we didn t reset the selection, the resize handles and selection bounds would be unaffected by any transformation. This might cause ink to flow outside the selection bounds edge or the selection bounds to be too large. By clearing and restoring the selection, we cause the InkOverlay to recompute the proper positioning and dimensions of the UI.



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