Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CenterOwner with Win32 owner #9018

Open
chucker opened this issue Apr 11, 2024 · 1 comment
Open

CenterOwner with Win32 owner #9018

chucker opened this issue Apr 11, 2024 · 1 comment
Labels
Investigate Requires further investigation by the WPF team.

Comments

@chucker
Copy link

chucker commented Apr 11, 2024

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".

@Kuldeep-MS Kuldeep-MS added the Investigate Requires further investigation by the WPF team. label Apr 12, 2024
@chucker
Copy link
Author

chucker commented Apr 12, 2024

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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Investigate Requires further investigation by the WPF team.
Projects
None yet
Development

No branches or pull requests

2 participants