In the previous article, we were able to provide a drag & drop functionality for moving XAML Shapes. We go further: Let's give the ability to the user to move the origin or the end of the line with an anchor. This way, we would be able to change the size, the orientation, the connections of the lines.
The baseline code
The starting point? The exact same as usual, related to the article Use the UWP Inking platform as input for advanced scenarios.
<Grid>
<InkCanvas x:Name="inkCanvas" />
<!-- Canvas for displaying the "recognized" XAML Shapes -->
<Canvas x:Name="ShapesCanvas" />
</Grid>
public MainPage()
{
this.InitializeComponent();
// Initialize the InkCanvas
inkCanvas.InkPresenter.InputDeviceTypes =
Windows.UI.Core.CoreInputDeviceTypes.Mouse |
Windows.UI.Core.CoreInputDeviceTypes.Pen |
Windows.UI.Core.CoreInputDeviceTypes.Touch;
// When the user finished to draw something on the InkCanvas
inkCanvas.InkPresenter.StrokesCollected += InkPresenter_StrokesCollected;
}
private void InkPresenter_StrokesCollected(
Windows.UI.Input.Inking.InkPresenter sender,
Windows.UI.Input.Inking.InkStrokesCollectedEventArgs args)
{
InkStroke stroke = inkCanvas.InkPresenter.StrokeContainer.GetStrokes().Last();
// Action 1 = We use a function that we will implement just after to create the XAML Line
Line line = ConvertStrokeToXAMLLine(stroke);
// Action 2 = We add the Line in the second Canvas
ShapesCanvas.Children.Add(line);
// We delete the InkStroke from the InkCanvas
stroke.Selected = true;
inkCanvas.InkPresenter.StrokeContainer.DeleteSelected();
}
private Line ConvertStrokeToXAMLLine(InkStroke stroke)
{
var line = new Line();
line.Stroke = new SolidColorBrush(Windows.UI.Colors.Green);
line.StrokeThickness = 6;
// The origin = (X1, Y1)
line.X1 = stroke.GetInkPoints().First().Position.X;
line.Y1 = stroke.GetInkPoints().First().Position.Y;
// The end = (X2, Y2)
line.X2 = stroke.GetInkPoints().Last().Position.X;
line.Y2 = stroke.GetInkPoints().Last().Position.Y;
return line;
}
Adding the anchors
In the ConvertStrokeToXAMLLine method, just before returning the line object, we add a handler for the Tapped event which is raised no matter if we use the touch, mouse or pen.
line.Tapped += Line_Tapped;
The Line_Tapped will be the place to draw the anchors on the lines ends and initiate the handlers for allowing the drag & drop actions on these anchors.
1. Get the line + some cosmetics
We get the line tapped from the sender object passed as parameter for the event handler. To visually show the selected line, we can modify the stroke or the color. Here is a sample:
Line line = (Line)sender;
line.Stroke = new SolidColorBrush(Windows.UI.Colors.DarkRed);
line.StrokeThickness = 10;
2. Draw the origin and the end of the line
To display an anchor at the ends of the lines, we draw a circle by using the XAML Ellipse class with the same Height and Width. We create 2 circles: anchorOrigin and anchorEnd for respectively the origin and the end of the line. We add these circles to the canvas with ShapesCanvas.Children.Add(MyObject)
.
int size_EndLines = 25;
// Create 2 circles for the ends of the line
Ellipse anchorOrigin = new Ellipse
{
Fill = new SolidColorBrush(Windows.UI.Colors.OrangeRed),
Height = size_EndLines,
Width = size_EndLines
};
ShapesCanvas.Children.Add(anchorOrigin);
Ellipse anchorEnd = new Ellipse
{
Fill = new SolidColorBrush(Windows.UI.Colors.OrangeRed),
Height = size_EndLines,
Width = size_EndLines
};
ShapesCanvas.Children.Add(anchorEnd);
We set the position of each circle in order that its center matches the position of the end of the line. For this, we use two functions to modify the Canvas.Left and Canvas.Top attached properties of the circles:
// Put the anchors at the origin and at the end of the line
Canvas.SetLeft(anchorOrigin, line.X1 - size_EndLines / 2);
Canvas.SetLeft(anchorEnd, line.X2 - size_EndLines / 2);
Canvas.SetTop(anchorOrigin, line.Y1 - size_EndLines / 2);
Canvas.SetTop(anchorEnd, line.Y2 - size_EndLines / 2);
3. Add the event handlers for the circles
The strategy here is the exact same as described in the previous article with the Manipulation events applied to both anchors:
- The ManipulationMode property can be used to restraint the move of the shape in only on direction. In our sample, we would like to move on both axis X and Y.
- The ManipulationStarted event is raised when we start the drag & drop.
- The ManipulationDelta event gives the possibility to give visual feedback for every move during the drag & drop.
- The ManipulationCompleted event occurs when the drag & drop is finished.
Note: For the ManipulationDelta and ManipulationCompleted events, we are forced to use two different handlers because the code is modifying the position of the ends of the lines and we have to know which end (origin or end) to modify. The handler for the ManipulationStarted event is the same for both anchors.
// Enable manipulations on the anchors
anchorOrigin.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
anchorOrigin.ManipulationStarted += Anchor_ManipulationStarted;
anchorOrigin.ManipulationDelta += Anchor_Origin_ManipulationDelta;
anchorOrigin.ManipulationCompleted += Anchor_Origin_ManipulationCompleted;
anchorEnd.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
anchorEnd.ManipulationStarted += Anchor_ManipulationStarted;
anchorEnd.ManipulationDelta += Anchor_End_ManipulationDelta;
anchorEnd.ManipulationCompleted += Anchor_End_ManipulationCompleted;
4. Keep track of the selected line
We need to store some object to make the drag & drop working and to be also able to 'unselect' and 'select' a new line. Let me explain:
- The line and its initial coordinates (x1,y1) - (x2,y2) are necessary to finalize the move when the drag & drop is complete.
- The anchors of the line have to be deleted when the user click or touch another line.
To do this, we add a structure and we use it as a field that can be accessible by the code:
private struct ActiveLine
{
public Line line;
public double initialX1;
public double initialY1;
public double initialX2;
public double initialY2;
public Ellipse AnchorOrigin;
public Ellipse AnchorEnd;
}
private ActiveLine activeLine;
Then, we create method to do the initialization:
private void InitializeActiveLine(Line line, Ellipse origin, Ellipse end)
{
activeLine.line = line;
activeLine.initialX1 = line.X1;
activeLine.initialY1 = line.Y1;
activeLine.initialX2 = line.X2;
activeLine.initialY2 = line.Y2;
activeLine.AnchorOrigin = origin;
activeLine.AnchorEnd = end;
}
We use this method as the last instruction of the Line_Tapped event handler:
InitializeActiveLine(line, anchorOrigin, anchorEnd);
5. Unselect the line
This step is, in fact the very first we have to do while entering to the Line_Tapped event handler
private void Line_Tapped(object sender, TappedRoutedEventArgs e)
{
// Remove the anchor from the selected line
UnselectActiveLine();
// ...
The code of UnselectActiveLine just put back the original color/stroke of the line and more important: delete the anchors for this line because we would like to have the anchors only for the lines we select.
public void UnselectActiveLine()
{
if (activeLine.line != null
&& activeLine.AnchorOrigin!= null
&& activeLine.AnchorEnd != null)
{
activeLine.line.Stroke = new SolidColorBrush(Windows.UI.Colors.Green);
activeLine.line.StrokeThickness = 6;
ShapesCanvas.Children.Remove(activeLine.AnchorOrigin);
ShapesCanvas.Children.Remove(activeLine.AnchorEnd);
}
}
Note: This 'unselection' is the way we chose for our code. The other possibility would be to use a 'switch to modification mode' button on the UI in order to go to a different mode: We would deactivate drawing on the InkCanvas and we could create/display all anchors of all lines to allow modifications.
6. All pieces of the Line_Tapped event handler
Let's put together all we just explained. Here is the final code of the Line_Tapped method:
private void Line_Tapped(object sender, TappedRoutedEventArgs e)
{
// Remove the anchor from the selected line
UnselectActiveLine();
Line line = (Line)sender;
line.Stroke = new SolidColorBrush(Windows.UI.Colors.DarkRed);
line.StrokeThickness = 10;
int size_EndLines = 25;
// Create 2 circles for the ends of the line
Ellipse anchorOrigin = new Ellipse
{
Fill = new SolidColorBrush(Windows.UI.Colors.OrangeRed),
Height = size_EndLines,
Width = size_EndLines
};
ShapesCanvas.Children.Add(anchorOrigin);
Ellipse anchorEnd = new Ellipse
{
Fill = new SolidColorBrush(Windows.UI.Colors.OrangeRed),
Height = size_EndLines,
Width = size_EndLines
};
ShapesCanvas.Children.Add(anchorEnd);
// Put the anchors at the origin and at the end of the line
Canvas.SetLeft(anchorOrigin, line.X1 - size_EndLines / 2);
Canvas.SetLeft(anchorEnd, line.X2 - size_EndLines / 2);
Canvas.SetTop(anchorOrigin, line.Y1 - size_EndLines / 2);
Canvas.SetTop(anchorEnd, line.Y2 - size_EndLines / 2);
// Enable manipulations on the anchors
anchorOrigin.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
anchorOrigin.ManipulationStarted += Anchor_ManipulationStarted;
anchorOrigin.ManipulationDelta += Anchor_Origin_ManipulationDelta;
anchorOrigin.ManipulationCompleted += Anchor_Origin_ManipulationCompleted;
anchorEnd.ManipulationMode = ManipulationModes.TranslateX | ManipulationModes.TranslateY;
anchorEnd.ManipulationStarted += Anchor_ManipulationStarted;
anchorEnd.ManipulationDelta += Anchor_End_ManipulationDelta;
anchorEnd.ManipulationCompleted += Anchor_End_ManipulationCompleted;
InitializeActiveLine(line, anchorOrigin, anchorEnd);
}
The manipulations' events for the anchors
Initiate the manipulation
As described and explained in the previous article, we instantiate a TranslateTransform object (named dragTranslation) and we affect it to the RenderTransform property of the anchor. The goal is to apply a translation for every move during the drag & drop action.
// XAML Shapes manipulations
private TranslateTransform dragTranslation;
private void Anchor_ManipulationStarted(object sender,
ManipulationStartedRoutedEventArgs e)
{
Ellipse anchor = (Ellipse)sender;
// Initialize the transforms that will be used to manipulate the shape
dragTranslation = new TranslateTransform();
anchor.RenderTransform = dragTranslation;
anchor.Fill = new SolidColorBrush(Windows.UI.Colors.Orange);
}
Provide visual feedback
The handler for the ManipulationDelta has to know if we are handling/moving the origin or the end of the line in order to be able to move the line correctly. That is why we have two different handlers but calling the same sub method. The third parameter is a boolean indicating if we hare modifying the origin (true) or not (false):
private void Anchor_Origin_ManipulationDelta(object sender,
ManipulationDeltaRoutedEventArgs e)
{
AnchorManipulationDelta(sender, e, true);
}
private void Anchor_End_ManipulationDelta(object sender,
ManipulationDeltaRoutedEventArgs e)
{
AnchorManipulationDelta(sender, e, false);
}
So, in the AnchorManipulationDelta method,
- We first modify the dragTranslation which acts on the anchor moves. This code effectively change the coordinates x and y of the anchor corresponding to the move of the mouse, pen or mouse.
- After, we changes accordingly the position of either the origin or the end of the line.
private void AnchorManipulationDelta(object sender,
ManipulationDeltaRoutedEventArgs e,
bool OriginofLine)
{
double x = e.Delta.Translation.X;
double y = e.Delta.Translation.Y;
dragTranslation.X += x;
dragTranslation.Y += y;
if (OriginofLine)
{
activeLine.line.X1 += x;
activeLine.line.Y1 += y;
}
else
{
activeLine.line.X2 += x;
activeLine.line.Y2 += y;
}
}
Finish the manipulation
The same hack is used to identify if we did a move on the origin or on the end of the line.
private void Anchor_Origin_ManipulationCompleted(object sender,
ManipulationCompletedRoutedEventArgs e)
{
AnchorManipulationCompleted(sender, e, true);
}
private void Anchor_End_ManipulationCompleted(object sender,
ManipulationCompletedRoutedEventArgs e)
{
AnchorManipulationCompleted(sender, e, false);
}
The actions we have to perform for terminating the drag & drop are:
- 'Resetting' the translation applied to the anchor
- Getting the total translation on the X axis and Y axis with the parameter ManipulationCompletedRoutedEventArgs.Cumulative.Translation.
- Adding this translation to the original coordinates of either the origin or the end of the line in order to modify definitely its position.
- Affecting the new position of the end of the line to the field activeLine that tracks the selected line.
private void AnchorManipulationCompleted(object sender,
ManipulationCompletedRoutedEventArgs e,
bool OriginofLine)
{
Ellipse anchor = (Ellipse)sender;
anchor.RenderTransform = null;
double x = e.Cumulative.Translation.X;
double y = e.Cumulative.Translation.Y;
Canvas.SetLeft(anchor, Canvas.GetLeft(anchor) + x);
Canvas.SetTop(anchor, Canvas.GetTop(anchor) + y);
anchor.Fill = new SolidColorBrush(Windows.UI.Colors.Black);
if (OriginofLine)
{
activeLine.line.X1 = activeLine.initialX1 + x;
activeLine.line.Y1 = activeLine.initialY1 + y;
activeLine.initialX1 = activeLine.line.X1;
activeLine.initialY1 = activeLine.line.Y1;
}
else
{
activeLine.line.X2 = activeLine.initialX2 + x;
activeLine.line.Y2 = activeLine.initialY2 + y;
activeLine.initialX2 = activeLine.line.X2;
activeLine.initialY2 = activeLine.line.Y2;
}
}
Here we are!
Wrapping up
The Manipulation events are really powerful and can be applied to any XAML Shapes. In this sample, we just create two circles like anchors at the ends of the line in order to give the ability to the user to manipulate the line. We could think of building a more sophisticated UI with different buttons/shapes to apply translations, rotations or any actions on any other shapes of the application!
--
The source is available on GitHub - https://github.com/sbovo/UWP-Advanced-Inking
@sbovo for the AppConsult team.
Inking series' articles
This article is part of a series exploring concepts about inking and XAML Shapes. Here are all links:
- Use the UWP Inking platform as input for advanced scenarios
- Handling zoom in Inking applications
- Turning to the dark side of inking = UnprocessedInput
- Free your mind: Start manipulating XAML Shapes
- XAML Shapes manipulation level up ⇐ You are here
References
- Source code of this article - https://github.com/sbovo/UWP-Advanced-Inking
- Ellipse Class - https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Shapes.Ellipse
- Manipulation events https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh465387(v=win.10)#using-manipulation-events
- ManipulationMode https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.manipulationmode
- UIElement.ManipulationStarted - https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.manipulationstarted
- UIElement.ManipulationDelta - https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.manipulationdelta
- UIElement.ManipulationCompleted - https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.manipulationcompleted
- TranslateTransform - https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Media.TranslateTransform