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:
- Call the
static
method GetAdornerLayer
to find an adorner layer for the element children of which are to be adorned.
- 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:
- Overriding the
OnRenderSizeChanged
method and drawing adorner's visual content using the DrawingContext
object methods.
- 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.
public class SimpleCircleAdorner : Adorner
{
public SimpleCircleAdorner(UIElement adornedElement)
: base(adornedElement){}
protected override void OnRender(DrawingContext drawingContext)
{
Rect adornedElementRect = new Rect( this.AdornedElement.DesiredSize );
SolidColorBrush renderBrush = new SolidColorBrush( Colors.Green );
renderBrush.Opacity = 0.2;
Pen renderPen = new Pen( new SolidColorBrush( Colors.Navy ), 1.5 );
double renderRadius = 5.0;
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.
<
Sources of example above.
Second approach:
public class ResizingSelection : Adorner
{
VisualCollection m_visualChildren;
public ResizingSelection(UIElement adornedElement)
: base(adornedElement)
{
m_visualChildren = new VisualCollection(this);
m_visualChildren.Add(...);
}
protected override int VisualChildrenCount
{
get
{
return m_visualChildren.Count;
}
}
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
private HyperControl m_child;
private double m_OffsetX = 0d;
#endregion
#region Initialization
public ControlAdorner( UIElement adornedElement )
: base( adornedElement )
{
m_child = new HyperControl( this );
AddLogicalChild( m_child );
AddVisualChild( m_child );
}
#endregion
#region Implementation
protected override Size MeasureOverride( Size constraint )
{
m_child.Measure( constraint );
return AdornedElement.RenderSize;
}
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;
}
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 );
}
}
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
{
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.