0
votes

I'm trying to make our WinAPI application DPI-aware, and ran into an apparently infinite loop where the window constantly receives WM_DPICHANGED. The program always calls SetWindowPos with the lParam of the WM_DPICHANGED message, as the documentation says.

My monitor setup has monitor 1 on the right, 1920x1080, 100% scaling, and monitor 2 on the left, aligned at the bottom, 3840x2160, 150% scaling. Running the following program, move the window so that it straddles the two monitors, but is scaled at 100%. Now, grab the window on the left-hand monitor, and move the window up and down vertically so that it switches between the two resolutions. At some point, it goes into a loop which seems to stall the whole window manager (ctrl-alt-del will interrupt it -- no need to actually terminate the program).

In this loop, the rectangle passed in lParam has size 500x500 for both 96 and 144 DPI! Why is WM_DPICHANGED getting a rectangle whose size isn't that returned from WM_GETDPISCALEDSIZE?

I know I'm doing something unusual by having the window smaller on the higher DPI monitor (in the real app, our windows have complex layouts which change depending on the resolution).

Why is this looping and how can we avoid the DPI-change cycle?

#include "stdafx.h"

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    case WM_GETDPISCALEDSIZE:
    {
        LPSIZE lpSize = (LPSIZE)lParam;
        int dpi = wParam;
        lpSize->cy = lpSize->cx = (dpi == 96 ? 200 : 500);
        return TRUE;
    }
    case WM_DPICHANGED:
    {
        LPRECT lpRect = (LPRECT)lParam;
        SetWindowPos(hWnd, nullptr, lpRect->left, lpRect->top, lpRect->right - lpRect->left, lpRect->bottom - lpRect->top, SWP_NOZORDER | SWP_NOACTIVATE);
        return 0;
    }
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

WCHAR szTitle[] = L"Test";                  // The title bar text
WCHAR szWindowClass[] = L"TESTCLASS";            // the main window class name

ATOM                MyRegisterClass(HINSTANCE);

int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
                     _In_opt_ HINSTANCE hPrevInstance,
                     _In_ LPWSTR    lpCmdLine,
                     _In_ int       nCmdShow)
{
    MyRegisterClass(hInstance);

   SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_VISIBLE,
      200, 200, 200, 200, nullptr, nullptr, hInstance, nullptr);

    MSG msg;

    while (GetMessage(&msg, nullptr, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }

    return (int) msg.wParam;
}

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    WNDCLASSEXW wcex;

    wcex.cbSize = sizeof(WNDCLASSEX);

    wcex.style          = CS_HREDRAW | CS_VREDRAW;
    wcex.lpfnWndProc    = WndProc;
    wcex.cbClsExtra     = 0;
    wcex.cbWndExtra     = 0;
    wcex.hInstance      = hInstance;
    wcex.hIcon          = nullptr;
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = nullptr;
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = nullptr;

    return RegisterClassExW(&wcex);
}
1
Can't see anything wrong with your code. Windows has a lot of per-monitor DPI related bugs, I suspect you've just stumbled across one of them.Jonathan Potter
Your window could constantly change monitor which has the largest area of it. To verify use MonitorFromWindow - log result to console or file.Daniel Sęk

1 Answers

0
votes

It seems possible to work around this situation by detecting whether the DPI in wParam matches that returned by GetDpiForWindow, and ignoring WM_DPICHANGED in that case.