Adorners in Avalon

I’ve had a lot of questions about Adorners lately. Guess it’s about time to post an example. First, some background.

What is an Adorner?
In Avalon, an Adorner is a UI widget that can be applied to elements to allow a user to manipulate that element - resize, rotate, move, etc. Avalon does not provide concrete Adorners but it does provide the basic infrastructure. That means that you need to write your own, which is what I show in this posting. Some quick terms:

Adorner
This is a base Adorner from which you will need to subclass.

AdornerLayer
The AdornerLayer can be thought of as a plane in which the Adorners are drawn.

AdornedElement
The AdornedElement is the one to which the Adorner has been applied.

This Example
In this example, I author a CustomResizeAdorner which is applied to the children of a Canvas.  As the name implies, the CustomResizeAdorner allows the user to resize the AdornedElement.  The application markup looks like the following.  Notice the Canvas Named mainCanvas which contains a Button, ListBox and another Canvas.  The children of mainCanvas will be what I apply the CustomResizeAdorners to.

 <Window x:Class="AvalonApplication7.Window1"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
Text="AvalonApplication7"
Loaded="StartUp">
<Canvas Name="mainCanvas">
<Button Canvas.Left="50" Canvas.Top="150">Hello world</Button>
<ListBox Canvas.Left="150" Canvas.Top="95" Height="80">
<ListBoxItem>Item 1</ListBoxItem>
<ListBoxItem>Item 2</ListBoxItem>
<ListBoxItem>Item 3</ListBoxItem>
<ListBoxItem>Item 4</ListBoxItem>
<ListBoxItem>Item 5</ListBoxItem>
</ListBox>
<Canvas Background="yellow" Width="40" Height="90" Canvas.Left="350" Canvas.Top="150"/>
</Canvas>
</Window>

I have also defined my Adorner corners with the following style and template in the application resources.

<Style TargetType="{x:Type Thumb}">
<Setter Property="Template" Value="{StaticResource AdornerTemplate}"/>
<Setter Property="Width" Value="15"/>
<Setter Property="Height" Value="15"/>
</Style>

<ControlTemplate x:Key="AdornerTemplate" TargetType="{x:Type Thumb}">
<Border Background="green" BorderThickness="1" BorderBrush="black" />
</ControlTemplate>
 

The result:
 

 After a few resizes.

Now, after some small tweaks the style for the corners, I have the following UI.

Again, after a few resizes:

The markup for the updated Adorner widgets is shown below.

<Style TargetType="{x:Type Thumb}">
<Setter Property="Template" Value="{StaticResource AdornerTemplate}"/>
<Setter Property="Width" Value="18"/>
<Setter Property="Height" Value="18"/>
</Style>

<ControlTemplate x:Key="AdornerTemplate" TargetType="{x:Type Thumb}">
<Border Background="VerticalGradient purple silver" Opacity=".65" BorderThickness="0" BorderBrush="Navy" CornerRadius="7,2,7,2">
<Border Background="VerticalGradient #DDFFDD purple" Margin="2" CornerRadius="5,2,5,2"/>
</Border>
</ControlTemplate>

Putting It All Together
I created this example on the May CTP bits using VS to create a new Avalon project. Below I have pasted in the three main files that I created for this example. I have added some rough comments to the code but the key is the CustomResizeAdorner. If you have question, feel free to contact me.

MyApp.xaml

<Application x:Class="AvalonApplication7.MyApp"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
StartingUp="AppStartingUp"
>
<Application.Resources>
<Style TargetType="{x:Type Thumb}">
<Setter Property="Template" Value="{StaticResource AdornerTemplate}"/>
<Setter Property="Width" Value="15"/>
<Setter Property="Height" Value="15"/>
</Style>
<ControlTemplate x:Key="AdornerTemplate" TargetType="{x:Type Thumb}">
<Border Background="green" BorderThickness="1" BorderBrush="black" />
</ControlTemplate>
</Application.Resources>
</Application>

Window1.xaml
<Window x:Class="AvalonApplication7.Window1"
xmlns="https://schemas.microsoft.com/winfx/avalon/2005"
xmlns:x="https://schemas.microsoft.com/winfx/xaml/2005"
Text="Adorner Example"
Width ="700"
Height="500"
Loaded="StartUp">

<Canvas Name="mainCanvas">
<Button Canvas.Left="50" Canvas.Top="150">Hello world</Button>
<ListBox Canvas.Left="150" Canvas.Top="95" Height="80">
<ListBoxItem>ListBoxItem 1</ListBoxItem>
<ListBoxItem>ListBoxItem 2</ListBoxItem>
<ListBoxItem>ListBoxItem 3</ListBoxItem>
<ListBoxItem>ListBoxItem 4</ListBoxItem>
<ListBoxItem>ListBoxItem 5</ListBoxItem>
<ListBoxItem>ListBoxItem 6</ListBoxItem>
<ListBoxItem>ListBoxItem 7</ListBoxItem>
<ListBoxItem>ListBoxItem 8</ListBoxItem>
<ListBoxItem>ListBoxItem 9</ListBoxItem>
</ListBox>
<Canvas Background="yellow" Width="40" Height="90" Canvas.Left="350" Canvas.Top="150"/>
</Canvas>
</Window>

Window1.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Input;
using System.Windows.Controls.Primitives;

namespace AvalonApplication7
{

public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
}

//Initialize the application by calling a method
//to apply adorners to all children of the respective
//Canvas.
private void StartUp(object sender, EventArgs args)
{
ApplyAdornersToCanvasChildren(mainCanvas);
}

//Iterates through the Canvas' children finding all of
//the FrameworkElements; it then applies a CustomResizeAdorner
//to each one.

void ApplyAdornersToCanvasChildren(Canvas canvas)
{
AdornerLayer al = AdornerDecorator.GetAdornerLayer(canvas);

foreach (FrameworkElement fxe in canvas.Children)
if (al.GetAdorners(fxe) == null)
al.Add(new CustomResizeAdorner(fxe));
}

}

public class CustomResizeAdorner : Adorner
{
//The visual elements used as the Adorners. The Thumb
//element is used because it takes care of handling the
//lower level mouse input.
Thumb topLeft, topRight, bottomLeft, bottomRight;

//Initialize the CustomResizeAdorner.
public CustomResizeAdorner(UIElement adornedElement) : base(adornedElement)
{
//Call a helper method to instantiate the Thumbs
//with a given Cursor.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);

//Add handlers for resizing on the bottom left and right.
//Leaving the handling of the other two corners as an exercise
//to the reader.
bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
}

//Handle resize for the bottom right adorner widget.
void HandleBottomRight(object sender, DragDeltaEventArgs args)
{
FrameworkElement fxe = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (fxe == null || hitThumb == null)
return;

EnforceSize(fxe);

//Change the size by the amount the user drags the mouse as
//long as it's larger than the width or height of an adorner, respectively.
fxe.Width = Math.Max(args.HorizontalChange + fxe.Width, hitThumb.DesiredSize.Width);
fxe.Height = Math.Max(args.VerticalChange + fxe.Height, hitThumb.DesiredSize.Height);
}

//Handle resize for the bottom left adorner widget.
void HandleBottomLeft(object sender, DragDeltaEventArgs args)
{
FrameworkElement fxe = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;

if (fxe == null || hitThumb == null)
return;

EnforceSize(fxe);

//Change the size by the amount the user drags the mouse as
//long as it's larger than the width or height of an adorner, respectively.
//Also, update the left position by the amount the user drags as long as
//it's not past the right edge minus the adorner widget width.

Canvas.SetLeft(fxe, Math.Min((double)Canvas.GetLeft(fxe) + args.HorizontalChange,(double)Canvas.GetLeft(fxe) + fxe.Width - hitThumb.DesiredSize.Width));
fxe.Width = Math.Max(fxe.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
fxe.Height = Math.Max(args.VerticalChange + fxe.Height, hitThumb.DesiredSize.Height);
}

//Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{

//w & h are the width and height of the element
//that's being adorned. These will be used to place
//the Adorner at the corners. adornerWidth &
//adornerHeight are used for placement as well.

double w = AdornedElement.DesiredSize.Width;
double h = AdornedElement.DesiredSize.Height;
double adornerWidth = this.DesiredSize.Width;
double adornerHeight = this.DesiredSize.Height;

topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
topRight.Arrange(new Rect(w - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
bottomLeft.Arrange(new Rect(-adornerWidth / 2, h - adornerHeight / 2, adornerWidth, adornerHeight));
bottomRight.Arrange(new Rect(w - adornerWidth / 2, h -adornerHeight / 2, adornerWidth, adornerHeight));

//Just using the size that the
//adorner layer was arranged at.
return finalSize;

}

//Helper code to instantiate the Thumbs, set the
//Cursor property and add the elements to the
//Visual tree.

void BuildAdornerCorner(ref Thumb cornerThumb, Cursor c)
{
if (cornerThumb != null) return;
cornerThumb = new Thumb();
cornerThumb.Cursor = c;
VisualOperations.GetChildren(this).Add(cornerThumb);
}

//This method ensures that the Widths and Heights
//are initialized. Sizing to content produces
//Width and Height values of Double.NaN. Because
//this Adorner explicitly resizes, the Width and Height
//need to be set first.
void EnforceSize(FrameworkElement fxe)
{
if (fxe.Width.Equals(Double.NaN))
fxe.Width = fxe.DesiredSize.Width;
if (fxe.Height.Equals(Double.NaN))
fxe.Height = fxe.DesiredSize.Height;
}

}
}

Comments