在 Windows 上以字节数组形式获取屏幕截图,错误的 bmp 标头数据

问题描述 投票:0回答:1

我正在尝试使用 c++ 和 windows api 获取屏幕截图数据,以表示位图图像的数据字节数组,然后将其保存到磁盘。我的代码基于这个问题

保存的图像无法在 Windows 照片查看器中打开,但是我可以在 gimp 中打开它,并且图像可见,但 gimp 给我一个警告:gimp-image-set-resolution 失败,分辨率超出范围,因此默认分辨率用来。所以我猜想有些东西没有正确复制到位图的标题数据中。

这是完整的代码:

#include <iostream>
#include <windows.h>
#include <fstream>



void WINAPI CaptureScreenIntoByteArray(BYTE*& screen_bytes, DWORD& screen_bytes_size)
{
    BITMAPFILEHEADER bfHeader;
    BITMAPINFOHEADER biHeader;
    BITMAPINFO bInfo;
    HGDIOBJ hTempBitmap;
    HBITMAP hBitmap;
    BITMAP bAllDesktops;
    HDC hDC, hMemDC;
    LONG lWidth, lHeight;
    BYTE* sb = NULL;

    ZeroMemory(&bfHeader, sizeof(BITMAPFILEHEADER));
    ZeroMemory(&biHeader, sizeof(BITMAPFILEHEADER));
    ZeroMemory(&bInfo, sizeof(BITMAPINFO));
    ZeroMemory(&bAllDesktops, sizeof(BITMAP));

    hDC = GetDC(NULL);
    hTempBitmap = GetCurrentObject(hDC, OBJ_BITMAP);
    GetObjectW(hTempBitmap, sizeof(BITMAP), &bAllDesktops);

    lWidth = bAllDesktops.bmWidth;
    lHeight = bAllDesktops.bmHeight;

    DeleteObject(hTempBitmap);

    bfHeader.bfType = (WORD)('B' | ('M' << 8));
    bfHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

    biHeader.biSize = sizeof(BITMAPINFOHEADER);
    biHeader.biBitCount = 24;
    biHeader.biCompression = BI_RGB;
    biHeader.biPlanes = 1;
    biHeader.biWidth = lWidth;
    biHeader.biHeight = lHeight;

    bInfo.bmiHeader = biHeader;

    screen_bytes_size = (((24 * lWidth + 31) & ~31) / 8) * lHeight;

    hMemDC = CreateCompatibleDC(hDC);
    hBitmap = CreateDIBSection(hDC, &bInfo, DIB_RGB_COLORS, (VOID**)&sb, NULL, 0);
    SelectObject(hMemDC, hBitmap);

    int x = GetSystemMetrics(SM_XVIRTUALSCREEN);
    int y = GetSystemMetrics(SM_YVIRTUALSCREEN);
    BitBlt(hMemDC, 0, 0, lWidth, lHeight, hDC, x, y, SRCCOPY);

    // copy image data, i guess something is not correctly copied
    screen_bytes = new BYTE[sizeof(BITMAPFILEHEADER) + sizeof (BITMAPINFOHEADER) + screen_bytes_size];
    std::copy(&bfHeader, &bfHeader + 1, (BITMAPFILEHEADER*)screen_bytes);
    std::copy(&biHeader, &biHeader + 1, (BITMAPINFOHEADER*)(screen_bytes + sizeof(BITMAPFILEHEADER)));
    std::copy(sb, sb + screen_bytes_size, screen_bytes + sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER));



    DeleteDC(hMemDC);
    ReleaseDC(NULL, hDC);
    DeleteObject(hBitmap);
}
int main() {

    //get image data array
    BYTE* screen_bytes = NULL;
    DWORD screen_bytes_size = 0;
    CaptureScreenIntoByteArray(screen_bytes,screen_bytes_size);

    //save image data array as bmp file
    FILE* file = fopen( "output.bmp", "wb" );
    fwrite( screen_bytes, screen_bytes_size, 1, file );
    fclose(file);

   //do further work with image data array
    return 0;
}


Windows 照片查看器无法读取图像,图像标题可能未正确写入。

如何正确地将字节数据写入bmp文件?

c++ image winapi io
1个回答
0
投票

您发现的代码设置了一个糟糕的例子:它试图实现您不应该1实现的内容,并且也犯了错误。代码有很多问题,与其一一解决,不如直接解决问题。

问题有两个部分:

    捕获屏幕内容。
  1. 将屏幕截图编码为图像格式。
第一部分在

捕获图像2中概述。该示例实现了一个完整的应用程序,该应用程序执行两件事:缩放屏幕捕获以立即显示并将其保存到文件中。这很好,但它也分散了核心原则的注意力。让我们保持简单3,只做最低限度的事情:

#include <Windows.h> #include <wil/resource.h> #include <wil/result.h> /// @brief Captures the entire desktop into a bitmap resource. /// /// @param use_alpha Controls whether the returned bitmap contains an alpha /// channel. /// /// @return Returns a `unique_hbitmap` holding the screen contents on success. /// Errors are reported via C++ exceptions. /// [[nodiscard]] ::wil::unique_hbitmap capture_desktop(bool use_alpha = false) { // Get DC for the desktop auto const desktop_dc = ::wil::GetDC(nullptr); THROW_HR_IF_NULL(E_FAIL, desktop_dc); // "Borrow" the DC's current bitmap (doesn't need to be cleaned up) auto const desktop_bmp = ::GetCurrentObject(desktop_dc.get(), OBJ_BITMAP); THROW_HR_IF_NULL(E_FAIL, desktop_bmp); // Gather bitmap dimensions BITMAP bmp = {}; THROW_HR_IF(E_FAIL, ::GetObjectW(desktop_bmp, sizeof(bmp), &bmp) == 0); auto const cx = bmp.bmWidth; auto const cy = bmp.bmHeight; // Construct memory DC auto const mem_dc = ::wil::unique_hdc { ::CreateCompatibleDC(desktop_dc.get()) }; THROW_HR_IF_NULL(E_FAIL, mem_dc); // Construct DIB auto const bmi = BITMAPINFO { .bmiHeader = { .biSize = sizeof(BITMAPINFOHEADER), .biWidth = cx, .biHeight = cy, .biPlanes = 1, .biBitCount = static_cast<WORD>(use_alpha ? 32 : 24), .biCompression = BI_RGB } }; void* pbits = nullptr; // memory is owned by the DIB auto mem_bmp = ::wil::unique_hbitmap { ::CreateDIBSection(desktop_dc.get(), &bmi, DIB_RGB_COLORS, &pbits, nullptr, 0) }; THROW_LAST_ERROR_IF_NULL(mem_bmp); // Perform the `BitBlt`; the extra scope is required to restore `mem_dc` { auto const restore = ::wil::SelectObject(mem_dc.get(), mem_bmp.get()); THROW_LAST_ERROR_IF(!::BitBlt(mem_dc.get(), 0, 0, cx, cy, desktop_dc.get(), 0, 0, SRCCOPY)); } // Transfer ownership of `mem_bmp` to caller return mem_bmp; }
此实现将桌面捕获到 

DIB 中,可选择包含 Alpha 通道4。它有一个明显的界面,这是它唯一的好处。

第二部分是关于图像编码的。 Windows 附带了

Windows Imaging Component (WIC),它为多种图像格式提供了编码器。以下代码是我在here发布的代码的 C++ 版本,并进行了以下更改:

#include <Shlwapi.h> #include <Windows.h> #include <objidl.h> #include <ocidl.h> #include <wincodec.h> #include <wil/com.h> #include <wil/result.h> #pragma comment(lib, "Shlwapi.lib") /// @brief Encodes a source bitmap `bmp` using the BMP image format. /// /// @param bmp The image source. This function fails if this isn't a DIB. If /// `bmp` uses more than 24bpp the encoded BMP contains an alpha /// channel. /// /// @return Returns a stream of bytes that represent the encoded image on /// success. Errors are reported via C++ exceptions. /// [[nodiscard]] ::wil::com_ptr<IStream> encode_bmp(HBITMAP bmp) { // Validate input (we're expecting a valid DIB) THROW_HR_IF_NULL(E_INVALIDARG, bmp); auto bmp_info { DIBSECTION {} }; THROW_HR_IF(E_INVALIDARG, ::GetObject(bmp, sizeof(bmp_info), &bmp_info) != sizeof(bmp_info)); auto const use_alpha = bmp_info.dsBm.bmBitsPixel > 24; // Create a factory that exposes WIC services auto const factory = ::wil::CoCreateInstance<IWICImagingFactory>(CLSID_WICImagingFactory); // Create a WIC bitmap from the source GDI bitmap auto const options = use_alpha ? WICBitmapUsePremultipliedAlpha : WICBitmapIgnoreAlpha; ::wil::com_ptr<IWICBitmap> wic_bitmap {}; THROW_IF_FAILED(factory->CreateBitmapFromHBITMAP(bmp, nullptr, options, wic_bitmap.put())); // Create a memory stream the encoder can write into auto stream { ::wil::com_ptr { ::SHCreateMemStream(nullptr, 0) } }; THROW_HR_IF_NULL(E_FAIL, stream); // Create a BMP encoder ::wil::com_ptr<IWICBitmapEncoder> encoder {}; THROW_IF_FAILED(factory->CreateEncoder(GUID_ContainerFormatBmp, nullptr, encoder.put())); THROW_IF_FAILED(encoder->Initialize(stream.get(), WICBitmapEncoderNoCache)); // Create and initialize a frame to write into (BMP files only support a // single frame) ::wil::com_ptr<IWICBitmapFrameEncode> frame {}; ::wil::com_ptr<IPropertyBag2> enc_options {}; THROW_IF_FAILED(encoder->CreateNewFrame(frame.put(), enc_options.put())); if (use_alpha) { // Request that WIC writes a `BITMAPV5HEADER` // This is required so that image viewers can reliably determine // existence of an alpha channel OLECHAR name[] = L"EnableV5Header32bppBGRA"; auto prop { PROPBAG2 { .dwType = PROPBAG2_TYPE_DATA, .vt = VT_BOOL, .pstrName = name } }; auto val { VARIANT { .vt = VT_BOOL, .boolVal = VARIANT_TRUE } }; THROW_IF_FAILED(enc_options->Write(1, &prop, &val)); } THROW_IF_FAILED(frame->Initialize(enc_options.get())); THROW_IF_FAILED(frame->SetSize(bmp_info.dsBm.bmWidth, bmp_info.dsBm.bmHeight)); auto pixel_format { use_alpha ? GUID_WICPixelFormat32bppBGRA : GUID_WICPixelFormat24bppBGR }; THROW_IF_FAILED(frame->SetPixelFormat(&pixel_format)); // Copy source into frame THROW_IF_FAILED(frame->WriteSource(wic_bitmap.get(), nullptr)); // Commit all writes to the memory stream THROW_IF_FAILED(frame->Commit()); THROW_IF_FAILED(encoder->Commit()); // Pass ownership of the stream to the caller return stream; }
这将返回一个 

IStream

,将编码图像表示为 BMP。这不是一个数组,但它具有类似的属性:您可以
seek到任意位置并从中read,就像使用数组一样。

以下是将

ISream

 的内容复制到文件中的函数示例:

#include <Shlwapi.h> #include <Windows.h> #include <wil/com.h> #include <wil/result.h> #include <filesystem> namespace fs = ::std::filesystem; #pragma comment(lib, "Shlwapi.lib") /// @brief Creates a new filesystem object `path` and copies the contents of /// `src` into it. /// /// @param path The destination filesystem pathname /// @param src The data source /// /// @remarks Errors are reported via C++ exceptions. When this function returns, /// the seek pointer of `src` cannot be assumed to be valid. /// void stream_to(fs::path path, IStream* src) { // Validate input THROW_HR_IF_NULL(E_INVALIDARG, src); // Create an `IStream` over a filesystem object ::wil::com_ptr<IStream> file {}; THROW_IF_FAILED(::SHCreateStreamOnFileEx(path.c_str(), STGM_WRITE | STGM_SHARE_DENY_WRITE | STGM_CREATE, FILE_ATTRIBUTE_NORMAL, true, nullptr, file.put())); // Query source for its size in bytes auto size = ULARGE_INTEGER {}; THROW_IF_FAILED(src->Seek({}, STREAM_SEEK_END, &size)); // Reset source stream pointer to the beginning THROW_IF_FAILED(src->Seek({}, STREAM_SEEK_SET, nullptr)); // Copy all bytes from the source stream into the file stream THROW_IF_FAILED(src->CopyTo(file.get(), size, nullptr, nullptr)); }
代码很多,其中大部分都很难理解。值得庆幸的是,使用它并不那么复杂。这是一个 C++ 程序,它捕获屏幕并将其保存到文件中:

int main() { // Initialize COM THROW_IF_FAILED(::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE)); auto const capture = capture_desktop(); auto const stream = encode_bmp(capture.get()); stream_to(L"output.bmp", stream.get()); }
编译上述代码的先决条件:

    安装WIL;当使用 Visual Studio 时,以下
  • packages.config 即可 <?xml version="1.0" encoding="utf-8"?> <packages> <package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231028.1" targetFramework="native" /> </packages>
    
    
  • C++20 编译器由于使用了
  • 指定的初始化器

1 BMP 文件格式 极其复杂。

2 这是基于GDI的,不支持某些渲染技术(例如Direct3D)。 屏幕截图解释了如何使用Windows.Graphics.Capture

命名空间。它能够捕获任何窗口内容,但管理起来要复杂一些。

3 代码使用 Windows 实现库 (WIL) 进行资源管理和错误报告。使用“简单”这个词可能会产生错误的期望。

4 Alpha 通道实际上仅在多显示器设置中有意义,其中桌面矩形的某些部分未被任何显示器显示表面覆盖。

© www.soinside.com 2019 - 2024. All rights reserved.