0

I have created a small demo app which window looks as below:

MVCE

When I run this demo app, and press any key, I want to capture the part of the screen bitmap.

The part of the screen I am interested in, is the one my window occupies, namely the content of the top rectangle in my window that holds letters. The captured screen bitmap should look like below:

enter image description here

The problem I face is that screen capturing code captures wrong part of the screen.

Below is the full code (bare in mind that I tried to keep things as minimal as possible):

#include <Windows.h>

void foo(HWND hWnd)
{
    HDC hdcScreen;
    HDC hdcWindow;

    hdcScreen = GetDC(NULL);
    hdcWindow = GetDC(hWnd);
    
    RECT rcClient;
    GetClientRect(hWnd, &rcClient);

    // map window's client coordinates to screen coordinates
    // HERE IS THE PROBLEM, SOMEHOW COORDINATES ARE NOT TRANSLATED CORRECTLY 
    // do not know how to fix this, but I am trying  :( 
    RECT rc1 = rcClient;
    MapWindowPoints(hWnd, NULL, (LPPOINT)&rc1, 2);  

    // capture desktop portion of the image
    // that corresponds to the window's top rectangle (the one that has letters in it)
    // and blit the result in the bottom rectangle
    // so result can be visually compared
    if (!BitBlt(hdcWindow, 
        rcClient.left + 50, // coordinates of the bottom rectangle 
        rcClient.top + 70,  // sorry for the "magic numbers" 
        75, 35,             // I am low on time :( 
        hdcScreen,           
        rc1.left + 50,      // screen coordinates of the top rectangle     
        rc1.top + 20,       // (the one that contains letters) 
        SRCCOPY))            
    {
        OutputDebugString(L"StretchBlt has failed");
        ReleaseDC(NULL, hdcScreen);
        ReleaseDC(hWnd, hdcWindow);
        return;
    }
    
    RECT rcBottomRect;                        // Frame again the bottom rectangle in the window,  
    rcBottomRect.left = rcClient.left + 50;   // to make visual comparing easier
    rcBottomRect.top = rcClient.top + 70;     // and to verify that I didn't screw up
    rcBottomRect.right = rcClient.left + 125; // the coordinates
    rcBottomRect.bottom = rcClient.top + 105;

    HBRUSH br = (HBRUSH)GetStockObject(BLACK_BRUSH);
    FrameRect(hdcWindow, &rcBottomRect, br);

    ReleaseDC(NULL, hdcScreen);
    ReleaseDC(hWnd, hdcWindow);
}

LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_KEYUP:      // easiest handler to add that keeps things minimal
        foo(hwnd);      // capture screen      
        break;
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hwnd, &ps);

        RECT rcClient;
        GetClientRect(hwnd, &rcClient);

        RECT rcTopRect;            
        rcTopRect.left = rcClient.left + 50;
        rcTopRect.top = rcClient.top + 20;
        rcTopRect.right = rcTopRect.left + 75;
        rcTopRect.bottom = rcTopRect.top + 35;

        HBRUSH br = (HBRUSH)GetStockObject(BLACK_BRUSH);

        TextOut(hdc, 20, 30, L"Asdf ghj kkl oioio 4545 676767", ARRAYSIZE(L"Asdf ghj kkl oioio 4545 676767"));
        FrameRect(hdc, &rcTopRect, br);

        RECT rcBottomRect;
        rcBottomRect.left = rcClient.left + 50;
        rcBottomRect.top = rcClient.top + 70;
        rcBottomRect.right = rcClient.left + 125;
        rcBottomRect.bottom = rcClient.top + 105;

        FrameRect(hdc, &rcBottomRect, br);

        EndPaint(hwnd, &ps);
    }
    break;
    case WM_CLOSE:
        DestroyWindow(hwnd);
        break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }
    return 0;
}

// BOILERPLATE CODE...
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
    LPSTR lpCmdLine, int nCmdShow)
{
    WNDCLASSEX wc;
    HWND hwnd;
    MSG Msg;

    wc.cbSize = sizeof(WNDCLASSEX);
    wc.style = 0;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = hInstance;
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.lpszMenuName = NULL;
    wc.lpszClassName = L"myWindowClass";
    wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION);

    if (!RegisterClassEx(&wc))
    {
        MessageBox(NULL, L"Window Registration Failed!", L"Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    // Step 2: Creating the Window
    hwnd = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        L"myWindowClass",
        L"MVCE",
        WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, CW_USEDEFAULT, 300, 170,
        NULL, NULL, hInstance, NULL);

    if (hwnd == NULL)
    {
        MessageBox(NULL, L"Window Creation Failed!", L"Error!",
            MB_ICONEXCLAMATION | MB_OK);
        return 0;
    }

    ShowWindow(hwnd, nCmdShow);
    UpdateWindow(hwnd);

    // Step 3: The Message Loop
    while (GetMessage(&Msg, NULL, 0, 0) > 0)
    {
        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    return Msg.wParam;
}

QUESTION:

How to fix calculation "error" (maybe it is not an error, maybe I am misusing the API?) with MapWindowPoints?

UPDATE:

I have forgotten to mention that I have 2 monitors. After testing the app on second monitor everything worked fine.

After going through the settings for the first monitor, I have found out that it is set to scale text, apps and other items to 150%.

Reverting it to 100% made the code work, but now I need to find a solution for this case, as I may not force users to change their settings.

Any help would be appreciated.

AlwaysLearningNewStuff
  • 2,939
  • 3
  • 31
  • 84
  • Have you verified, that the issue is related to the coordinates, rather than attempting to render outside your `WM_PAINT` handler? – IInspectable Jul 08 '20 at 13:35
  • @IInspectable: No I haven't, I will move the code from `foo` to the `WM_PANT` and report my results. – AlwaysLearningNewStuff Jul 08 '20 at 14:29
  • @IInspectable: I have found the problem, you can see the UPDATE at teh bottom of the OP, but I haven't found a solution yet... – AlwaysLearningNewStuff Jul 08 '20 at 15:03
  • 1
    @AlwaysLearningNewStuff Solution has already been pointed out in answers: make your app **DPI aware per monitor**. You can add a manifest file to your project and rename it to: `YourAppName.exe.manifest`. For the content of this manifest file you can refer to ["Setting default awareness with the application manifest"](https://learn.microsoft.com/en-us/windows/win32/hidpi/setting-the-default-dpi-awareness-for-a-process#setting-default-awareness-with-the-application-manifest) – Rita Han Jul 16 '20 at 08:09
  • @RitaHan-MSFT: That did the trick, I just can not decide which answer to accept as both pointed this out... – AlwaysLearningNewStuff Jul 16 '20 at 20:08

2 Answers2

1

You can’t force user to change the DPI, but you can ask Windows to stop messing with the coordinates in your application. To do that, include manifest into the main .exe of your program. You probably need max. settings, true/pm and PerMonitorV2.

See this article for more info.

Soonts
  • 20,079
  • 9
  • 57
  • 130
1

The documentation for MapWindowPoints is a bit vague, but it seems like it literally wants coordinates relative to your window not your window's client area (which is what you're giving it). That seems like it would explain the symptom since your vertical offset looks to be the same size as your window's title bar. I've always used ClientToScreen, which is more clear.

DPI scaling is also a likely source of problems, but they don't usually manifest in just the y-axis. Make sure you mark you application as high DPI multi-monitor aware so that the system doesn't do any scaling behind your back. With that set, you can make this work most of the time with GDI, but there are a couple limitations. (1) It's very hard if your monitors have different scaling factors, and (2) if the scaling factor is changed dynamically while your program is already running, you can get notified, but many APIs will still tell you the DPI settings from before the change.

Adrian McCarthy
  • 45,555
  • 16
  • 123
  • 175
  • I have upvoted, have accepted the other answer since it linked to the sample manifest file, which helped me a lot since I am new to this. Wish I could accept 2 answers. Thank you so much anyway, regards until next time. – AlwaysLearningNewStuff Jul 17 '20 at 14:36