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

Comments

  • Anonymous
    March 27, 2008
    Excuse the stupid question - it says full source code is included in the attached VS project. Where is the attachment? Thanks

  • Anonymous
    March 29, 2008
    Oops - sorry about that. It's there now. Thanks, Stefan Wick

  • Anonymous
    July 01, 2008
    Hi Stefan, There also seems to be a problem with the ActualHeight property of an inkCanvas control. If you place an inkCanvas in a grid the xaml below reports an ActualHeight of 250 instead of 74 with .NET 3.5 and Vista SP1. <Window x:Class="HeightTest.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    Title="Window1" Height="300" Width="300">    <Grid Name="grid1">        <Grid.RowDefinitions>            <RowDefinition Height="74*" />            <RowDefinition Height="188*" />        </Grid.RowDefinitions>        <Canvas Name="inkCanvas1" Grid.Row="0"/>    </Grid> </Window>        void Window1_LayoutUpdated(object sender, EventArgs e)        {            Console.WriteLine("Height {0}", inkCanvas1.ActualHeight);        } However if you simply change the InkCanvas to a Canvas, the height is correctly reported as the height of the containing grid row (74). Any ideas for obtaining the accurate height of the inkCanvas? Robert

  • Anonymous
    September 30, 2008
    I've noticed that a richtextbox that has a margin set does not get moved or resized properly even if the InkCanvas.LeftProperty and the InkCanvas.TopProperty are set. It seems that these values get reset  to their values minus the top and left margin values.