Изменить размер окна WPF, но сохранить пропорции?

У меня есть окно WPF с изменяемым размером, которое я хочу ограничить, чтобы соотношение сторон окна оставалось постоянным.

В идеале я хотел бы ограничить положение мыши при изменении размера окна, перетаскивая угол в позиции, которые поддерживают правильное соотношение сторон. Если размер края изменяется с помощью мыши, другой размер должен измениться одновременно.

Есть ли простой способ сделать это или хороший онлайн-пример, о котором кто-нибудь знает?

Если не появятся лучшие решения, я опубликую то, что я сделал, после того, как немного доработаю его.


person Charley Rathkopf    schedule 18.03.2010    source источник
comment
Посмотрите, поможет ли это: stackoverflow.com/questions/288954/   -  person    schedule 28.06.2012


Ответы (6)


Я нашел хороший ответ от Нира здесь. Есть еще некоторые недостатки, в основном изменение размера в верхнем правом углу, нижнем правом углу и нижней стороне будет в порядке, другие стороны и углы - нет. Хорошая сторона в том, что соотношение сторон плавно сохраняется все время.

РЕДАКТИРОВАТЬ: Я нашел способ устранить большинство проблем. Когда начинается изменение размера, размер, который будет искусственно скорректирован для сохранения соотношения сторон, определяется путем размещения позиции мыши относительно окна. Единственные оставшиеся недостатки, которые я обнаружил, - это то, что положение окна может измениться при изменении размера от углов (кроме правого нижнего).

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>

Код позади:

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;
        }
    }
}
person Mike Fuchs    schedule 11.04.2013
comment
Это действительно очень хорошо работает. Единственное, почему он не позволяет мне изменять размер по бокам? Пускает только сверху / снизу или по диагонали. Спасибо хоть! - person It'sNotALie.; 12.04.2013

Хотя это не заставляет окно иметь определенное соотношение (как просил OP), мне удалось получить СОДЕРЖАНИЕ окна для масштабирования при сохранении исходного соотношения сторон, заключив содержимое в Viewbox и установив свойства растяжения как Stretch="Uniform". Никакого кода программной части не требуется.

WPF:

<Viewbox Name="MainViewbox" Stretch="Uniform">
    ... your content here
</Viewbox>
person gbmhunter    schedule 16.01.2016

Это помогает:

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) {

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

Нашел здесь.

person 742    schedule 04.05.2010

В приведенном выше ответе предпочтение отдается изменению ширины по сравнению с изменением высоты, поэтому, если вы сильно отрегулируете высоту, но из-за позиционирования мыши ширина также немного изменится, пользователь все равно будет видеть почти то же самое окно. У меня есть этот код, который отрабатывает процентные изменения в каждом измерении, отдавая предпочтение наибольшему изменению как наиболее интересному для пользователя.

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);
    }
person Mark    schedule 07.03.2011
comment
Математика идеальна, но когда она реализована в коде, изменение размера происходит очень скачко, а не плавно. - person mandarin; 16.06.2015

В окне - вы можете просто прослушать сообщение Win32 API:

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

В этом коде вы всегда берете более короткую сторону и устанавливаете ее равной более длинной. Вы всегда можете воспользоваться противоположным подходом и установить, что чем длиннее, тем короче. Я нашел решение здесь: http://social.msdn.microsoft.com/forums/en-US/wpf/thread/b0df3d1f-e211-4f54-a079-09af0096410e

person yossharel    schedule 17.11.2011
comment
Проблема в том, что WM_EXITSIZEMOVE отправляется в окно только тогда, когда пользователь завершает изменение размера. Сообщение WM_SIZING - это то, что срабатывает постоянно, но у меня с этим были прерывистые, спастические результаты. - person lordcheeto; 24.04.2012

@Mike Fuchs ответ не идеален. Если вы измените размер окна из верхнего левого угла и из левого нижнего угла окна, окно будет перемещаться, пока вы изменяете его размер.

Я нашел более элегантный способ без этих проблем.

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

    }
}
person gil123    schedule 05.02.2021