Rendering Digital Ink

Rendering Digital Ink

If you take a look through the full list of properties and methods for the Ink and Stroke classes, you ll notice that any functions for drawing ink strokes are conspicuously missing. The Tablet PC Platform in fact provides a lot of functionality to do this so much so that it is encapsulated into its own class, called Renderer.

Renderer Class

The primary purpose of the Renderer class is to draw ink into a viewport and maintain a transformation on the ink space. A viewport can be thought of as an opening to a drawing surface such as the screen, a printer, or a bitmap. The transformation on the ink space is used to achieve zooming, sizing, scrolling, and rotation effects without having to modify the (x,y) data of the Stroke objects themselves.

An instance of the Renderer class is able to draw individual Stroke objects or the Stroke objects referenced by a Strokes collection. In addition, ink formatting style attributes such as color and width can be nondestructively overridden; that is, the ink data won t be modified as a result of drawing it in an arbitrary style.

In its simplest use, a Renderer object will draw ink to the screen using a 1:1 mapping from HIMETRIC units to pixels. More complex use may mean multiple Renderer objects are maintained, with each drawing to a separate view in the application and perhaps providing robust zooming and scrolling behaviors.

Figure 5-4 illustrates the role that a Renderer object performs to draw ink and transform ink coordinates. To draw a Stroke object to the viewport, a Renderer object obtains the ink coordinates from the Stroke object, transforms them, converts them into pixels, and finally renders that data on the viewport. Note that the use of pixels here doesn t necessarily correspond to pixels on your monitor they are whatever unit the Graphics object or graphics device context (HDC) being drawn to is set to use. Whenever an application interacts with a Renderer object, ink coordinates are always specified and returned in their transformed version.

figure 5-4 how a renderer object draws ink strokes to a viewport.

Figure 5-4. How a Renderer object draws ink strokes to a viewport.

Creating and Maintaining Renderer Objects

The InkCollector and InkOverlay classes each reference a Renderer object, much like they do an Ink object. When an InkCollector or InkOverlay is instantiated, so is a Renderer, accessible via the Renderer property. This Renderer object is used for the drawing of Stroke objects on the window that the InkCollector or InkOverlay is attached to. It can also be used to draw to any Graphics object or device context.

Renderer objects can be created with the C# new operator, and they can be used either temporarily or kept around indefinitely. The lifetime or use of Renderer objects aren t tied to Ink or to any other type, and they can be used to draw ink from different Ink objects.

Drawing Ink

The Renderer class supports drawing ink to either a Graphics object or a device context with the Draw method. It comes in a variety of overloads, which are listed here:

void Draw(Graphics g, Stroke s) void Draw(Graphics g, Stroke s, DrawingAttributes da) void Draw(Graphics g, Strokes strokes) void Draw(IntPtr hdc, Stroke s) void Draw(IntPtr hdc, Stroke s, DrawingAttributes da) void Draw(IntPtr hdc, Strokes strokes)

There are really three main flavors of the method: drawing a single stroke, drawing a single stroke while overriding its formatting, and drawing multiple strokes. Versions for each case can draw to either a Graphics object or a Windows GDI device context (HDC).

The location on the Graphics device or HDC where the Stroke objects are drawn is not explicitly supplied in the function because Stroke objects have their position as part of their data. The HIMETRIC ink coordinates of the Stroke objects (x,y) values are converted into pixels by using the Graphics object s or HDC s dpi setting by the Renderer object.

Sample Application InkLayers

The InkLayers sample application shows an implementation of virtual layers of ink it s kind of like having a stack of transparencies that ink can be drawn on, and each transparency can be shown or hidden. The application uses a single Ink object to hold all the ink, and one Strokes collection is maintained for each layer. The input panel s paint handler selectively draws the Stroke objects for each layer that is visible, using the InkOverlay s instance of a Renderer.

InkLayers.cs

//////////////////////////////////////////////////////////////////// // // InkLayers.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates usage of the Strokes and Renderer // classes by showing how layers of ink can be implemented. // //////////////////////////////////////////////////////////////////// using System; using System.Collections; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { // Data structure for an ink layer private class Layer { public Strokes Strokes; public bool bVisible; } // Total number of layers we'll allow private const int nNumLayers = 3; // Layer management private ArrayList arrLayers; private int nCurrLayer; // User interface private InkInputPanel pnlInput; private ComboBox cbxLayer; private CheckBox cbVisible; private InkOverlay inkOverlay; // 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 pnlInput = new InkInputPanel(); pnlInput.BackColor = Color.White; pnlInput.BorderStyle = BorderStyle.Fixed3D; pnlInput.Location = new Point(8, 8); pnlInput.Size = new Size(352, 192); cbxLayer = new ComboBox(); cbxLayer.DropDownStyle = ComboBoxStyle.DropDownList; cbxLayer.Location = new Point(8, 204); cbxLayer.Size = new Size(72, 24); cbxLayer.SelectedIndexChanged += new EventHandler(cbxLayer_SelIndexChg); cbVisible = new CheckBox(); cbVisible.Location = new Point(88, 204); cbVisible.Size = new Size(80, 20); cbVisible.Text = "Visible"; cbVisible.Click += new EventHandler(cbVisible_Click); // Configure the form itself ClientSize = new Size(368, 236); Controls.AddRange(new Control[] { pnlInput, cbxLayer, cbVisible}); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "InkLayers"; ResumeLayout(false); // Fill up the layers combobox for (int i = 0; i < nNumLayers; i++) { cbxLayer.Items.Add("Layer " + (i+1).ToString()); } // Create a new InkOverlay, using pnlInput for the collection // area inkOverlay = new InkOverlay(pnlInput.Handle); // Turn off auto-redraw since we'll be drawing the ink in our // paint handler inkOverlay.AutoRedraw = false; // Add the paint handler so we can conditionally draw ink layers pnlInput.Paint += new PaintEventHandler(pnlInput_Paint); // Create all of the Layers objects arrLayers = new ArrayList(); for (int i = 0; i < nNumLayers; i++) { Layer layer = new Layer(); layer.Strokes = inkOverlay.Ink.CreateStrokes(); layer.bVisible = true; arrLayers.Add(layer); } // Select the current layer in the combobox nCurrLayer = 0; cbxLayer.SelectedIndex = nCurrLayer; // Listen for new strokes so they can be added to the current // layer inkOverlay.Stroke += new InkCollectorStrokeEventHandler(inkOverlay_Stroke); // We're now set to go, so turn on tablet input inkOverlay.Enabled = true; } // Handle the selection changed of the layers combobox private void cbxLayer_SelIndexChg(object sender, EventArgs e) { // Set the selection strokes collection to be all strokes nCurrLayer = cbxLayer.SelectedIndex; Layer layer = arrLayers[nCurrLayer] as Layer; cbVisible.Checked = layer.bVisible; } // Handle the click of the visible checkbox private void cbVisible_Click(object sender, EventArgs e) { Layer layer = arrLayers[nCurrLayer] as Layer; layer.bVisible = cbVisible.Checked; pnlInput.Invalidate(); } // Handle the input panel's painting private void pnlInput_Paint(object sender, PaintEventArgs e) { // Paint all the visible layers for (int i = 0; i < nNumLayers; i++) { Layer layer = arrLayers[i] as Layer; if (layer.bVisible) { inkOverlay.Renderer.Draw(e.Graphics, layer.Strokes); } } } // Handle the new stroke of the InkOverlay private void inkOverlay_Stroke(object sender, InkCollectorStrokeEventArgs e) { // Add the new stroke to the current layer Layer layer = arrLayers[nCurrLayer] as Layer; layer.Strokes.Add(e.Stroke); // Show the layer if it's currently invisible if (!layer.bVisible) { layer.bVisible = true; cbVisible.Checked = true; pnlInput.Invalidate(); } } }

The sample defines a simple data type for a layer of ink a class that owns a Strokes collection along with a flag indicating the visible state of the layer. During initialization, the application creates an InkOverlay and a simple UI to select a layer and turn it on and off using a ComboBox and CheckBox, respectively. An array of Layer objects is also created, used to track the data and state of the layers.

Because the application itself will be painting the Stroke objects contained by the InkOverlay s Ink object, the AutoRedraw property of the InkOverlay is set to false. A paint event handler for the pnlInput window is then installed that performs the custom painting operation.

When a Stroke object is created, it s added to the currently active Layer object s Strokes collection. The ComboBox containing the list of layers is used to determine which Layer object is current. As the visible CheckBox is checked and unchecked, the current Layer object s visible state is toggled, and the input panel is invalidated to cause a repaint.

The painting operation that performs the selective rendering is implemented like so:

// Paint all the visible layers for (int i = 0; i < nNumLayers; i++) { Layer layer = arrLayers[i] as Layer; if (layer.bVisible) { inkOverlay.Renderer.Draw(e.Graphics, layer.Strokes); } }

Each Layer object is iterated over, and if its visibility state indicates the Layer object is visible, the Layer s Strokes collection is rendered in its entirety to the Graphics object passed into the Paint handler. Layer objects that are not visible are skipped, and hence no ink will be rendered for them.

Converting Between Ink Space and Pixels

Often you ll find the need to convert from ink space to pixels, or vice versa. This need particularly comes into play when writing code that allows interaction with ink, as we ll see in the upcoming sample application.

The Renderer class provides two methods, InkSpaceToPixel and PixelToInkSpace, that allow for the conversion to occur. Each has four variants, enabling the conversion of a single point or an array of points, using either a Graphics object or an HDC to obtain the pixel dpi.

// The various overloads of InkSpaceToPixel void InkSpaceToPixel(Graphics g, ref Point pt) void InkSpaceToPixel(Graphics g, ref Point[] pts) void InkSpaceToPixel(IntPtr hdc, ref Point pt) void InkSpaceToPixel(IntPtr hdc, ref Point[] pts) // The various overloads of PixelToInkSpace void PixelToInkSpace(Graphics g, ref Point pt) void PixelToInkSpace(Graphics g, ref Point[] pts) void PixelToInkSpace(IntPtr hdc, ref Point pt) void PixelToInkSpace(IntPtr hdc, ref Point[] pts)

Additionally, the BuildingTabletApps helper library (included on the Book s CD) contains versions of InkSpaceToPixel and PixelToInkSpace that operate on a Rect structure, found in the RendererEX class.

Renderer Transforms Scrolling and Zooming

You ve now seen how the Renderer class can draw Stroke objects. Earlier it was mentioned how a renderer can maintain a transformation on ink coordinates; this ability is very useful to facilitate functionality such as zooming, resizing, and scrolling ink.

The Renderer class actually maintains two transformations, known as the view transformation and object transformation. They are two-dimensional transformation matrices, and they can be stored and retrieved using the Matrix class. The difference between the two transformations is quite subtle and has to do with how stroke thickness is treated the view transformation will scale stroke thickness, whereas the object transformation will not. The two transformations otherwise can serve the same purpose. The object transformation is applied before the view transformation when transforming stroke point data.

Common use of a Renderer s transformation capability will be with the view transformation; this is because when scaling is applied, the typically desired result is for ink strokes to appear to move closer or farther away their thickness changes proportionately with their size. If scaling is applied via the object transformation, however, ink strokes will appear to grow or shrink in size their thickness will not change. An object transformation is typically used to alter the size of the ink strokes drawn with the Renderer without altering the actual stroke data, for example, to have ink stretch to fit inside an input area.

Big Ink vs. Little Ink

Up to this point we have used a single instance of the Ink class as the container for all the ink strokes in a document. This model is informally known as Big Ink because all the document s ink strokes are contained by one Ink object. However, some applications might want to use multiple Ink objects in a document; for example, each annotation or even each word could be represented by one Ink object. This model is known as the Little Ink model because multiple Ink objects are used to contain the ink strokes. Each Ink object is little in comparison to all the ink data in the document. Further discussion of the pros and cons of Big Ink vs. Little Ink is in Chapter 9.

The Renderer class provides the functions shown in Table 5-4 to obtain and alter the transformations.

Table 5-4. The Renderer Class s Methods to Work With the Object and View Transformations

Method Name

Description

void Move(float offsetX, offsetY)

Offsets the current view transformation by offset x and offset y amounts, specified in ink coordinates

void Rotate(float degrees) void Rotate(float degrees, Point point)

Rotates the view transformation by degrees amount, either about the origin (0,0) or the point specified

void Scale(float scaleX, scaleY) void Scale(float scaleX, scaleY, bool applyOnPenWidth)

Scales the view transformation by scale x and scale y, or if applyOnPenWidth=false then scales the object transformation by scale x and scale y

void GetObjectTransform(ref Matrix m)

Returns the transformation matrix of the object transform

void GetViewTransform(ref Matrix m)

Returns the transformation matrix of the view transform

void SetObjectTransform(Matrix m)

Sets the transformation matrix of the object transform

void SetViewTransform(Matrix m)

Sets the transformation matrix of the view transform

The Move, Rotate, and Scale methods make easy work of adjusting the view transformation matrix; alternatively you could get, adjust, and set the Matrix object explicitly with the GetViewTransform and SetViewTransform methods. Note that an overload of the Scale method can also adjust the object transformation matrix if its applyOnPenWidth parameter is false Move and Rotate don t have overloads with this parameter because their results don t affect ink thickness.

Sample Application InkMagnify

The InkMagnify sample application, shown in Figure 5-5, implements zooming and scrolling functionality. An InkControl object and a Panel control are created on a form, with the Panel mirroring the ink in the InkControl as it s created, deleted, moved, and resized. The mirrored ink displayed in the panel can be scrolled by dragging within the panel, and it can be magnified by tapping two Zoom buttons on the form.

figure 5-5 the inkmagnify application implements zooming and scrolling.

Figure 5-5. The InkMagnify application implements zooming and scrolling.

InkMagnify.cs

//////////////////////////////////////////////////////////////////// // // InkMagnify.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates how to render ink at a different // magnifications, as well as scroll it around via a "pan" mode. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl inkCtl; private InkInputPanel pnlViewer; private Button btnZoomIn; private Button btnZoomOut; private Renderer rndrMagnified; private StrokesEventHandler evtInkAdded; private StrokesEventHandler evtInkDeleted; private Point ptStart; private bool fPanning = false; // 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); pnlViewer = new InkInputPanel(); pnlViewer.BackColor = Color.White; pnlViewer.BorderStyle = BorderStyle.Fixed3D; pnlViewer.Cursor = System.Windows.Forms.Cursors.SizeAll; pnlViewer.Location = new Point(8, 228); pnlViewer.Size = new Size(352, 192); pnlViewer.MouseDown += new MouseEventHandler(pnlViewer_MouseDown); pnlViewer.MouseMove += new MouseEventHandler(pnlViewer_MouseMove); pnlViewer.MouseUp += new MouseEventHandler(pnlViewer_MouseUp); pnlViewer.Paint += new PaintEventHandler(pnlViewer_Paint); btnZoomIn = new Button(); btnZoomIn.Location = new Point(8, 428); btnZoomIn.Size = new Size(60, 20); btnZoomIn.Text = "Zoom in"; btnZoomIn.Click += new EventHandler(btnZoomIn_Click); btnZoomOut = new Button(); btnZoomOut.Location = new Point(76, 428); btnZoomOut.Size = new Size(60, 20); btnZoomOut.Text = "Zoom out"; btnZoomOut.Click += new EventHandler(btnZoomOut_Click); // Configure the form itself ClientSize = new Size(368, 456); Controls.AddRange(new Control[] { inkCtl, pnlViewer, btnZoomIn, btnZoomOut}); FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; Text = "InkMagnify"; ResumeLayout(false); // Create a new Renderer to draw the ink in the viewer panel rndrMagnified = new Renderer(); // Hook up to various stroke modification events so the viewer // display can be updated inkCtl.InkOverlay.SelectionMoved += new InkOverlaySelectionMovedEventHandler(inkCtl_Moved); inkCtl.InkOverlay.SelectionResized += new InkOverlaySelectionResizedEventHandler(inkCtl_Resized); 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); // We're now set to go, so turn on tablet input inkCtl.InkOverlay.Enabled = true; } // Handles the mouse button going down/pen touching the surface in // the magnified view - starts the panning operation private void pnlViewer_MouseDown(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) { Graphics g = pnlViewer.CreateGraphics(); // Store the start point of this mouse drag in ink coords ptStart = new Point(e.X, e.Y); rndrMagnified.PixelToInkSpace(g, ref ptStart); g.Dispose(); fPanning = true; } } // Handles the mouse/pen moving in the magnifyied view - continues // the panning operation if one is occurring private void pnlViewer_MouseMove(object sender, MouseEventArgs e) { if ((e.Button == MouseButtons.Left) && fPanning) { Graphics g = pnlViewer.CreateGraphics(); // Offset the viewer by the position of the pen in relation // to its starting point Point ptCurr = new Point(e.X, e.Y); rndrMagnified.PixelToInkSpace(g, ref ptCurr); rndrMagnified.Move((ptCurr.X - ptStart.X), (ptCurr.Y - ptStart.Y)); g.Dispose(); // Update the viewer display pnlViewer.Invalidate(); } } // Handles the mouse button being released/pen being lifted in the // magnified view - ends the panning operation if one is occurring private void pnlViewer_MouseUp(object sender, MouseEventArgs e) { if ((e.Button == MouseButtons.Left) && fPanning) { fPanning = false; } } // Helper function for zooming private void ZoomByFactor(float fFactor) { Graphics g = pnlViewer.CreateGraphics(); // Reset the current view to (0,0) Point ptOldOrg = new Point(0,0); rndrMagnified.PixelToInkSpace(g, ref ptOldOrg); rndrMagnified.Move(ptOldOrg.X / fFactor, ptOldOrg.Y / fFactor); // Compute the center point Point ptOldCenter = new Point( pnlViewer.ClientSize.Width/2, pnlViewer.ClientSize.Height/2); rndrMagnified.PixelToInkSpace(g, ref ptOldCenter); // Scale the view transformation rndrMagnified.Scale(fFactor, fFactor); // Compute the new center point Point ptNewCenter = new Point( pnlViewer.ClientSize.Width/2, pnlViewer.ClientSize.Height/2); rndrMagnified.PixelToInkSpace(g, ref ptNewCenter); // Offset the current view to the initial center point rndrMagnified.Move( (-(ptOldOrg.X + ptOldCenter.X - ptNewCenter.X) * fFactor), (-(ptOldOrg.Y + ptOldCenter.Y - ptNewCenter.Y) * fFactor)); g.Dispose(); // Update the viewer display pnlViewer.Invalidate(); } // Handles zooming out private void btnZoomIn_Click(object sender, EventArgs e) { // Increase magnification by 25% ZoomByFactor(1.25f); } // Handles zooming in private void btnZoomOut_Click(object sender, EventArgs e) { // Decrease magnification by 20% ZoomByFactor(0.8f); } // Handler for the selection getting moved private void inkCtl_Moved(object sender, InkOverlaySelectionMovedEventArgs e) { // Update the viewer display pnlViewer.Invalidate(); } // Handler for the selection getting resized private void inkCtl_Resized(object sender, InkOverlaySelectionResizedEventArgs e) { // Update the viewer display pnlViewer.Invalidate(); } // Handler for ink getting 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) { // Update the viewer display pnlViewer.Invalidate(); } // Handler for ink getting 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) { // Update the viewer display pnlViewer.Invalidate(); } // Handler for painting the viewer panel private void pnlViewer_Paint(object sender, PaintEventArgs e) { // Draw the Ink object using the viewer's Renderer object rndrMagnified.Draw(e.Graphics, inkCtl.InkOverlay.Ink.Strokes); } }

The application creates InkControl and InkInputPanel controls used for the input area and ink viewer, respectively. The InkInputPanel class found in the BuildingTabletApps utility library is a simple derivative of the Panel control; it gets rid of flickering during painting by employing .NET Framework support for double buffering. Because the InkInputPanel will be mirroring the ink strokes found in the InkControl s InkOverlay, a Paint event handler is installed. Event handlers for MouseDown, MouseMove, and MouseUp events are also installed to facilitate drag scrolling otherwise known as panning .

The renderer object rndrMagnified maintains the current view transformation for the InkInputPanel viewer and is used for painting the ink strokes. Event handlers for various ink stroke manipulation events are used to trigger repainting the InkInputPanel viewer, and again take notice that the InkAdded and InkDeleted events are triggered on the main thread of the application s execution to avoid multithreading problems.

The MouseDown event for pnlInput initiates scrolling. The location of the mouse button press is converted into ink coordinates, used later in the MouseMove event:

if (e.Button == MouseButtons.Left) { Graphics g = pnlViewer.CreateGraphics(); // Store the start point of this mouse drag in ink coords ptStart = new Point(e.X, e.Y); rndrMagnified.PixelToInkSpace(g, ref ptStart); g.Dispose(); fPanning = true; }

A Graphics object is temporarily created to allow the conversion from pixels to ink space. The fPanning member simply tracks whether a panning operation is taking place.

The MouseMove handler for pnlInput performs the scrolling operation by offsetting the view transformation by the difference between the button down point and the current point:

if ((e.Button == MouseButtons.Left) && fPanning) { Graphics g = pnlViewer.CreateGraphics(); // Offset the viewer by the position of the pen in relation // to its starting point Point ptCurr = new Point(e.X, e.Y); rndrMagnified.PixelToInkSpace(g, ref ptCurr); rndrMagnified.Move((ptCurr.X - ptStart.X), (ptCurr.Y - ptStart.Y)); g.Dispose(); // Update the viewer display pnlViewer.Invalidate(); }

Recall that the units for Renderer s Move method are ink coordinates (HIMETRIC). Once the transformation has been applied, the pnlInput control is invalidated to trigger a repaint.

Zooming is performed by the helper function ZoomByFactor that scales the view transformation by a desired amount, preserving the current center point to give the effect of moving inward:

private void ZoomByFactor(float fFactor) { Graphics g = pnlViewer.CreateGraphics(); // Reset the current view to (0,0) Point ptOldOrg = new Point(0,0); rndrMagnified.PixelToInkSpace(g, ref ptOldOrg); rndrMagnified.Move(ptOldOrg.X / fFactor, ptOldOrg.Y / fFactor); // Compute the center point Point ptOldCenter = new Point( pnlViewer.ClientSize.Width/2, pnlViewer.ClientSize.Height/2); rndrMagnified.PixelToInkSpace(g, ref ptOldCenter); // Scale the view transformation rndrMagnified.Scale(fFactor, fFactor); // Compute the new center point Point ptNewCenter = new Point( pnlViewer.ClientSize.Width/2, pnlViewer.ClientSize.Height/2); rndrMagnified.PixelToInkSpace(g, ref ptNewCenter); // Offset the current view to the initial center point rndrMagnified.Move( (-(ptOldOrg.X + ptOldCenter.X - ptNewCenter.X) * fFactor), (-(ptOldOrg.Y + ptOldCenter.Y - ptNewCenter.Y) * fFactor)); g.Dispose(); // Update the viewer display pnlViewer.Invalidate(); }

The key operation in this method is rndrMagnified.Scale, which adjusts the Renderer s scaling amount (hence, the zoom effect). The translations done with rndrMagnified.Move preserve the center point of the view. Similar to the panning case in MouseMove, pnlInput is invalidated to trigger a repaint once the transformation has been updated. If you like, try using the other overload of the Scale method to see the effect of setting the object transform by passing false as the value to the applyOnPenWidth parameter.

The Tablet PC Platform SDK also contains a sample application demonstrating scrolling and zooming, called InkZoom. It takes on a more traditional UI of having scrollbars control scrolling and a ComboBox specify the zoom level.

Adding Style The DrawingAttributes Class

Now that you know how to draw ink to the screen or other graphics device, let s take a look at the various properties that define the ink s visual characteristics. The DrawingAttributes class encapsulates the formatting information that defines the style electronic ink is rendered with.

The Windows Journal utility found in Windows XP Tablet PC Edition provides a number of predefined pen types for writing and highlighting. This is because the purpose or context of ink drawn on a page isn t always consistent. The style of ink can be categorized by its usage, and it can take on many different forms. A few of the common ones are listed here:

  • Writing ink

    Typically black or blue in color, thin, circular pen tip, and primarily used for note taking and diagramming

  • Markup ink

    Often brighter in color, such as red or green, thicker than writing ink, and mostly used to annotate documents

  • Highlighter ink

    Usually yellow, pink, or light green, very thick with a rectangular tip, transparent in appearance, and often used to highlight text or writing

The DrawingAttributes class encapsulates the properties used to render ink to a device. This class s full list of properties is found in Table 5-5 along with a brief description of each.

Table 5-5. Properties of the DrawingAttributes Class

Property Name

Type

Description

AntiAliased

bool

Turns antialiasing on (true) and off (false).

Color

Color

The color used to draw the ink.

FitToCurve

bool

Whether ink is rendered as a series of straight lines (false) or B zier curves (true).

Height

float

The height of the ink specified in ink coordinates when using the rectangle pen tip.

IgnorePressure

bool

Whether to avoid varying the thickness of ink with pressure data (true) or not (false).

PenTip

PenTip

The style of tip used to draw ink: PenTip.Ball or PenTip.Rectangle.

RasterOperation

RasterOperation

The raster operation (ROP) used when drawing ink. The most common value is RasterOperation.CopyPen though highlighter ink uses RasterOperation.MaskPen.

Transparency

Byte

The transparency amount of the ink, where 0 = opaque and 255 = invisible.

Width

Float

The thickness of the ink when using the ball pen tip, or the width of the ink specified in ink coordinates when using the rectangle pen tip.

Let s take a look at some of these properties in greater detail.

Antialiased

Jaggies occur when the pixels used to render graphics are not of high enough resolution to make angled or curved edges appear smooth to the human eye. Antialiasing is a remedy for this problem it takes advantage of how humans perceive contrast by making edge pixels translucent. The translucency is proportional to how much the rendered pixel is contained within the logical edge of an image. Algorithms to compute antialiasing aside, all we really care about here is that antialiasing ink is visually more pleasing to most users.

You will almost always want to use antialiasing when rendering your ink, as it is a dramatic step toward realism. Future versions of the Tablet PC Platform will include ClearInk, a technique borrowing from ClearType in which subpixel rendering is employed, offering even more dramatic results in perceived smoothness.

FitToCurve Property

The FitToCurve property specifies whether curve fitting (also known as smoothing or B zier fitting) is employed.

As we know, Stroke objects are stored as a series of (x,y) data, perhaps along with some other packet properties. In essence, a stroke is a polyline. By definition a polyline is a sequence of lines, and by definition a line is straight. This can cause ink to look very polygonal (and artificial) if the (x,y) points are spaced far enough apart, perhaps as a result of high-velocity inking. A solution to this problem is to calculate a series of curves that intersect the points of the ink stroke, to better approximate the path of the pen when the stroke was initially drawn. This process is known as curve fitting; Figure 5-6 illustrates how it is performed.

High-Velocity Inking

Don t ink and drive.

figure 5-6 curve fitting smooths out the straight edges of polyline-based strokes.

Figure 5-6. Curve fitting smooths out the straight edges of polyline-based strokes.

Like antialiasing, curve fitting does a great job of making digital ink appear more realistic therefore, you ll typically want to take advantage of this feature by setting the property to true.

NOTE
If a stroke that is in the process of being created with the pen has its DrawingAttributes s FitToCurve property set to true, curve fitting will not be applied unless the stroke is explicitly redrawn. This is done on purpose. Because of the inexact fitting of B zier points during curve fitting, a stroke would appear to wiggle around slightly while it is being drawn if curve fitting were constantly applied. Therefore, it is best practice to wait until the stroke has been completed before setting the FitToCurve property, and invalidate the area occupied by the stroke so it is redrawn.

IgnorePressure Property

In the last chapter we saw that if a tablet device used to collect ink supports the NormalPressure packet property, and that property is listed in the DesiredPacketDescription attribute of the InkCollector or InkOverlay used to collect ink, the collected Stroke objects can be rendered with varying thickness, corresponding to the pressure value.

The IgnorePressure property specifies whether the NormalPressure packet property of an ink stroke will be used to vary ink thickness. If an ink stroke doesn t contain any NormalPressure data, altering the value of this property will have no effect.

PenTip Attribute

The PenTip attribute is used to indicate the shape of the pen used to draw ink to the device Values are PenTip.Ball and PenTip.Rectangle.

When using the PenTip.Rectangle tip, both of the DrawingAttributes Width and Height properties are used to specify size. In contrast, the PenTip.Ball tip style only uses the Width property the Height property is ignored because a purely circular pen tip was implemented, in keeping with a physical ballpoint pen. Both properties values are specified in ink space coordinates.

RasterOperation Property

The RasterOperation property allows specification of the manner in which the pixels of the ink stroke are combined with the destination drawing surface. This is known as the ROP code in its short form, and it harkens back to the Graphical Device Interface (GDI) library for its origins. The Win32 Platform SDK documentation has lots of information about ROP codes, so we suggest looking them up via the SetROP2 function you ll be glad you did.

There are two ROP codes that are particularly useful when drawing Stroke objects: RasterOperation.CopyPen and RasterOperation.MaskPen. The former is the default value, resulting in ink being drawn on top of the destination surface. The latter is used typically with highlighter ink, taking away color from the destination surface. This is used so that highlighter ink doesn t obscure any ink that may be underneath it (for instance, we don t want black ink turned yellow).

Using the DrawingAttributes Property

In the last chapter we were briefly introduced to the DefaultDrawingAttributes property of the InkCollector and InkOverlay classes, which denotes the set of drawing attributes to be used on future ink stroke creation. Once an InkCollector or InkOverlay object is created, the DefaultDrawingAttributes property can be immediately modified or even set to another DrawingAttributes instance entirely.

NOTE
The rendering code in the Tablet PC Platform primarily utilizes the GDI+ library for drawing functionality specifically, it is used for antialiasing and transparency effects. Transparency is a great effect for ink as it can add an extra dimension of visual realism. Unfortunately, the GDI+ library doesn t support ROP codes, so when a ROP code other than RasterOperation.CopyPen is desired, ye olde GDI library is enlisted to help. This results in the loss of GDI+ effects such as antialiasing and transparency! Please keep this in mind when using ROP codes in a DrawingAttributes object.

The DefaultDrawingAttributes property applies to any new ink strokes that are created from the InkCollector or InkOverlay instance. Also, if the digitizer hardware supports distinguishing between pens, it is possible to have DrawingAttributes associated on a per-cursor basis. Setting the Cursor class s DrawingAttributes property overrides the current DefaultDrawingAttributes of an InkCollector or InkOverlay.

Setting the cursors class s DrawingAttributes property to null indicates the cursor will use the InkCollector s or InkOverlay s DefaultDrawingAttribute.

After a Stroke object has been created, it may be useful to change the DrawingAttributes used to render it for example, a format ink dialog may be desirable to include in an application to allow a user to change ink color, thickness, and so forth, or perhaps if the stroke was generated programmatically then some initial DrawingAttributes may be desirable to set. The Stroke class s DrawingAttributes property exposes the drawing attributes used to render the Stroke object. When modifying the drawing attributes of a Stroke, they are reflected the next time the Stroke object is drawn via a Renderer object.

The drawing attributes on multiple Stroke objects can be set using the Strokes collection class s ModifyDrawingAttributes method. The method takes a DrawingAttributes instance as the sole argument, which is then assigned to the DrawingAttributes property of the referenced Stroke objects.

The Stroke class s DrawingAttributes property and the Strokes collection class s ModifyDrawingAttributes method both permanently alter the drawing attributes of Stroke objects. However, sometimes it is useful to temporarily override the drawing attributes without modifying the ink data, for example, when highlighting ink as the result of a search operation, or using high-contrast colors to support accessibility. As we saw earlier in the chapter, the Renderer class contains an override of the Draw method to perform this task:

void Draw(Graphics g, Stroke s, DrawingAttributes da)

The drawing attributes are used only to render the ink stroke and do not alter the Stroke settings in any way.

Sample Application InkFormatter

This last sample in the chapter, shown in Figure 5-7, allows us to play around with the various settings of DrawingAttributes. The application uses an InkControl to collect and edit ink, but its Color button is replaced by a Format button that s used to present a dialog in which the various DrawingAttributes properties can be changed. The sample takes advantage of a class we ll add to the BuildingTabletApps utility library called FormatInkDlg that implements a UI to edit all the properties in a DrawingAttributes object.

figure 5-7 the inkformatter application provides a ui to edit all the drawingattributes class s properties.

Figure 5-7. The InkFormatter application provides a UI to edit all the DrawingAttributes class s properties.

FormatInkDlg.cs

//////////////////////////////////////////////////////////////////// // // FormatInkDlg.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This class implements a dialog which provides editing of a // DrawingAttributes object. // //////////////////////////////////////////////////////////////////// using System; using System.Collections; using System.Drawing; using System.Reflection; using System.Windows.Forms; using Microsoft.Ink; namespace MSPress.BuildingTabletApps { public class FormatInkDlg : Form { private DrawingAttributes attrsData; private Button btnColor; private ComboBox cbxThickness; private CheckBox cbAntialiased; private CheckBox cbFitToCurve; private CheckBox cbIgnorePressure; private ComboBox cbxTipStyle; private ComboBox cbxROP; private ComboBox cbxTransparency; private Button btnOK; private Button btnCancel; // Dialog setup public FormatInkDlg() { SuspendLayout(); // Create and place all of our controls Label lblColor = new Label(); lblColor.Location = new Point(8, 8); lblColor.Size = new Size(96, 20); lblColor.Text = "Color:"; lblColor.TextAlign = ContentAlignment.MiddleRight; btnColor = new Button(); btnColor.Location = new Point(104, 8); btnColor.Size = new Size(45, 20); btnColor.Text = ""; btnColor.Click += new System.EventHandler(btnColor_Click); Label lblThickness = new Label(); lblThickness.Location = new Point(8, 36); lblThickness.Size = new Size(96, 20); lblThickness.Text = "Thickness:"; lblThickness.TextAlign = ContentAlignment.MiddleRight; cbxThickness = new ComboBox(); cbxThickness.Location = new Point(104, 36); cbxThickness.Size = new Size(45, 20); cbxThickness.DropDownStyle = ComboBoxStyle.DropDownList; cbxThickness.SelectedIndexChanged += new System.EventHandler(cbxThickness_SelIndexChg); Label lblTipStyle = new Label(); lblTipStyle.Location = new Point(8, 64); lblTipStyle.Size = new Size(96, 20); lblTipStyle.Text = "Tip style:"; lblTipStyle.TextAlign = ContentAlignment.MiddleRight; cbxTipStyle = new ComboBox(); cbxTipStyle.DropDownStyle = ComboBoxStyle.DropDownList; cbxTipStyle.Location = new Point(104, 64); cbxTipStyle.Size = new Size(90, 20); cbxTipStyle.SelectedIndexChanged += new System.EventHandler(cbxTipStyle_SelIndexChg); cbAntialiased = new CheckBox(); cbAntialiased.CheckAlign = ContentAlignment.MiddleRight; cbAntialiased.Location = new Point(8, 92); cbAntialiased.Size = new Size(108, 16); cbAntialiased.Text = "Antialiased:"; cbAntialiased.TextAlign = ContentAlignment.MiddleRight; cbAntialiased.CheckStateChanged += new System.EventHandler( cbAntialiased_CheckStateChanged); cbFitToCurve = new CheckBox(); cbFitToCurve.CheckAlign = ContentAlignment.MiddleRight; cbFitToCurve.Location = new Point(8, 116); cbFitToCurve.Size = new Size(108, 16); cbFitToCurve.Text = "Fit to curve:"; cbFitToCurve.TextAlign = ContentAlignment.MiddleRight; cbFitToCurve.CheckStateChanged += new System.EventHandler(cbFitToCurve_CheckStateChanged); cbIgnorePressure = new CheckBox(); cbIgnorePressure.CheckAlign = ContentAlignment.MiddleRight; cbIgnorePressure.Location = new Point(8, 140); cbIgnorePressure.Size = new Size(108, 16); cbIgnorePressure.Text = "Ignore pressure:"; cbIgnorePressure.TextAlign = ContentAlignment.MiddleRight; cbIgnorePressure.CheckStateChanged += new System.EventHandler( cbIgnorePressure_CheckStateChanged); Label lblROP = new Label(); lblROP.Location = new Point(8, 164); lblROP.Size = new Size(96, 20); lblROP.Text = "ROP code:"; lblROP.TextAlign = ContentAlignment.MiddleRight; cbxROP = new ComboBox(); cbxROP.DropDownStyle = ComboBoxStyle.DropDownList; cbxROP.Location = new Point(104, 164); cbxROP.Size = new Size(90, 20); cbxROP.SelectedIndexChanged += new System.EventHandler(cbxROP_SelIndexChg); Label lblTransparency = new Label(); lblTransparency.Location = new Point(8, 192); lblTransparency.Size = new Size(96, 20); lblTransparency.Text = "Transparency:"; lblTransparency.TextAlign = ContentAlignment.MiddleRight; cbxTransparency = new ComboBox(); cbxTransparency.DropDownStyle = ComboBoxStyle.DropDownList; cbxTransparency.Location = new Point(104, 192); cbxTransparency.Size = new Size(90, 20); cbxTransparency.SelectedIndexChanged += new System.EventHandler(cbxTransparency_SelIndexChg); btnOK = new Button(); btnOK.Location = new Point(64, 226); btnOK.Size = new Size(60, 20); btnOK.Text = "OK"; btnOK.Click += new System.EventHandler(btnOK_Click); btnCancel = new Button(); btnCancel.Location = new Point(132, 226); btnCancel.Size = new Size(60, 20); btnCancel.Text = "Cancel"; btnCancel.Click += new System.EventHandler(btnCancel_Click); // Configure the form itself AcceptButton = btnOK; CancelButton = btnCancel; ClientSize = new Size(200, 256); Controls.AddRange(new Control[] { lblColor, btnColor, lblThickness, cbxThickness, lblTipStyle, cbxTipStyle, cbAntialiased, cbFitToCurve, cbIgnorePressure, lblROP, cbxROP, lblTransparency, cbxTransparency, btnOK, btnCancel}); FormBorderStyle = FormBorderStyle.FixedDialog; MinimizeBox = false; MaximizeBox = false; Text = "Format Ink"; ResumeLayout(false); // Fill up the thickness combobox with some values cbxThickness.Items.Add(1f); cbxThickness.Items.Add(50f); cbxThickness.Items.Add(100f); cbxThickness.Items.Add(150f); cbxThickness.Items.Add(300f); cbxThickness.Items.Add(500f); // Fill up the pen tip combobox with all pen tip values foreach (PenTip t in PenTip.GetValues(typeof(PenTip))) { cbxTipStyle.Items.Add(t); } // Fill up the ROP code combobox with all raster operation // values foreach (RasterOperation o in RasterOperation.GetValues(typeof(RasterOperation))) { cbxROP.Items.Add(o); } // Fill up the transparency combobox with some values cbxTransparency.Items.Add((byte)0); cbxTransparency.Items.Add((byte)64); cbxTransparency.Items.Add((byte)128); cbxTransparency.Items.Add((byte)192); cbxTransparency.Items.Add((byte)255); } // Allow the dialog data to be externally viewable and settable public DrawingAttributes DrawingAttributes { get { return attrsData; } set { attrsData = value.Clone(); // Reflect the values of the DrawingAttributes in the // controls btnColor.BackColor = attrsData.Color; // Add thickness value to the combobox if not there if (!cbxThickness.Items.Contains(attrsData.Width)) { int nLoc = 0; while (nLoc < cbxThickness.Items.Count && (float)cbxThickness.Items[nLoc] < attrsData.Width) { nLoc++; } if (nLoc < cbxThickness.Items.Count) { cbxThickness.Items.Insert(nLoc, attrsData.Width); } else { cbxThickness.Items.Add(attrsData.Width); } } cbxThickness.SelectedItem = attrsData.Width; cbxTipStyle.SelectedItem = attrsData.PenTip; cbAntialiased.CheckState = attrsData.AntiAliased ? CheckState.Checked : CheckState.Unchecked; cbFitToCurve.CheckState = attrsData.FitToCurve ? CheckState.Checked : CheckState.Unchecked; cbIgnorePressure.CheckState = attrsData.IgnorePressure ? CheckState.Checked : CheckState.Unchecked; cbxROP.SelectedItem = attrsData.RasterOperation; // Add transparenccy value to the combobox if not there if (!cbxTransparency.Items.Contains( attrsData.Transparency)) { int nLoc = 0; while (nLoc < cbxTransparency.Items.Count && (byte)cbxTransparency.Items[nLoc] < attrsData.Transparency) { nLoc++; } if (nLoc < cbxTransparency.Items.Count) { cbxTransparency.Items.Insert(nLoc, attrsData.Transparency); } else { cbxTransparency.Items.Add( attrsData.Transparency); } } cbxTransparency.SelectedItem = attrsData.Transparency; } } // Handle the click of the color button private void btnColor_Click(object sender, System.EventArgs e) { // Display the common color dialog ColorDialog dlgColor = new ColorDialog(); dlgColor.AllowFullOpen = false; dlgColor.Color = attrsData.Color; if (dlgColor.ShowDialog(this) == DialogResult.OK) { // Set the current ink color to the selection chosen in // the dialog attrsData.Color = dlgColor.Color; btnColor.BackColor = attrsData.Color; } } // Handle the selection change of the width combobox private void cbxThickness_SelIndexChg(object sender, System.EventArgs e) { attrsData.Width = (float)cbxThickness.SelectedItem; attrsData.Height = attrsData.Width; } // Handle the selection change of the pen tip combobox private void cbxTipStyle_SelIndexChg(object sender, System.EventArgs e) { attrsData.PenTip = (PenTip)cbxTipStyle.SelectedItem; } // Handle the checked state change of the antialiased checkbox private void cbAntialiased_CheckStateChanged(object sender, System.EventArgs e) { attrsData.AntiAliased = (cbAntialiased.CheckState == CheckState.Checked); } // Handle the checked state change of the fit to curve checkbox private void cbFitToCurve_CheckStateChanged(object sender, System.EventArgs e) { attrsData.FitToCurve = (cbFitToCurve.CheckState == CheckState.Checked); } // Handle the checked state change of the ignore pressure checkbox private void cbIgnorePressure_CheckStateChanged(object sender, System.EventArgs e) { attrsData.IgnorePressure = (cbIgnorePressure.CheckState == CheckState.Checked); } // Handle the selection change of the raster operation combobox private void cbxROP_SelIndexChg(object sender, System.EventArgs e) { attrsData.RasterOperation = (RasterOperation)cbxROP.SelectedItem; } // Handle the selection change of the transparency combobox private void cbxTransparency_SelIndexChg(object sender, System.EventArgs e) { attrsData.Transparency = (byte)cbxTransparency.SelectedItem; } // Handle the button click of the OK button private void btnOK_Click(object sender, System.EventArgs e) { DialogResult = DialogResult.OK; } // Handle the button click of the cancel button private void btnCancel_Click(object sender, System.EventArgs e) { DialogResult = DialogResult.Cancel; } } }

To accommodate the FormatInkDlg UI, we ll duplicate the InkControl class and rename it to InkControl2. The format functionality can then be added in place of color choosing. We ll take this approach in an effort to keep the utility library s implementation as illustrative as possible, as opposed to a nicer design such as using derivation.

The Format button handler s implementation is fairly similar to the Color button handler of the original InkControl, except the entire DrawingAttributes object is used:

// Handle the click of the color button private void btnFormat_Click(object sender, System.EventArgs e) { // Display the common color dialog FormatInkDlg dlgFormat = new FormatInkDlg(); dlgFormat.DrawingAttributes = inkOverlay.DefaultDrawingAttributes; if (dlgFormat.ShowDialog(this) == DialogResult.OK) { // Set the current drawing attributes to the values // chosen in the dialog inkOverlay.DefaultDrawingAttributes = dlgFormat.DrawingAttributes; } }

Finally, here is the rather short and simple listing of the application itself. It creates an instance of the InkControl2 control and enables tablet input. The result of the functionality is courtesy of our new InkControl2 class.

InkFormatter.cs

//////////////////////////////////////////////////////////////////// // // InkFormatter.cs // // (c) 2002 Microsoft Press // by Rob Jarrett // // This program demonstrates the various rendering properties of ink. // //////////////////////////////////////////////////////////////////// using System; using System.Drawing; using System.Windows.Forms; using Microsoft.Ink; using MSPress.BuildingTabletApps; public class frmMain : Form { private InkControl2 inkCtl; // Entry point of the program static void Main() { Application.Run(new frmMain()); } // Main form setup public frmMain() { SuspendLayout(); // 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 = "InkFormatter"; ResumeLayout(false); // We're now set to go, so turn on tablet input inkCtl.InkOverlay.Enabled = true; } }

Now, let s try running the application and playing around with all the different settings the FormatInkDlg makes available. Table 5-6 lists some common pen styles and their attributes that you might incorporate into your applications.

Table 5-6. Common Pen Styles and Their Attributes

Pen Style

Color

Thickness (Ink Units)

Pen Tip

ROP Code

Writing ink

Black, blue

1 60

Ball

CopyPen

Markup pen

Red, dark green

60 150

Ball

CopyPen

Highlighter ink

Yellow, pink, light green

500 1000

Rectangle

MaskPen

You, the developer extraordinaire, must implement the saving and restoring of pen style settings. Typically pen style is user-specific data, hence it should be written and read on a per-user basis. It might be nice if a future version of the Tablet PC Platform provided a central repository for pen styles, but for now we ll have to make do on our own.

Special Rendering Effects

Using the DrawingAttributes class to achieve the look of a specific type of ink isn t its only use. Overriding drawing attributes on existing Stroke objects can allow for some interesting UI effects, for example, the UI might show ink selection or perhaps flash the object s colors to show that it s the target of some operation.

Rendering Selection State

The Tablet PC Platform version 1.0 provides no programmatic support for drawing ink in a selected state. There are cases where we might want to maintain our own set of selected strokes or implement our own select mode functionality, which means we ll have to implement drawing of ink in a selected state on our own. Fortunately, manually drawing ink strokes in a selected state is easier than it looks. Figure 5-8 shows an example of manually drawn ink strokes.

figure 5-8 drawing ink in the halo style to indicate selection.

Figure 5-8. Drawing ink in the halo style to indicate selection.

The algorithm to achieve the selected look is fairly simple:

  1. Create a new DrawingAttributes instance from the one currently set on the stroke via the Clone method

  2. Set IgnorePressure to true to avoid an outline with varying thickness

  3. Inflate the stroke s thickness by 4 pixels (use Renderer s PixelToInkSpace to compute how many ink units that is)

  4. Draw the ink stroke, overriding its DrawingAttributes with the new instance this is the outline of the stroke

  5. Restore the original thickness for the new DrawingAttributes instance and set the Color property to the inking area s background color

  6. Draw the ink stroke using the new DrawingAttributes instance this will cut out the center of the ink

The listing below shows a sample implementation of this algorithm:

// Compute selection ink thickness Point ptThickness = new Point(4, 4); renderer.PixelToInkSpace(graphics, ref ptThickness); // Copy the DrawingAttributes since we'll be modifying them DrawingAttributes da = stroke.DrawingAttributes.Clone(); // Turn off pressure so thickness draws uniformly da.IgnorePressure = true; // Draw the thick outer stroke da.Width += ptThickness.X; da.Height += ptThickness.Y; renderer.Draw(graphics, stroke, da); // Draw the inner "transparent" stroke da.Width -= ptThickness.X; da.Height -= ptThickness.Y; da.Color = Color.White; renderer.Draw(graphics, stroke, da);

The utility library includes a method named RendererEx.DrawSelected to draw a Stroke object or Strokes collection in a selected state. We ll be making use of that function in the next chapter when we implement our own selection UI.

Glowing Ink

Let s have a little more fun, and embellish the selected ink appearance by taking advantage of the DrawingAttributes property Transparency. If we alter the transparency value while adjusting ink thickness, we can make the ink appear to glow.

The code to achieve this is quite similar to the code for drawing selection, except it has a couple more steps involved; Figure 5-9 illustrates how it s done.

figure 5-9 a method to draw glowing ink.

Figure 5-9. A method to draw glowing ink.

// Compute glowing ink thickness Point ptThickness = new Point(3, 3); renderer.PixelToInkSpace(graphics, ref ptThickness); // Draw the thick outer stroke da.Width += ptThickness.X*4; da.Height += ptThickness.Y*4; da.Transparency = 224; renderer.Draw(graphics, stroke, da); da.Width -= ptThickness.X; da.Height -= ptThickness.Y; da.Transparency = 192; renderer.Draw(graphics, stroke, da); da.Width -= ptThickness.X; da.Height -= ptThickness.Y; da.Transparency = 128; renderer.Draw(graphics, stroke, da); da.Width -= ptThk.X; da.Height -= ptThk.Y; renderer.Draw(graphics, stroke, da); // Draw the inner "transparent" stroke da.Transparency = 0; da.Width -= ptThickness.X; da.Height -= ptThickness.Y; da.Color = Color.White; renderer.Draw(graphics, stroke, da);

Truly Transparent Ink

Both methods to draw ink in a selected state have assumed that the interior hollowed out part of the stroke is a solid color. If the background that the ink was being drawn to was a bitmap with a lot of color variation, or if some other ink was behind the stroke, the hollowed out appearance would not be realized. The proper solution to hollowing out ink is to make the center of the stroke truly transparent. There are at least a couple of methods to do this: the first is to draw the outline of the stroke with a path generated by surrounding the stroke to be selected, and the second is to draw the thick version of the stroke to a bitmap object, manually adjust the interior pixels to be transparent, and draw the bitmap to the screen. We have found that the latter method, while easier to implement, is slower in performance.

Performance Implications of These Methods

The cost of drawing ink strokes is unfortunately pretty high. This comes primarily from the GDI+ library s performance for basic drawing operations, and the speed at which B zier curves can be generated for polylines. Antialiasing, curve fitting, and variable thickness from pressure are all quite performance intensive, so they should be used with care. As a big proponent of realistic-looking ink (something we feel is very important to the Tablet PC experience), it s hard for us to say no to those effects because they look so good. Some advice: you might want to offer some UI in your ink-enabled application to turn off some of or all these effects to ultimately leave the choice up to the user. After all, they matter the most!

There is a good performance analysis of rendering operations in Chapter 9.



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