文章总结: 文章指出Windows截屏不全根源在于未声明DPIAwareness与误用主屏坐标,给出调用SetProcessDpiAwarenessContext、GetSystemMetrics虚拟屏参数、EnumDisplayMonitors枚举、GetDC(NULL)取整屏DC及BitBlt一次截取再按rcMonitor裁剪的完整代码路径,可一次性解决高DPI、多显示器、负坐标场景下的截图缺失与错位问题,适用于Win32/C++/C#等GDI方案。 综合评分: 88 文章分类: 安全工具,安全开发,终端安全,应用安全,技术标准
为什么你的截屏老是截不全
原创
黑晶
黑晶
2026年1月13日 19:30 浙江
为什么你的截屏老是截不全
文章涉及到的技术点,仅供研究参考,不要做非法的事情。
这个文章讲一下以前新手朋友提到的截屏不全的问题,尤其是屏幕开启缩放的场景下,类似CS都会出现这种问题。
一、问题背景
在 Windows 平台上,实现“全屏截图 + 多显示器支持”看似简单,但在实际工程中,很多开源通用截图代码都会遇到两个经典问题:
- 开启屏幕缩放(DPI Scaling)后,截图不完整
- 多显示器环境下,只能截到主屏幕
这些问题并主要是对 Windows DPI 机制和多显示器坐标体系理解不完整造成的。
这篇文章会从 Windows API 编程角度出发,解释:
- 失败的原因
- 如何实现一个正常工作的截屏
- 每一个关键 API的作用
以下内容可将示例代码理解为 直接调用 Windows API
二、Windows 截屏的两个“隐形陷阱”
DPI 虚拟化(DPI Virtualization)
问题本质
如果一个进程 不是 DPI-aware,而默认情况下,比如你c++写的console等程序,都不是的:
- Windows 会对该进程返回逻辑坐标
GetSystemMetrics、EnumDisplayMonitors得到的尺寸 ≠ 真实屏幕像素
结果:
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 无法正确解析位图数据
从“大图”裁剪每个显示器
思路
- 截一张 完整虚拟屏幕图
- 使用
rcMonitor做裁剪
裁剪坐标转换
显示器坐标 - 虚拟屏幕左上角 = 子图坐标
完美解决:
- 主屏
- 副屏
- 左侧 / 上方屏幕
五、最终效果对比
| 场景 | 通用代码 | 本实现 | | — | — | — | | DPI 缩放 150% | ❌ 截不全 | ✅ 完整 | | 多显示器 | ❌ 只有主屏 | ✅ 全部 | | 不同 DPI | ❌ 错位 | ✅ 正确 | | 负坐标屏幕 | ❌ 黑边 | ✅ 正常 |
六、总结(核心原因)
是 Windows API 使用方式的问题
我们之所以能解决问题,是因为:
- 正确声明 DPI Awareness
- 理解并使用虚拟屏幕坐标体系
- 严格区分屏幕 DC 与内存 DC
- 一次完整截取 + 精确裁剪
七、适用范围
- Win32 / C / C++
- Go / Rust / C#
- GDI 截图方案
- 多显示器 / 高 DPI 场景
BlackCat 截屏效果(我这个是个双屏,主屏带鱼屏开了125%缩放,副副屏2k):
***C2学习代码👇***:
欢迎加入交流圈
扫码获取更多精彩
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:黑晶 黑晶《为什么你的截屏老是截不全》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论