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
Anonymous
July 20, 2005
Henry has posted a sample on Adorners, showing how to create and use them, and how to leverage the power...Anonymous
August 21, 2005
Putting Constants in your XAML File? x:Static Is Your Friend.
Building an Avalon application: Part...Anonymous
September 06, 2005
Un adorner in Avalon (chiamarlo WPF mi è ancora ostico :-)) è un elemento che può essere agganciato a...Anonymous
February 22, 2006
the method AdornerDecorator.GetAdornerLayer fails,
i am using the January CTP release.
GAnonymous
June 30, 2006
PingBack from http://www.netronproject.com/netron/?p=187Anonymous
October 04, 2007
PingBack from http://wangmo.wordpress.com/2007/10/04/learning-adorner/Anonymous
June 12, 2009
PingBack from http://greenteafatburner.info/story.php?id=3703Anonymous
June 17, 2009
PingBack from http://patioumbrellasource.info/story.php?id=645