Resize a window in WinUI (part 1: find the DPI)

Let’s continue our work to resize a WinUI 3 app with the AppWindow resizing APIs, like AppWindow.MoveAndResize.

A confused person compares two differently-sized boxes both labeled '800 x 800' A confused person compares two differently-sized boxes both labeled '800 x 800'

In Part 0, we explored DPI on Windows: WinUI pixels are typically device-independent pixels (DIPs), but MoveAndResize expects physical pixels. To let us continue working in DIP-space, we simply need to convert our DIPs to physical pixels. From Part 0:

physical pixels = DIPs ×
logical DPI over 96

For that, we need to get Windows’ DPI setting (its logical DPI). We’re still starting with a program like this:

public sealed partial class MainWindow : Window {
    public MainWindow() {
        InitializeComponent(); // ...

        // These numbers don't match the behavior of other WinUI pixels
        this.AppWindow.MoveAndResize(new RectInt32() {
            Height = 800,
            Width = 600,
            X = 0,
            Y = 0
        });
    }
}

You can find code examples in my corresponding GitHub repo.

Attempt 1: Windows.­Graphics.­Display.­DisplayInformation

The Windows.Graphics.Display.DisplayInformation class seems to be the right tool — it gives us lots of tools for scaling pixels: LogicalDpi, RawDpiX, RawDpiY, RawPixelsPerViewPixel, and ResolutionScale. And it also has a DpiChanged event, which can help us handle changes if users adjust their screen resolution.

Awesome:

// ❌ Fails
var displayInfo = Windows.Graphics.Display.DisplayInformation
    .GetForCurrentView();

Well, no. This method requires a CoreApplicationView, which is from the older UWP framework and isn’t supported in WinAppSDK. You get a COMException:

Element not found.

Windows.Graphics.Display: GetForCurrentView must be called on a thread that is associated with a CoreWindow.

OK, let’s try using an interop class, specifically IDisplayInformationStaticsInterop. Change your <TargetFramework> to at least <TargetFramework>­net8.0-windows10.0.22621.0­</TargetFramework> (see the Minimum supported client on its docs page), then:

// ⚠️ Fails?
var displayInfo2 = DisplayInformationInterop.GetForWindow(
    (nint)this.AppWindow.Id.Value);

It might work, but you get the nifty warning:

This call site is reachable on: “Windows” 10.0.17763.0 and later. “DisplayInformationInterop.­GetForWindow(nint)” is only supported on: “Windows” 10.0.22621.0 and later.

That corresponds to the 22H2 build of Windows, which might be more recent than you want — it only came out September 2022, as lamented in this StackOverflow question. (Plus, both .GetForMonitor() and .GetForWindow() failed with other COMExceptions in brief testing on my 24H2 machine, and honestly I didn’t care to pursue this further).

Attempt 2: CsWin32

Instead, let’s use CsWin32 (if you’re unfamiliar, CsWin32 auto-generates P/​Invoke code for Win32 APIs. P/​Invoke is C#’s way of calling non-C# code, including Win32 APIs). Let’s use CsWin32 to call the Win32 API GetDpiForWindow. Like any other API in CsWin32:

  1. Add the CsWin32 NuGet package to your project.

  2. Add the function to your NativeMethods.txt:

    // ...
    GetDpiForWindow
  3. Call it:

    // ⚠️ Works?
    uint dpi = PInvoke.GetDpiForWindow(
       new HWND((nint)this.AppWindow.Id.Value));

But again, we hit a snag. The app I’m writing for uses DisableRuntimeMarshalling (<DisableRuntimeMarshalling>True</DisableRuntimeMarshalling>) to enable ahead-of-time compilation, which breaks the generated code:

warning CA1420: Setting SetLastError to “true” requires runtime marshalling to be enabled

This isn’t too bad! The issue is documented, with a fix:

  1. Add a NativeMethods.json that disables marshalling in the generated code:

    {
        "$schema": "https://aka.ms/CsWin32.schema.json",
        "allowMarshaling": false
    }

It works!

But the previous warnings stumped me for a while. I actually assumed CsWin32 was unusable in this case — and wrote this whole article assuming we needed another method! This method works, and I’ve since left a comment on the original issue, but, for completion’s sake, let’s write the code manually.

Attempt 3: P/​Invoke by hand

If CsWin32 isn’t your jam, you can just write the P/​Invoke code by hand:

// ✅ Works!
[LibraryImport("user32.dll", SetLastError = true)]
private static partial uint GetDpiForWindow(nint hwnd);

public MainWindow()
{
    // ...
    var dpi = GetDpiForWindow((nint)this.AppWindow.Id.Value);
}

Done! And with a minimal amount of code. We can now get Windows’ logical DPI setting in our C# code(although handling DPI changed events will take more work).

Now we just, uh, need to resize the window. See you next week!


Thanks Evan Koschik for consulting on this article. Thanks to Ari Krumbein & Atherai Maran for editing.