// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// File: InkHitTest.cs
// Ink Hit Test Sample Application
//
// This sample illustrates two techniques for finding ink given a
// screen location:
//
// 1. Hit Testing: The hit testing mode determines whether
// a collection of strokes is either completely inside or
// intersected by a circular cursor. When a hit occurs, the
// circular cursor changes from black to red.
//
// 2. Nearest Point: The nearest point mode calculates the
// the point on a stroke within an Ink object that is
// nearest to the cursor's location. The sample displays
// a red line connecting the cursor to this point.
//
// The features used are: InkCollector, Ink.HitTest,Ink.NearestPoint.
//
//--------------------------------------------------------------------------
using System;
using System.Drawing;
using System.Windows.Forms;
// The Ink namespace, which contains the Tablet PC Platform API
using Microsoft.Ink;
namespace Microsoft.Samples.TabletPC.InkHitTest
{
///
/// Enumeration of all possible application modes:
///
/// Ink: The user is drawing new strokes
/// HitTest: The user is performing hit testing
/// NearestPoint: The app should calculate the nearest point
///
///
public enum ApplicationMode
{
None = 0,
Ink = 1,
HitTest = 2,
NearestPoint = 3
};
///
/// The InkHitTest Sample Application form class
///
public class InkHitTest : System.Windows.Forms.Form
{
// --------------- Constants ---------------
// The radius of the circle used for hit testing
// in pixels.
private const int HitSize = 30;
// --------------- Fields ---------------
// The inkcollector is the central object for ink support.
// Through this object, we will collect ink and then get access
// to it via the Ink object.
private InkCollector ic = null;
// The pen that is currently used for drawing
private Pen activePen;
// Black and red pens used for drawing
private Pen blackPen;
private Pen redPen;
// Stores the current location of the pen
private Point penPt = Point.Empty;
// Stores the current nearest point (when applicable)
private Point nearestPt = Point.Empty;
// Stores the region of the screen that needs to be invalidated
private Rectangle invalidateRect;
private ApplicationMode mode;
#region Standard Template Code
private System.Windows.Forms.MainMenu mainMenu1;
private System.Windows.Forms.MenuItem FileItem;
private System.Windows.Forms.MenuItem ModeItem;
private System.Windows.Forms.MenuItem ClearItem;
private System.Windows.Forms.MenuItem InkItem;
private System.Windows.Forms.MenuItem HitTestItem;
private System.Windows.Forms.MenuItem NearestPointItem;
private System.Windows.Forms.MenuItem miExit;
///
/// Required designer variable.
///
private System.ComponentModel.Container components = null;
#endregion
public InkHitTest()
{
#region Standard Template Code
//
// Required for Windows Form Designer support
//
InitializeComponent();
#endregion
}
# region Standard Template Code
///
/// Clean up any resources being used.
///
protected override void Dispose( bool disposing )
{
if( disposing )
{
if (components != null)
{
components.Dispose();
}
if (ic != null)
{
ic.Dispose();
}
}
base.Dispose( disposing );
}
#endregion
#region Windows Form Designer generated code
///
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
///
private void InitializeComponent()
{
this.mainMenu1 = new System.Windows.Forms.MainMenu();
this.FileItem = new System.Windows.Forms.MenuItem();
this.miExit = new System.Windows.Forms.MenuItem();
this.ModeItem = new System.Windows.Forms.MenuItem();
this.InkItem = new System.Windows.Forms.MenuItem();
this.HitTestItem = new System.Windows.Forms.MenuItem();
this.NearestPointItem = new System.Windows.Forms.MenuItem();
this.ClearItem = new System.Windows.Forms.MenuItem();
//
// mainMenu1
//
this.mainMenu1.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.FileItem,
this.ModeItem,
this.ClearItem});
//
// FileItem
//
this.FileItem.Index = 0;
this.FileItem.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.miExit});
this.FileItem.Text = "&File";
//
// miExit
//
this.miExit.Index = 0;
this.miExit.Text = "E&xit";
this.miExit.Click += new System.EventHandler(this.miExit_Click);
//
// ModeItem
//
this.ModeItem.Index = 1;
this.ModeItem.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] {
this.InkItem,
this.HitTestItem,
this.NearestPointItem});
this.ModeItem.Text = "&Mode";
//
// InkItem
//
this.InkItem.Checked = true;
this.InkItem.Index = 0;
this.InkItem.Text = "&Ink";
this.InkItem.Click += new System.EventHandler(this.enterInkMode);
//
// HitTestItem
//
this.HitTestItem.Index = 1;
this.HitTestItem.Text = "&Hit Test";
this.HitTestItem.Click += new System.EventHandler(this.enterHitTestMode);
//
// NearestPointItem
//
this.NearestPointItem.Index = 2;
this.NearestPointItem.Text = "&Nearest Point";
this.NearestPointItem.Click += new System.EventHandler(this.enterNearestPointMode);
//
// ClearItem
//
this.ClearItem.Index = 2;
this.ClearItem.Text = "&Clear!";
this.ClearItem.Click += new System.EventHandler(this.onClearClick);
//
// InkHitTest
//
this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
this.ClientSize = new System.Drawing.Size(292, 245);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedSingle;
this.MaximizeBox = false;
this.Menu = this.mainMenu1;
this.Name = "InkHitTest";
this.Text = "Tablet PC InkHitTest Sample";
this.Load += new System.EventHandler(this.InkHitTest_Load);
this.Paint += new System.Windows.Forms.PaintEventHandler(this.InkHitTestForm_Paint);
this.MouseMove += new System.Windows.Forms.MouseEventHandler(this.onMouseMove);
}
#endregion
#region Standard Template Code
///
/// The main entry point for the application.
///
[STAThread]
static void Main()
{
Application.Run(new InkHitTest());
}
#endregion
// --------------- Form Events ---------------
///
/// Event Handler from Form Load Event
/// Setup the ink collector for collection
///
/// The control that raised the event.
/// The event arguments.
private void InkHitTest_Load(object sender, System.EventArgs e)
{
//
// Setup some useful objects for drawing
//
activePen = blackPen = new Pen(Color.Black, 3);
redPen = new Pen(Color.Red, 3);
invalidateRect = new Rectangle(0,0,0,0);
//
// Create the InkCollector, and turn it on
//
ic = new InkCollector(Handle); // attach it to the form's frame window
// default to inking mode
mode = ApplicationMode.Ink;
// turn the collector on
ic.Enabled = true;
}
///
/// Event Handler from Form Mouse Move Event
/// Handle Mouse/Pen movement. If we are not in ink mode, then we will
/// be doing either nearest point or hit test computations.
///
/// The control that raised the event.
/// The event arguments.
private void onMouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
{
// Update the location of the pen
penPt.X = e.X;
penPt.Y = e.Y;
if ( mode == ApplicationMode.HitTest)
{
handleHitTest(e);
}
else if (mode == ApplicationMode.NearestPoint)
{
handleNearestPoint(e);
}
}
///
/// Event Handler from Form Paint Event
///
/// This sample has a very straightforward repaint algorithm. We "know" at
/// all times how to paint the control. Since AutoRedraw is on by default,
/// the ink will take care of painting itself.
///
/// That leaves just repaints of the circle hit test cursor or the nearest point
/// line. To simplify redraw, we just remember a "bounding box" for the area that
/// we paint in the invalidateRect member variable. We will invalidate this region
/// each time something new happens.
///
///
/// The control that raised the event.
/// The event arguments.
private void InkHitTestForm_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if( mode == ApplicationMode.HitTest)
{
e.Graphics.DrawEllipse(activePen, penPt.X - HitSize/2, penPt.Y - HitSize/2, HitSize, HitSize);
}
else if( mode == ApplicationMode.NearestPoint )
{
e.Graphics.DrawLine(redPen, penPt, nearestPt);
}
}
///
/// Event Handler from Form Paint Event
/// Clears all of the ink in the form
///
/// The control that raised the event.
/// The event arguments.
private void onClearClick(object sender, System.EventArgs e)
{
// Retrieve all strokes in the ink object
Strokes strokesToDelete = ic.Ink.Strokes;
// Check to ensure that the ink collector isn't currently
// in the middle of a stroke before clearing the ink.
// Deleting a stroke that is currently being collected
// will result in an error condition.
if (!ic.CollectingInk)
{
// Delete the strokes from the ink object
ic.Ink.DeleteStrokes(strokesToDelete);
// Put the application in ink mode
enterInkMode(sender, e);
//go ahead and redraw the form, now that it has been cleared.
Refresh();
}
else
{
MessageBox.Show("Cannot clear ink while the ink collector is busy");
}
}
///
/// Event Handler from Mode->Ink Menu Item
/// Helper function to enter Ink mode.
/// In this mode, the user can draw ink.
///
/// The control that raised the event.
/// The event arguments.
private void enterInkMode(object sender, System.EventArgs e)
{
clearCheck();
mode = ApplicationMode.Ink;
InkItem.Checked = true;
ic.Enabled = true;
Invalidate(invalidateRect);
}
///
/// Event Handler from Mode->Hit Test Menu Item
/// Helper function to enter HitTest mode.
/// In this mode, we highlight the cursor when it is over
/// an ink stroke.
///
/// The control that raised the event.
/// The event arguments.
private void enterHitTestMode(object sender, System.EventArgs e)
{
if (!ic.CollectingInk)
{
Cursor = System.Windows.Forms.Cursors.Cross;
// Turn off the ink collector and set flags enable the hit
// test code.
clearCheck();
mode = ApplicationMode.HitTest;
HitTestItem.Checked = true;
ic.Enabled = false;
Invalidate(invalidateRect);
}
else
{
// If user is actively inking, we cannot disable the collector.
MessageBox.Show("Cannot switch to HitTest mode while collecting ink.");
}
}
///
/// Event Handler from Mode->Nearest Point Menu Item
/// Helper function to enter NearestPoint mode.
/// In this mode, we draw a line to the nearest point in the ink
/// from the current location of the cursor.
///
/// The control that raised the event.
/// The event arguments.
private void enterNearestPointMode(object sender, System.EventArgs e)
{
if (!ic.CollectingInk)
{
Cursor = System.Windows.Forms.Cursors.Default;
// Turn off the ink collector and set flags to enable
// the nearest point code
clearCheck();
mode = ApplicationMode.NearestPoint;
NearestPointItem.Checked = true;
ic.Enabled = false;
Invalidate(invalidateRect);
}
else
{
// If user is actively inking, we cannot disable the collector.
MessageBox.Show("Cannot switch to Nearest Point mode while collecting ink.");
}
}
///
/// Event Handler from File->Exit Menu Item
///
/// The control that raised the event.
/// The event arguments.
private void miExit_Click(object sender, System.EventArgs e)
{
// Although not required, it is good practice to disable
// ink collection before exiting
ic.Enabled = false;
Application.Exit();
}
// --------------- Helper Methods ---------------
///
/// small helper method to clear the Mode menu
/// check marks
///
private void clearCheck()
{
switch(mode)
{
case ApplicationMode.Ink:
InkItem.Checked = false;
break;
case ApplicationMode.HitTest:
HitTestItem.Checked = false;
break;
case ApplicationMode.NearestPoint:
NearestPointItem.Checked = false;
break;
}
mode = ApplicationMode.None;
}
///
/// Helper method implementing the hit test functionality
///
/// The mouse event args from MouseMove
private void handleHitTest(System.Windows.Forms.MouseEventArgs e)
{
// remember two points:
// 1. the location of the pen
// 2. the location that is HitSize/2 pixels across from the pen
// location. This will be the radius of our hit test area.
Point pt1 = new Point(e.X, e.Y);
Point pt2 = new Point(e.X + HitSize/2, e.Y);
// Convert these points into ink space coordinates. Recall that ink is
// kept in a high dpi "ink space" by default. Thus, we need to map
// screen pixel locations to their corresponding locations in ink
// space. The distance between the X coordinates will be used to compute
// the ink space radius of the hit test circle. We can make this simplification
// as long as the renderer has a scalar transformation (it wouldn't work if a
// shear or rotation transformation had been applied to the renderer).
//
// For simplicity, we are assuming "square" pixels in this example. As a result,
// the hit test 'circle' will really be an ellispe on most monitors. Applications
// wishing to draw true circles, etc. will want to take the screen aspect ratio
// into account for their pixel computations.
using (Graphics g = CreateGraphics())
{
ic.Renderer.PixelToInkSpace(g, ref pt1);
ic.Renderer.PixelToInkSpace(g, ref pt2);
}
// Retrieve the strokes (if any) that are either completely inside or
// intersected by the circular region around the pen.
//
// Note that this method takes the stroke's drawing attributes into account
// when it computes the intersection, including the pen width, Bezier smoothing,
// and shape of the pen tip.
Strokes strokes = ic.Ink.HitTest(pt1, (float)(pt2.X - pt1.X));
if( strokes.Count > 0 )
{
activePen = redPen;
}
else
{
activePen = blackPen;
}
// Clear out the previous drawing and trigger a draw
Invalidate(invalidateRect);
invalidateRect.X = e.X - HitSize/2;
invalidateRect.Y = e.Y - HitSize/2;
invalidateRect.Width = invalidateRect.Height = HitSize;
// Since the hit test circle has thickness, it is necessary
// to inflate the invalidation area to account for it (the
// invalidation area is the hit test area + the pen width).
invalidateRect.Inflate((int)activePen.Width, (int)activePen.Width);
Invalidate(invalidateRect);
}
///
/// Helper function implementing the nearest point functionality
///
/// The mouse event args from MouseMove
private void handleNearestPoint(System.Windows.Forms.MouseEventArgs e)
{
using (Graphics g = CreateGraphics())
{
// Remember pen location
Point inkPenPt = new Point(e.X, e.Y);
// Convert the pen location into a location in ink space
ic.Renderer.PixelToInkSpace(g, ref inkPenPt);
// Get the nearest point. NearestPoint will return the
// stroke *and* the index of the actual location within
// the stroke.
//
// The location is represented as a floating point value.
// 3.3 for example, would indicate that the nearest point
// is 30% along the vector between point index 3 and point
// index 4 in the stroke.
float fIndex;
Stroke stroke = ic.Ink.NearestPoint(inkPenPt, out fIndex);
// Provided that the stroke isn't null, use the fIndex to
// compute the ink space coordinates of the nearest point.
if( stroke != null )
{
// If the findex lies directly over one of the stroke's
// points, retrieve the coordinates of this point.
if ((int)fIndex == fIndex)
{
Point pt = stroke.GetPoint((int)fIndex);
nearestPt = new Point(pt.X, pt.Y);
}
// Otherwise, it is necessary to approximate the coordinates
// of the stroke using the points on the stroke that bound
// the nearest point findex
else
{
// Retrieve the two points on the stroke on either side of
// the nearest point findex.
Point[] pts = stroke.GetPoints((int)fIndex, 2);
// Since we already handled the case where the findex was directly
// over one of the stroke's points, the call above should always
// return two points.
if( pts.Length == 2 )
{
// To compute the coordinates of the nearest point from
// the findex: take the difference between the two points,
// scale it by the percentage offset, and then add the
// percentage offset back in. This yeilds the following formula:
//
// X1 * p + X0 * q
// Y1 * p + Y0 * q
//
// Where:
// p == percentage offset from point 1
// q == percentage offset from point 2 (p-1)
//
float p = fIndex - (int)fIndex;
float q = 1F - p;
nearestPt = new Point((int)((float)(pts[1].X) * p + (float)(pts[0].X) * q),
(int)((float)(pts[1].Y) * p + (float)(pts[0].Y) * q ));
}
}
}
else
{
// if there is no ink on the page, just reference ourselves.
nearestPt = inkPenPt;
}
// Now that we have the ink space coordinates of the nearest
// point, convert it back into pixel coordinates. Again, we
// assume perfectly square pixels in this example for simplicity.
ic.Renderer.InkSpaceToPixel(g, ref nearestPt);
// Clear out the previous drawing and trigger a draw
Invalidate(invalidateRect);
invalidateRect.X = Math.Min(e.X, nearestPt.X);
invalidateRect.Y = Math.Min(e.Y, nearestPt.Y);
invalidateRect.Width = Math.Abs(e.X - nearestPt.X);
invalidateRect.Height = Math.Abs(e.Y - nearestPt.Y);
// Since the red nearest point line has thickness, it is
// necessary to inflate the invalidation area to account for it
// (the invalidation area is the rectangle around the nearest point
// line + the pen width).
invalidateRect.Inflate((int)redPen.Width, (int)redPen.Width);
Invalidate(invalidateRect);
}
}
}
}