Resize a window in WinUI (part 2: resize the window)

We’re continuing our journey to resize a WinUI 3 window reliably.

In Part 0, we explained DPI. In Part 1, we investigated how to obtain DPI for windows in WinUI 3. Now, let’s use that DPI information to resize our WinUI 3 app properly.

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'

From the past parts, we’ve stitched together a simple app that can fetch DPI, but we don’t do anything with it yet:

public sealed partial class MainWindow : Window {
    [LibraryImport("user32.dll", SetLastError = true)]
    private static partial uint GetDpiForWindow(nint hwnd);

    public MainWindow() {
        InitializeComponent(); // ...

        var dpi = GetDpiForWindow((nint)this.AppWindow.Id.Value);

        // ???

        this.AppWindow.MoveAndResize(new RectInt32() {
            Height = 800,
            Width = 600,
            X = 0,
            Y = 0
        });
    }
}

Step 1: convert pixels

As discussed in the previous parts, converting device-independent pixels (DIPs) to physical pixels and back is simple (see MSDN, DPI and device-independent pixels):

physical pixels = DIPs ×
logical DPI over 96

So let’s make a conversion function:

private static int DipToPhysical(double dip, uint dpi) {
    return (int)(dip * dpi / 96.0);
}

And, for fun, let’s invert it:

private static double PhysicalToDip(int px, uint dpi) {
    return 96.0 * px / dpi;
}

Note that I use doubles here, to match XAML. But this can cause rounding issues: see For those without a framework (and also those with one) at the end of this article.

Step 2: use the conversion

Now, whenever we handle with physical pixels, we can convert them to DIPs, e.g.:

var height = DipToPhysical(800, dpi);

Putting it all together:

public sealed partial class MainWindow : Window {
    [LibraryImport("user32.dll", SetLastError = true)]
    private static partial uint GetDpiForWindow(nint hwnd);

    private static int DipToPhysical(double dip, uint dpi) {
        return (int)(dip * dpi / 96.0);
    }

    private static double PhysicalToDip(int px, uint dpi) {
        return 96.0 * px / dpi;
    }

    public MainWindow() {
        InitializeComponent(); // ...

        var dpi = GetDpiForWindow((nint)this.AppWindow.Id.Value);

        this.AppWindow.MoveAndResize(new RectInt32() {
            Height = DipToPhysical(800, dpi),
            Width = DipToPhysical(600, dpi),
            X = 50,
            Y = 50
        });
    }
}

And hey, look, here’s an example on MSDN doing just that. And a similar helper class written by an ex-Microsoft dev (and his larger set of helpers, DesktopWindow).

Conclusion

Now we account for DPI! We’ve gone from this:

A 400 by 400 square and a much-smaller 400 by 400 window

Before: the window is much smaller than the square

To this:

A 400 by 400 square and a very-similar 400 by 400 window

After: much better!

We’re done, right? Well, not quite:

The square is still slightly bigger than the window

That’s not quite right…

The window bounds for AppWindow.MoveAndResize include the invisible draggable region around it. You know, what used to be the window border:

The square is still slightly bigger than the window

Thick window borders in XP (via the pyglet documentation)

Behind the scenes, MoveAndResize probably calls SetWindowPos, which has the same behavior. You’ll need to account for that space, either by setting the client area instead (AppWindow.ResizeClient) or by manually adjusting your window size (e.g. by fetching DWMWA_EXTENDED_FRAME_BOUNDS).

But that’s a journey for a separate article.

As is the extra credit: handling DPI-changed (WM_DPICHANGED) events with a custom message loop.


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