Search code examples
c#wpfpaintadorneradornerlayer

Allow users to resize into negative height / width


I am creating a paint like tool in WPF and ran in to the following problem, when the user clicks on a drawn shape (DrawingShape) a Adorner will appear with 8 thumbs (one for each side of the rectangle around the shape). The resize is done in the class below. The problem I am facing is that I want the user to be able to "go in to negative height / width", what I mean by that is that when the user edits a drawn rectangle and drags the bottom thumb over the top of the rectangle (making the height 0) it will keep draggin towards that direction creating the DrawingShape on the other side. Is there an easy way to accomplish this?

public class ResizeThumb : Thumb
{
    private bool dragStarted;
    private bool isHorizontalDrag;

    static ResizeThumb()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ResizeThumb),
            new FrameworkPropertyMetadata(typeof(ResizeThumb)));
    }

    private readonly DrawingShape childElement;

    public ResizeThumb(DrawingShape adornedElement, CornerOrSide thumbCornerOrSide)
    {
        this.childElement = adornedElement;

        switch (thumbCornerOrSide)
        {
            case CornerOrSide.TopLeft:
                this.DragDelta += OnTopLeftDragDelta;
                break;
            case CornerOrSide.TopRight:
                this.DragDelta += OnTopRightDragDelta;
                break;
            case CornerOrSide.BottomRight:
                this.DragDelta += OnBottomRightDragDelta;
                break;
            case CornerOrSide.BottomLeft:
                this.DragDelta += OnBottomLeftDragDelta;
                break;
            case CornerOrSide.Top:
                this.DragDelta += OnTopDragDelta;
                break;
            case CornerOrSide.Bottom:
                this.DragDelta += OnBottomDragDelta;
                break;
            case CornerOrSide.Left:
                this.DragDelta += OnLeftDragDelta;
                break;
            case CornerOrSide.Right:
                this.DragDelta += OnRightDragDelta;
                break;
            default:
                throw new ArgumentOutOfRangeException(nameof(thumbCornerOrSide), thumbCornerOrSide, null);
        }

        this.DragStarted += OnDragStarted;
        this.DragCompleted += OnDragCompleted;
    }

    #region Drag Delta Events per Thumb


    private void OnDragStarted(object _, DragStartedEventArgs e) => this.dragStarted = false;

    private void OnDragCompleted(object _, DragCompletedEventArgs e) => this.dragStarted = false;

    private void OnTopDragDelta(object _, DragDeltaEventArgs e) => ResizeTopHeight(e.VerticalChange);

    private void OnBottomDragDelta(object _, DragDeltaEventArgs e) => ResizeHeight(e.VerticalChange);

    private void OnLeftDragDelta(object _, DragDeltaEventArgs e) => ResizeLeftWidth(e.HorizontalChange);

    private void OnRightDragDelta(object _, DragDeltaEventArgs e) => ResizeWidth(e.HorizontalChange);


    private void OnTopLeftDragDelta(object _, DragDeltaEventArgs e)
    {
        ResizeLeftWidth(e.HorizontalChange);
        ResizeTopHeight(e.VerticalChange);
    }

    private void OnBottomRightDragDelta(object _, DragDeltaEventArgs e)
    {
        ResizeWidth(e.HorizontalChange);
        ResizeHeight(e.VerticalChange);
    }

    private void OnBottomLeftDragDelta(object _, DragDeltaEventArgs e)
    {
        ResizeLeftWidth(e.HorizontalChange);
        ResizeHeight(e.VerticalChange);
    }

    private void OnTopRightDragDelta(object _, DragDeltaEventArgs e)
    {
        ResizeTopHeight(e.VerticalChange);
        ResizeWidth(e.HorizontalChange);
    }

    #endregion

    #region Resize Width / Height / X / Y methods

    private void ResizeWidth(double e)
    {
        childElement.Width += e;
    }

    private void ResizeHeight(double e)
    {
        childElement.Height += e;
    }

    /// <summary>
    /// AKA as ResizeX
    /// </summary>
    private void ResizeTopHeight(double e)
    {
        childElement.Top += e; // < this is Canvas.SetTop
        childElement.Height -= e; // Decrease Height if Top moves
    }

    /// <summary>
    /// AKA as ResizeY
    /// </summary>
    private void ResizeLeftWidth(double e)
    {
        childElement.Left += e; // < this is Canvas.SetLeft
        childElement.Width -= e; // Decrease Width of Left moves
    }

    #endregion
}

I tried multiple things like checking if the height is 0 and then moving the childElement.Top value instead but that results in very jittery behaviour.


Solution

  • I confront this frequently and use a pretty simple approach. Start by defining a method like this:

        public static Rect NormalizeRect(double x1, double y1, double x2, double y2)
        {
            return new Rect(
                Math.Min(x1, x2),
                Math.Min(y1, y2),
                Math.Abs(x1 - x2),
                Math.Abs(y1 - y2));
        }
    

    This creates an always-conforming Rect with the smallest x and y coordinates always at the top left, and positive width and height. I find myself using this a lot so I like to keep it as its own static method somewhere accessible to the whole project.

    Then a simple helper property:

        private Rect ChildElementBounds
        {
            get => new Rect(
                Canvas.GetLeft(childElement),
                Canvas.GetTop(childElement),
                childElement.Width,
                childElement.Height);
            set
            {
                Canvas.SetLeft(childElement, value.Left);
                Canvas.SetTop(childElement, value.Top);
                childElement.Width = value.Width;
                childElement.Height = value.Height;
            }
        }
    

    And one more helper method:

    private void ResizeChild(
       double deltaLeft = 0, 
       double deltaTop = 0, 
       double deltaRight = 0, 
       double deltaBottom = 0)
    {
        var rc = this.ChildElementBounds;
        this.ChildElementBounds = NormalizeRect(
           rc.Left + deltaLeft,
           rc.Top + deltaTop,
           rc.Right + deltaRight,
           rc.Bottom + deltaBottom);
    }
    

    Now every time there's a change in any side's coordinate, all you need to do is call ResizeChild with the approrpiate offsets. For example:

    private void OnLeftDragDelta(object _, DragDeltaEventArgs e) =>
        ResizeChild(deltaLeft: e.HorizontalChange);
    
    private void OnTopLeftDragDelta(object _, DragDeltaEventArgs e) =>
        ResizeChild(deltaLeft: e.HorizontalChange, deltaTop: e.VerticalChange);
    
    private void OnBottomRightDragDelta(object _, DragDeltaEventArgs e) =>
        ResizeChild(deltaRight: e.HorizontalChange, deltaBottom: e.VerticalChange);
    

    Etc.