C++实现DXGI屏幕截屏与BMP文件生成分割技术详解

📅 2026/7/12 6:55:19 👁️ 阅读次数 📝 编程学习
C++实现DXGI屏幕截屏与BMP文件生成分割技术详解

1. 项目概述:从屏幕到像素的掌控

在桌面应用开发、自动化测试、远程协助甚至是游戏辅助工具的开发中,屏幕截屏都是一个基础且高频的需求。你可能需要录制一段操作教程,或者监控某个软件界面的变化,又或者想做一个简单的屏幕取色工具。而BMP,作为Windows系统下最“原始”的位图格式,结构简单明了,是理解图像数据在内存中如何组织的绝佳入口。今天,我们就来聊聊如何用C++,不依赖任何第三方图像库,直接与系统底层图形接口对话,实现高效的屏幕截屏,并将捕获到的像素数据,按照BMP的标准格式“组装”成文件。更进一步,我们还会探讨如何对这个BMP文件进行分割操作,比如将一张大图切成九宫格,或者按指定区域提取子图。这个过程不仅能让你掌握Windows图形编程的核心接口(如DXGI),更能让你深刻理解从显存到硬盘,一张图片是如何“诞生”的。无论你是想为你的C++项目增加一个截图功能模块,还是单纯对底层图形处理感兴趣,这篇内容都将提供一条清晰的实践路径。

2. 核心技术选型与方案设计

2.1 为什么选择DXGI而非传统GDI?

提到Windows下的截屏,很多人的第一反应是使用古老的GDI(Graphics Device Interface)函数,比如BitBlt。这个方法确实简单,几行代码就能把桌面内容拷贝到一个设备上下文(DC)中。但在高DPI屏幕、多显示器、以及涉及DirectX/Direct3D渲染的应用(尤其是游戏和视频播放器)场景下,GDI截屏往往会失效,截取到一片黑色或残缺的画面。这是因为GDI无法捕获由GPU直接渲染到显存中的内容。

因此,在现代Windows系统(Windows 8及以上)中,DXGI(DirectX Graphics Infrastructure)桌面复制API成为了更强大和可靠的选择。它是DirectX家族的一部分,能够直接访问桌面合成器(Desktop Window Manager)管理的最终帧缓冲区,无论窗口是用GDI、Direct2D还是Direct3D渲染的,都能正确捕获。其核心思路是创建一个“桌面副本”(Desktop Duplication),这个副本对象会持续接收桌面图像更新,我们可以从中“窃取”一帧画面。虽然API调用看起来比GDI复杂,但它提供了无与伦比的兼容性和性能,尤其是在需要高帧率截屏(如录屏)时。

2.2 BMP格式:简单背后的结构之美

选择BMP作为输出格式,并非因为它高效(事实上,它通常很臃肿),而是因为它结构透明,易于手动构造。一个完整的BMP文件主要由两部分组成:文件头(BITMAPFILEHEADER)和信息头(BITMAPINFOHEADER),紧接着就是原始的像素数据。

  • BITMAPFILEHEADER:定义了文件类型(必须是“BM”)、文件大小以及像素数据在文件中的起始偏移量。这个偏移量就是文件头和信息头的大小之和。
  • BITMAPINFOHEADER:包含了图像的元数据,如宽度、高度、颜色位深(通常是24位RGB或32位ARGB)、压缩方式(我们使用不压缩的BI_RGB)等。
  • 像素数据:这里存放着每个像素的颜色值。需要注意的是,BMP文件规定像素数据按行从下到上存储,即图像的第一行(屏幕的顶部)在文件数据段的最后。同时,每行像素的字节数必须向上对齐到4字节的倍数(行对齐),不足的部分用0填充。

理解这个结构是手动生成BMP文件,以及后续进行分割操作的基础。我们不需要任何图像解码库,只需要按照这个格式,将我们捕获到的内存数据“打包”写入文件即可。

2.3 整体流程设计

我们的项目将遵循一个清晰的管道式流程:

  1. 初始化DXGI环境:创建D3D设备,获取DXGI适配器和输出(显示器),最终创建桌面副本对象。
  2. 捕获一帧桌面图像:从桌面副本中获取下一帧图像资源,并将其拷贝到一个CPU可读的“暂存纹理”中。
  3. 映射内存获取像素数据:将暂存纹理映射到线性内存,得到一个指向原始像素数据的指针和每行字节数(Pitch)。
  4. 构造并写入BMP文件:根据上一步得到的图像尺寸和位深,填充BMP的两个头结构,然后将像素数据按BMP规则(可能涉及垂直翻转和行对齐)写入文件。
  5. 图片分割操作:读取已保存的BMP文件,解析其头结构,根据分割需求(如等分、按区域)计算出子图的范围,然后提取对应的像素数据块,为每个子图重新生成文件头和像素数据,保存为新的BMP文件。

这个设计将截屏和图像处理解耦,使得每个步骤都相对独立,便于理解、调试和扩展。

3. 核心细节解析与实操要点

3.1 DXGI桌面复制API的关键对象与生命周期

使用DXGI API,你需要和几个核心COM对象打交道,管理好它们的生命周期(通过AddRefRelease)至关重要。

  1. ID3D11Device 与 ID3D11DeviceContext:这是Direct3D 11的设备和上下文对象。我们虽然不进行3D渲染,但需要它们来创建和管理GPU资源。设备代表显示适配器,上下文用于执行渲染命令(这里用于拷贝纹理)。
  2. IDXGIOutputDuplication:这是核心对象,代表了对一个显示输出的桌面复制会话。通过它,我们可以轮询或等待新的桌面帧。它的创建成本较高,应尽量复用。
  3. IDXGIResource / ID3D11Texture2D:从AcquireNextFrame获取的资源对象,它本质上是一个纹理(Texture),包含了当前桌面的图像数据。但这个纹理通常位于显存中,且可能被GPU频繁读写,CPU无法直接访问。
  4. ID3D11Texture2D (Staging):为了解决CPU访问问题,我们需要创建一个用途(Usage)为D3D11_USAGE_STAGING、CPU访问标志(CPUAccessFlags)为D3D11_CPU_ACCESS_READ的纹理。这个“暂存纹理”位于能被CPU访问的内存中。我们将桌面纹理拷贝到它里面,然后再进行映射操作。

注意AcquireNextFrameReleaseFrame必须成对调用。在成功获取一帧后,你必须调用ReleaseFrame来释放这一帧的所有权,然后才能获取下一帧,否则后续调用会失败。这是一个常见的坑点。

3.2 BMP文件生成的字节序与对齐陷阱

手动构造BMP文件时,有两个细节极易出错:

1. 垂直翻转问题:屏幕坐标系的原点通常在左上角,Y轴向下。而BMP像素数据的存储顺序是从下到上。这意味着,如果你把从屏幕捕获的、自上而下存储的像素数据直接写入BMP文件,得到的图片将是上下颠倒的。解决方案有两种:

  • 在写入像素数据时,从最后一行开始写,逐行向前。
  • 更巧妙的做法是,在填写BITMAPINFOHEADER时,将biHeight字段设置为负数(例如-1080)。这告诉BMP解析器:“此图像的像素数据是自上而下存储的,无需翻转”。我们通常采用这种方案,因为它避免了在内存中重新排列大量数据,性能更高。

2. 行字节对齐:BMP文件要求每行像素数据的字节数必须是4的倍数。对于24位RGB(每个像素3字节)的图像,如果宽度不是4的倍数,就需要在每行末尾补0。例如,一张宽度为3像素的24位BMP,每行实际数据是3 * 3 = 9字节,但写入文件时需要补3个字节的0,凑齐12字节。计算每行字节数的公式是:RowSize = ((Width * BitsPerPixel) + 31) / 32 * 4。对于32位ARGB(每个像素4字节)的图像,由于4字节本身就是4的倍数,所以不需要额外对齐,这简化了处理。

3.3 像素格式的转换与取舍

DXGI桌面复制API默认捕获的像素格式是DXGI_FORMAT_B8G8R8A8_UNORM。这意味着在内存中,每个像素的4个字节(32位)按**蓝(B)、绿(G)、红(R)、透明度(A)的顺序排列。而标准的24位BMP格式期望的是红(R)、绿(G)、蓝(B)**的顺序。如果我们想保存为24位BMP,就需要在写入文件前进行像素格式的转换,丢弃A通道,并交换R和B通道。

在我们的示例中,为了简化,我们直接保存为32位BMP(设置biBitCount=32),并将biCompression设为BI_RGB。虽然BMP标准支持32位,但许多简单的图片查看器可能只识别24位。如果你需要广泛的兼容性,就必须实现这个转换逻辑。一个高效的转换方法是使用SIMD指令(如SSE/AVX)或并行算法来批量处理,但对于学习和基础功能,逐像素转换在大多数场景下也是可接受的。

4. 实操过程与核心环节实现

4.1 环境准备与项目配置

首先,你需要一个支持C++11及以上的开发环境,如Visual Studio 2019或2022。项目配置是关键一步。

  1. 创建项目:新建一个C++控制台应用程序项目。
  2. 链接库:DXGI和D3D11是系统组件,但我们需要显式链接它们的导入库。有两种方法:
    • 在代码中使用#pragma comment:如示例所示,#pragma comment(lib, "d3d11.lib")#pragma comment(lib, "dxgi.lib")。这是最简单的方式。
    • 在项目属性中配置:打开“项目属性” -> “链接器” -> “输入” -> “附加依赖项”,添加d3d11.lib; dxgi.lib;
  3. 包含头文件:确保包含了必要的Windows和DirectX头文件。
    #include <windows.h> #include <d3d11.h> #include <dxgi1_2.h> // 注意是1_2,包含了DuplicateOutput接口 #include <stdio.h> // 用于文件操作 #include <stdint.h> // 用于明确大小的整数类型

4.2 逐步实现DXGI屏幕捕获

让我们分解示例代码中的main函数,理解每一步。

第一步:创建设备与上下文

ID3D11Device* pDevice = nullptr; ID3D11DeviceContext* pContext = nullptr; D3D_FEATURE_LEVEL featureLevel; HRESULT hr = D3D11CreateDevice( nullptr, // 指定适配器,nullptr表示默认 D3D_DRIVER_TYPE_HARDWARE, // 优先使用硬件驱动 nullptr, // 软件驱动句柄,硬件驱动时为nullptr 0, // 设备创建标志 nullptr, // 功能级别数组,nullptr使用默认 0, // 功能级别数组大小 D3D11_SDK_VERSION, // SDK版本 &pDevice, // 返回的设备指针 &featureLevel, // 返回支持的最高功能级别 &pContext // 返回的设备上下文指针 ); if (FAILED(hr)) { // 处理错误,可以尝试回落到D3D_DRIVER_TYPE_WARP(软件渲染) }

这里我们直接请求硬件驱动类型。如果失败(比如在无显卡的服务器上),可以尝试D3D_DRIVER_TYPE_WARP,它是一个高性能的软件光栅化器。

第二步:获取DXGI输出与创建副本

IDXGIDevice* pDxgiDevice = nullptr; pDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&pDxgiDevice); IDXGIAdapter* pAdapter = nullptr; pDxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&pAdapter); IDXGIOutput* pOutput = nullptr; // 通常索引0代表主显示器 pAdapter->EnumOutputs(0, &pOutput); IDXGIOutput1* pOutput1 = nullptr; pOutput->QueryInterface(__uuidof(IDXGIOutput1), (void**)&pOutput1); IDXGIOutputDuplication* pDeskDupl = nullptr; hr = pOutput1->DuplicateOutput(pDevice, &pDeskDupl);

DuplicateOutput是核心,它创建了桌面副本对象。一个输出(显示器)只能同时存在一个激活的副本。

第三步:捕获帧并拷贝到暂存纹理

IDXGIResource* pDesktopResource = nullptr; DXGI_OUTDUPL_FRAME_INFO frameInfo; // 等待最多20毫秒获取新的一帧 hr = pDeskDupl->AcquireNextFrame(20, &frameInfo, &pDesktopResource); if (hr == DXGI_ERROR_WAIT_TIMEOUT) { // 没有新帧,继续循环或等待 return; } if (FAILED(hr)) { /* 处理其他错误 */ } // 获取纹理接口 ID3D11Texture2D* pAcquiredTexture = nullptr; pDesktopResource->QueryInterface(__uuidof(ID3D11Texture2D), (void**)&pAcquiredTexture); // 查询纹理描述以获取尺寸和格式 D3D11_TEXTURE2D_DESC desc; pAcquiredTexture->GetDesc(&desc); // 创建CPU可读的暂存纹理(如果尚未创建或尺寸变化) if (pStagingTexture == nullptr || desc.Width != stagingDesc.Width || desc.Height != stagingDesc.Height) { // 释放旧的,根据desc创建新的pStagingTexture D3D11_TEXTURE2D_DESC stagingDesc = desc; stagingDesc.Usage = D3D11_USAGE_STAGING; stagingDesc.BindFlags = 0; stagingDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; stagingDesc.MiscFlags = 0; pDevice->CreateTexture2D(&stagingDesc, nullptr, &pStagingTexture); } // 将捕获的纹理拷贝到暂存纹理 pContext->CopyResource(pStagingTexture, pAcquiredTexture);

这里引入了暂存纹理的缓存。如果桌面分辨率没有改变,我们可以复用同一个暂存纹理,避免反复创建销毁的开销。

第四步:映射内存并获取像素数据

IDXGISurface* pSurface = nullptr; pStagingTexture->QueryInterface(__uuidof(IDXGISurface), (void**)&pSurface); DXGI_MAPPED_RECT mappedRect; hr = pSurface->Map(&mappedRect, DXGI_MAP_READ); if (SUCCEEDED(hr)) { // mappedRect.pBits 指向像素数据的起始地址 // mappedRect.Pitch 是每行数据的字节数(已对齐) // 注意:Pitch可能大于 width * bytesPerPixel,因为可能存在内存对齐填充 int bytesPerPixel = 4; // 对应DXGI_FORMAT_B8G8R8A8_UNORM uint8_t* pData = (uint8_t*)mappedRect.pBits; // 现在pData和Pitch可以用来保存或处理图像了 // ... pSurface->Unmap(); }

Pitch非常重要。它代表了纹理中一行数据在内存中的跨度(字节数)。由于GPU内存对齐的要求,Pitch通常大于或等于宽度 * 每像素字节数。在拷贝数据时,必须使用Pitch作为行跨度。

4.3 实现BMP文件保存函数

我们将捕获到的像素数据(B8G8R8A8格式,自上而下存储)保存为32位BMP。

bool SaveAsBMP(const char* filename, uint8_t* pData, int width, int height, int pitch) { // 1. 计算文件大小 // 32位图,无行对齐问题,因为每行字节数 = width * 4 本身就是4的倍数。 int pixelDataSize = height * width * 4; // 实际数据大小 int fileSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + pixelDataSize; // 2. 准备文件头 BITMAPFILEHEADER bmfh = {0}; bmfh.bfType = 0x4D42; // 'BM' bmfh.bfSize = fileSize; bmfh.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER); // 像素数据偏移 // 3. 准备信息头 BITMAPINFOHEADER bmih = {0}; bmih.biSize = sizeof(BITMAPINFOHEADER); bmih.biWidth = width; bmih.biHeight = -height; // 关键!负数表示像素数据是自上而下存储 bmih.biPlanes = 1; bmih.biBitCount = 32; // 32位,带Alpha通道 bmih.biCompression = BI_RGB; // 不压缩 bmih.biSizeImage = pixelDataSize; // 可设为0,但填上更规范 bmih.biXPelsPerMeter = 0; bmih.biYPelsPerMeter = 0; bmih.biClrUsed = 0; bmih.biClrImportant = 0; // 4. 写入文件 FILE* fp = nullptr; fopen_s(&fp, filename, "wb"); if (!fp) return false; fwrite(&bmfh, sizeof(BITMAPFILEHEADER), 1, fp); fwrite(&bmih, sizeof(BITMAPINFOHEADER), 1, fp); // 5. 写入像素数据 // 因为设置了负的biHeight,且Pitch可能大于width*4,我们需要按行拷贝 for (int row = 0; row < height; ++row) { // 源数据每一行的起始地址 uint8_t* srcRow = pData + row * pitch; // 目标:直接写入文件,一行就是width*4字节 fwrite(srcRow, 1, width * 4, fp); // 注意:这里假设pitch >= width*4。如果pitch小于,则会发生错误,但通常不会。 } fclose(fp); return true; }

这个函数清晰地展示了BMP文件的组装过程。注意,我们直接将B8G8R8A8数据写入,没有交换R和B通道。生成的BMP文件在大多数现代软件(如Windows照片查看器、Photoshop)中能正确显示,但颜色是BGR顺序。如果需要标准的RGB顺序,需要在写入前进行交换。

4.4 实现BMP图片分割操作

分割操作建立在能够正确解析BMP文件的基础上。我们实现一个函数,将一张BMP水平或垂直等分成N份。

bool SplitBMP(const char* inputFile, int horizontalParts, int verticalParts) { // 1. 读取并解析原BMP文件 FILE* fpIn = nullptr; fopen_s(&fpIn, inputFile, "rb"); if (!fpIn) return false; BITMAPFILEHEADER bmfh; BITMAPINFOHEADER bmih; fread(&bmfh, sizeof(bmfh), 1, fpIn); fread(&bmih, sizeof(bmih), 1, fpIn); // 检查是否是支持的格式(非压缩,24或32位) if (bmih.biCompression != BI_RGB) { fclose(fpIn); return false; } bool isTopDown = bmih.biHeight < 0; int absHeight = abs(bmih.biHeight); int width = bmih.biWidth; int bitsPerPixel = bmih.biBitCount; int bytesPerPixel = bitsPerPixel / 8; // 计算原图行对齐后的字节数 int srcRowPitch = ((width * bitsPerPixel + 31) / 32) * 4; int pixelDataSize = srcRowPitch * absHeight; // 移动到像素数据开始处 fseek(fpIn, bmfh.bfOffBits, SEEK_SET); std::vector<uint8_t> srcData(pixelDataSize); fread(srcData.data(), 1, pixelDataSize, fpIn); fclose(fpIn); // 2. 计算每个子块的尺寸 int subWidth = width / horizontalParts; int subHeight = absHeight / verticalParts; // 计算子块行对齐后的字节数 int dstRowPitch = ((subWidth * bitsPerPixel + 31) / 32) * 4; // 3. 为每个子块创建新的BMP文件 for (int rowPart = 0; rowPart < verticalParts; ++rowPart) { for (int colPart = 0; colPart < horizontalParts; ++colPart) { // 计算子块在原图中的起始像素坐标 int startX = colPart * subWidth; int startY = isTopDown ? rowPart * subHeight : (absHeight - (rowPart + 1) * subHeight); // 注意:对于自下而上的BMP(biHeight>0),Y坐标计算更复杂,这里假设为自上而下简化处理。 // 准备子块的文件头和信息头 BITMAPFILEHEADER subBmfh = bmfh; BITMAPINFOHEADER subBmih = bmih; subBmih.biWidth = subWidth; subBmih.biHeight = isTopDown ? -subHeight : subHeight; // 保持与原图相同的存储方向 subBmih.biSizeImage = dstRowPitch * subHeight; subBmfh.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + subBmih.biSizeImage; // 生成文件名 char outputFile[256]; snprintf(outputFile, sizeof(outputFile), "%s_part_%d_%d.bmp", inputFile /* 最好去掉扩展名 */, rowPart, colPart); FILE* fpOut = nullptr; fopen_s(&fpOut, outputFile, "wb"); if (!fpOut) continue; fwrite(&subBmfh, sizeof(subBmfh), 1, fpOut); fwrite(&subBmih, sizeof(subBmih), 1, fpOut); // 4. 拷贝像素数据 for (int row = 0; row < subHeight; ++row) { // 计算原图中当前行的起始位置 int srcY = startY + row; if (!isTopDown) { // 如果是自下而上存储,需要反向计算Y坐标 srcY = absHeight - 1 - (startY + row); } uint8_t* srcRowStart = srcData.data() + srcY * srcRowPitch + startX * bytesPerPixel; // 计算目标行数据(可能需要对齐填充) std::vector<uint8_t> dstRow(dstRowPitch, 0); memcpy(dstRow.data(), srcRowStart, subWidth * bytesPerPixel); fwrite(dstRow.data(), 1, dstRowPitch, fpOut); } fclose(fpOut); } } return true; }

这个分割函数考虑了BMP的行对齐和存储方向(自上而下或自下而上),是一个健壮的实现。你可以修改它来实现非等分、按区域坐标分割等更复杂的功能。

5. 常见问题与排查技巧实录

在实际编码和运行过程中,你几乎一定会遇到下面这些问题。这里记录了我的踩坑经验和解决方案。

5.1 DXGI初始化与权限问题

问题1:D3D11CreateDeviceDuplicateOutput返回E_ACCESSDENIED错误。这通常发生在远程桌面(RDP)会话、某些虚拟机环境,或者没有正确权限的情况下。桌面复制API需要特殊的权限,并且可能被远程会话禁用。

  • 排查:确保程序在本地控制台会话(物理机或控制台登录的虚拟机)中运行。以管理员身份运行有时能解决某些权限问题,但并非总是有效。
  • 替代方案:如果在不支持桌面复制的环境中,必须回退到GDI截屏方法(BitBlt)。可以在运行时检测DuplicateOutput的返回值,如果失败则切换到GDI方案。

问题2:捕获到的纹理尺寸或格式与预期不符。ID3D11Texture2D::GetDesc返回的描述信息是权威的。不要对显示器的分辨率做任何假设。

  • 技巧:始终根据desc.Widthdesc.Height来分配内存和处理数据。desc.Format通常是DXGI_FORMAT_B8G8R8A8_UNORM,但也可能是其他格式(如R8G8B8A8_UNORM),编写通用代码时应能处理。

5.2 BMP文件生成与查看问题

问题3:保存的BMP文件无法打开,或提示“无效的位图文件”。这几乎总是文件头或信息头填写错误。

  • 检查清单
    1. 文件类型标识bfType必须是0x4D42(即字符'BM')。注意字节序,在x86/x64小端序机器上,写作0x4D42是正确的。
    2. 偏移量bfOffBits必须是文件头和信息头大小的和。如果后面还有调色板,还要加上调色板大小。对于24/32位无调色板位图,就是14 + 40 = 54字节。
    3. 文件大小bfSize必须是整个文件的大小。计算时务必准确:文件头 + 信息头 + 像素数据大小。像素数据大小要使用对齐后的每行字节数乘以高度。
    4. 信息头大小biSize必须为40sizeof(BITMAPINFOHEADER))。
    5. 高度符号:如果你希望像素数据自上而下存储(避免手动翻转),biHeight必须为负数。很多开源代码忘记这一点。
    6. 写入模式:文件必须以二进制模式("wb")打开,而不是文本模式("w"),否则换行符等字节可能被转换。

问题4:图片颜色异常(比如蓝色和红色互换)。这是像素格式问题。DXGI捕获的是B8G8R8A8,而许多软件(尤其是旧的)期望BMP是R8G8B8R8G8B8A8

  • 解决方案:在写入文件前,交换每个像素的R和B通道。对于32位数据,可以这样快速交换:
    for (int i = 0; i < width * height; ++i) { uint8_t* pixel = pData + i * 4; std::swap(pixel[0], pixel[2]); // 交换B和R // Alpha通道(pixel[3])保持不变 }
    对于性能要求高的场景,可以使用SIMD指令进行优化。

5.3 性能与资源管理

问题5:截屏循环导致CPU占用率过高。如果在一个紧密循环中不断调用AcquireNextFrame,并且没有新帧时立即返回,会导致CPU空转。

  • 优化:使用AcquireNextFrame的超时参数。示例中设置为20毫秒,这意味着API会等待最多20毫秒来获取新帧。如果没有新帧,它会返回DXGI_ERROR_WAIT_TIMEOUT,此时你可以让线程睡眠一小段时间(例如Sleep(10)),或者将循环改为事件驱动(DXGI也支持通过帧事件通知,但更复杂)。
  • 另一个思路:对于非实时性要求极高的应用,可以降低捕获频率,比如每秒捕获10-30帧,而不是尽可能快。

问题6:内存泄漏。所有COM接口对象(以I开头的指针)都必须调用Release()。一个良好的习惯是使用ComPtr智能指针(Microsoft::WRL::ComPtr),它来自Windows Runtime C++ Template Library (WRL),可以自动管理COM对象的生命周期。

#include <wrl/client.h> using Microsoft::WRL::ComPtr; ComPtr<ID3D11Device> device; ComPtr<IDXGIOutputDuplication> deskDupl; // 创建对象... // 无需手动Release,ComPtr析构时会自动调用

5.4 图片分割中的边界与对齐处理

问题7:分割后子图边缘有杂色或错位。这几乎总是由于行对齐计算错误导致的。原图和子图的每行字节数都必须按照BMP规则进行4字节对齐。

  • 关键计算
    int stride = ((width * bitsPerPixel + 31) / 32) * 4; // 正确的对齐计算
    在从原图拷贝数据到子图缓冲区时,必须分别使用原图的stride_src和子图的stride_dst作为内存行跨度,而不是简单的width * bytesPerPixel
  • 调试技巧:对于小尺寸图片(比如10x10),手动计算并打印出stride_srcstride_dst,确保拷贝循环中的地址计算是正确的。

问题8:原图尺寸不能被整除时,分割不均匀。我们的示例是简单整除,这可能导致最后一个子块尺寸不同或丢失边缘像素。

  • 改进方案:在计算subWidthsubHeight时,可以使用除法取整,然后让最后一个子块包含剩余的所有像素。例如:
    int subWidth = width / horizontalParts; int lastSubWidth = width - subWidth * (horizontalParts - 1); // 对前N-1个子块用subWidth,最后一个用lastSubWidth
    这确保了所有原始像素都被分配到某个子块中。

将DXGI截屏和BMP手动处理结合起来,你就能在C++中构建一个不依赖任何外部图像库的、高效的屏幕捕获与基础图像处理模块。这个过程中对底层API、内存布局和文件格式的理解,其价值远超过仅仅调用一个现成的截图函数。当你下次需要处理图形数据时,这份对“像素如何从屏幕走到文件”的掌控感,会让你更加得心应手。