Skip to content

Control.CreateHandle should specify dpiAwarenessContext (of current thread) in Application.ParkHandle #2279

@chenxinyanc

Description

@chenxinyanc
  • .NET Core Version: (e.g. 3.0 Preview1, or daily build number, use dotnet --info) 5.0-alpha1
  • Have you experienced this same bug with .NET Framework?: N/A

Problem description:
I came across the problem when trying to add a UT for System.Windows.Forms that involves controls of different DPI-awareness. The code is like this

[WinFormsFact]
public void Control_Parent_DifferentDpi_ThrowsWin32Exception()
{
    Control childControl = null, parentControl = null;
    IntPtr? originalAwareness = null;
    try
    {
        try
        {
            // DpiHelper.EnterDpiAwarenessScope will do nothing if current process is SYSTEM_UNAWARE or SYSTEM_AWARE
            //
            // We cannot use DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,
            // Somehow GetDpiAwarenessContextForWindow will return DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 for such window.
            originalAwareness = CommonUnsafeNativeMethods.SetThreadDpiAwarenessContext(new IntPtr((int)DpiAwarenessContext.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2));
            Assert.Equal(0, Marshal.GetLastWin32Error());
            childControl = new Control();
            childControl.CreateControl();
            Assert.Equal(DpiAwarenessContext.DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,
                CommonUnsafeNativeMethods.GetDpiAwarenessContextForWindow(childControl.Handle));
            CommonUnsafeNativeMethods.SetThreadDpiAwarenessContext(new IntPtr((int)DpiAwarenessContext.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE));
            Assert.Equal(0, Marshal.GetLastWin32Error());
            parentControl = new Control();
            parentControl.CreateControl();
           // [Below]  Assertion failure: Expected: DPI_AWARENESS_CONTEXT_SYSTEM_AWARE; Actual:   DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
            Assert.Equal(DpiAwarenessContext.DPI_AWARENESS_CONTEXT_SYSTEM_AWARE,
                CommonUnsafeNativeMethods.GetDpiAwarenessContextForWindow(parentControl.Handle));
        }
        finally
        {
            if (originalAwareness != null)
            {
                CommonUnsafeNativeMethods.SetThreadDpiAwarenessContext(originalAwareness.Value);
            }
        }
        Assert.Throws<Win32Exception>(() => childControl.Parent = parentControl);
        Assert.Null(childControl.Parent);
    }
    finally
    {
        childControl?.Dispose();
        parentControl?.Dispose();
    }
}

There are actually three problems I have came across during writing this unit test.

  • DpiHelper.EnterDpiAwarenessScope will do nothing if the DPI-awareness of current process is SYSTEM_UNAWARE or SYSTEM_AWARE. I'm not sure if that's by design.
  • CommonUnsafeNativeMethods.GetDpiAwarenessContextForWindow is returning PER_MONITOR_AWARE_V2 for windows that is actually PER_MONITOR_AWARE. It seems that you will need to use AreDpiAwarenessContextsEqual and compare the target windows' DPI awareness context with IntPtr(-3) or IntPtr(-4) to determine whether it's V2 or not.
  • That's the blocking issue for adding UT for Check for the return value of native SetParent calls in Control class #2262 : Control.CreateHandle is calling Application.ParkHandle without specifying the second parameter, leaving its dpiAwarenessContext set to DPI_AWARENESS_CONTEXT_UNSPECIFIED.
    // And if we are WS_CHILD, ensure we have a parent handle.
    if (cp.Parent == IntPtr.Zero && (cp.Style & (int)User32.WS.CHILD) != 0)
    {
    Debug.Assert((cp.ExStyle & (int)User32.WS_EX.MDICHILD) == 0, "Can't put MDI child forms on the parking form");
    Application.ParkHandle(cp);
    }

    This will cause the parked control having the same DPI awareness as the parking window for DPI_AWARENESS_CONTEXT_UNSPECIFIED, that is, actually the thread DPI awareness when creating the parking window.
    using (DpiHelper.EnterDpiAwarenessScope(context))
    {
    parkingWindow = new ParkingWindow();
    }

    In this case, I think we should retrieve the DPI awareness context for the current thread, then pass it as the dpiAwarenessContext argument when calling Application.ParkHandle, so the created control will have the same DPI awareness context as the current thread.

Actual behavior:

Expected behavior:

Minimal repro:

Metadata

Metadata

Assignees

No one assigned

    Labels

    🪲 bugProduct bug (most likely)area-HDPI-SAIssues related to high DPI SystemAware mode

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions