Reputation: 4788
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
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
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
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
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
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
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
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