15

I am trying to center a Window to the owner window. I also need the child window to move along the owner window. A cross-post on the MSDN WPF forum's can be found here.

To achieve this I subscribe to the LocationChanged and SizeChanged events (and also the StateChanged event) of the owner of my child window. When those events are triggered I recompute the location of child window. I do this in the code-behind of the child window.

The code is very straight forward:

Top = Owner.Top + ((Owner.ActualHeight - ActualHeight) / 2);
Left = Owner.Left + ((Owner.ActualWidth - ActualWidth) / 2);

If you compile and run the sample program I provided you will see that it works when the main window is as-is, and moved around. So that part works.

The problem arises when the owner window is maximized. (And after being maximized, set back to normal.) Because I subscribe to three events I enter the relocate function three times. After printing out the owner data I get different results. Most annoyingly the Top and Left values of the owner window are off. It seems it gets the correct Top and Left values when the state changes, but then the ActualWidth and ActualHeight values are wrong. When the LocationChanged or SizeChanged events are triggered the ActualWidth and ActualHeight values are OK, but the Top and Left values are incorrect. It seems these are the previous values. How can this be? What is causing this? And is there a proper fix for this?

Since the same code seemed to have worked in .net 3.5 I am under the impression something changed in .net 4. (Or I had a weird timing issue which caused the problem not to appear.) But I cannot find any documented change in this part.

.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

So the main question remains: why are the Top and Left values of the owner incorrect?

Jensen
  • 3,498
  • 2
  • 26
  • 43
  • I found a thread on SO that mentions how to catch the maximiaze/restore events by overriding WndProc: http://stackoverflow.com/questions/1295999/event-when-a-window-gets-maximized-un-maximized Try the code in the answer http://stackoverflow.com/a/3382336/137972 – Christoffer Mar 22 '12 at 08:29
  • 1
    Since the T and L from the LocationChanged event is correct I can cache them and use the W and H from the SizeChanged event as that event comes AFTER the LocationChanged even, but just as your suggestion it's a workaround. I'd rather now why this behavior is different. – Jensen Mar 22 '12 at 08:55
  • I am doing these calculations across two windows, on difference dispatchers and I'm very thankful I came across this post! In the end, I also used the caching approach. – RichardOD Dec 08 '12 at 22:41
  • The best solution I've found so far uses reflection: http://stackoverflow.com/a/8056464/691790 – Matt Zappitello Jun 26 '13 at 13:45

4 Answers4

16

The comment from Mataniko regarding the migration issues in .NET 4.0 was correct. Since my code was working when the WindowState was set to Normal, I could keep that. I just had to foresee something when the WindowState was Maximized.

I implemented the native GetWindowRect() function to achieve this, as it could give me the proper dimensions.

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

Next, when the Owner.WindowState is Maximized, we use the GetWindowRectangle function to get the actual dimensions. We don't care about a border at this point, but if needed it can be incorporated using the GetSystemMetrics function.

if (Owner.WindowState == WindowState.Maximized)
{
    var rect = GetWindowRectangle(Owner);

    Top = rect.Top + ((rect.Bottom - ActualHeight) / 2);
    Left = rect.Left + ((rect.Right - ActualWidth) / 2);
}
Community
  • 1
  • 1
Jensen
  • 3,498
  • 2
  • 26
  • 43
3

I like a solution proposed in this similar question. The most up-voted answer used a single method to get the window left value regardless of the window state using reflection instead of native code. It also worked for the actual top. Here's the code just in case the question/answer gets erased.

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;
    }
}
Matt Becker
  • 2,338
  • 1
  • 28
  • 36
2

I don't know if it's a bug in .NET 4.0 or intended behavior. My suspicion is that there is a race condition between the events registered and actually firing. Even though LocationChanged is fired first, SizeChanged is registered first with the still incorrect values of the Location. You can easily get around this by creating a local variable in the child window that registers the Owner top and left in the LocationChanged event.

Example:

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);
        }
    }
Mataniko
  • 2,212
  • 16
  • 18
  • 1
    One more thing: LocationChanged is also fired when the WindowState changes, you don't need to register to StateChanged event and only use the Top Left values from LocationChanged and Width/Height values from SizeChanged. Source: [MSDN](http://msdn.microsoft.com/en-us/library/system.windows.window.locationchanged.aspx) – Mataniko Mar 22 '12 at 09:32
  • This still poses a problem. If the owner window maximized BEFORE the child window is opened the child window cannot get decent Top and Left values and the child window is again positioned incorrectly. – Jensen Mar 22 '12 at 09:39
  • In that case you can register for the SizeChanged/LocationChanged events in the main form itself and open the child form with those values. It's a clunk but it does look like a regression in .NET 4.0. It might be worth opening a report with MS, since this still happens with .NET 4.5 – Mataniko Mar 22 '12 at 10:08
  • 3
    I take it back, this is intended behavior: "The Top, Left, Width, and Height properties for a maximized or minimized window now contain the correct restore location of the window instead of other values, depending on the monitor." Source: [.NET 4.0 Known Migration Issues](http://msdn.microsoft.com/en-us/library/ee941656.aspx) – Mataniko Mar 22 '12 at 10:11
  • It is bad of Microsoft, to close that "road" without showing another "road" to get to the needed values. Exposing the ```_actualLeft``` and ```_actualTop``` to the public like it is done with the ```ActualWidth```,```ActualHeigth``` is not that complicated, is it? The issue is still here in v 4.7 in the late 2018s. -.- – FrankM Oct 18 '18 at 12:14
-1

Have you tried..

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

And to revert (order is important)...

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

if (maximised)
    WindowState = FormWindowState.Maximized;
damichab
  • 162
  • 10
  • According to its specs (https://msdn.microsoft.com/en-us/library/system.windows.window.restorebounds(v=vs.110).aspx) RestoreBounds claims to be the Window's bounds right before it got Maximized or Minimized. So no this would not be the solution to the problem at hand. Sadly RestoreBounds does not even give you what it claims it would at least not in my test on Windows 7 .NET 4.5 – ILIA BROUDNO Feb 24 '16 at 20:33
  • All I can say to that is that it works for me, but the 'if' statement is important - you need to check the windows state before getting the parameters. Assigning the parameters is obviously the reverse. – damichab Feb 01 '17 at 01:56
  • I have updated the answer in case anyone else had problems with it. Late I know! – damichab Feb 01 '17 at 02:14
  • Doesn't work for me either, and not what RestoreBounds is. – Epirocks Feb 22 '17 at 15:41