如何在WPF窗口中只允许等比例缩放?

18

我不希望我的窗口被 "仅水平" 或 "仅垂直" 调整大小。是否有属性可以在我的窗口上设置以强制执行此操作,或者是否有巧妙的代码后台技巧可以使用?


我曾回答过一个类似的问题,即如何在调整大小时保持WPF窗口的纵横比。请查看我的帖子这里,它基于Nir的答案。 - Mike Fuchs
9个回答

13

你可以始终处理WM_WINDOWPOSCHANGING消息,这样就可以在调整大小过程中控制窗口的大小和位置(而不是在用户完成调整大小后进行修复)。

以下是如何在WPF中实现的,我从几个源代码中组合了这段代码,所以可能会有一些语法错误。

internal enum WM
{
   WINDOWPOSCHANGING = 0x0046,
}

[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 void Window_SourceInitialized(object sender, EventArgs ea)
{
   HwndSource hwndSource = (HwndSource)HwndSource.FromVisual((Window)sender);
   hwndSource.AddHook(DragHook);
}

private static IntPtr DragHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handeled)
{
   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;
          }

          bool changedPos = false;

          // ***********************
          // Here you check the values inside the pos structure
          // if you want to override tehm just change the pos
          // structure and set changedPos to true
          // ***********************

          if (!changedPos)
          {
             return IntPtr.Zero;
          }

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

   return IntPtr.Zero;
}

这是一种非常巧妙的做法。 - Ben Doerr
有趣的是,我们不知道用户是否正在更改宽度、高度(或两者都在更改)。我们需要知道这一点,以确定我们必须调整高度还是宽度。 - Serge Wautier
工作得非常好,比在SizeChanged/RenderSizeChanged中进行调整要得到更平滑的结果。 - Mike Fuchs

13

使用WPF的ViewBox与具有固定宽度和高度的控件,可以保留内容的纵横比。

让我们试一试。您可以更改ViewBox的“Stretch”属性以获得不同的结果。

这是我的屏幕截图:enter image description here

<Window x:Class="TestWPF.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">

    <Viewbox Stretch="Uniform">
        <StackPanel Background="Azure" Height="400" Width="300" Name="stackPanel1" VerticalAlignment="Top">
            <Button Name="testBtn" Width="200" Height="50">
                <TextBlock>Test</TextBlock>
            </Button>
        </StackPanel>
    </Viewbox>

</Window>

3
“让我们试试看”——这是描述WPF的完美说法——它会起作用吗?谁也不知道!让我们试试看。我还是坚持使用Win32,只需告诉操作系统:“这是我的大小,请处理它。” - Frank Krueger
7
@Frank Krueger:在Win32中并不像这么简单,唯一的区别是你已经了解了它的怪癖和预期行为。一旦你在WPF中了解了这些内容,想做任何事情都同样容易。 - Pop Catalin
1
哦不不,弗兰克。我实际上测试了上面的代码,它完美地运行正常。可能是有些误解 :) 请原谅我的糟糕英语。 - Gant
我想我会包含一些屏幕截图。 - Gant
这种方法有些可行(我曾经使用过一段时间),但是当调整大小时,我不希望我的按钮/复选框/文本实际上变得更大或更小。 - Mark Carpenter
8
这将导致窗口的内容被均匀地调整大小,而不是窗口本身... - Thomas Levesque

8
这是我的解决方案。
您需要将以下内容添加到您的控件/窗口标签中:
Loaded="Window_Loaded"

你需要将以下代码放置在你的后端代码中:

private double aspectRatio = 0.0;

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    aspectRatio = this.ActualWidth / this.ActualHeight;
}

protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
    if (sizeInfo.WidthChanged)
    {
        this.Width = sizeInfo.NewSize.Height * aspectRatio;
    }
    else
    {
        this.Height = sizeInfo.NewSize.Width * aspectRatio;
    }
}

我尝试了Viewbox技巧,但是并不喜欢它。我希望将窗口边框锁定在特定大小上。这在窗口控件上进行了测试,但我认为对边框也适用。


2
这种方法是可行的,但缺点是在调整大小操作期间窗口边框会闪烁(用不太准确的词来形容)- 首先将大小设置为用户自由调整的大小,然后再调整边框以适应覆盖的大小。 - Tomi Junnila

1

可能有点晚了,但您可以将它简单地放在您的代码后面...

Private Sub UserControl1_SizeChanged(ByVal sender As Object, ByVal e As System.Windows.SizeChangedEventArgs) Handles Me.SizeChanged
    If e.HeightChanged Then
        Me.Width = Me.Height
    Else
        Me.Height = Me.Width
    End If
End Sub

1
你可以尝试复制 Flash 视频网站上经常看到的效果。它们允许你随意扩展浏览器窗口,但只会拉伸演示区域以适应高度或宽度最小的一侧。
例如,如果你垂直拉伸窗口,你的应用程序不会重新调整大小。它只会在显示区域顶部和底部添加黑色条,并保持垂直居中。
这在 WPF 中可能是可能的,也可能不可能,我不确定。

1
在代码示例中:
if (sizeInfo.WidthChanged)     
{         
    this.Width = sizeInfo.NewSize.Height * aspectRatio;    
}     
else     
{         
    this.Height = sizeInfo.NewSize.Width * aspectRatio; 
} 

我认为第二个计算应该是:

this.Height = sizeInfo.NewSize.Width * (1/aspectRatio);  

我在“SizeChanged”事件处理程序中制作了这项工作的变体。由于我希望宽度成为控制维度,所以我使用以下形式的计算强制将高度与之匹配:
if (aspectRatio > 0)
// enforce aspect ratio by restricting height to stay in sync with width.  
this.Height = this.ActualWidth * (1 / aspectRatio);

你可能会注意到检查 aspectRatio > 0 的部分...我这样做是因为我发现在“Load”方法分配 aspectRatio 之前,它往往会调用我的调整大小处理程序。


1

我原本以为你可以使用值转换器将宽度与高度进行双向绑定,以保持纵横比。将纵横比作为转换器参数传递会使其更具通用性。

因此,我首先尝试了没有转换器的绑定:

<Window 
    ...
    Title="Window1" Name="Win" Height="500" 
    Width="{Binding RelativeSource={RelativeSource self}, 
                    Path=Height, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <StackPanel>
        <TextBlock>Width:</TextBlock>
        <TextBlock Text="{Binding ElementName=Win, Path=Width}" />
        <TextBlock>Height:</TextBlock>
        <TextBlock Text="{Binding ElementName=Win, Path=Height}" />
    </StackPanel>    
</Window>

奇怪的是,绑定好像是单向的,并且窗口的报告宽度(如TextBlock所示)与屏幕上的大小不一致!

这个想法可能值得追求,但首先需要解决这种奇怪的行为。

希望这能帮到你!


0

我有一种方法,不依赖于Windows平台特定的API,同时提供可接受的用户体验(在拖动窗口时不会抖动)。它使用定时器在0.1秒后调整窗口大小,因此用户看不到它抖动。

public partial class MainWindow : Window
{
    private DispatcherTimer resizeTimer;
    private double _aspectRatio;
    private SizeChangedInfo? _sizeInfo;

    public MainWindow()
    {
        InitializeComponent();
        _aspectRatio = Width / Height;
        resizeTimer = new DispatcherTimer();
        resizeTimer.Interval = new TimeSpan(100*10000); // 0.1 seconds
        resizeTimer.Tick += ResizeTimer_Tick;
    }

    private void ResizeTimer_Tick(object? sender, EventArgs e)
    {
        resizeTimer.Stop();
        if (_sizeInfo == null) return;
        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;
    }

    protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
    {
        _sizeInfo = sizeInfo;
        resizeTimer.Stop();
        resizeTimer.Start();
        base.OnRenderSizeChanged(sizeInfo);
    }
}

0

或许有点晚了,但我在Mike O'Brien的博客上找到了一个解决方案,它非常有效。 http://www.mikeobrien.net/blog/maintaining-aspect-ratio-when-resizing/ 以下是他博客中的代码:

<Window ... SourceInitialized="Window_SourceInitialized" ... >
    ...
Window>

public partial class Main : Window
{
    private void Window_SourceInitialized(object sender, EventArgs ea)
    {
        WindowAspectRatio.Register((Window)sender);
    }
    ...
}


internal class WindowAspectRatio
{
    private double _ratio;

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

    public static void Register(Window window)
    {
        new WindowAspectRatio(window);
    }

    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;

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

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

        return IntPtr.Zero;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接