0
votes

I need to keep the aspect ratio of my WPF window when it gets resized. Only solution I have found to solve the aspect ratio was to use the following WINAPI code and register the Window to the WindowAspectRatio class in the Window_SourceInitialized event handler.

The Window_SourceInitialized event handler.

private void Window_SourceInitialized(object sender, EventArgs e)
    {
        ratio = WindowAspectRatio.Register((Window)sender);
        double scaleOfScreen = 0.5;

        double w = (double)WpfScreen.GetScreenFrom(this).WorkingArea.Width;
        double h = (double)WpfScreen.GetScreenFrom(this).WorkingArea.Height;
        if (w < h)
        {
            Application.Current.MainWindow.Width = w * scaleOfScreen;
            Application.Current.MainWindow.Height = w * scaleOfScreen / ratio;
        }
        else
        {
            Application.Current.MainWindow.Height = h * scaleOfScreen;
            Application.Current.MainWindow.Width = h * ratio * scaleOfScreen;
        }

        this.InvalidateVisual();
    }

The WindowAspectRatio class

 internal class WindowAspectRatio
{
    private double ratio_;
    private Window windowToTrack_;

    private WindowAspectRatio(Window window)
    {
        ratio_ = window.Width / window.Height;
        windowToTrack_ = window;
        ((HwndSource)HwndSource.FromVisual(window)).AddHook(DragHook);
    }

    public static double Register(Window window)
    {
        return new WindowAspectRatio(window).ratio_;
    }

    internal enum WM
    {
        WINDOWPOSCHANGING = 0x0046,
    }

    [Flags()]
    public enum SWP
    {
        NoMove = 0x2,
    }

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

    private IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
    {
        if ((WM)msg == WM.WINDOWPOSCHANGING)
        {
            WINDOWPOS position = (WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));

            if ((position.flags & (int)SWP.NoMove) != 0 ||
                HwndSource.FromHwnd(hwnd).RootVisual == null) return IntPtr.Zero;


            double screenW = (double)WpfScreen.GetScreenFrom(windowToTrack_).WorkingArea.Width;
            double screenH = (double)WpfScreen.GetScreenFrom(windowToTrack_).WorkingArea.Height;

            if (position.cy >= screenH)
                position.cy = (int)screenH;

            position.cx = (int)(position.cy * ratio_);

            if (position.cx >= screenW)
            {
                position.cx = (int)screenW;
                position.cy = (int)(position.cx / ratio_);
            }

            Marshal.StructureToPtr(position, lParam, true);
            handeled = true;
        }

        return IntPtr.Zero;
    }

This works well. When resizing the window by dragging in a Window corner, it works just perfect and aspect ratio is kept.

If I move the mouse pointer to the bottom edge of the window, where one would resize the Window's height, it works fine too to drag that edge. The Window is resized and aspect ratio is still kept.

Problem is when moving the mouse pointer to the left Window edge I get the resize mouse pointer icon, but when trying to resize the window, the window now instead just moves horizontally.

Also when moving the mouse pointer to the right Window edge I get the resize mouse pointer icon, but when trying to resize nothing happens at all.

For me it would be no problem to live with that only resizing by the corner would work if one could just prevent the the resize mouse pointer icon to show up when trying to resize horizontally (by left or right edge) or vertical only (by bottom edge), allowing only a resize by grabbing the window's corner. So the question, is it possible to somehow prevent the horizontal and vertical resize mouse pointer icons but still allow the resize (vertically and horizontally) at the same time by grabbing the corner?

Alternatively, if someone has an idea how one could resolve the problem of horizontal resizing not working as expected using the code above that would also solve the problem.

2
I believe binding the ActualHeight to the ActualWidth property (with a ValueConverter) can achieve the goal of keeping w/h ratio while the window is being resized. Preventing some user interactions can do the trick but not a good implementation to your original requirement.kennyzx
It is a good point that may also work.Johan

2 Answers

0
votes

In the meantime I managed to prevent all horizontal / vertical only resizing by changing the WINAPI code to the following. If one could just prevent the mouse pointer icon to show the resize icons at bottom, left and right Window edges I would have an acceptable solution.

    private IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
    {
        if ((WM)msg == WM.WINDOWPOSCHANGING)
        {
            WINDOWPOS position = (WINDOWPOS)Marshal.PtrToStructure(lParam, typeof(WINDOWPOS));

            if ((position.flags & (int)SWP.NoMove) != 0 ||
                HwndSource.FromHwnd(hwnd).RootVisual == null) return IntPtr.Zero;


            double screenW = (double)WpfScreen.GetScreenFrom(windowToTrack_).WorkingArea.Width;
            double screenH = (double)WpfScreen.GetScreenFrom(windowToTrack_).WorkingArea.Height;


            if (Math.Abs(position.cx - windowToTrack_.Width) < Math.Abs(position.cy - windowToTrack_.Height))
            {
                if (position.cx >= screenW)
                    position.cx = (int)screenW;
                position.cy = (int)(position.cx / ratio_);
                if (position.cy >= screenH)
                    position.cy = (int)screenH;
                position.cx = (int)(position.cy * ratio_);
            }
            else
            {
                if (
                    (position.cx < windowToTrack_.Width &&
                    position.cy == windowToTrack_.Height )
                    ||
                    (position.cx > windowToTrack_.Width &&
                    position.cy == windowToTrack_.Height )
                    )
                {
                    handeled = true;
                    position.x = (int)windowToTrack_.Left;
                    position.y = (int)windowToTrack_.Top;
                    position.cx = (int)windowToTrack_.Width;
                    position.cy = (int)windowToTrack_.Height;
                    Marshal.StructureToPtr(position, lParam, true);
                    return IntPtr.Zero;
                }

                if (position.cy >= screenH)
                    position.cy = (int)screenH;
                position.cx = (int)(position.cy * ratio_);
                if (position.cx >= screenW)
                    position.cx = (int)screenW;
                position.cy = (int)(position.cx / ratio_);
            }

            Marshal.StructureToPtr(position, lParam, true);
            handeled = true;
        }

        return IntPtr.Zero;
    }
}
0
votes

Okay I found an easy solution. Someone had solved the same problem also based on the WINAPI approach allowing all borders to resize keeping the aspect ratio. The essential part of the solution below.

public partial class MainWindow : Window
    {
        private double _aspectRatio;
        private bool? _adjustingHeight = null;

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

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

            _aspectRatio = this.Width / this.Height;
        }



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

        [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()
        {
            Win32Point w32Mouse = new Win32Point();
            GetCursorPos(ref w32Mouse);
            return new Point(w32Mouse.X, w32Mouse.Y);
        }

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

    }