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.
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 double
s 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.
Nit: well-defined numbers 96
is universal across Windows. It happens to be defined as a constant, USER_DEFAULT_SCREEN_DPI
; if you prefer the CsWin32 method in part 2, you can add it to your NativeMethods.txt
& use it directly. But there’s no harm in hard-coding 96
. Part 0 describes this more.
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 ).
The math doesn’t check out Notice that I’m only using these new DipToPhysical
& PhysicalToDip
functions to adjust the window’s width & height. I’m not using them to adjust its XY position .
These functions are linear transformations that assume your coordinate space starts at (0, 0)
. This works for client coordinates, which are per-window and always start at (0, 0)
, but it does not work for screen coordinates, which are global: (0, 0)
is the top-left corner of your primary monitor. If your other monitors have different DPIs (which is the whole point of this work!), then scaling screen coordinates will yield wacky, invalid coordinates.
Instead, for per-monitor–aware windows, always treat screen coordinates as physical pixels. Only scale window-specific coordinates (client coordinates).
Conclusion
Now we account for DPI! We’ve gone from this:
Before: the window is much smaller than the square
To this:
After: much better!
We’re done, right? Well, not quite:
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:
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.
For those without a framework (and also those with one) This series — and specifically this article — assumes that you are using WinUI. Building a DPI-aware app with a different framework (or no framework at all) has its own concerns, beyond just finding the right units to resize the window. For example, WPF supports an older form of DPI-awareness by default, so upgrading to the new version requires additional work .
In general, the math is the same (96
, logical DPI, DIPs, etc.), but there’s more technical work to do.
Some selected bits from Evan Koschik, since I am no expert:
Prefer integer coordinates , even for your DIP types. Floating point types lead to rounding errors, especially when trying to align windows to the sides of the screen. XAML uses double
s, and they recommend using multiples of 4 for all sizes, since that scales well for common DPIs (see Screen sizes and breakpoints ).
My examples use double
s because XAML uses double
s internally. But if you work with integers, consider using MulDiv
to do your scaling calculations; that’s what the OS uses internally.
Handle WM_DPICHANGED
events properly . They provide a new window size & position; you must call SetWindowPos
with those new coordinates, or risk "the wobble": where you accidentally place the window on a different monitor, it changes DPI again, and this continues forever. The provided size is not a direct rescaling of your window (because some non-client areas scale non-linearly); if you need to adjust this size, handle WM_GETDPISCALEDSIZE
beforehand.
This adjustment can be quite complicated: see work Evan did on Windows Terminal to handle it. I do not demonstrate it here because WinUI windows implement their own WndProc & presumably handle these events properly.
Be careful querying points on other monitors & windows . Remember that they may have a completely different DPI setting than you, even for different windows in the same app. Especially if your app isn’t per-monitor DPI aware, the results you get may be surprising & unexpected.
There are also some recommendations for working with physical screen coordinates in general:
Don’t scale screen coordinates . As mentioned in The math doesn’t check out above, scaling screen coordinates can yield wacky, invalid coordinates.
Instead, for per-monitor–aware windows, always treat & store screen coordinates as physical pixels. Only scale window-specific coordinates (client coordinates).
Use GetDpiForWindow
instead of GetDpiForMonitor
. Especially when dragging a window between monitors, there are brief moments where its desired DPI (GetDpiForWindow
) will not match its reported monitor (MonitorFromWindow
then GetDpiForMonitor
). If you use GetDpiForMonitor
, you can end up sizing to the wrong DPI & never getting a subsequent DPI changed event to re-render.
Prefer GetDpiForWindow
unless you’re explicitly trying to look up points on a different monitor.
This is only the briefest list, filtered by me from an expert. These warrant a deeper examination — some other time.