// 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: InkDividerForm.cs // Simple Ink Divider Sample Application // // This sample is based on the Ink Collection sample. An InkDivider // is created and asked to do layout analysis. In Automatic Layout // Analysis mode (accessible through Mode->Automatic Layout Analysis // menu item) the Ink Divider is invoked whenever the ink is changed. // Otherwise, the Ink Division can be invoked through File->Divide // menu item. In either case, the default recognizer is used, if // available. The divide method is called. The UI is then updated by // drawing a bounding rectangle around each parsed unit. Besides using // different colors, these rectangles are inflated by different amounts // to ensure that none of the rectangles is obscured by others. // The following table specifies the color and inflation for each units: // // |-----------|-----------|-----------| // | Unit | Color | Inflation | // |-----------|-----------|-----------| // | Word | Green | 1 | // |-----------|-----------|-----------| // | Line | Magenta | 3 | // |-----------------------|-----------| // | Paragraph | Blue | 5 | // |-----------------------|-----------| // | Drawing | Red | 1 | // |-----------------------------------| // // The application has the capability of erasing strokes. Two menu items // Mode->Ink and Mode->Erase are provided to switch between inking and // erasing modes. As the new ink is added or deleted to or from the Ink // object, the ink divider is kept updated. It makes InkDivider.Divide() // method to work fast. // The menu item for Mode->Automatic Layout Analysis is checked by default. // With this option checked, the InkCollector object's Stroke and StrokeDeleted // event handlers call this method every time a stroke is made or deleted. With // more than a few strokes present, the call to the Divider object's Divide // method creates a noticeable delay. In practice, call the Divide method only // when you need the division result. // // The features used are: InkDivider, InkOverlay, Erasing Ink, // InkCollectorStrokeEventHandler, InkOverlayStrokesDeletingEventHandler, // InkOverlayStrokesDeletedEventHandler and InkSpaceToPixel. // //-------------------------------------------------------------------------- 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.InkDivider { /// /// InkDivider sample application form class. /// public class InkDividerForm : System.Windows.Forms.Form { // Declare the Ink Overlay object private InkOverlay myInkOverlay = null; // Declare the ink divider object private Divider myInkDivider = null; // Collection of Bounding Boxes for words, drawings, lines and paragraphs Rectangle[] myWordBoundingBoxes; Rectangle[] myDrawingBoundingBoxes; Rectangle[] myLineBoundingBoxes; Rectangle[] myParagraphBoundingBoxes; #region Standard Template Code private System.Windows.Forms.MainMenu mainMenu; private System.Windows.Forms.Panel DrawArea; private System.Windows.Forms.StatusBar statusBar; private System.Windows.Forms.StatusBarPanel statusBarPanelDummy; private System.Windows.Forms.StatusBarPanel statusBarPanelDrawing; private System.Windows.Forms.StatusBarPanel statusBarPanelParagraph; private System.Windows.Forms.StatusBarPanel statusBarPanelWord; private System.Windows.Forms.StatusBarPanel statusBarPanelLine; private System.Windows.Forms.MenuItem miFile; private System.Windows.Forms.MenuItem miDivide; private System.Windows.Forms.MenuItem miExit; private System.Windows.Forms.MenuItem miMode; private System.Windows.Forms.MenuItem miInk; private System.Windows.Forms.MenuItem miErase; private System.Windows.Forms.MenuItem miAutomaticLayoutAnalysis; private System.Windows.Forms.MenuItem miSeparate; /// /// Required designer variable. /// private System.ComponentModel.Container components = null; #endregion public InkDividerForm() { #region Standard Template Code // // Required for Windows Form Designer support // InitializeComponent(); // // Add any constructor code after InitializeComponent call to be added here // #endregion } #region Standard Template Code /// /// Clean up any resources being used. /// protected override void Dispose( bool disposing ) { if( disposing ) { if (components != null) { components.Dispose(); } // dispose the ink overlay's resources if (myInkOverlay != null) { myInkOverlay.Dispose(); } // dispose the divider's resources if (myInkDivider != null) { // dispose the recognizer context that we associated // with the divider if (myInkDivider.RecognizerContext != null) { myInkDivider.RecognizerContext.Dispose(); } // dispose the strokes that we associated // with the divider if (myInkDivider.Strokes != null) { myInkDivider.Strokes.Dispose(); } myInkDivider.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.mainMenu = new System.Windows.Forms.MainMenu(); this.miFile = new System.Windows.Forms.MenuItem(); this.miDivide = new System.Windows.Forms.MenuItem(); this.miExit = new System.Windows.Forms.MenuItem(); this.miMode = new System.Windows.Forms.MenuItem(); this.miInk = new System.Windows.Forms.MenuItem(); this.miErase = new System.Windows.Forms.MenuItem(); this.miSeparate = new System.Windows.Forms.MenuItem(); this.miAutomaticLayoutAnalysis = new System.Windows.Forms.MenuItem(); this.DrawArea = new System.Windows.Forms.Panel(); this.statusBar = new System.Windows.Forms.StatusBar(); this.statusBarPanelDummy = new System.Windows.Forms.StatusBarPanel(); this.statusBarPanelWord = new System.Windows.Forms.StatusBarPanel(); this.statusBarPanelLine = new System.Windows.Forms.StatusBarPanel(); this.statusBarPanelParagraph = new System.Windows.Forms.StatusBarPanel(); this.statusBarPanelDrawing = new System.Windows.Forms.StatusBarPanel(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelDummy)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelWord)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelLine)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelParagraph)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelDrawing)).BeginInit(); this.SuspendLayout(); // // mainMenu // this.mainMenu.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.miFile, this.miMode}); // // miFile // this.miFile.Index = 0; this.miFile.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.miDivide, this.miExit}); this.miFile.Text = "&File"; // // miDivide // this.miDivide.Index = 0; this.miDivide.Text = "&Divide"; this.miDivide.Click += new System.EventHandler(this.miDivide_Click); // // miExit // this.miExit.Index = 1; this.miExit.Text = "E&xit"; this.miExit.Click += new System.EventHandler(this.miExit_Click); // // miMode // this.miMode.Index = 1; this.miMode.MenuItems.AddRange(new System.Windows.Forms.MenuItem[] { this.miInk, this.miErase, this.miSeparate, this.miAutomaticLayoutAnalysis}); this.miMode.Text = "&Mode"; // // miInk // this.miInk.Index = 0; this.miInk.Text = "&Ink"; this.miInk.Click += new System.EventHandler(this.miInk_Click); // // miErase // this.miErase.Index = 1; this.miErase.Text = "&Erase"; this.miErase.Click += new System.EventHandler(this.miErase_Click); // // miSeparate // this.miSeparate.Index = 2; this.miSeparate.Text = "-"; // // miAutomaticLayoutAnalysis // this.miAutomaticLayoutAnalysis.Checked = true; this.miAutomaticLayoutAnalysis.Index = 3; this.miAutomaticLayoutAnalysis.Text = "&Automatic Layout Analysis"; this.miAutomaticLayoutAnalysis.Click += new System.EventHandler(this.miAutomaticLayoutAnalysis_Click); // // DrawArea // this.DrawArea.Anchor = (((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) | System.Windows.Forms.AnchorStyles.Left) | System.Windows.Forms.AnchorStyles.Right); this.DrawArea.BackColor = System.Drawing.SystemColors.Window; this.DrawArea.BorderStyle = System.Windows.Forms.BorderStyle.Fixed3D; this.DrawArea.Name = "DrawArea"; this.DrawArea.Size = new System.Drawing.Size(560, 420); this.DrawArea.TabIndex = 2; this.DrawArea.Paint += new System.Windows.Forms.PaintEventHandler(this.DrawArea_Paint); // // statusBar // this.statusBar.Location = new System.Drawing.Point(0, 420); this.statusBar.Name = "statusBar"; this.statusBar.Panels.AddRange(new System.Windows.Forms.StatusBarPanel[] { this.statusBarPanelDummy, this.statusBarPanelWord, this.statusBarPanelLine, this.statusBarPanelParagraph, this.statusBarPanelDrawing}); this.statusBar.RightToLeft = System.Windows.Forms.RightToLeft.No; this.statusBar.ShowPanels = true; this.statusBar.Size = new System.Drawing.Size(560, 21); this.statusBar.TabIndex = 3; // // statusBarPanelDummy // this.statusBarPanelDummy.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Spring; this.statusBarPanelDummy.BorderStyle = System.Windows.Forms.StatusBarPanelBorderStyle.None; this.statusBarPanelDummy.Width = 200; // // statusBarPanelWord // this.statusBarPanelWord.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Contents; this.statusBarPanelWord.Text = "Green: Word"; this.statusBarPanelWord.Width = 79; // // statusBarPanelLine // this.statusBarPanelLine.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Contents; this.statusBarPanelLine.Text = "Magenta: Line"; this.statusBarPanelLine.Width = 86; // // statusBarPanelParagraph // this.statusBarPanelParagraph.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Contents; this.statusBarPanelParagraph.Text = "Blue: Paragraph"; this.statusBarPanelParagraph.Width = 96; // // statusBarPanelDrawing // this.statusBarPanelDrawing.AutoSize = System.Windows.Forms.StatusBarPanelAutoSize.Contents; this.statusBarPanelDrawing.Text = "Red: Drawing"; this.statusBarPanelDrawing.Width = 83; // // InkDividerForm // this.AutoScaleBaseSize = new System.Drawing.Size(5, 13); this.ClientSize = new System.Drawing.Size(560, 441); this.Controls.AddRange(new System.Windows.Forms.Control[] { this.statusBar, this.DrawArea}); this.Menu = this.mainMenu; this.Name = "InkDividerForm"; this.Text = "Ink Divider"; this.Load += new System.EventHandler(this.InkDividerForm_Load); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelDummy)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelWord)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelLine)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelParagraph)).EndInit(); ((System.ComponentModel.ISupportInitialize)(this.statusBarPanelDrawing)).EndInit(); this.ResumeLayout(false); } #endregion #region Standard Template Code /// /// The main entry point for the application. /// [STAThread] static void Main() { Application.Run(new InkDividerForm()); } #endregion /// /// Paint method gets called everytime when the window is refreshed. /// /// /// private void DrawArea_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { // Create the Pen used to draw bounding boxes. // First set of bounding boxes drawn here are the bounding boxes of paragraphs. // These boxes are drawn with Blue pen. Pen penBox = new Pen(Color.Blue, 2); // First, draw the bounding boxes for Paragraphs if(null != myParagraphBoundingBoxes) { // Draw bounding boxes for Paragraphs e.Graphics.DrawRectangles(penBox, myParagraphBoundingBoxes); } // Next, draw the bounding boxes for Lines if(null != myLineBoundingBoxes) { // Color is Magenta pen penBox.Color = Color.Magenta; // Draw the bounding boxes for Lines e.Graphics.DrawRectangles(penBox, myLineBoundingBoxes); } // Then, draw the bounding boxes for Words if(null != myWordBoundingBoxes) { // Color is Green penBox.Color = Color.Green; // Draw bounding boxes for Words e.Graphics.DrawRectangles(penBox, myWordBoundingBoxes); } // Finally, draw the boxes for Drawings if(null != myDrawingBoundingBoxes) { // Color is Red pen penBox.Color = Color.Red; // Draw bounding boxes for Drawings e.Graphics.DrawRectangles(penBox, myDrawingBoundingBoxes); } } /// /// Event Handler from Form Load Event /// Setup the ink overlay for collection /// /// The control that raised the event. /// The event arguments. private void InkDividerForm_Load(object sender, System.EventArgs e) { // Create the ink overlay and associate it with the form myInkOverlay = new Microsoft.Ink.InkOverlay(DrawArea.Handle); // Hook event handler for the Stroke event to myInkOverlay_Stroke. // This is necessary since the application needs to pass the strokes // to the ink divider. myInkOverlay.Stroke += new InkCollectorStrokeEventHandler(myInkOverlay_Stroke); // Hook the event handler for StrokeDeleting event to myInkOverlay_StrokeDeleting. // This is necessary as the application needs to remove the strokes from // ink divider object as well. myInkOverlay.StrokesDeleting += new InkOverlayStrokesDeletingEventHandler(myInkOverlay_StrokeDeleting); // Hook the event handler for StrokeDeleted event to myInkOverlay_StrokeDeleted. // This is necessary to update the layout analysis result when automatic layout analysis // option is selected. myInkOverlay.StrokesDeleted += new InkOverlayStrokesDeletedEventHandler(myInkOverlay_StrokeDeleted); // Create the ink divider object myInkDivider = new Divider(); // Add a default recognizer context to the divider object // without adding the recognizer context, the divider would // not use a recognizer to do its word segmentation and would // have less accurate results. // Adding the recognizer context will slow down the call to // myInkDivider.Divide though. // It is possible that there is no recognizer installed on the // machine for this language. In that case the divider will // not use a recognizer to improve its accuracy. // Get the default recognizer if any try { Recognizers recognizers = new Recognizers(); myInkDivider.RecognizerContext = recognizers.GetDefaultRecognizer().CreateRecognizerContext(); } catch (InvalidOperationException) { //We are in the case where no default recognizers can be found } // The LineHeight property helps the InkDivider distinguish between // drawing and handwriting. The value should be the expected height // of the user's handwriting in ink space units (0.01mm). // Here we set the LineHeight to 840, which is about 1/3 of an inch. myInkDivider.LineHeight = 840; // Assign ink overlay's strokes collection to the ink divider // This strokes collection will be updated in the event handler myInkDivider.Strokes = myInkOverlay.Ink.Strokes; // Enable ink collection myInkOverlay.Enabled = true; // Set check for ink menu item miInk.Checked = true; } /// /// Helper function to obtain array of rectangles from the /// division result of the division type of interest. Each rectangle /// is inflated by the amount specified in the third parameter. This /// is done to ensure the visibility of all rectangles. /// /// Ink Divider division result /// Division type /// Number of Pixels by which the rectangles are inflated /// Array of rectangles containing bounding boxes of /// division type specified by divType. The rectangles are in pixel unit. private Rectangle[] GetUnitBBoxes(DivisionResult divResult, InkDivisionType divType, int inflate) { // Declare the array of rectangles to hold the result Rectangle[] divRects; // Get the division units from the division result of division type DivisionUnits units = divResult.ResultByType(divType); // If there is at least one unit, we construct the rectangles if((null != units) && (0 < units.Count)) { // We need to convert rectangles from ink units to // pixel units. For that, we need Graphics object // to pass to InkRenderer.InkSpaceToPixel method using (Graphics g = DrawArea.CreateGraphics()) { // Construct the rectangles divRects = new Rectangle[units.Count]; // InkRenderer.InkSpaceToPixel takes Point as parameter. // Create two Point objects to point to (Top, Left) and // (Width, Height) properties of ractangle. (Width, Height) // is used instead of (Right, Bottom) because (Right, Bottom) // are read-only properties on Rectangle Point ptLocation = new Point(); Point ptSize = new Point(); // Index into the bounding boxes int i = 0; // Iterate through the collection of division units to obtain the bounding boxes foreach(DivisionUnit unit in units) { // Get the bounding box of the strokes of the division unit divRects[i] = unit.Strokes.GetBoundingBox(); // The bounding box is in ink space unit. Convert them into pixel unit. ptLocation = divRects[i].Location; ptSize.X = divRects[i].Width; ptSize.Y = divRects[i].Height; // Convert the Location from Ink Space to Pixel Space myInkOverlay.Renderer.InkSpaceToPixel(g, ref ptLocation); // Convert the Size from Ink Space to Pixel Space myInkOverlay.Renderer.InkSpaceToPixel(g, ref ptSize); // Assign the result back to the corresponding properties divRects[i].Location = ptLocation; divRects[i].Width = ptSize.X; divRects[i].Height = ptSize.Y; // Inflate the rectangle by inflate pixels in both directions divRects[i].Inflate(inflate, inflate); // Increment the index ++i; } } // Relinquish the Graphics object } else { // Otherwise we return null divRects = null; } // Return the Rectangle[] object return divRects; } /// /// Helper function that calls Ink Divider to perform the ink division. /// This function is called by File->Divide menu handler and strokes /// event handler. /// private void DivideInk() { // Ink Divider produces result based on its own Strokes object // Invoke the Ink Divider DivisionResult divResult = myInkDivider.Divide(); // Call helper function to get the bounding boxes for Words // Rectangles are inflated by 1 pixel in all direction to // avoid overlapping with stroke myWordBoundingBoxes = GetUnitBBoxes(divResult, InkDivisionType.Segment, 1); // Call helper function to get the bounding boxes for Lines // Rectangles are inflated by 3 pixels in all directions myLineBoundingBoxes = GetUnitBBoxes(divResult, InkDivisionType.Line, 3); // Call helper function to get the bounding boxes for Paragraphs // Rectangles are inflated by 5 pixels in all directions myParagraphBoundingBoxes = GetUnitBBoxes(divResult, InkDivisionType.Paragraph, 5); // Call helper function to get the bounding boxes for Drawings // The rectangles are inflated by 1 pixel in all directions myDrawingBoundingBoxes = GetUnitBBoxes(divResult, InkDivisionType.Drawing, 1); // Update the form to reflect these changes DrawArea.Refresh(); } /// /// Event handler for File->Divide menu item /// /// The control that raised the event. /// The event arguments. private void miDivide_Click(object sender, System.EventArgs e) { // Call DivideInk to perform the ink division DivideInk(); } /// /// Event Handler from Ink Overlay's Stroke event /// This event is fired when a new stroke is drawn. /// In this case, it is necessary to update the ink divider's /// strokes collection. The event is fired even when the eraser stroke is created. /// The event handler must filter out the eraser strokes. /// /// The control that raised the event. /// The event arguments. private void myInkOverlay_Stroke(object sender, InkCollectorStrokeEventArgs e ) { // Filter out the eraser stroke. if(InkOverlayEditingMode.Ink == myInkOverlay.EditingMode) { // Add the new stroke to the ink divider's strokes collection myInkDivider.Strokes.Add(e.Stroke); if(miAutomaticLayoutAnalysis.Checked) { // Call DivideInk DivideInk(); // Repaint the screen to reflect the change DrawArea.Refresh(); } } } /// /// Event Handler for Ink Overlay's StrokeDeleting event. /// This event is fired when a set of stroke is about to be deleted. /// The stroke should also be removed from the ink divider's /// stroke collection as well /// /// The control that raised the event /// The event arguments void myInkOverlay_StrokeDeleting(object sender, InkOverlayStrokesDeletingEventArgs e) { // Remove the strokes to be deleted from the ink divider's stroke collection myInkDivider.Strokes.Remove(e.StrokesToDelete); } /// /// Event handler for Ink Overlay's StrokeDeleted event. /// This event is fired when the set of strokes were actually deleted. /// DivideInk method is called to analyze the current layout. /// /// The control that raised the event /// The event argument void myInkOverlay_StrokeDeleted(object sender, System.EventArgs e) { // If automatic layout analysis is turned on, call DivideInk if(miAutomaticLayoutAnalysis.Checked) { // Call Divide Ink DivideInk(); // Repaint the screen to reflect the change DrawArea.Refresh(); } } /// /// Event handler for File->Exit menu item /// /// The control that raised the event. /// The event arguments. private void miExit_Click(object sender, System.EventArgs e) { this.Close(); } /// /// Event handler for Mode->Ink menu item /// /// The control that raised the event. /// The event arguments. private void miInk_Click(object sender, System.EventArgs e) { // Turn on the inking mode myInkOverlay.EditingMode = InkOverlayEditingMode.Ink; // Update the state of the Ink and Erase menu items miInk.Checked = true; miErase.Checked = false; // Update the UI this.Refresh(); } /// /// Event handler for Mode->Erase menu item /// /// The control that raised the event. /// The event argument. private void miErase_Click(object sender, System.EventArgs e) { // Turn on the ink deletion mode myInkOverlay.EditingMode = InkOverlayEditingMode.Delete; // Update the state of the Ink and Erase menu items miInk.Checked = false; miErase.Checked = true; // Update the UI this.Refresh(); } /// /// Event handler for Mode->AutomaticLayoutAnalysis menu item. /// /// The control that raised the event. /// The event argument. private void miAutomaticLayoutAnalysis_Click(object sender, System.EventArgs e) { // Toggle the check on the menu item. miAutomaticLayoutAnalysis.Checked = !miAutomaticLayoutAnalysis.Checked; // Update the window. this.Refresh(); } } }