【问题标题】:Windows GetDIBits not returning expected valuesWindows GetDIBits 未返回预期值
【发布时间】:2019-01-28 02:34:53
【问题描述】:

我目前正在编写一个扫描屏幕并查找像素的小程序。我的问题是 GetDIBits 函数似乎没有返回正确的屏幕截图。

将位图复制到剪贴板确实会将正确的屏幕图像放入剪贴板。 我决定将函数的输出打印到 BMP 文件中,以了解正在发生的事情,这显然不是我所期望的。

我还要提到我有 3 个显示器,以防这可以解释为什么它的行为不像预期的那样。

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
    }
};

这是代码给出的结果(而不是屏幕截图):

它似乎从我的屏幕顶部占用了几个像素并将它们拉伸成图像。屏幕截图来自正在打开的 Visual Studio(橙色部分是通知)。

如果我在屏幕上放置一个巨大的红色方块 (255, 0, 0),如果它的高度不为 0,则像素数组将不包含单个红色像素。

【问题讨论】:

  • 您没有初始化 screenWidth 和其他全局变量。编辑您的问题以包含minimal reproducible example
  • 在您将captureBitmap 放入剪贴板时 (1) 您尚未捕获位图并且 (2) 您不再拥有该位图句柄 - 系统拥有。

标签: c++ windows winapi screenshot pixel


【解决方案1】:

BitBlt 执行实际的复制。剪贴板函数应该在BitBlt之后调用

另请注意,在多显示器设置中,SM_CXSCREEN/Y... 提供主显示器的大小。对整个屏幕使用SM_XVIRTUALSCREEN/XV...SM_XVIRTUALSCREEN/Y 将给出 X/Y 坐标(通常为零)

完成后请务必释放所有手柄并删除使用过的对象。事实上,没有必要将targetDC等声明为类成员。

如果应用程序不支持 DPI,则位图可能看起来更小,具体取决于 DPI 设置。在程序开始时调用SetProcessDPIAware() 进行快速修复,或设置清单。

如评论中所述,系统使用SetClipboardData(CF_BITMAP, captureBitmap); 接管captureBitmap。避免调用此函数或复制位图以传递到剪贴板。

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);
}

【讨论】:

  • “像素”数组似乎仍然会导致上图。
  • 我已将其添加到帖子中。我还尝试在控制台中的特定坐标 (x,y) 处打印颜色,结果与图像上的颜色相同。该数组中唯一的颜色似乎来自我屏幕上的第一行像素。
  • 您正在以 32 位格式读取文件,像素为 32 位,然后您以 24 位写入。请参阅以 32 位格式保存位图的更新。或者将像素更改为 24 位并保存为 24 位。
  • 这里有很多好的信息,但这仍然存在一个问题,一旦你将位图提供给剪贴板,它就不再是你的了。您无法使用 GetDIBits 可靠地读取它。如果您在此代码(或 OP 中)消除对剪贴板的调用,您将在 pixels 中获得预期数据。
  • @BarmakShemirani:来自 SetClipboardData 的文档:“如果 SetClipboardData 成功,则系统拥有由 hMem 参数标识的对象。一旦所有权转移到系统,应用程序可能无法写入或释放数据,但在调用 CloseClipboard 函数之前,它可以锁定和读取数据。”由于已调用 CloseClipboard,您甚至无法确定数据是否仍然存在。此外,如果省略剪贴板操作,该代码对我有用。
【解决方案2】:

GetDIBits function参考,备注部分:

hbmp参数标识的位图一定不能选入 应用程序调用此函数时的设备上下文。

在调用GetBIBits之前取消选择位图。

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

记得添加清理代码(恢复位图、销毁位图、销毁或释放设备上下文)。

【讨论】:

  • 它仍然给我带来不好的结果。它似乎从我的屏幕顶部占用了几个像素,并以一种奇怪的方式放置它们。 (我提供的图片是打开 Visual Studio 的结果,橙色位是显示在顶部栏中的通知标志)
【解决方案3】:

其他错误:

    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);
        }
    }

我觉得应该是的

    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);
        }
    }

但是行需要填充到 4 个字节的倍数:

    // 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);
    }

调整文件大小(对每像素 24 位有效):

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

【讨论】:

    猜你喜欢
    • 2013-09-04
    • 2016-02-13
    • 2015-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-12
    • 2015-03-04
    相关资源
    最近更新 更多