Saturday, August 13, 2016

Adorners in WPF


Adorner is a special framework element which is bound to UIElement. You can check WPF Class Hierarchy for more about WPF controls. Adorners is the way to extend controls via adding extra visual functionality.

WPF internally uses adorners in validating controls, you must have seen red border on controls whose validation failed. The red border is adorner layer which is placed on top of the control. You can check my article How to validate data in WPF to know more about validations.

Adorners are bound to UIElement and placed top of it and independent of rendering of UIElement. You can add additional functionalities of UIElement like resize, move, rotate etc via adding Adorner layer to UIElement. Adorners are visual decoration to decorate UIElement.

See below example which uses adorner to select/resize UIElement on canvas.

XAML
<Window x:Class="AdornerExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:AdornerExample"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Canvas Name="myCanvas">
        <Button Height="50" Width="150" Content="Click Me!"
                Canvas.Left="50" Canvas.Top="50"
                Name="myButton" />
        <TextBox Height="25" Width="120"
                 Text="TextBox"
                 Canvas.Top="200" Canvas.Left="100" />

    </Canvas>
</Window>

Code –  
Custom adorner for decorating UIElement
public class BorderAdorner : Adorner
{
    //use thumb for resizing elements
    Thumb topLeft, topRight, bottomLeft, bottomRight;
    //visual child collection for adorner
    VisualCollection visualChilderns;

    public BorderAdorner(UIElement element) : base(element)
    {
        visualChilderns = new VisualCollection(this);
           
        //adding thumbs for drawing adorner rectangle and setting cursor
        BuildAdornerCorners(ref topLeft, Cursors.SizeNWSE);
        BuildAdornerCorners(ref topRight, Cursors.SizeNESW);
        BuildAdornerCorners(ref bottomLeft, Cursors.SizeNESW);
        BuildAdornerCorners(ref bottomRight, Cursors.SizeNWSE);

        //registering drag delta events for thumb drag movement
        topLeft.DragDelta += TopLeft_DragDelta;
        topRight.DragDelta += TopRight_DragDelta;
        bottomLeft.DragDelta += BottomLeft_DragDelta;
        bottomRight.DragDelta += BottomRight_DragDelta;
    }

    private void BottomRight_DragDelta(object sender, DragDeltaEventArgs e)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb bottomRightCorner = sender as Thumb;
        //setting new height and width after drag
        if (adornedElement != null && bottomRightCorner != null)
        {
            EnforceSize(adornedElement);

            double oldWidth = adornedElement.Width;
            double oldHeight = adornedElement.Height;

            double newWidth = Math.Max(adornedElement.Width + e.HorizontalChange, bottomRightCorner.DesiredSize.Width);
            double newHeight = Math.Max(e.VerticalChange + adornedElement.Height , bottomRightCorner.DesiredSize.Height);
               
            adornedElement.Width = newWidth;
            adornedElement.Height = newHeight;
        }
    }

    private void TopRight_DragDelta(object sender, DragDeltaEventArgs e)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb topRightCorner = sender as Thumb;
        //setting new height, width and canvas top after drag
        if (adornedElement != null && topRightCorner != null)
        {
            EnforceSize(adornedElement);

            double oldWidth = adornedElement.Width;
            double oldHeight = adornedElement.Height;

            double newWidth = Math.Max(adornedElement.Width + e.HorizontalChange, topRightCorner.DesiredSize.Width);
            double newHeight = Math.Max(adornedElement.Height - e.VerticalChange, topRightCorner.DesiredSize.Height);
            adornedElement.Width = newWidth;

            double oldTop = Canvas.GetTop(adornedElement);
            double newTop = oldTop - (newHeight - oldHeight);
            adornedElement.Height = newHeight;
            Canvas.SetTop(adornedElement, newTop);
        }
    }

    private void TopLeft_DragDelta(object sender, DragDeltaEventArgs e)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb topLeftCorner = sender as Thumb;
        //setting new height, width and canvas top, left after drag
        if (adornedElement != null && topLeftCorner != null)
        {
            EnforceSize(adornedElement);

            double oldWidth = adornedElement.Width;
            double oldHeight = adornedElement.Height;

            double newWidth = Math.Max(adornedElement.Width - e.HorizontalChange, topLeftCorner.DesiredSize.Width);
            double newHeight = Math.Max(adornedElement.Height - e.VerticalChange, topLeftCorner.DesiredSize.Height);

            double oldLeft = Canvas.GetLeft(adornedElement);
            double newLeft = oldLeft - (newWidth - oldWidth);
            adornedElement.Width = newWidth;
            Canvas.SetLeft(adornedElement, newLeft);

            double oldTop = Canvas.GetTop(adornedElement);
            double newTop = oldTop - (newHeight - oldHeight);
            adornedElement.Height = newHeight;
            Canvas.SetTop(adornedElement, newTop);
        }
    }

    private void BottomLeft_DragDelta(object sender, DragDeltaEventArgs e)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb topRightCorner = sender as Thumb;
        //setting new height, width and canvas left after drag
        if (adornedElement != null && topRightCorner != null)
        {
            EnforceSize(adornedElement);

            double oldWidth = adornedElement.Width;
            double oldHeight = adornedElement.Height;

            double newWidth = Math.Max(adornedElement.Width - e.HorizontalChange, topRightCorner.DesiredSize.Width);
            double newHeight = Math.Max(adornedElement.Height + e.VerticalChange, topRightCorner.DesiredSize.Height);

            double oldLeft = Canvas.GetLeft(adornedElement);
            double newLeft = oldLeft - (newWidth - oldWidth);
            adornedElement.Width = newWidth;
            Canvas.SetLeft(adornedElement, newLeft);

            adornedElement.Height = newHeight;
        }
    }
       
    public void BuildAdornerCorners(ref Thumb cornerThumb, Cursor customizedCursors)
    {
        //adding new thumbs for adorner to visual childern collection
        if (cornerThumb != null) return;
        cornerThumb = new Thumb() { Cursor = customizedCursors, Height = 10, Width = 10, Opacity = 0.5, Background = new SolidColorBrush(Colors.Red) };
        visualChilderns.Add(cornerThumb);
    }
       
    public void EnforceSize(FrameworkElement element)
    {
        if (element.Width.Equals(Double.NaN))
            element.Width = element.DesiredSize.Width;
        if (element.Height.Equals(Double.NaN))
            element.Height = element.DesiredSize.Height;
           
        //enforce size of element not exceeding to it's parent element size
        FrameworkElement parent = element.Parent as FrameworkElement;

        if (parent != null)
        {
            element.MaxHeight = parent.ActualHeight;
            element.MaxWidth = parent.ActualWidth;
        }
    }
       
    protected override Size ArrangeOverride(Size finalSize)
    {
        base.ArrangeOverride(finalSize);

        double desireWidth = AdornedElement.DesiredSize.Width;
        double desireHeight = AdornedElement.DesiredSize.Height;

        double adornerWidth = this.DesiredSize.Width;
        double adornerHeight = this.DesiredSize.Height;
           
        //arranging thumbs
        topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
        topRight.Arrange(new Rect(desireWidth - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
        bottomLeft.Arrange(new Rect(-adornerWidth / 2, desireHeight - adornerHeight / 2, adornerWidth, adornerHeight));
        bottomRight.Arrange(new Rect(desireWidth - adornerWidth / 2, desireHeight - adornerHeight / 2, adornerWidth, adornerHeight));

        return finalSize;
    }
    protected override int VisualChildrenCount { get { return visualChilderns.Count; } }
    protected override Visual GetVisualChild(int index) { return visualChilderns[index]; }
    protected override void OnRender(DrawingContext drawingContext)
    {
        base.OnRender(drawingContext);
    }
}


MainWindow code -
public partial class MainWindow : Window
{
    bool isDown, isDragging, isSelected;
    UIElement selectedElement = null;
    double originalLeft, originalTop;
    Point startPoint;

    AdornerLayer adornerLayer;

    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        //registering mouse events
        this.MouseLeftButtonDown += MainWindow_MouseLeftButtonDown;
        this.MouseLeftButtonUp += MainWindow_MouseLeftButtonUp;
        this.MouseMove += MainWindow_MouseMove;
        this.MouseLeave += MainWindow_MouseLeave;

        myCanvas.PreviewMouseLeftButtonDown += MyCanvas_PreviewMouseLeftButtonDown;
        myCanvas.PreviewMouseLeftButtonUp += MyCanvas_PreviewMouseLeftButtonUp;
    }

    private void StopDragging()
    {
        if (isDown)
        {
            isDown = isDragging = false;
        }
    }


    private void MyCanvas_PreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        StopDragging();
        e.Handled = true;
    }

    private void MyCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        //removing selected element
        if (isSelected)
        {
            isSelected = false;
            if (selectedElement != null)
            {
                adornerLayer.Remove(adornerLayer.GetAdorners(selectedElement)[0]);
                selectedElement = null;
            }
        }

        // select element if any element is clicked other then canvas
        if (e.Source != myCanvas)
        {
            isDown = true;
            startPoint = e.GetPosition(myCanvas);

            selectedElement = e.Source as UIElement;

            originalLeft = Canvas.GetLeft(selectedElement);
            originalTop = Canvas.GetTop(selectedElement);

            //adding adorner on selected element
            adornerLayer = AdornerLayer.GetAdornerLayer(selectedElement);
            adornerLayer.Add(new BorderAdorner(selectedElement));
            isSelected = true;
            e.Handled = true;
        }
    }

    private void MainWindow_MouseMove(object sender, MouseEventArgs e)
    {
        //handling mouse move event and setting canvas top and left value based on mouse movement
        if (isDown)
        {
            if ((!isDragging) &&
                ((Math.Abs(e.GetPosition(myCanvas).X - startPoint.X) > SystemParameters.MinimumHorizontalDragDistance) ||
                (Math.Abs(e.GetPosition(myCanvas).Y - startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)))
                isDragging = true;

            if (isDragging)
            {
                Point position = Mouse.GetPosition(myCanvas);
                Canvas.SetTop(selectedElement, position.Y - (startPoint.Y - originalTop));
                Canvas.SetLeft(selectedElement, position.X - (startPoint.X - originalLeft));
            }
        }
    }

    private void MainWindow_MouseLeave(object sender, MouseEventArgs e)
    {
        //stop dragging on mouse leave
        StopDragging();
        e.Handled = true;
    }

    private void MainWindow_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        //stop dragging on mouse left button up
        StopDragging();
        e.Handled = true;
    }

    private void MainWindow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        //remove selected element on mouse down
        if (isSelected)
        {
            isSelected = false;
            if (selectedElement != null)
            {
                adornerLayer.Remove(adornerLayer.GetAdorners(selectedElement)[0]);
                selectedElement = null;
            }
        }
    }
}


Output –






As you can see in above example, two controls Button and Textbox added to Canvas. When user click on any control, the red corners will appear to control. This red corners are added as an adorner to UIElement or control like button or textbox etc. Now you can resize UIElement via resizing red corners. You can move UIElement via Mouse Drag. So this way you can add additional functionality to UIElement visually via adorner.

To create custom adorner, you need to create class deriving from Adorner class. Thumb is special class derived from FrameworkElement. Thumb class can be used to draw rectangle or other shape for Adorner. In above example you can see Red rectangle at each corner of selected UIElement, is created using Thumb class. Thumb class has event called DragDelta. You need to handle DragDelta event for each thumb you create. Once you create Thumbs for adorner you need to add all Thumbs to VisualCollection.

In MainWondow file, you need to handle different window mouse events like MouseMove, MouseLeave, MouseLeftButtonUp, MouseLeftButtonDown etc to handle drag/drop and mouse move event of selected element. Also you need write code to select UIElement and apply adorner to selected element. This is handled in MyCanvas_PreviewMouseLeftButtonDown event of Canvas in above example.

I hope this article helps you to understand more about Adorners. Please give your feedback in comments below.


References –

See Also –


4 comments:

  1. Awesome fella - thank you. Just what I was after.

    ReplyDelete
  2. but i want to remove the adorner layer of controls, also i tried it but process failed.
    anyone help me.

    ReplyDelete
  3. i have another question for the same that when i rotate any controls on canvas , controls rotates successfully but adorner layer is unable to rotate according to controls.

    ReplyDelete