Windows 全屏自动截屏教程
用 PowerShell 一键截取完整桌面,彻底解决 DPI 缩放导致截图不全的坑 😤
问题背景
你想用 PowerShell 自动截屏,于是写了这样的代码:
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds
$bitmap = New-Object System.Drawing.Bitmap $screen.Width, $screen.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen(0, 0, 0, 0, $screen.Size)
$bitmap.Save("screenshot.png", [System.Drawing.Imaging.ImageFormat]::Png)
结果一运行——截图只有一部分! 右边被截断了,底部也没了 🤡
原因:DPI 缩放在搞鬼
Windows 为了让高分屏显示更清晰,引入了 DPI 缩放机制:
| 缩放比例 | 逻辑分辨率 | 物理分辨率 | 差距 |
|---|---|---|---|
| 100% | 1920×1080 | 1920×1080 | 无 |
| 125% ⚠️ | 1536×864 | 1920×1080 | 差 384×216! |
| 150% | 1280×720 | 1920×1080 | 差更多 |
Screen.PrimaryScreen.Bounds返回的是 逻辑分辨率(缩放后的虚拟尺寸)CopyFromScreen按逻辑分辨率去截,自然就截不全了- 你的屏幕实际像素是 1920×1080,但系统告诉你是 1536×864
💡 简单理解:DPI 缩放就像把一张大图缩小了显示,但你截图时按缩小后的尺寸去截,当然只能截到一部分。
解决方案:用 Win32 API 获取物理分辨率
核心思路:绕过 .NET 的封装,直接调用 Windows 底层 API 获取真实的物理像素尺寸。
完整可用代码
# ============================================
# Windows 全屏截屏(DPI 适配版)
# 解决 DPI 缩放导致的截图不全问题
# ============================================
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# 定义 C# 类,通过 P/Invoke 调用 Win32 API 获取物理分辨率
$cSharp = @'
using System;
using System.Runtime.InteropServices;
public class ScreenHelper
{
[DllImport("user32.dll")]
private static extern IntPtr GetDC(IntPtr hwnd);
[DllImport("gdi32.dll")]
private static extern int GetDeviceCaps(IntPtr hdc, int nIndex);
[DllImport("user32.dll")]
private static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);
/// <summary>
/// 获取屏幕物理像素分辨率(不受 DPI 缩放影响)
/// </summary>
public static int[] GetPhysicalResolution()
{
IntPtr dc = GetDC(IntPtr.Zero);
int width = GetDeviceCaps(dc, 118); // DESKTOPHORZRES → 物理宽度
int height = GetDeviceCaps(dc, 117); // DESKTOPVERTRES → 物理高度
ReleaseDC(IntPtr.Zero, dc);
return new int[] { width, height };
}
}
'@
# 编译 C# 类
Add-Type -TypeDefinition $cSharp
# 获取物理分辨率
$resolution = [ScreenHelper]::GetPhysicalResolution()
$width = $resolution[0]
$height = $resolution[1]
Write-Host "物理分辨率: ${width}x${height}"
# 创建位图并截取全屏
$bitmap = New-Object System.Drawing.Bitmap $width, $height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen(0, 0, 0, 0, (New-Object System.Drawing.Size($width, $height))
# 保存截图(支持 PNG/JPG/BMP 等格式)
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$outputPath = "$HOME\Desktop\screenshot_${timestamp}.png"
$bitmap.Save($outputPath, [System.Drawing.Imaging.ImageFormat]::Png)
# 释放资源
$graphics.Dispose()
$bitmap.Dispose()
Write-Host "截图已保存: $outputPath"
核心原理
关键在于 GetDeviceCaps 这个 Win32 API 函数:
| 参数常量 | 值 | 返回值 | 说明 |
|---|---|---|---|
HORZRES |
8 | 逻辑宽度 | ❌ 受 DPI 影响 |
VERTRES |
10 | 逻辑高度 | ❌ 受 DPI 影响 |
DESKTOPHORZRES |
118 | 物理宽度 | ✅ 真实像素 |
DESKTOPVERTRES |
117 | 物理高度 | ✅ 真实像素 |
┌─────────────────────────────────────────────┐
│ 物理屏幕 (1920×1080) │
│ ┌───────────────────────────────────┐ │
│ │ 逻辑屏幕 (1536×864) │ │
│ │ ← Screen.Bounds 只能拿到这个 → │ │
│ └───────────────────────────────────┘ │
│ ← 这部分截不到!!! │
└─────────────────────────────────────────────┘
为什么不用 SnippingTool?
你可能想问:Windows 自带的截图工具不能命令行调用吗?
# ❌ 这样不行!SnippingTool 不支持纯命令行自动截屏
SnippingTool /clipMode Full
SnippingTool 的命令行参数非常有限,不支持无交互的全屏截图并保存文件。所以还是得用 CopyFromScreen + Win32 API 的方案。
进阶用法
1. 定时截屏(每隔 N 秒截一次)
# 在上述代码基础上加个循环
$interval = 60 # 每 60 秒截一次
$count = 10 # 共截 10 次
for ($i = 1; $i -le $count; $i++) {
# ... 截图代码 ...
Write-Host "[$i/$count] 已截图,等待 ${interval} 秒..."
Start-Sleep -Seconds $interval
}
2. 截取指定区域
# 只截取 (100, 100) 开始的 800×600 区域
$regionX = 100
$regionY = 100
$regionW = 800
$regionH = 600
$bitmap = New-Object System.Drawing.Bitmap $regionW, $regionH
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($regionX, $regionY, 0, 0,
(New-Object System.Drawing.Size($regionW, $regionH)))
3. 截图后压缩为 JPG(文件更小)
# PNG → 无损但文件大(适合需要高质量的场景)
$bitmap.Save("screenshot.png", [System.Drawing.Imaging.ImageFormat]::Png)
# JPG → 有损但文件小(适合日常记录、监控)
$jpegCodec = (
[System.Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
Where-Object { $_.MimeType -eq "image/jpeg" }
)[0]
$encoderParams = New-Object System.Drawing.Imaging.EncoderParameters(1)
$encoderParams.Param[0] = New-Object System.Drawing.Imaging.EncoderParameter(
[System.Drawing.Encoding]::Quality, 90L # 质量 1-100
)
$bitmap.Save("screenshot.jpg", $jpegCodec, $encoderParams)
常见问题
Q: 怎么查看自己电脑的 DPI 缩放比例?
设置 → 显示 → 显示缩放,或者:
# 查看 DPI 缩放信息
Add-Type -AssemblyName System.Windows.Forms
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
Write-Host "逻辑分辨率: $($screen.Bounds.Width)x$($screen.Bounds.Height)"
# 通过注册表查看具体缩放比例
$dpiscale = (Get-ItemProperty "HKCU:\Control Panel\Desktop").LogPixels
Write-Host "DPI 值: $dpiscale ($( [math]::Round($dpiscale/96*100) )%)"
Q: 多显示器环境怎么办?
上面的代码默认只截主显示器。如果要多屏截图:
# 截取所有显示器组成的虚拟桌面
Add-Type -AssemblyName System.Windows.Forms
$vWidth = [System.Windows.Forms.SystemInformation]::VirtualScreen.Width
$vHeight = [System.Windows.Forms.SystemInformation]::VirtualScreen.Height
$vX = [System.Windows.Forms.SystemInformation]::VirtualScreen.X
$vY = [System.Windows.Forms.SystemInformation]::VirtualScreen.Y
$bitmap = New-Object System.Drawing.Bitmap $vWidth, $vHeight
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($vX, $vY, 0, 0, (New-Object System.Drawing.Size($vWidth, $vHeight))
Q: 截图时被其他窗口挡住了怎么办?
这是 CopyFromScreen 的天然限制——它截的是屏幕上当前显示的内容,不是某个窗口。如果需要截取特定窗口,可以用:
# 通过窗口句柄获取窗口位置和大小,然后激活该窗口再截图
Add-Type @'
using System;
using System.Runtime.InteropServices;
public class WinAPI {
[DllImport("user32.dll")] public static extern IntPtr Find(string cls, string title);
[DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, ref RECT r);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int L,T,R,B; }
}
'@
Add-Type -TypeDefinition $cSharp2
$hwnd = [WinAPI]::Find($null, "记事本")
[WinAPI]::SetForegroundWindow($hwnd)
Start-Sleep -Milliseconds 300 # 等待窗口置顶
# ... 再执行截图 ...
总结
| 方案 | 能否全屏 | 是否受 DPI 影响 | 推荐度 |
|---|---|---|---|
Screen.Bounds + CopyFromScreen |
❌ 截不全 | ❌ 是 | ⭐ |
| SnippingTool 命令行 | ❌ 不支持 | - | 💀 |
| GetDeviceCaps(118/117) + CopyFromScreen | ✅ 完整 | ❌ 否 | ⭐⭐⭐⭐⭐ |
一句话总结:Windows 截屏永远用物理分辨率,不要信 .NET 给你的逻辑分辨率。用 GetDeviceCaps(DESKTOPHORZRES=118, DESKTOPVERTRES=117) 拿真实像素,稳如老狗 🐕
本文实测环境:Windows 11,DPI 125%,物理分辨率 1920×1080