18
votes

I have a user resizable WPF Window that I want to constrain the resizing so the aspect ratio of the window stays constant.

Ideally I would like to constrain mouse location when the window is being resized by dragging a corner to positions that maintain the proper aspect ration. If an edge is resized with the mouse, the other dimension should change at the same time.

Is there a simple way to do this or a good on-line example that anyone knows of?

If no better solutions come up, I'll post what I've done after I've refined it a bit.

6
See if this helps: stackoverflow.com/questions/288954/…user1488365

6 Answers

23
votes

I've found a good answer by Nir here. There are still some flaws, basically resizing in top right corner, bottom right corner and bottom side will be fine, other sides and corners are not. The bright side is, the aspect ratio is smoothly kept all the time.

EDIT: I found a way to remove most of the problems. When sizing starts, the dimension that will be artificially adjusted to keep the aspect ratio is determined by locating the mouse position relative to the window. The only remaining imperfections I found are that the position of the window may change when resizing from the corners (except bottom right).

xaml:

<Window x:Class="WpfApplication1.ConstantAspectRatioWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="ConstantAspectRatioWindow" MinHeight="100" MinWidth="150" SizeToContent="WidthAndHeight">
    <Grid>
        <Border Width="300" Height="200" Background="Navy"/>
        <Border Width="150" Height="100" Background="Yellow" />
    </Grid>
</Window>

Code behind:

using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace WpfApplication1
{
    public partial class ConstantAspectRatioWindow : Window
    {
        private double _aspectRatio;
        private bool? _adjustingHeight = null;

        internal enum SWP
        {
            NOMOVE = 0x0002
        }
        internal enum WM
        {
            WINDOWPOSCHANGING = 0x0046,
            EXITSIZEMOVE = 0x0232,
        }

        public ConstantAspectRatioWindow()
        {
            InitializeComponent();
            this.SourceInitialized += Window_SourceInitialized;
        }

        [StructLayout(LayoutKind.Sequential)]
        internal struct WINDOWPOS
        {
            public IntPtr hwnd;
            public IntPtr hwndInsertAfter;
            public int x;
            public int y;
            public int cx;
            public int cy;
            public int flags;
        }

        [DllImport("user32.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        internal static extern bool GetCursorPos(ref Win32Point pt);

        [StructLayout(LayoutKind.Sequential)]
        internal struct Win32Point
        {
            public Int32 X;
            public Int32 Y;
        };

        public static Point GetMousePosition() // mouse position relative to screen
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }


        private void Window_SourceInitialized(object sender, EventArgs ea)
        {
            HwndSource hwndSource = (HwndSource)HwndSource.FromVisual((Window)sender);
            hwndSource.AddHook(DragHook);

            _aspectRatio = this.Width / this.Height;
        }

        private IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
        {
            switch ((WM)msg)
            {
                case WM.WINDOWPOSCHANGING:
                    {
                        WINDOWPOS pos = (WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));

                        if ((pos.flags & (int)SWP.NOMOVE) != 0)
                            return IntPtr.Zero;

                        Window wnd = (Window)HwndSource.FromHwnd(hwnd).RootVisual;
                        if (wnd == null)
                            return IntPtr.Zero;

                        // determine what dimension is changed by detecting the mouse position relative to the 
                        // window bounds. if gripped in the corner, either will work.
                        if (!_adjustingHeight.HasValue)
                        {
                            Point p = GetMousePosition();

                            double diffWidth = Math.Min(Math.Abs(p.X - pos.x), Math.Abs(p.X - pos.x - pos.cx));
                            double diffHeight = Math.Min(Math.Abs(p.Y - pos.y), Math.Abs(p.Y - pos.y - pos.cy));

                            _adjustingHeight = diffHeight > diffWidth;
                        }

                        if (_adjustingHeight.Value)
                            pos.cy = (int)(pos.cx / _aspectRatio); // adjusting height to width change
                        else
                            pos.cx = (int)(pos.cy * _aspectRatio); // adjusting width to heigth change

                        Marshal.StructureToPtr(pos, lParam, true);
                        handled = true;
                    }
                    break;
                case WM.EXITSIZEMOVE:
                    _adjustingHeight = null; // reset adjustment dimension and detect again next time window is resized
                    break;
            }

            return IntPtr.Zero;
        }
    }
}
4
votes

Although this doesn't force the Window to be of a specific ratio (as the OP asked), I have managed to get the CONTENT of a window to scale, while maintaining the original aspect ratio, by wrapping the contents in a Viewbox and setting the stretch propety as Stretch="Uniform". No code-behind is needed.

WPF:

<Viewbox Name="MainViewbox" Stretch="Uniform">
    ... your content here
</Viewbox>
2
votes

Does that do the trick:

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) {

    if (sizeInfo.WidthChanged) this.Width = sizeInfo.NewSize.Height * aspect;
    else this.Height = sizeInfo.NewSize.Width / aspect;
}

Found it here.

2
votes

The answer given above favors width change over height change so if you adjust the height a lot but, because of mouse positioning, the width also changes a bit, the user will still see pretty much the same window. I have this code that works off percentage changes in each dimension favoring the largest change as the one the user is most interested in.

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        var percentWidthChange = Math.Abs(sizeInfo.NewSize.Width - sizeInfo.PreviousSize.Width) / sizeInfo.PreviousSize.Width;
        var percentHeightChange = Math.Abs(sizeInfo.NewSize.Height - sizeInfo.PreviousSize.Height) / sizeInfo.PreviousSize.Height;

        if (percentWidthChange > percentHeightChange)
            this.Height = sizeInfo.NewSize.Width / _aspectRatio;
        else
            this.Width = sizeInfo.NewSize.Height * _aspectRatio;

        base.OnRenderSizeChanged(sizeInfo);
    }
0
votes

On Window - you can listen to message of Win32 API simply:

 private double ratio = 1.33; // retio of 3:4

        protected override void OnSourceInitialized(EventArgs e)
        {
            base.OnSourceInitialized(e);
            HwndSource source = HwndSource.FromVisual(this) as HwndSource;
            if (source != null)
            {
                source.AddHook(new HwndSourceHook(WinProc));
            }
        }

        public const Int32 WM_EXITSIZEMOVE = 0x0232;
        private IntPtr WinProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, ref Boolean handled)
        {
            IntPtr result = IntPtr.Zero;
            switch (msg)
            {
                case WM_EXITSIZEMOVE:
                    {
                        if (Width < Height)
                        {
                            Width = Height * ratio;
                        }
                        else
                        {
                            Height = Width / ratio;
                        }
                    }
                    break;
            }

            return result;
        }

On this code you always take the shorter side and set it to be equal to the longer. You can always take the opposite approach and set the longer to be equal to the shorter. I found the solution here: http://social.msdn.microsoft.com/forums/en-US/wpf/thread/b0df3d1f-e211-4f54-a079-09af0096410e

0
votes

@Mike Fuchs answer is not perfect. If you resize the window from top-left conner and from bottom-left conner, the window will move while you resize it.

I found more elegat way without these problems.

XMAL:

<Window x:Class="WindowTop.UI.ResizeExample"
        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:WindowTop.UI"
        mc:Ignorable="d"
        Title="ResizeExample" Height="450" Width="800">
    <Grid>
        
    </Grid>
</Window>

C#

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

namespace WindowTop.UI
{
    /// <summary>
    /// Interaction logic for ResizeExample.xaml
    /// </summary>
    public partial class ResizeExample : Window
    {
        public ResizeExample()
        {
            InitializeComponent();
        }


        IntPtr hWnd = IntPtr.Zero;
        double xRatio = 1;
        double yRatio = 1;
        int sizingEdge = 0;


        [StructLayout(LayoutKind.Sequential)]
        internal struct WINDOWPOS
        {
            public IntPtr hwnd;
            public IntPtr hwndInsertAfter;
            public int x;
            public int y;
            public int cx;
            public int cy;
            public int flags;
        }

        IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
        {
            const int WM_SIZE = 0x0005;
            const int WM_SIZING = 0x0214;
            const int WM_WINDOWPOSCHANGING = 0x0046;


            // https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-sizing
            const int WMSZ_BOTTOM = 6;
            const int WMSZ_BOTTOMLEFT = 7;
            const int WMSZ_BOTTOMRIGHT = 8;
            const int WMSZ_LEFT = 1;
            const int WMSZ_RIGHT = 2;
            const int WMSZ_TOP = 3;
            const int WMSZ_TOPLEFT = 4;
            const int WMSZ_TOPRIGHT = 5;

            switch (msg)
            {
                case WM_SIZING:
                    sizingEdge = wParam.ToInt32();
                    break;

                case WM_WINDOWPOSCHANGING:


                    var position =
                        (WINDOWPOS) Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));

                    if (position.cx == this.Width && position.cy == this.Height)
                        return IntPtr.Zero;

                    switch (sizingEdge)
                    {
                        case WMSZ_TOP: // Top edge
                        case WMSZ_BOTTOM: // Bottom edge
                        case WMSZ_TOPRIGHT: // Top-right corner
                            position.cx = (int) (position.cy * xRatio);
                            break;

                        case WMSZ_LEFT: // Left edge
                        case WMSZ_RIGHT: // Right edge
                        case WMSZ_BOTTOMRIGHT: // Bottom-right corner
                        case WMSZ_BOTTOMLEFT: // Bottom-left corner
                            position.cy = (int) (position.cx * yRatio);
                            break;


                        case WMSZ_TOPLEFT: // Top-left corner
                            position.cx = (int) (position.cy * xRatio);
                            position.x = (int) Left - (position.cx - (int) Width);
                            break;
                    }

                    Marshal.StructureToPtr(position, lParam, true);
                    break;
            }


            return IntPtr.Zero;
        }


        public void Show()
        {

            xRatio = Width / Height;
            yRatio = Height / Width;

            base.Show();

            if (hWnd == IntPtr.Zero)
            {
                var interopHelper = new WindowInteropHelper(this);

                hWnd = interopHelper.Handle;

                var source = HwndSource.FromHwnd(hWnd);
                source?.AddHook(DragHook);
            }
        }

    }
}