diff --git a/src/interactivity/win32/window.cpp b/src/interactivity/win32/window.cpp index ed42acb60d5..46d360f1955 100644 --- a/src/interactivity/win32/window.cpp +++ b/src/interactivity/win32/window.cpp @@ -964,6 +964,30 @@ void Window::s_CalculateWindowRect(const til::size coordWindowInChars, prectWindow->bottom = prectWindow->top + rectProposed.height(); } +// Expands a rect by the size of the non-client area (caption bar, resize borders, +// scroll bars, etc), which depends on the window styles and DPI +void Window:: s_ExpandRectByNonClientSize(HWND const hWnd, + UINT dpi, + _Inout_ til::rect* const prectWindow) +{ + DWORD dwStyle = GetWindowStyle(hWnd); + DWORD dwExStyle = GetWindowExStyle(hWnd); + BOOL fMenu = FALSE; + + ServiceLocator::LocateWindowMetrics()->AdjustWindowRectEx( + prectWindow, dwStyle, fMenu, dwExStyle, dpi); + + // Note: AdjustWindowRectEx does not account for scroll bars :(. + if (WI_IsFlagSet(dwStyle, WS_HSCROLL)) + { + prectWindow->bottom += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CYHSCROLL, dpi); + } + if (WI_IsFlagSet(dwStyle, WS_VSCROLL)) + { + prectWindow->bottom += ServiceLocator::LocateHighDpiApi()->GetSystemMetricsForDpi(SM_CXVSCROLL, dpi); + } +} + til::rect Window::GetWindowRect() const noexcept { RECT rc{}; diff --git a/src/interactivity/win32/window.hpp b/src/interactivity/win32/window.hpp index ea678628e1e..19ba8266bea 100644 --- a/src/interactivity/win32/window.hpp +++ b/src/interactivity/win32/window.hpp @@ -135,6 +135,7 @@ namespace Microsoft::Console::Interactivity::Win32 void _HandleDrop(const WPARAM wParam) const; [[nodiscard]] HRESULT _HandlePaint() const; void _HandleWindowPosChanged(const LPARAM lParam); + bool _HandleGetDpiScaledSize(UINT dpiNew, _Inout_ SIZE* pSizeNew) const; // Accessibility/UI Automation [[nodiscard]] LRESULT _HandleGetObject(const HWND hwnd, @@ -173,6 +174,9 @@ namespace Microsoft::Console::Interactivity::Win32 const til::size coordBufferSize, _In_opt_ HWND const hWnd, _Inout_ til::rect* const prectWindow); + static void s_ExpandRectByNonClientSize(HWND const hWnd, + UINT dpi, + _Inout_ til::rect* const prectWindow); static void s_ReinitializeFontsForDPIChange(); diff --git a/src/interactivity/win32/windowproc.cpp b/src/interactivity/win32/windowproc.cpp index 0dc8cf3853b..71db72d348b 100644 --- a/src/interactivity/win32/windowproc.cpp +++ b/src/interactivity/win32/windowproc.cpp @@ -244,45 +244,13 @@ static constexpr TsfDataProvider s_tsfDataProvider; case WM_GETDPISCALEDSIZE: { - // This message will send us the DPI we're about to be changed to. - // Our goal is to use it to try to figure out the Window Rect that we'll need at that DPI to maintain - // the same client rendering that we have now. - - // First retrieve the new DPI and the current DPI. - const auto dpiProposed = (WORD)wParam; - - // Now we need to get what the font size *would be* if we had this new DPI. We need to ask the renderer about that. - const auto& fiCurrent = ScreenInfo.GetCurrentFont(); - FontInfoDesired fiDesired(fiCurrent); - FontInfo fiProposed(L"", 0, 0, { 0, 0 }, 0); - - const auto hr = g.pRender->GetProposedFont(dpiProposed, fiDesired, fiProposed); - // fiProposal will be updated by the renderer for this new font. - // GetProposedFont can fail if there's no render engine yet. - // This can happen if we're headless. - // Just assume that the font is 1x1 in that case. - const auto coordFontProposed = SUCCEEDED(hr) ? fiProposed.GetSize() : til::size{ 1, 1 }; - - // Then from that font size, we need to calculate the client area. - // Then from the client area we need to calculate the window area (using the proposed DPI scalar here as well.) - - // Retrieve the additional parameters we need for the math call based on the current window & buffer properties. - const auto viewport = ScreenInfo.GetViewport(); - auto coordWindowInChars = viewport.Dimensions(); - - const auto coordBufferSize = ScreenInfo.GetTextBuffer().GetSize().Dimensions(); - - // Now call the math calculation for our proposed size. - til::rect rectProposed; - s_CalculateWindowRect(coordWindowInChars, dpiProposed, coordFontProposed, coordBufferSize, hWnd, &rectProposed); - - // Prepare where we're going to keep our final suggestion. - const auto pSuggestionSize = (SIZE*)lParam; - - pSuggestionSize->cx = rectProposed.width(); - pSuggestionSize->cy = rectProposed.height(); + SIZE* pSizeNew = (SIZE*)lParam; + UINT dpiNew = (WORD)wParam; + if (!_HandleGetDpiScaledSize(dpiNew, pSizeNew)) + { + return FALSE; + } - // Format our final suggestion for consumption. UnlockConsole(); return TRUE; } @@ -884,6 +852,55 @@ void Window::_HandleWindowPosChanged(const LPARAM lParam) } } +// WM_GETDPISCALEDSIZE is sent prior to the window changing DPI, allowing us to +// choose the size at the new DPI (overriding the default, linearly scaled). +// +// This is used to keep the rows and columns from changing when the DPI changes. +bool Window::_HandleGetDpiScaledSize(UINT dpiNew, _Inout_ SIZE* pSizeNew) const +{ + // Get the current DPI and font size. + HWND hwnd = GetWindowHandle(); + UINT dpiCurrent = ServiceLocator::LocateHighDpiApi()->GetDpiForWindow(hwnd); + const auto& fontInfoCurrent = GetScreenInfo().GetCurrentFont(); + til::size fontSizeCurrent = fontInfoCurrent.GetSize(); + + // Scale the current font to the new DPI and get the new font size. + FontInfoDesired fontInfoDesired(fontInfoCurrent); + FontInfo fontInfoNew(L"", 0, 0, { 0, 0 }, 0); + if (!SUCCEEDED(ServiceLocator::LocateGlobals().pRender->GetProposedFont( + dpiNew, fontInfoDesired, fontInfoNew))) + { + return false; + } + til::size fontSizeNew = fontInfoNew.GetSize(); + + // The provided size is the window rect, which includes non-client area + // (caption bars, resize borders, scroll bars, etc). To scale the client + // area by the new/previous font sizes, the non-client area size is removed + // from the rect here and added back at the end. + + // Expand an empty rect by the size of the current non-client area (using + // the window's current DPI). This gives us the size of the non client area. + // Reduce the provided size by the non-client area size, which gives us the + // new client area size at the previous DPI. + til::rect rc; + s_ExpandRectByNonClientSize(hwnd, dpiCurrent, &rc); + pSizeNew->cx -= rc.width(); + pSizeNew->cy -= rc.height(); + + // Scale the size of the client rect by the change in font size. + pSizeNew->cx = MulDiv(pSizeNew->cx, fontSizeNew.width, fontSizeCurrent.width); + pSizeNew->cy = MulDiv(pSizeNew->cy, fontSizeNew.height, fontSizeCurrent.height); + + // Expand the scaled client size by the non-client area (using the new DPI). + rc = { 0, 0, pSizeNew->cx, pSizeNew->cy }; + s_ExpandRectByNonClientSize(hwnd, dpiNew, &rc); + pSizeNew->cx = rc.width(); + pSizeNew->cy = rc.height(); + + return true; +} + // Routine Description: // - This helper method for the window procedure will handle the WM_PAINT message // - It will retrieve the invalid rectangle and dispatch that information to the attached renderer