`CenterOwner` with Win32 owner
Description
I see that a code path exists for using WindowStartupLocation.CenterOwner with a Win32 owner, namely the else branch after https://source.dot.net/#PresentationFramework/System/Windows/Window.cs,3659.
And indeed, this works, as long as that owner isn't maximized. If it is maximized, the window position isn't what I would expect.
Reproduction Steps
Take a WPF app template.
Enable WinForms interop in the csproj:
<UseWindowsForms>true</UseWindowsForms>
Modify App.xaml to not have a StartupUri:
<Application x:Class="WpfApp1.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApp1">
<Application.Resources>
</Application.Resources>
</Application>
Modify MainWindow.xaml to be a little smaller:
<Window x:Class="WpfApp1.MainWindow"
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:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="600" Width="600">
<Grid>
</Grid>
</Window>
Finally, add a constructor to App that creates a WinForms form and uses MainWindow:
using System.Windows;
using System.Windows.Interop;
namespace WpfApp1
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
var form = new System.Windows.Forms.Form
{
Width = 1_000,
Height = 1_000
};
form.WindowState = System.Windows.Forms.FormWindowState.Maximized;
form.Show();
var window = new MainWindow
{
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
_ = new WindowInteropHelper(window)
{
Owner = form.Handle
};
window.ShowDialog();
}
}
}
Expected behavior
The WPF window should show up as concentric to the form.
Actual behavior
If we comment out form.WindowState = System.Windows.Forms.FormWindowState.Maximized; and thus the form shows as a regular window, this does work.
But if the form is maximized, the WPF window instead is concentric to where the form would be if it weren't maximized. I don't believe this behavior makes sense.
Regression?
None.
Reproducible in net47, net7.0-windows, net8.0-windows.
Known Workarounds
I would like one, especially one that works with .NET Framework 4.7.2.
I'm guessing I need to call SetupInitialState, perhaps by calling CreateSourceWindow(duringShow: false), then fixing the location, then call Show()?
Impact
We have a legacy primarily WinForms app that we're adding WPF stuff to bit by bit, including by adding WPF-based dialogs — which, preferably, would center correctly.
Configuration
net472,net7.0-windows,net8.0-windows- Windows 11 22H2 23612
- x64 (because of
net472), ARM64
I don't think this is specific to the above configuration.
Other information
It appears CalculateWindowLocation does consider the case of "what if the owning WPF window is maximized", but not "what if the owning Win32 form is maximized".
I've written/amended extension methods to help myself in the meantime. Sharing this for others who run into the same problem. This will work in net472, net7.0-windows and newer.
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
using WinForms = System.Windows.Forms;
namespace EL.Client.WpfUtils.WinFormsInterop
{
public static class WindowOwnerExtensions
{
public static void SetOwner(this Window window, WinForms.Control winFormsControl)
{
_ = new WindowInteropHelper(window)
{
Owner = winFormsControl.Handle
};
}
/// <summary>
/// <para>
/// Set a WPF window's owner to a Win32/WinForms owner, centers to, then
/// opens it. Do not set <c>WindowStartupLocation</c> separately.
/// </para>
///
/// <para>
/// This is necessary because WPF's code
/// https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Window.cs,1872024dfc3ed928
/// gets the wrong metrics when a Win32 owner is maximized: it treats
/// the owner as though it <em>weren't</em> maximized. Thus, with
/// <see cref="WindowStartupLocation.CenterOwner"/> and a Win32 owner
/// that's maximized, the center would be wrong.
/// </para>
/// </summary>
/// <param name="window">The WPF window</param>
/// <param name="winFormsControl">The owning Win32 control</param>
public static void ShowWithConcentricOwner(this Window window, WinForms.Control winFormsControl)
{
SetOwner(window, winFormsControl);
bool isMaximized = IsWinFormsOwnerMaximized(winFormsControl);
if (!isMaximized)
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
else
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
window.Show();
}
/// <summary>
/// <para>
/// Set a WPF window's owner to a Win32/WinForms owner, centers to, then
/// opens it. Do not set <c>WindowStartupLocation</c> separately.
/// </para>
///
/// <para>
/// Returns only when the window is closed.
/// </para>
///
/// <para>
/// This is necessary because WPF's code
/// https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Window.cs,1872024dfc3ed928
/// gets the wrong metrics when a Win32 owner is maximized: it treats
/// the owner as though it <em>weren't</em> maximized. Thus, with
/// <see cref="WindowStartupLocation.CenterOwner"/> and a Win32 owner
/// that's maximized, the center would be wrong.
/// </para>
/// </summary>
/// <param name="window">The WPF window</param>
/// <param name="winFormsControl">The owning Win32 control</param>
public static bool? ShowDialogWithConcentricOwner(this Window window, WinForms.Control winFormsControl)
{
SetOwner(window, winFormsControl);
bool isMaximized = IsWinFormsOwnerMaximized(winFormsControl);
if (!isMaximized)
window.WindowStartupLocation = WindowStartupLocation.CenterOwner;
else
window.WindowStartupLocation = WindowStartupLocation.CenterScreen;
return window.ShowDialog();
}
private static bool IsWinFormsOwnerMaximized(WinForms.Control winFormsControl)
{
bool isMaximized;
Windows.Win32.UI.WindowsAndMessaging.WINDOWPLACEMENT windowPlacement = new();
windowPlacement.length = (uint)Marshal.SizeOf(windowPlacement);
Windows.Win32.Foundation.HWND windowHandle = (Windows.Win32.Foundation.HWND)winFormsControl.Handle;
if (!Windows.Win32.PInvoke.GetWindowPlacement(windowHandle, ref windowPlacement))
isMaximized = false; // fall back to WPF code
else
isMaximized = windowPlacement.showCmd == Windows.Win32.UI.WindowsAndMessaging.SHOW_WINDOW_CMD.SW_MAXIMIZE;
return isMaximized;
}
}
}
(GetWindowPlacement and related symbols are source-generated with CsWin32.)