在.NET 4中,当最大化窗口时,窗口的顶部和左侧值没有正确更新。

15
我正在尝试将窗口居中于其所有者窗口。我还需要子窗口随其所有者窗口一起移动。可以在MSDN WPF论坛上找到交叉帖here
为了实现这一点,我订阅了子窗口所有者的LocationChangedSizeChanged事件(以及StateChanged事件)。当这些事件被触发时,我重新计算子窗口的位置。我在子窗口的代码后台完成此操作。
这段代码非常简单:
Top = Owner.Top + ((Owner.ActualHeight - ActualHeight) / 2);
Left = Owner.Left + ((Owner.ActualWidth - ActualWidth) / 2);

如果您编译和运行我提供的示例程序,您会发现当主窗口保持原样或移动时它可以工作。所以那部分是有效的。
问题出现在所有者窗口被最大化后(并且恢复正常后)。因为我订阅了三个事件,我进入重新定位函数三次。在打印所有者数据后,我得到不同的结果。最令人烦恼的是所有者窗口的顶部和左侧值是错误的。似乎当状态改变时它获得了正确的顶部和左侧值,但是实际宽度和高度值是错误的。当触发LocationChanged或SizeChanged事件时,ActualWidth和ActualHeight值是正确的,但是顶部和左侧值是不正确的。似乎这些是以前的值。这是怎么回事?是什么导致了这个问题?是否有适当的解决方法?
由于相同的代码似乎在.net 3.5中起作用,我认为在.net 4中发生了一些变化。(或者我有一个奇怪的定时问题导致问题没有出现。)但是我找不到任何关于这部分的文档更改。

.NET 3.5:

OnOwnerLocationChanged
T: -8; L: -8; W: 640; H: 480
OnOwnerStateChanged
T: -8; L: -8; W: 640; H: 480
OnOwnerSizeChanged
T: -8; L: -8; W: 1936; H: 1066

.NET 4.0:

OnOwnerLocationChanged
T: -8; L: -8; W: 640; H: 480
OnOwnerStateChanged
T: 494; L: 33; W: 640; H: 480
OnOwnerSizeChanged
T: 494; L: 33; W: 1936; H: 1066

所以主要问题仍然是:为什么所有者的Top和Left值是不正确的?

我在SO上找到了一个线程,提到如何通过覆盖WndProc来捕获最大化/还原事件:https://dev59.com/ZnM_5IYBdhLWcg3wn0vT 尝试使用答案中的代码https://dev59.com/ZnM_5IYBdhLWcg3wn0vT#3382336 - Christoffer
1
由于 LocationChanged 事件中的 T 和 L 是正确的,我可以将它们缓存并使用 SizeChanged 事件中的 W 和 H,因为该事件在 LocationChanged 事件之后发生,但正如您所建议的那样,这只是一个解决方法。我更想知道为什么这种行为不同。 - Jensen
我正在两个窗口之间进行这些计算,使用不同的调度程序,非常感谢我遇到了这篇文章!最终,我也采用了缓存方法。 - RichardOD
到目前为止,我发现的最佳解决方案使用了反射技术:https://dev59.com/gVDTa4cB1Zd3GeqPGiFJ#8056464 - Matt Zappitello
4个回答

16

comment中的Mataniko关于.NET 4.0中迁移问题的评论是正确的。由于当WindowState设置为Normal时我的代码可以正常工作,所以我可以保留它。我只需要在WindowStateMaximized时预见一些问题。

我实现了本地的GetWindowRect()函数来实现这一点,因为它可以给我正确的尺寸。

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);

// Make sure RECT is actually OUR defined struct, not the windows rect.
public static RECT GetWindowRectangle(Window window)
{
    RECT rect;
    GetWindowRect((new WindowInteropHelper(window)).Handle, out rect);

    return rect;
}

接下来,当Owner.WindowStateMaximized时,我们使用GetWindowRectangle函数获取实际尺寸。此时我们不关心边框,但如果需要可以使用GetSystemMetrics函数加入。
if (Owner.WindowState == WindowState.Maximized)
{
    var rect = GetWindowRectangle(Owner);

    Top = rect.Top + ((rect.Bottom - ActualHeight) / 2);
    Left = rect.Left + ((rect.Right - ActualWidth) / 2);
}

3

我喜欢在这个类似问题中提出的解决方案。最受欢迎的答案使用了反射来获取窗口左侧值,而不是使用本地代码,因此可以获取窗口实际上的顶部值。以下是代码,以防问题/答案被删除。

public static class WindowExtensions
{
    /// <summary>
    /// Gets the window left.
    /// </summary>
    /// <param name="window">The window.</param>
    /// <returns></returns>
    public static double GetWindowLeft(this Window window)
    {
        if (window.WindowState == WindowState.Maximized)
        {
            var leftField = typeof(Window).GetField("_actualLeft", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            return (double)leftField.GetValue(window);
        }
        else
            return window.Left;
    }

    /// <summary>
    /// Gets the window top.
    /// </summary>
    /// <param name="window">The window.</param>
    /// <returns></returns>
    public static double GetWindowTop(this Window window)
    {
        if (window.WindowState == WindowState.Maximized)
        {
            var topField = typeof(Window).GetField("_actualTop", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            return (double)topField.GetValue(window);
        }
        else
            return window.Top;
    }
}

2
我不知道这是.NET 4.0的错误还是意图行为。我怀疑事件注册和实际触发之间存在竞争条件。尽管LocationChanged首先被触发,但SizeChanged使用的仍然是Location的错误值。您可以通过在子窗口中创建一个本地变量,在LocationChanged事件中注册Owner的top和left来轻松解决此问题。
示例:
private Point _ownerLocation;

private void OnOwnerLocationChanged(object sender, EventArgs e)
    {
        Console.WriteLine("OnOwnerLocationChanged");
        _ownerLocation = new Point(Owner.Top, Owner.Left);
        SetLocationToOwner();
    }

private void SetLocationToOwner()
    {
        if (IsVisible && (Owner != null))
        {
            Console.WriteLine("T: {0}; L: {1}; W: {2}; H: {3}", Owner.Top, Owner.Left, Owner.ActualWidth, Owner.ActualHeight);

            Top = _ownerLocation.X + ((Owner.ActualHeight - ActualHeight) / 2);
            Left = _ownerLocation.Y + ((Owner.ActualWidth - ActualWidth) / 2);
        }
    }

1
还有一件事:当WindowState改变时,也会触发LocationChanged事件,您不需要注册StateChanged事件,只需使用来自LocationChanged的Top Left值和来自SizeChanged的Width/Height值即可。 - Mataniko
这仍然存在一个问题。如果在打开子窗口之前,父窗口已经最大化了,那么子窗口将无法获取合适的上部和左边值,导致子窗口的位置再次错误地被定位。 - Jensen
在这种情况下,您可以在主窗体本身中注册SizeChanged/LocationChanged事件,并使用这些值打开子窗体。这很笨拙,但它看起来像是.NET 4.0中的一个回归错误。也许值得向微软提交一份报告,因为这在.NET 4.5中仍然存在。 - Mataniko
3
我收回之前的说法,这是预期行为: “最大化或最小化窗口的顶部、左侧、宽度和高度属性现在包含窗口正确恢复位置的值,而不是其他值,具体取决于显示器。” 来源: .NET 4.0已知迁移问题 - Mataniko
微软这样关闭“道路”而不显示另一条“道路”以获取所需值是不好的。像ActualWidthActualHeight那样向公众公开_actualLeft_actualTop并不复杂,是吧?这个问题在2018年末的v4.7版本仍然存在。-.- - FrankM

-1

你试过了吗?

int left, top, width, height;
bool maximised;

if (WindowState == FormWindowState.Maximized)
{
    maximised = true;
    left = RestoreBounds.X;
    top = RestoreBounds.Y;
    width = RestoreBounds.Width;
    height = RestoreBounds.Height;
}
else
{
    maximised = false;
    left = Left;
    top = Top;
    width = Width;
    height = Height;
}

而要还原(顺序很重要)...

StartPosition = FormStartPosition.Manual;
Location = new Point(left, top);
Size = new Size(width, height);

if (maximised)
    WindowState = FormWindowState.Maximized;

根据其规格(https://msdn.microsoft.com/en-us/library/system.windows.window.restorebounds(v=vs.110).aspx),RestoreBounds 声称是窗口最大化或最小化之前的边界。因此,这不是解决手头问题的方法。遗憾的是,在 Windows 7 .NET 4.5 上进行测试时,RestoreBounds 甚至没有提供它所声称的内容。 - ILIA BROUDNO
我能说的是,这对我有效,但“if”语句很重要——在获取参数之前,您需要检查窗口状态。分配参数显然是相反的。 - damichab
我已经更新了答案,以防其他人也遇到了问题。我知道有点晚了! - damichab
对我也不起作用,而且不是RestoreBounds的作用。 - Epirocks

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