Windows用PowerShell截屏


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


文章作者: 摸鱼
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 摸鱼 !
  目录