Charley Rathkopf
Charley Rathkopf

Reputation: 4788

Resize a WPF window, but maintain proportions?

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.

Upvotes: 19

Views: 31374

Answers (8)

RicDre
RicDre

Reputation: 1

Thanks to "gil123" and "BlyZe" for creating the "AspectRatioWindow" class code, it is just what I was looking for. My project was in VB.NET, so I translated the code and made some minor changes to it and attached it below in case anyone may be interested in the VB.NET equivalent.

Imports MS.Internal.WindowsRuntime
Imports System.Windows.Interop
Imports System.Runtime.InteropServices
Imports System
Imports System.Windows
Public Class AspectRatioWindow
    Inherits Window

    ''' <summary>
    ''' Structure containing Windows Position data
    ''' </summary>
    <StructLayout(LayoutKind.Sequential)>
    Private Structure WINDOWPOS

        Public hwnd As IntPtr
        Public hwndInsertAfter As IntPtr
        Public x As Integer
        Public y As Integer
        Public cx As Integer
        Public cy As Integer
        Public flags As Integer
    End Structure

    ' Windows message constants
    Private Const WM_SIZING As Integer = &H214
    Private Const WM_WINDOWPOSCHANGING As Integer = &H46
    Private Const WMSZ_LEFT As Integer = 1
    Private Const WMSZ_RIGHT As Integer = 2
    Private Const WMSZ_TOP As Integer = 3
    Private Const WMSZ_TOPLEFT As Integer = 4
    Private Const WMSZ_TOPRIGHT As Integer = 5
    Private Const WMSZ_BOTTOM As Integer = 6
    Private Const WMSZ_BOTTOMLEFT As Integer = 7
    Private Const WMSZ_BOTTOMRIGHT As Integer = 8

    ''' <summary>Handle for window</summary>
    Private _hWnd As IntPtr = System.IntPtr.Zero

    ''' <summary>Ratio of horizonal to vertical</summary>
    Private _xRatio As Double = 1

    ''' <summary>Ratio of vertical to Horizontal</summary>
    Private _yRatio As Double = 1

    ''' <summary>Edge where resizing is taking place</summary>
    Private _sizingEdge As Integer = 0

    ''' <summary>Handle to the underliying Win32 Window</summary>
    Private _HwndSource As HwndSource = Nothing

    ''' <summary>Property containing the Horizontal to Vertical ratio</summary>
    Public Property AspectRatio As Double
        Get
            Return _xRatio
        End Get
        Set(value As Double)
            _xRatio = value
            _yRatio = 1.0 / _xRatio
        End Set
    End Property

    ''' <summary>
    ''' Method called when the window is loaded
    ''' </summary>
    ''' <param name="sender"></param>
    ''' <param name="ea"></param>
    Private Sub Me_Loaded(sender As Object, ea As EventArgs) Handles Me.Loaded

        If _HwndSource Is Nothing Then
            _HwndSource = DirectCast(HwndSource.FromVisual(DirectCast(sender, Window)), HwndSource)
            _HwndSource.AddHook(AddressOf DragHook)

            _xRatio = Width / Height
            _yRatio = Height / Width
        End If
    End Sub

    ''' <summary>
    ''' Method executed when a drag operation occurs
    ''' </summary>
    Private Function DragHook(hwnd As IntPtr, msg As Integer, wParam As IntPtr, lParam As IntPtr, ByRef handeled As Boolean) As IntPtr

        ' Make sure the window in in the Normal state
        If WindowState = WindowState.Normal Then
            Select Case (msg)
                Case WM_SIZING
                    _sizingEdge = wParam.ToInt32()

                Case WM_WINDOWPOSCHANGING

                    Dim position As WINDOWPOS = DirectCast(Marshal.PtrToStructure(lParam, GetType(WINDOWPOS)), WINDOWPOS)

                    If (position.cx = Width AndAlso position.cy = Height) Then
                        Return IntPtr.Zero
                    End If

                    If position.cx = 0 AndAlso position.cy = 0 Then
                        Return IntPtr.Zero
                    End If

                    Select Case (_sizingEdge)
                        Case WMSZ_TOP, WMSZ_BOTTOM, WMSZ_TOPRIGHT
                            position.cx = CInt(CDbl(position.cy) * _xRatio)
                        Case WMSZ_LEFT, WMSZ_RIGHT, WMSZ_BOTTOMRIGHT, WMSZ_BOTTOMLEFT
                            position.cy = CInt(CDbl(position.cx) * _yRatio)

                        Case WMSZ_TOPLEFT
                            position.cx = CInt(CDbl(position.cy) * _xRatio)
                            position.x = CInt(Left - CDbl(position.cx) - Width)
                    End Select

                    Marshal.StructureToPtr(position, lParam, True)
                    handeled = True
            End Select
        End If

        Return IntPtr.Zero
    End Function

End Class

Upvotes: 0

BlyZe
BlyZe

Reputation: 39

Based on @gil123's answer I created a class where you can inherit from.

namespace WpfUIExtensions;

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

public class AspectRatioWindow : Window
{
    private const int WM_SIZING = 0x0214;
    private const int WM_WINDOWPOSCHANGING = 0x0046;

    private const int WMSZ_LEFT = 1;
    private const int WMSZ_RIGHT = 2;
    private const int WMSZ_TOP = 3;
    private const int WMSZ_TOPLEFT = 4;
    private const int WMSZ_TOPRIGHT = 5;
    private const int WMSZ_BOTTOM = 6;
    private const int WMSZ_BOTTOMLEFT = 7;
    private const int WMSZ_BOTTOMRIGHT = 8;

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

    private IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
    {
        switch (msg)
        {
            case WM_SIZING: sizingEdge = wParam.ToInt32(); break;

            case WM_WINDOWPOSCHANGING:

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

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

                switch (sizingEdge)
                {
                    case WMSZ_TOP or WMSZ_BOTTOM or WMSZ_TOPRIGHT:
                        position.cx = (int)(position.cy * xRatio);
                        break;

                    case WMSZ_LEFT or WMSZ_RIGHT or WMSZ_BOTTOMRIGHT or WMSZ_BOTTOMLEFT:
                        position.cy = (int)(position.cx * yRatio);
                        break;

                    case WMSZ_TOPLEFT:
                        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 new void Show()
    {
        xRatio = Width / Height;
        yRatio = Height / Width;

        base.Show();

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

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

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

Inherit it like a normal Window:

public sealed partial class MainWindow : AspectRatioWindow { }

And in XAML:

<ext:AspectRatioWindow ext="clr-namespace:WinUIExtensions"
                           x:Class="MyApp.MainWindow
           xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
           xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                           Title="MainWindow"
                           MinHeight="600"
                           MinWidth="800"
                           Height="600"
                           Width="800"
                           ResizeMode="CanResize">

<!--Your controls...-->

</ext:AspectRatioWindow>

Upvotes: 0

gil123
gil123

Reputation: 679

@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://learn.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);
            }
        }

    }
}

Upvotes: 0

gbmhunter
gbmhunter

Reputation: 1847

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>

Upvotes: 4

Mike Fuchs
Mike Fuchs

Reputation: 12319

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;
        }
    }
}

Upvotes: 23

yossharel
yossharel

Reputation: 1929

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

Upvotes: 0

Mark
Mark

Reputation: 21

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);
    }

Upvotes: 2

742
742

Reputation: 3069

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.

Upvotes: 2

Related Questions