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="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 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="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.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 (8)
  1. Henry has posted a sample on Adorners, showing how to create and use them, and how to leverage the power…

  2. Putting Constants in your XAML File? x:Static Is Your Friend.

    Building an Avalon application: Part…

  3. Un adorner in Avalon (chiamarlo WPF mi &#232; ancora ostico :-)) &#232; un elemento che pu&#242; essere agganciato a…

  4. gedw99 says:

    the method AdornerDecorator.GetAdornerLayer fails,

    i am using the January CTP release.

    G

Comments are closed.

Skip to main content