我正在尝试使用 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文件?
您发现的代码设置了一个糟糕的例子:它试图实现您不应该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++ 版本,并进行了以下更改:
IStream
,而不是直接写入文件
#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());
}
编译上述代码的先决条件:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.ImplementationLibrary" version="1.0.231028.1" targetFramework="native" />
</packages>
2 这是基于GDI的,不支持某些渲染技术(例如Direct3D)。 屏幕截图解释了如何使用Windows.Graphics.Capture
3 代码使用 Windows 实现库 (WIL) 进行资源管理和错误报告。使用“简单”这个词可能会产生错误的期望。
4 Alpha 通道实际上仅在多显示器设置中有意义,其中桌面矩形的某些部分未被任何显示器显示表面覆盖。