Updated Code Sample: Windows 8.1 Input: Ink sample in C#/Xaml
Original Code Sample: Windows 8 Input: Ink sample in C#/Xaml
In the previous posts (Drawing Api I and Drawing Api II), we mainly talked about the basics of the Windows Ink Api. The methods revised were related to the rendering of the ink strokes, saving/loading the ink strokes from and to the canvas.
Selecting Strokes
So after a long time, I had finally sometime to update the code sample to Windows 8.1 and worked a little more on the select functionality and working with different pointer devices. I wanted to share my experience while updating the ink solution. I am not going to talk about every aspect of the update but mainly the functions we are interested in. (There were some changes related to UI and ink related functionality was moved to a CanvasManager class)
For selecting the strokes created by the ink manager, I chose to create a selection rectangle (Windows.Foundation.Rect) while the user is selecting an area; and use this rectangle for both:
- Visually display the selection area
- Select the strokes in the ink manager that intersects with it
To identify the selection area, we use the existing subscriptions for Pointer events. So when the user presses the mouse (or touches the screen, will come back to it in my next post) we create an initial selection point and as the user moves the pointer we update our selection rectangle with the new position.
According to the selection rectangle, to visualize the selection area, we use a simple Rectangle (Windows.UI.Xaml.Shapes.Rectangle) with dashed borders. Once we have the visual rectangle, we can add it to our canvas.
/// <summary> /// Creates a dashed rectangle shape to show the selection bounds /// </summary> /// <returns></returns> private Rectangle RenderSelectionBound() { var selectionRectangle = new Rectangle { StrokeDashArray = new DoubleCollection {3, 5}, StrokeDashCap = PenLineCap.Round, Width = m_SelectionRectangle.Width, Height = m_SelectionRectangle.Height, StrokeThickness = 4, Stroke = new SolidColorBrush(Colors.Red), Margin = new Thickness(m_SelectionRectangle.Left, m_SelectionRectangle.Top, m_SelectionRectangle.Right, m_SelectionRectangle.Bottom) }; m_Canvas.Children.Add(selectionRectangle); return selectionRectangle; }
Here, the important thing to notice is the fact that we only need to create a new rectangle when the canvas is cleared (i.e. refreshing the canvas and redrawing the strokes) and/or there is no parent assigned to our rectangle. Technically, when the mouse is still moving and not yet released, we can simply update the current shape and update layout.
When the pointer is released, we first need to identity the selected strokes. In order to do this, I used a simple intersect between each stroke and the selection rectangle. We can improve this by adding additional checks to see if the strokes bounding rectangle is completely encompassed by the selection rectangle.
private void UpdateStrokeSelections() { var inkStrokes = m_InkManager.GetStrokes(); foreach (var inkStroke in inkStrokes) { var boundingBox = inkStroke.BoundingRect; boundingBox.Intersect(m_SelectionRectangle); // // Don't select the item if the intersecting area is empty inkStroke.Selected = boundingBox != Rect.Empty; } }
So finally when the stroke selections are in place, we can go ahead and redraw the canvas. (I am using double thickness for selected strokes while rendering them.)
Further Implementation
InkManager actually supports two different methods for selecting strokes according to the points that are passed into the method. (i.e. SelectWithLine and SelectWithPolyLine). SelectWithLine function does exactly what we are doing here, and makes a selection creating a bounding rectangle between two points.
Dragging Selected Strokes
Normal drag scenario deals with the gestures on a specific control, however, in our implementation we want to select a number of strokes and move only these selected items.
For the drag implementation, in select mode, we make (kind of) a hit test on the abstract selection rectangle; checking if the pointer pressed event occurred inside the bounds of the selected area (and if there are any strokes selected).
if (m_InkManager.GetStrokes().Any(stroke => stroke.Selected) && m_SelectionRectangle.Contains(m_SelectionInitialPoint)) { m_IsDragging = true; m_PreviousSelectionRectangle = m_SelectionRectangle; }
Once the dragging starts (note that we are taking a backup of the selection rectangle), we update the selection rectangle according to the pointer deltas; invalidating the selection visual with the new coordinates.
var currentPoint = e.GetCurrentPoint(m_Canvas).Position; var xDelta = currentPoint.X - m_SelectionInitialPoint.X; var yDelta = currentPoint.Y - m_SelectionInitialPoint.Y; m_SelectionRectangle.X += xDelta; m_SelectionRectangle.Y += yDelta;
When the user releases the pointer, we update our selection rectangle with the deltas and the original backup of the selection rectangle. And we use the MoveSelected function of the ink manager with the x and y deltas.
var currentPoint = e.GetCurrentPoint(m_Canvas).Position; var xDelta = currentPoint.X - m_SelectionInitialPoint.X; var yDelta = currentPoint.Y - m_SelectionInitialPoint.Y; m_PreviousSelectionRectangle.X += xDelta; m_PreviousSelectionRectangle.Y += yDelta; m_SelectionRectangle = m_PreviousSelectionRectangle; m_InkManager.MoveSelected(new Point(xDelta, yDelta));
Further Implementation
It would be to actually nice to change the mouse cursor according to the current function (i.e. Select or Drag). Also in the implementation, I keep on calling GetStrokes function on the ink manager. This might have performance penalties. I think a caching list can come in handy in this implementation. Another nice thing to have would be to redraw the strokes with the new coordinates inside the selection box while the user is dragging them.
Copy / Paste Selected Strokes to Clipboard
Ink manager comes with many useful functionality. There are two functions that deal with the clipboard information. (i.e. CopySelectedToClipboard and PasteFromClipboard).
So when the user selects a group of segments, he/she can use the Copy action (i.e. in the flyout that opens up with the More application bar button) to copy these strokes to clipboard. These strokes can be pasted on any application that supports the ISF (Ink Serialized Format) content type (i.e. you can paste them onto a bitmap file on paint).
public async void CopyToClipboard() { if (m_CurrentMode != CanvasMode.Select) SelectAllStrokes(); m_InkManager.CopySelectedToClipboard(); }
Pasting the clipboard data is pretty much the same. The only difference is that we have to first check if there is any data in the clipboard and the ink manager can actually retrieve the data that is in the clipboard. For instance, if you try to draw something on paint, and then try to copy/paste it to ink manage; you will get a “false” from the CanPasteFromClipboard method.
public async void CopyToClipboard() { if (m_CurrentMode != CanvasMode.Select) SelectAllStrokes(); m_InkManager.CopySelectedToClipboard(); }
NOTE: In both of these methods, if the current mode is “Select” we just deal with the selected bunch of the strokes, if the current mode is something else; we select all the strokes for the user.
In the next post, I will try to describe how I got away with the touch input device usage and how we can support multi-touch supported devices.
Happy coding everyone…