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:
AdornerThis is a base Adorner from which you will need to subclass.
AdornerLayerThe AdornerLayer can be thought of as a plane in which the Adorners are drawn.
AdornedElementThe AdornedElement is the one to which the Adorner has been applied.
This ExampleIn 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="http://schemas.microsoft.com/winfx/avalon/2005" xmlns:x="http://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 TogetherI 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="http://schemas.microsoft.com/winfx/avalon/2005" xmlns:x="http://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="http://schemas.microsoft.com/winfx/avalon/2005" xmlns:x="http://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.csusing 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;
//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; }
}}