TabletPC Development Gotchas Part 6: InkCanvas Element Selection/Move/Resize

WPF's InkCanvas element provides a lot of built-in functionality for several common, ink-related tasks like stylus gesture recognition, point and stroke erase, as well as the selection, resizing and moving of ink strokes. The key to those features is the 'EditingMode' property - which is nicely demonstrated in the InkCanvas EditingModes SDK sample (btw, it also demonstrates an implementation of an Undo/Redo stack for ink operations).

One little known, yet very cool feature is not demonstrated in this sample, though: The selection, resizing and moving functioality (i.e. EditingMode="Select") can not only be applied to ink strokes, but also to child elements of the InkCanvas. This can be very handy if you want to build, for example, a note-taking application that can also host text, pictures and other content besides the handwritten ink. You can then use the 'Select" mode to let the user re-arrange and resize all their content.

The "gotcha" I wanted to point in this blog post is a limitation in this feature: Child elements in an InkCanvas may be positioned in a variety of different ways: for example you can position them by setting a "Margin" or by setting any combination of "Canvas.Left/Right/Top/Bottom" attached properties. InkCanvas's selection/moving/resizing feature, however, only works on elements that have absolute positioning set via "Canvas.Left/Top".

Let's look at the following example markup:

<Window x:Class="InkCanvasElementSelection.Window1"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    Title="InkCanvasElementSelection"

    ResizeMode="NoResize"

    Height="450" Width="450">

  <Window.Resources>

    <Style TargetType="Rectangle">

      <Setter Property="Width" Value="100"/>

      <Setter Property="Height" Value="100"/>

    </Style>

  </Window.Resources>

  <Grid>

    <InkCanvas Name="inkCanvas" EditingMode="Select"

               Background="LightGoldenrodYellow"

               SelectionChanging="OnSelectionChanging">

     

      <!-- InkCanvas' Child Elements -->

      <Rectangle InkCanvas.Left="20" InkCanvas.Top="20" Fill="Blue"/>

      <Rectangle InkCanvas.Right="80" InkCanvas.Bottom="30" Fill="Green"/>

      <Rectangle Margin="50,150,20,20" Fill="Orange"/>

      <Rectangle Fill="Brown">

        <Rectangle.RenderTransform>

          <TranslateTransform X="280" Y="100"/>

        </Rectangle.RenderTransform>

      </Rectangle>

     

    </InkCanvas>

  </Grid>

</Window>

Note that the markup subscribes to the 'SelectionChanging' event on the InkCanvas. Let's leave the implementation of that event handler empty for now to demonstrate the limitation. Also note that the four rectangles use different ways to position themselves on the InkCanvas. Only the blue rectangle uses the "Canvas.Left/Top" attached properties for positioning.

Now you can use the lasso tool to select one or more elements, and then try to move and resize them. You will notice that it only works correctly for the blue rectangle (because that one has been positioned via "Canvas.Left/Top"). For the other rectangles you won't get the desired move or resize results.

Now what can I do if there are child elements that have not been positioned via "Canvas.Left/Top"? Here is a workaround to make this feature work even for those elements: In the 'SelectionChanging' event - which fires whenever you change what content is selected - you can walk the list of child elements and ensure they are positioned via "Canvas.Left/Top". Here is a piece you can add to the above sample to demonstrate this approach:

void OnSelectionChanging(object sender, InkCanvasSelectionChangingEventArgs e)

{

    ReadOnlyCollection<UIElement> elements = e.GetSelectedElements();

    foreach (UIElement element in elements)

    {

        // obtain actual location of element relative to InkCanvas

        Point locationInInkCanvas = element.TranslatePoint(new Point(0, 0),

                                                           inkCanvas);

    // set the location via Left/Top properties

        element.SetValue(InkCanvas.LeftProperty, locationInInkCanvas.X);

        element.SetValue(InkCanvas.TopProperty, locationInInkCanvas.Y);

        // un-set right/bottom properties

        element.SetValue(InkCanvas.RightProperty, double.NaN);

        element.SetValue(InkCanvas.BottomProperty, double.NaN);

        // re-translate any render transform

        Matrix matRender = element.RenderTransform.Value;

        matRender.Translate(-matRender.OffsetX, -matRender.OffsetY);

        element.RenderTransform = new MatrixTransform(matRender);

        // set margins to zero

        if (element is FrameworkElement)

        {

            ((FrameworkElement)element).Margin = new Thickness(0d);

        }

    }

}

Now if re-run the sample you will find all rectangles from the original markup can be selected, moved and resized as expected:

Full source code of this example is included in the attached Visual Studio project.

InkCanvasElementSelection.zip