Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to resize WPF controls on a grid at runtime (keeping aspect ratio)

I'm currently working on a C# WPF application which contains a fullscreen Grid with controls that are dynamically added to it at runtime. I have code in place to allow the user to move these controls across the Grid with mouse events. What I now want to do is allow the user to also resize the controls (while keeping the aspect ratio), also at runtime. I have seen various tutorials describing how to do this using a Canvas (and Thumb controls), but none on a Grid. As I cannot use a Canvas in my application, is there an efficient way to implement this on a grid? To give you an idea of what my code looks like, I placed my mouse events below: [Edited below]

MainWindow.xaml.cs:

public partial class MainWindow : Window
{
    //Orientation variables:
    public static Point _anchorPoint;
    public static Point _currentPoint;
    private static double _originalTop;
    private static double _originalLeft;
    private static Point _startPoint;
    private static bool _isDown = false;
    private static bool _isInDrag = false;
    private static bool _isDragging = false;
    public static UIElement selectedElement = null;
    public static bool elementIsSelected = false;
    public static Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform>();
    public static AdornerLayer aLayer;

    public MainWindow()
    {
        InitializeComponent();
    }
    //Control events:
    public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) //HUD element left mouse button up
    {       
        if (_isInDrag)
        {
            var element = sender as FrameworkElement;
            element.ReleaseMouseCapture();
            _isInDrag = false;
            e.Handled = true;
            aLayer = AdornerLayer.GetAdornerLayer(selectedElement);
            aLayer.Add(new ResizingAdorner(selectedElement));
        }
    }
    public static void HUD_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (elementIsSelected)
            {
                elementIsSelected = false;
                _isDown = false;
                if (selectedElement != null)
                {
                    aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
                    selectedElement = null;
                }
            }
            if (e.Source != mw.PACSGrid)
            {
                _isDown = true;
                _startPoint = e.GetPosition(mw.PACSGrid);
                selectedElement = e.Source as UIElement;
                _originalLeft = VisualWorker.GetLeft(selectedElement);
                _originalTop = VisualWorker.GetTop(selectedElement);
                aLayer = AdornerLayer.GetAdornerLayer(selectedElement);
                aLayer.Add(new ResizingAdorner(selectedElement));
                elementIsSelected = true;
                e.Handled = true;
            }
    }
    public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) //HUD element left mouse button down
    {
        if (elementIsSelected)
            {
                aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
                selectedElement = sender as UIElement;
                var element = sender as FrameworkElement;
                _anchorPoint = e.GetPosition(null);
                element.CaptureMouse();
                _isInDrag = true;
                e.Handled = true;
            }
    }
    public static void Control_MouseMove(object sender, MouseEventArgs e) //Drag & drop HUD element
    {
        if (_isInDrag) // The user is currently dragging the HUD element...
        {
            _currentPoint = e.GetPosition(null);
            TranslateTransform tt = new TranslateTransform();
            bool isMoved = false;
            if (PointDict.ContainsKey(sender))
            {
                tt = PointDict[sender];
                isMoved = true;
            }
            tt.X += _currentPoint.X - _anchorPoint.X;
            tt.Y += (_currentPoint.Y - _anchorPoint.Y);
            _anchorPoint = _currentPoint;
            (sender as UIElement).RenderTransform = tt;
            if (isMoved)
            {
                PointDict.Remove(sender);
            }
            PointDict.Add(sender, tt);       
        }
    }
}
    // Adorner Class:
public class ResizingAdorner : Adorner
{
    Thumb topLeft, topRight, bottomLeft, bottomRight;

    // To store and manage the adorner's visual children.
    VisualCollection visualChildren;

    // Initialize the ResizingAdorner.
    public ResizingAdorner(UIElement adornedElement)
        : base(adornedElement)
    {
        visualChildren = new VisualCollection(this);

        // Call a helper method to initialize the Thumbs
        // with a customized cursors.
        BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
        BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
        BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
        BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);

        // Add handlers for resizing.
        bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
        bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
        topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
        topRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
    }

    // Handler for resizing from the bottom-right.
    void HandleBottomRight(object sender, DragDeltaEventArgs args)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb hitThumb = sender as Thumb;

        if (adornedElement == null || hitThumb == null) return;
        FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        // 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.
        adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
        adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
    }

    // Handler for resizing from the top-right.
    void HandleTopRight(object sender, DragDeltaEventArgs args)
    {
        FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
        Thumb hitThumb = sender as Thumb;

        if (adornedElement == null || hitThumb == null) return;
        FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        // 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.
        adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
        //adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);

        double height_old = adornedElement.Height;
        double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
        double top_old = VisualWorker.GetTop(adornedElement);
        //double top_old = Canvas.GetTop(adornedElement);
        adornedElement.Height = height_new;
        //Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
        VisualWorker.SetTop(adornedElement, top_old - (height_new - height_old));
    }

    // Handler for resizing from the top-left.
    void HandleTopLeft(object sender, DragDeltaEventArgs args)
    {
        FrameworkElement adornedElement = AdornedElement as FrameworkElement;
        Thumb hitThumb = sender as Thumb;

        if (adornedElement == null || hitThumb == null) return;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        // 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.
        //adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
        //adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);

        double width_old = adornedElement.Width;
        double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
        double left_old = VisualWorker.GetLeft(adornedElement);
        //double left_old = Canvas.GetLeft(adornedElement);
        adornedElement.Width = width_new;
        VisualWorker.SetLeft(adornedElement, left_old - (width_new - width_old));

        double height_old = adornedElement.Height;
        double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
        double top_old = VisualWorker.GetTop(adornedElement);
        //double top_old = Canvas.GetTop(adornedElement);
        adornedElement.Height = height_new;
        //Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
        VisualWorker.SetTop(adornedElement, top_old - (height_new - height_old));
    }

    // Handler for resizing from the bottom-left.
    void HandleBottomLeft(object sender, DragDeltaEventArgs args)
    {
        FrameworkElement adornedElement = AdornedElement as FrameworkElement;
        Thumb hitThumb = sender as Thumb;

        if (adornedElement == null || hitThumb == null) return;

        // Ensure that the Width and Height are properly initialized after the resize.
        EnforceSize(adornedElement);

        // 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.
        //adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
        adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);

        double width_old = adornedElement.Width;
        double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
        double left_old = VisualWorker.GetLeft(adornedElement);
        //double left_old = Canvas.GetLeft(adornedElement);
        adornedElement.Width = width_new;
        //Canvas.SetLeft(adornedElement, left_old - (width_new - width_old));
        VisualWorker.SetLeft(adornedElement, left_old - (width_new - width_old));
    }

    // Arrange the Adorners.
    protected override Size ArrangeOverride(Size finalSize)
    {
        // desiredWidth and desiredHeight are the width and height of the element that's being adorned.  
        // These will be used to place the ResizingAdorner at the corners of the adorned element.  
        double desiredWidth = AdornedElement.DesiredSize.Width;
        double desiredHeight = AdornedElement.DesiredSize.Height;
        // adornerWidth & adornerHeight are used for placement as well.
        double adornerWidth = this.DesiredSize.Width;
        double adornerHeight = this.DesiredSize.Height;

        topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
        topRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
        bottomLeft.Arrange(new Rect(-adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
        bottomRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));

        // Return the final size.
        return finalSize;
    }

    // Helper method to instantiate the corner Thumbs, set the Cursor property, 
    // set some appearance properties, and add the elements to the visual tree.
    void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
    {
        if (cornerThumb != null) return;

        cornerThumb = new Thumb();

        // Set some arbitrary visual characteristics.
        cornerThumb.Cursor = customizedCursor;
        cornerThumb.Height = cornerThumb.Width = 10;
        cornerThumb.Opacity = 1;
        cornerThumb.Background = new ImageBrush(new BitmapImage(new Uri(@"pack://application:,,,/Images/Thumb 1.jpg")));
        visualChildren.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.  It also sets the maximum size of the adorned element.
    void EnforceSize(FrameworkElement adornedElement)
    {
        if (adornedElement.Width.Equals(Double.NaN))
            adornedElement.Width = adornedElement.DesiredSize.Width;
        if (adornedElement.Height.Equals(Double.NaN))
            adornedElement.Height = adornedElement.DesiredSize.Height;

        FrameworkElement parent = adornedElement.Parent as FrameworkElement;
        if (parent != null)
        {
            adornedElement.MaxHeight = parent.ActualHeight;
            adornedElement.MaxWidth = parent.ActualWidth;
        }
    }
    // Override the VisualChildrenCount and GetVisualChild properties to interface with 
    // the adorner's visual collection.
    protected override int VisualChildrenCount { get { return visualChildren.Count; } }
    protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
}
// Canvas alternative class:
public class VisualWorker
{
    public static void SetTop(UIElement uie, double top)
    {
        var frame = uie as FrameworkElement;
        frame.Margin = new Thickness(frame.Margin.Left, top, frame.Margin.Right, frame.Margin.Bottom);
    }
    public static void SetLeft(UIElement uie, double left)
    {
        var frame = uie as FrameworkElement;
        frame.Margin = new Thickness(left, frame.Margin.Top, frame.Margin.Right, frame.Margin.Bottom);
    }
    public static double GetTop(UIElement uie)
    {
        return (uie as FrameworkElement).Margin.Top;
    }
    public static double GetLeft(UIElement uie)
    {
        return (uie as FrameworkElement).Margin.Left;
    }
}

MainWindow.xaml (example):

<Window x:Name="MW" x:Class="MyProgram.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:MyProgram"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None" MouseLeave="HUD_MouseLeave">

    <Grid x:Name="MyGrid MouseDown="HUD_MouseDown" />
         <Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
         <TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />

Edit: I have found that using TranslateTransform does not change a control's margin. I need the margin to change appropriately in order for this to work.

Edit 2: Now I have added [modified] code for a resizing adorner (found here). It almost actually works. The problem is that I'm experiencing odd behavior with the adorners. On one of my controls (in the top left corner), all 4 appear in their appropriate corners. But the rest of my controls only get 1-2 adorners and they're not spaced correctly. The behavior with those is stranger as well. Yes, I realize this is a lot of code but I'd suspect the problem would be in the ResizingAdorner class.

Edit 3: Added 'boilerplate' code for those who want to copy&paste. It should compile with no problems. Let me know if you have any issues.

Edit 4 (1/10/2018): Still no good answer. It seems that the Thumb controls on the adorner only align themselves correctly if the control's Margin is 0,0. When moved from that position, the adorners space out from the element.

Edit 5 (1/15/2018): The adorner class was originally designed for a Canvas and running it on a Grid may contribute to the problem. My best guess is that the ArrangeOverride method was messed up because of it (that's where the thumbs are placed on their UIElement).

like image 466
Luke Dinkler Avatar asked Dec 09 '17 17:12

Luke Dinkler


1 Answers

Resize:

Maybe this is a starter...

XAML:

<Grid x:Name="Content">
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Background="Blue" Width="20" Height="20" x:Name="BorderToResize"/>
<Border HorizontalAlignment="Left" VerticalAlignment="Top" Background="Red" Width="10" Height="10" MouseLeftButtonDown="OnLeftMouseButtonDown" MouseLeftButtonUp="OnLeftMouseButtonUp" MouseMove="OnMouseMove" x:Name="BorderThumb">
    <Border.RenderTransform>
        <TranslateTransform X="15" Y="15" />
    </Border.RenderTransform>
</Border>

Code Behind:

   private void OnLeftMouseButtonDown(object sender, MouseButtonEventArgs e) {
        (sender as UIElement).CaptureMouse();
    }

    private void OnLeftMouseButtonUp(object sender, MouseButtonEventArgs e) {
        (sender as UIElement).ReleaseMouseCapture();
    }

    private void OnMouseMove(object sender, MouseEventArgs e) {
        if ((sender as UIElement).IsMouseCaptureWithin) {
            var pos = e.GetPosition(Content);
            BorderThumb.RenderTransform = new TranslateTransform(pos.X, pos.Y);
            BorderToResize.Height = pos.Y;
            BorderToResize.Width = pos.X;
        }
    }
like image 90
Markus Avatar answered Oct 17 '22 16:10

Markus