为什么你的截屏老是截不全

admin 2026-01-14 23:48:13 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 文章指出Windows截屏不全根源在于未声明DPIAwareness与误用主屏坐标,给出调用SetProcessDpiAwarenessContext、GetSystemMetrics虚拟屏参数、EnumDisplayMonitors枚举、GetDC(NULL)取整屏DC及BitBlt一次截取再按rcMonitor裁剪的完整代码路径,可一次性解决高DPI、多显示器、负坐标场景下的截图缺失与错位问题,适用于Win32/C++/C#等GDI方案。 综合评分: 88 文章分类: 安全工具,安全开发,终端安全,应用安全,技术标准


cover_image

为什么你的截屏老是截不全

原创

黑晶

黑晶

2026年1月13日 19:30 浙江

为什么你的截屏老是截不全

文章涉及到的技术点,仅供研究参考,不要做非法的事情。

这个文章讲一下以前新手朋友提到的截屏不全的问题,尤其是屏幕开启缩放的场景下,类似CS都会出现这种问题。

一、问题背景

在 Windows 平台上,实现“全屏截图 + 多显示器支持”看似简单,但在实际工程中,很多开源通用截图代码都会遇到两个经典问题:

  1. 开启屏幕缩放(DPI Scaling)后,截图不完整
  2. 多显示器环境下,只能截到主屏幕

这些问题并主要是对 Windows DPI 机制和多显示器坐标体系理解不完整造成的。

这篇文章会从 Windows API 编程角度出发,解释:

  • 失败的原因
  • 如何实现一个正常工作的截屏
  • 每一个关键 API的作用

以下内容可将示例代码理解为 直接调用 Windows API

二、Windows 截屏的两个“隐形陷阱”

DPI 虚拟化(DPI Virtualization)

问题本质

如果一个进程 不是 DPI-aware,而默认情况下,比如你c++写的console等程序,都不是的:

  • Windows 会对该进程返回逻辑坐标
  • GetSystemMetricsEnumDisplayMonitors 得到的尺寸 ≠ 真实屏幕像素

结果:

  • BitBlt 按“错误尺寸”拷贝
  • 截图被裁剪或缺失

多显示器的“虚拟屏幕坐标系”

Windows 的真实模型

  • 所有显示器组成一个 Virtual Screen

  • 坐标可能:

  • 从负数开始

  • 主屏不一定在 (0,0)

很多代码错误设置:

屏幕左上角 = (0,0)屏幕宽高 = 主屏分辨率

在多显示器下直接失效

三、常见的代码为什么解决不了?

常见错误模式总结

错误 1:未设置 DPI Awareness

// 什么都不做,使用默认 DPI-unaware

后果:

  • 系统返回的是“缩放后的逻辑坐标”
  • 实际 BitBlt 用的是物理像素
  • 尺寸不一致 → 截图不全

错误 2:只截主屏

GetSystemMetrics(SM_CXSCREEN);GetSystemMetrics(SM_CYSCREEN);

这些 API:

  • 只返回主屏尺寸
  • 与其他显示器无关

错误 3:错误使用 GetDIBits 的 DC

GetDIBits(hdcMem, hBitmap, ...); // ❌

这是 GDI 的经典坑:

  • GetDIBits必须使用屏幕 DC
  • 否则返回空数据或错误颜色

四、我们的代码是如何正确解决的?

1️⃣ 设置进程为 Per-Monitor DPI Aware

使用的 API

SetProcessDpiAwarenessContext(    DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);

注意:要在你的截屏例程最开始的时候就要设置好

作用说明

  • 告诉 Windows:

本进程自己处理 DPI,请返回真实物理像素

  • 所有坐标、尺寸均为真实值
  • 支持不同 DPI 的多显示器

失败兜底

SetProcessDPIAware(); // 兼容旧系统

正确理解“虚拟屏幕”

关键 API

GetSystemMetrics(SM_XVIRTUALSCREEN);GetSystemMetrics(SM_YVIRTUALSCREEN);GetSystemMetrics(SM_CXVIRTUALSCREEN);GetSystemMetrics(SM_CYVIRTUALSCREEN);

含义解释

| API | 含义 | | — | — | | SM_XVIRTUALSCREEN | 所有屏幕最左 X | | SM_YVIRTUALSCREEN | 所有屏幕最上 Y | | SM_CXVIRTUALSCREEN | 虚拟屏幕总宽度 | | SM_CYVIRTUALSCREEN | 虚拟屏幕总高度 |

这才是真正应该截的“整块区域”


枚举每一个显示器(不是猜)

使用 API

EnumDisplayMonitors(    NULL, NULL,    MonitorEnumProc,    0);

回调中获取真实显示器矩形

GetMonitorInfo(hMonitor, &mi);

返回的:

ounter(linemi.rcMonitor
  • 是 物理像素
  • 在 虚拟屏幕坐标系 中

一次 BitBlt,完整截取虚拟屏幕

获取屏幕 DC

HDC hdcScreen = GetDC(NULL);
  • NULL → 整个虚拟屏幕
  • 包含所有显示器

BitBlt 的关键参数

BitBlt(  hdcMem,  0, 0,  width, height,  hdcScreen,  rect.left,  rect.top,  SRCCOPY);

| 参数 | 含义 | | — | — | | hdcMem | 内存 DC(目标) | | width/height | 虚拟屏幕尺寸 | | hdcScreen | 屏幕 DC | | rect.left/top | 虚拟屏幕起点(可为负) |

这是最容易写错的地方


正确使用 GetDIBits(关键细节)

正确方式

GetDIBits(  hdcScreen,   // 必须是屏幕 DC  hBitmap,  ...);

原因

  • GDI 内部依赖屏幕 DC 的像素格式
  • 内存 DC 无法正确解析位图数据

从“大图”裁剪每个显示器

思路

  1. 截一张 完整虚拟屏幕图
  2. 使用 rcMonitor 做裁剪

裁剪坐标转换

显示器坐标 - 虚拟屏幕左上角 = 子图坐标

完美解决:

  • 主屏
  • 副屏
  • 左侧 / 上方屏幕

五、最终效果对比

| 场景 | 通用代码 | 本实现 | | — | — | — | | DPI 缩放 150% | ❌ 截不全 | ✅ 完整 | | 多显示器 | ❌ 只有主屏 | ✅ 全部 | | 不同 DPI | ❌ 错位 | ✅ 正确 | | 负坐标屏幕 | ❌ 黑边 | ✅ 正常 |


六、总结(核心原因)

是 Windows API 使用方式的问题

我们之所以能解决问题,是因为:

  1. 正确声明 DPI Awareness
  2. 理解并使用虚拟屏幕坐标体系
  3. 严格区分屏幕 DC 与内存 DC
  4. 一次完整截取 + 精确裁剪

七、适用范围

  • Win32 / C / C++
  • Go / Rust / C#
  • GDI 截图方案
  • 多显示器 / 高 DPI 场景

BlackCat 截屏效果(我这个是个双屏,主屏带鱼屏开了125%缩放,副副屏2k):

***C2学习代码👇***:

欢迎加入交流圈

扫码获取更多精彩


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:黑晶 黑晶《为什么你的截屏老是截不全》

评论:0   参与:  0