Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Adorners in WPF

0.00/5 (No votes)
10 Apr 2007 1  
An article on adorners in WPF

Introduction

In WPF, an Adorner is special FrameworkElement that can be bounded to UIElement to allow a user to manipulate that element. By manipulate we mean:

  • Adding functional handles to a UIElement that enable the user to manipulate the element in some way (resize, rotate, reposition, etc.).
  • Provide visual feedback to indicate various states, or in response to various events.
  • Overlay visual decorations on a UIElement.
  • Visually mask or override part or the whole of UIElement.

WPF does not provide concrete adorners but it does provide the basic infrastructure. That means that you need to write your own special Adorner class.

A base infrastructure consists of next classes:

  • Adorner
    This is a base Adorner from which you will need to subclass.
  • AdornerLayer
    The AdornerLayer can be considered as a plane in which the Adorners are drawn.
  • AdornerDecorator
    Defines the location in visual tree of an AdornerLayer. Note: the Windows class already has it in the visual tree. It adds the default adorner layer above all other controls in the window.

Adorning a Single UIElement

To bind an adorner to a particular UIElement, follow these steps:

  • Call the static method GetAdornerLayer of the AdornerLayer class to get an AdornerLayer object for the UIElement to be adorned. GetAdornerLayer walks up the visual tree, starting at the specified UIElement, and returns the first adorner layer it found. (If no adorner layers are found, the method returns null.)
  • Create new instance of the adorner you need and pass it the instance of the UIElement you need to adorn.
  • Call the Add method of the AdornerLayer, and pass it your newly created adorner to bind it to the target UIElement.

The following example binds SomeAdorner (our own Adorner) to a TextBox named "myTextBox".

myAdornerLayer = AdornerLayer.GetAdornerLayer(myTextBox); 
myAdornerLayer.Add(new SomeAdorner(myTextBox));

Adorning the Children of a Panel

To bind an adorner to the children of a Panel, follow these steps:

  1. Call the static method GetAdornerLayer to find an adorner layer for the element children of which are to be adorned.
  2. Enumerate the children of the parent element and call the Add method to bind an adorner to each child element.

The following example binds a SomeAdorner (our own Adorne) to the children of a StackPanel named "myStackPanel".

foreach (UIElement toAdorn in myStackPanel.Children) 
    myAdornerLayer.Add(new SomeAdorner(toAdorn));

Rendering

Adorners are rendered in an AdornerLayer, which is a rendering surface that is always on top of the adorned element or a collection of adorned elements. The rendering of adorner is independent of rendering the UIElement the to which the adorner is bound. An adorner is typically positioned relatively to the element to which it is bound, using the standard 2-D coordinate origin located at the upper-left of the adorned element. Anything placed in the adorner layer is rendered on top of the rest of the visual elements. In other words, adorners are always visually on top and cannot be overridden using z-order.

It is important to note that adorners do not include any inherent rendering behavior (by default the Adorner class does not render anything), ensuring that an adorner's rendering is the responsibility of the adorner's implementer.

This means that the adorner's rendering should be implemented by:

  1. Overriding the OnRenderSizeChanged method and drawing adorner's visual content using the DrawingContext object methods.
  2. Providing a visual children collection. Note: in this case you should override the GetVisualChild method and the VisualChildrenCount property)

The following examples show these approaches.

The first example adorner simply adorns the corners of a UIElement with circles.

// Adorners must subclass the abstract base class Adorner.

public class SimpleCircleAdorner : Adorner
{
    // Be sure to call the base class constructor.

    public SimpleCircleAdorner(UIElement adornedElement)
        : base(adornedElement){}
        
    // A common way to implement an adorner's rendering behavior is to 

    // override the OnRender method, which is called by the layout system as 

    // part of a rendering pass.

    protected override void OnRender(DrawingContext drawingContext)
    {
        Rect adornedElementRect = new Rect( this.AdornedElement.DesiredSize );
        
        // Some arbitrary drawing implements.

        SolidColorBrush renderBrush = new SolidColorBrush( Colors.Green );
        renderBrush.Opacity = 0.2;
        Pen renderPen = new Pen( new SolidColorBrush( Colors.Navy ), 1.5 );
        double renderRadius = 5.0;
        
        // Draw a circle at each corner.

        drawingContext.DrawEllipse( renderBrush, renderPen, 
            adornedElementRect.TopLeft, renderRadius, renderRadius );
        drawingContext.DrawEllipse( renderBrush, renderPen, 
            adornedElementRect.TopRight, renderRadius, renderRadius );
        drawingContext.DrawEllipse( renderBrush, renderPen, 
            adornedElementRect.BottomLeft, renderRadius, renderRadius );
        drawingContext.DrawEllipse( renderBrush, renderPen, 
            adornedElementRect.BottomRight, renderRadius, renderRadius );
    }
}

The following image shows the SimpleCircleAdorner applied to a TextBox.

<Screenshot - image1.png

Sources of example above.

Second approach:

public class ResizingSelection : Adorner
{     
    // To store and manage the adorner's visual children.

    VisualCollection m_visualChildren;
    
    // Initialize the ResizingAdorner.

    public ResizingSelection(UIElement adornedElement)
        : base(adornedElement)
    {
        m_visualChildren = new VisualCollection(this);
        m_visualChildren.Add(...);
    }
    
    //Gets visual children count.

    protected override int VisualChildrenCount
    {
        get
        {
            return m_visualChildren.Count;
        }
    }
    
    //Gets visual by index.

    protected override Visual GetVisualChild(int index)
    {
        return m_visualChildren\[index\];
    }
}

An example above is simplified. The complete version is here.

Purpose of the AdornerDecorator

As I mentioned above, there is another class that plays an important role in rendering, called AdornerDecorator.

AdornerDecorator determines the placement of AdornerLayer in the visual tree. This divides a visual tree in two parallel branches, one of which is the child of the AdornerDecorator with it's visual subtree, and the other one is AdornerLayer.

For better understanding we will consider the next example. Let us create a new project.

First of all let's implement our own Adorner class.

public class ControlAdorner: Adorner
{
    #region Private fields
    // Control that is the only child of the adorner and represents adorner.

    private HyperControl m_child;
    
    // Utilized for caching of offset by x co-ordinate.

    private double m_OffsetX = 0d;
    
    // Uri    private double m_OffsetY = 0d;

    #endregion
    
    #region Initialization
    public ControlAdorner( UIElement adornedElement )
        : base( adornedElement )
        {
            m_child = new HyperControl( this );
            AddLogicalChild( m_child );
            AddVisualChild( m_child );
        }
    #endregion
    
    #region Implementation
    //Measures content.

    protected override Size MeasureOverride( Size constraint )
    {
        m_child.Measure( constraint );
        return AdornedElement.RenderSize;
    }
    
    //Arranges child control to the full size.

    protected override Size ArrangeOverride( Size finalSize )
    {
        m_child.Arrange( new Rect( finalSize ) );
        return m_child.RenderSize;
    }
    
    public override GeneralTransform GetDesiredTransform( GeneralTransform 
        transform )
    {
        GeneralTransformGroup group = new GeneralTransformGroup();
        group.Children.Add( transform );
        group.Children.Add( new TranslateTransform( OffsetX, OffsetY ) );
        return group;
    }
    
    //Get visual by index.

    protected override Visual GetVisualChild( int index ) 
    {
        return m_child;
    }
    
    private static void OnOffsetXChanged( DependencyObject d, 
        DependencyPropertyChangedEventArgs e )
    {
        ControlAdorner instance = (ControlAdorner)d;
        instance.OnOffsetXChanged( e );
    }
    
    private void OnOffsetXChanged( DependencyPropertyChangedEventArgs e )
    {
        m_OffsetX = (double)e.NewValue;
        if( OffsetXChanged \!= null )
        {
            OffsetXChanged( this, e );
        }
    }
    
    private static void OnOffsetYChanged( DependencyObject d,  
        DependencyPropertyChangedEventArgs e )
    {
        ControlAdorner instance = (ControlAdorner)d;
        instance.OnOffsetYChanged( e );
    }
    
    private void OnOffsetYChanged( DependencyPropertyChangedEventArgs e )
    {
        m_OffsetY = (double)e.NewValue;
        if( OffsetYChanged \!= null )
        {
            OffsetYChanged( this, e );
        }
    }
    #endregion
    
    #region Events
    public event PropertyChangedCallback OffsetXChanged;
    public event PropertyChangedCallback OffsetYChanged;
    public double OffsetX
    {
        get
        {
            return m_OffsetX;
        }
        set
        {
            SetValue( OffsetXProperty, value );
        }
    }
    public double OffsetY
    {
        get
        {
            return m_OffsetY;
        }
        set
        {
            SetValue( OffsetYProperty, value );
        }
    }
    
    // Gets visual children count, always 1.

    protected override int VisualChildrenCount
    {
        get
        {
            return 1;
        }
    }
    #endregion
    
    #region Dependency properties
    public static readonly DependencyProperty OffsetXProperty = 
        DependencyProperty.Register( "OffsetX", typeof( double ), 
        typeof( ControlAdorner ), new FrameworkPropertyMetadata( 0d,    
        FrameworkPropertyMetadataOptions.AffectsArrange | 
        FrameworkPropertyMetadataOptions.AffectsParentArrange, new 
        PropertyChangedCallback( OnOffsetXChanged ) ) );
    
    public static readonly DependencyProperty OffsetYProperty = 
        DependencyProperty.Register( "OffsetY", typeof( double ), 
        typeof( ControlAdorner ),new FrameworkPropertyMetadata( 0d, 
        FrameworkPropertyMetadataOptions.AffectsArrange |
        FrameworkPropertyMetadataOptions.AffectsParentArrange, new 
        PropertyChangedCallback( OnOffsetYChanged ) ) );
    #endregion
}

As you have already seen we used the HyperContol class as a child of our Adorner. This class is also used to represent Adorner. Here is its implementation:

public class HyperControl : Control 
{
    //Creates new instance of the control and initializes it's

    //DefaultStyleKey property with the type of the adorner.

    public HyperControl( Adorner adorner )
    {
        SetValue( DefaultStyleKeyProperty, adorner.GetType() );
    }
}

After the above implementations we must create a generic.xaml resource file where ControlTemplate and Style for HyperControl should be created.

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/
    presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WindowAdornerDecorator">
    <ControlTemplate x:Key="ControlAdornerTemplate" 
        TargetType="{x:Type local:HyperControl}">
        <Border Name="border"  Background="Gray">
            <TextBlock VerticalAlignment="Center" HorizontalAlignment=
                "Center" Foreground="White" Text="Adorner"/>
        </Border>
        <ControlTemplate.Triggers>
            <Trigger Property="IsMouseOver" Value="True">
                <Setter TargetName="border" Property="Background"  
                    Value="Red"/>
            </Trigger>
        </ControlTemplate.Triggers>
    </ControlTemplate>
    <Style x:Key="{x:Type local:ControlAdorner}" TargetType=
        "{x:Type local:HyperControl}">
        <Setter Property="Template" Value="{StaticResource 
            ControlAdornerTemplate}"/>
    </Style>
</ResourceDictionary>

Now create GUI out of our project in Window1.xaml.

<Window x:Class="WindowAdornerDecorator.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowAdornerDecorator" Height="300" Width="300">
    <Grid Background="AliceBlue" Height="200" Width="200">
        <TextBlock x:Name="textBlock" VerticalAlignment="Center" 
            HorizontalAlignment="Center" 
        MouseDown="textBlock_MouseDown" FontFamily="Georgia" FontSize="18" 
            Text=" Test "/>
    </Grid>
</Window>

Implement code-behind file.

namespace WindowAdornerDecorator
{
    public partial class Window1 : System.Windows.Window
    {
        public Window1()
        {
            InitializeComponent();
        }
        
        AdornerLayer adornerLayer;
        ControlAdorner controlAdorner;
        Point m_startPoint;
        
        void textBlock_MouseDown( object sender, MouseEventArgs e )
        {
            adornerLayer = AdornerLayer.GetAdornerLayer( textBlock );
            controlAdorner = new ControlAdorner( textBlock );
            m_startPoint = Mouse.GetPosition( textBlock );
            adornerLayer.Add( controlAdorner );
            controlAdorner.MouseMove += new MouseEventHandler( 
                controlAdorner_MouseMove );
            controlAdorner.MouseUp += new MouseButtonEventHandler( 
                controlAdorner_MouseUp );
        }
        
        void controlAdorner_MouseUp( object sender, MouseButtonEventArgs e )
        {
            adornerLayer.Remove( controlAdorner );
        }
        
        void controlAdorner_MouseMove( object sender, MouseEventArgs e )
        {
            if( e.LeftButton == MouseButtonState.Pressed )
            {
                Point currentPoint = e.GetPosition( textBlock );
                controlAdorner.OffsetX = currentPoint.X - m_startPoint.X;
                controlAdorner.OffsetY = currentPoint.Y - m_startPoint.Y;
            }
        }
    }
}

Run that project and start dragging TextBlock with the Text property set to "Test". You can see that adorner can be draged above the entire Window.

You can get that project here.

After that, let's change Window1.xaml by adding AdornerDecorator in the Grid. Then set its ClipToBounds property to true:

<Window x:Class="WindowAdornerDecorator.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="WindowAdornerDecorator" Height="300" Width="300">
    <Grid Background="AliceBlue" Height="200" Width="200">
        <AdornerDecorator ClipToBounds="True">
            <TextBlock x:Name="textBlock" VerticalAlignment="Center" 
                HorizontalAlignment="Center" 
            MouseDown="textBlock_MouseDown" FontFamily="Georgia" 
                FontSize="18" Text=" Test "/>
        </AdornerDecorator>
    </Grid>
</Window>

Run that, and you will notice that adorner can be dragged only inside the grid element.

Source files of the project can be found here.

So, I hope that after that experience you will understand the purpose of the AdornerDecorator class.

Note: If you need to switch either adorner layer or adorned element in z-order, you should derive from the AdornerDecorator class and the override GetVisualChild method (its default implementation can be discovered using the awesome tool by Lutz Roeder's, named Reflector).

Events and Hit Testing

Adorners receive input events just like any other FrameworkElement. As the adorner always has a higher z-order than the element it adorns, the adorner receives input events that may be intended for the underlying adorned element. An adorner can listen for certain input events and pass these on to the underlying adorned element by re-raising the event.

To enable pass-through hit testing of elements under an adorner, set the hit test IsHitTestVisible property to false on the adorner.

External links

Part of the content of this article was taken from MSDN.

This and some more articles about WPF you can find here.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here