Hx0
Hx0

Reputation: 338

Windows GetDIBits not returning expected values

I'm currently writing a small program that scans the screen and looks for pixels. My issue is that GetDIBits function doesn't appear to return a proper screenshot of the screen.

Copying the bitmap to the clipboard does put the right screen image in the clipboard. I decided to print out the function's output to a BMP file to get an idea of what's going on and it clearly isn't what I'm expecting.

I'll also mention that I have 3 monitors, incase that could explain why it's not behaving like expected.

class Test {
    int screenWidth;
    int screenHeight;
    HWND targetWindow;
    HDC targetDC;
    HDC captureDC;
    RGBQUAD *pixels;
    HBITMAP captureBitmap;


    bool TakeScreenshot() {
        ZeroMemory(pixels, screenHeight*screenWidth);
        screenWidth = GetSystemMetrics(SM_CXSCREEN);
        screenHeight = GetSystemMetrics(SM_CYSCREEN);

        targetWindow = GetDesktopWindow();
        targetDC = GetDC(NULL);


        captureDC = CreateCompatibleDC(targetDC);

        captureBitmap = CreateCompatibleBitmap(targetDC, screenWidth, screenHeight);
        HGDIOBJ old = SelectObject(captureDC, captureBitmap);
        if (!old)
            printf("Error selecting object\n");

        OpenClipboard(NULL);
        EmptyClipboard();
        SetClipboardData(CF_BITMAP, captureBitmap);
        CloseClipboard();

        if (BitBlt(captureDC, 0, 0, screenWidth, screenHeight, targetDC, 0, 0, SRCCOPY)) {
            BITMAPINFO bmi = { 0 };
            bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
            bmi.bmiHeader.biWidth = screenWidth;
            bmi.bmiHeader.biHeight = -screenHeight;
            bmi.bmiHeader.biPlanes = 1;
            bmi.bmiHeader.biBitCount = 32;
            bmi.bmiHeader.biCompression = BI_RGB;
            bmi.bmiHeader.biSizeImage = 0;

            if (!SelectObject(captureDC, old))
                printf("Error unselecting object\n");
            if (!GetDIBits(captureDC,
                captureBitmap,
                0,
                screenHeight,
                pixels,
                &bmi,
                DIB_RGB_COLORS
            )) {
                printf("%s: GetDIBits failed\n", __FUNCTION__);
                return false;
            }

        }
        else {
            printf("%s: BitBlt failed\n", __FUNCTION__);
            return false;
        }
        return true;
    }
    // This is from somewhere on stackoverflow - can't find where.
    void MakePicture() {
        typedef struct                       /**** BMP file header structure ****/
        {
            unsigned int   bfSize;           /* Size of file */
            unsigned short bfReserved1;      /* Reserved */
            unsigned short bfReserved2;      /* ... */
            unsigned int   bfOffBits;        /* Offset to bitmap data */
        } BITMAPFILEHEADER;

        BITMAPFILEHEADER bfh;
        BITMAPINFOHEADER bih;

        unsigned short bfType = 0x4d42;
        bfh.bfReserved1 = 0;
        bfh.bfReserved2 = 0;
        bfh.bfSize = 2 + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + 2560 * 1440 * 3;
        bfh.bfOffBits = 0x36;

        bih.biSize = sizeof(BITMAPINFOHEADER);
        bih.biWidth = screenWidth;
        bih.biHeight = screenHeight;
        bih.biPlanes = 1;
        bih.biBitCount = 24;
        bih.biCompression = 0;
        bih.biSizeImage = 0;
        bih.biXPelsPerMeter = 5000;
        bih.biYPelsPerMeter = 5000;
        bih.biClrUsed = 0;
        bih.biClrImportant = 0;

        FILE *file;
        fopen_s(&file, "test.bmp", "wb");
        if (!file)
        {
            printf("Could not write file\n");
            return;
        }

        /*Write headers*/
        fwrite(&bfType, 1, sizeof(bfType), file);
        fwrite(&bfh, 1, sizeof(bfh), file);
        fwrite(&bih, 1, sizeof(bih), file);

        /*Write bitmap*/
        for (int y = 0; y < screenHeight; y++)
        {
            for (int x = 0; x < screenWidth; x++)
            {
                unsigned char r = pixels[x + y].rgbRed;
                unsigned char g = pixels[x + y].rgbGreen;
                unsigned char b = pixels[x + y].rgbBlue;
                fwrite(&b, 1, 1, file);
                fwrite(&g, 1, 1, file);
                fwrite(&r, 1, 1, file);
            }
        }
        fclose(file);
    }
    Test() {
        screenWidth = GetSystemMetrics(SM_CXSCREEN);
        screenHeight = GetSystemMetrics(SM_CYSCREEN);
        pixels = new RGBQUAD[screenWidth * screenHeight];
    }
    ~Test() {
        //cleanup
    }
};

Here is the result that the code is giving(instead of a screenshot): enter image description here

It appears like it takes a few pixels from the top of my screen and stretches them into an image. The screenshot is from Visual Studio being open(orange part being the notifications).

If I put a giant red square (255, 0, 0) in my screen, if it's height isn't 0, the pixels array will not contain a single red pixel.

Upvotes: 1

Views: 802

Answers (3)

Daniel Sęk
Daniel Sęk

Reputation: 2769

Additional bug:

    for (int y = 0; y < screenHeight; y++)
    {
        for (int x = 0; x < screenWidth; x++)
        {
            unsigned char r = pixels[x + y].rgbRed;
            unsigned char g = pixels[x + y].rgbGreen;
            unsigned char b = pixels[x + y].rgbBlue;
            fwrite(&b, 1, 1, file);
            fwrite(&g, 1, 1, file);
            fwrite(&r, 1, 1, file);
        }
    }

I think it should be

    for (int y = 0; y < screenHeight; y++)
    {
        for (int x = 0; x < screenWidth; x++)
        {
            unsigned char r = pixels[x + y*screenWidth].rgbRed;
            unsigned char g = pixels[x + y*screenWidth].rgbGreen;
            unsigned char b = pixels[x + y*screenWidth].rgbBlue;
            fwrite(&b, 1, 1, file);
            fwrite(&g, 1, 1, file);
            fwrite(&r, 1, 1, file);
        }
    }

But rows require padding to multiplies of 4 bytes:

    // Important: each row has to be padded to multiple of DWORD.
    // Valid only for 24 bits per pixel bitmaps.
    // Remark: 32 bits per pixel have rows always aligned (padding==0)
    int padding = 3 - (screenWidth*3 + 3)%4;
    // or
    // int padding = 3 - ((screenWidth*3 + 3) & 3);
    for (int y = 0; y < screenHeight; y++)
    {
        for (int x = 0; x < screenWidth; x++)
        {
            unsigned char r = pixels[x + y*screenWidth].rgbRed;
            unsigned char g = pixels[x + y*screenWidth].rgbGreen;
            unsigned char b = pixels[x + y*screenWidth].rgbBlue;
            fwrite(&b, 1, 1, file);
            fwrite(&g, 1, 1, file);
            fwrite(&r, 1, 1, file);
        }
        // Important: each row has to be padded to multiple of DWORD.
        fwrite("\0\0\0\0", 1, padding, file);
    }

Adjust file size (valid for 24 bits per pixel):

    bfh.bfSize =
            2
            + sizeof(BITMAPFILEHEADER)
            + sizeof(BITMAPINFOHEADER)
            + ((screenWidth*3 + 3) & ~3) * screenHeight;

Upvotes: 1

Barmak Shemirani
Barmak Shemirani

Reputation: 31599

BitBlt performs the actual copying. Clipboard functions should be called after BitBlt

Also note, in mutli-monitor settings, SM_CXSCREEN/Y... give the size for the primary monitor. Use SM_XVIRTUALSCREEN/XV... for the whole screen. SM_XVIRTUALSCREEN/Y will give the X/Y coordinate (it's usually zero)

Be sure to release all the handles and delete the used objects when you are finished. In fact, there is no need to declare targetDC etc. as class members.

If the application is not DPI aware, the bitmap may look smaller depending on DPI settings. Call SetProcessDPIAware() at the start of the program for a quick fix, or set the manifest.

As noted in comment, with SetClipboardData(CF_BITMAP, captureBitmap); the system takes over captureBitmap. Avoid calling this function or make a copy of the bitmap to pass to clipboard.

int main()
{
    int screenWidth = GetSystemMetrics(SM_CXVIRTUALSCREEN);
    int screenHeight = GetSystemMetrics(SM_CYVIRTUALSCREEN);
    int screen_x = GetSystemMetrics(SM_XVIRTUALSCREEN);
    int screen_y = GetSystemMetrics(SM_YVIRTUALSCREEN);

    screenWidth = GetSystemMetrics(SM_CXSCREEN);
    screenHeight = GetSystemMetrics(SM_CYSCREEN);
    screen_x = 0;
    screen_y = 0;

    RGBQUAD* pixels = new RGBQUAD[screenWidth * screenHeight];

    DWORD size = screenWidth * screenHeight * 4;

    ZeroMemory(pixels, size);

    HDC targetDC = GetDC(NULL);
    HDC captureDC = CreateCompatibleDC(targetDC);
    HBITMAP captureBitmap = CreateCompatibleBitmap(targetDC, screenWidth, screenHeight);
    HGDIOBJ old = SelectObject(captureDC, captureBitmap);

    if(!BitBlt(captureDC, 0, 0, screenWidth, screenHeight, targetDC,
        screen_x, screen_y, SRCCOPY))
        printf("BitBlt error\n");

    SelectObject(captureDC, old);

    BITMAPINFO bmi = { 0 };
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = screenWidth;
    bmi.bmiHeader.biHeight = -screenHeight;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;
    bmi.bmiHeader.biSizeImage = 0;

    if(OpenClipboard(NULL))
    {
        EmptyClipboard();
        SetClipboardData(CF_BITMAP, 
                    CopyImage(captureBitmap, IMAGE_BITMAP, 0, 0, LR_DEFAULTSIZE));
        CloseClipboard();
    }

    if(!GetDIBits(targetDC,
        captureBitmap,
        0,
        screenHeight,
        pixels,
        &bmi,
        DIB_RGB_COLORS
    ))
        printf("%s: GetDIBits failed\n", __FUNCTION__);

    BITMAPFILEHEADER filehdr = { 'MB', 54 + size, 0, 0, 54 };
    std::ofstream f("test.bmp", std::ios::binary);
    f.write((char*)&filehdr, sizeof(filehdr));
    f.write((char*)&bmi, sizeof(bmi));
    f.write((char*)pixels, size);

    //cleanup:      
    SelectObject(captureDC, old);
    DeleteObject(captureBitmap);
    DeleteDC(captureDC);
    ReleaseDC(0, targetDC);
}

Upvotes: 1

Daniel Sęk
Daniel Sęk

Reputation: 2769

GetDIBits function reference, remarks section:

The bitmap identified by the hbmp parameter must not be selected into a device context when the application calls this function.

Deselect bitmap before calling GetBIBits.

HBITMAP oldBitmap = SelectObject(captureDC, captureBitmap);
...
// Deselect captureBitmap by selecting oldBitmap.
SelectObject(captureDC, oldBitmap);

Remember to add cleanup code (restore bitmap, destroy bitmap, destroy or release device contexts).

Upvotes: 1

Related Questions