返回

教程:批处理结合PowerShell定时截取屏幕区域

windows

定时截取屏幕指定区域?用批处理和 PowerShell 搞定

问题来了:只想截屏幕的一部分

手里有个批处理脚本,挺好用的,能每隔 5 秒自动截个屏。代码大概长这样:

@echo off
REM 源自:https://www.reddit.com/r/Batch/comments/qadj3w/print_screen_batch/
Title Get a ScreenShot with Batch and Powershell
Set CaptureScreenFolder=C:\ScreenCapture\
If Not Exist "%CaptureScreenFolder%" MD "%CaptureScreenFolder%"
Set WaitTimeSeconds=5
:Loop
    Call :ScreenShot
    echo ScreenShot is taken on %Date% @ %Time% in Folder "%CaptureScreenFolder%"
    Timeout /T %WaitTimeSeconds% /NoBreak>nul
Goto Loop

::----------------------------------------------------------------------------
:ScreenShot
Powershell ^
$Path = '%CaptureScreenFolder%';^
Add-Type -AssemblyName System.Windows.Forms;^
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;^
$image = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height^);^
$graphic = [System.Drawing.Graphics]::FromImage($image^);^
$point = New-Object System.Drawing.Point(0,0^);^
$graphic.CopyFromScreen($point, $point, $image.Size^);^
$cursorBounds = New-Object System.Drawing.Rectangle([System.Windows.Forms.Cursor]::Position,[System.Windows.Forms.Cursor]::Current.Size^);^
[System.Windows.Forms.Cursors]::Default.Draw($graphic, $cursorBounds^);^
$FileName = $((Get-Date).ToString('dd-MM-yyyy_HH_mm_ss')+'.jpg');^
$FilePath = $Path+$FileName;^
$FormatJPEG = [System.Drawing.Imaging.ImageFormat]::jpeg;^
$image.Save($FilePath,$FormatJPEG^)
Exit /B

用起来没啥毛病,但有个新需求:我不想截整个屏幕,只想截取屏幕上的一个特定区域。比如,我想指定一个矩形区域,用坐标来定义,像从左下角的 (100, 100) 点到右上角的 (1000, 1000) 点这块区域。怎么改这个批处理脚本才能实现这个效果?

为啥原脚本只能截全屏?

要改脚本,得先弄明白它为啥现在是截全屏的。关键在于 :ScreenShot 部分调用的 PowerShell 命令。

仔细看这段 PowerShell 代码:

  1. $screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;
    这一行获取的是主显示器 的边界信息,包含了整个屏幕的宽度 ($screen.Width) 和高度 ($screen.Height)。

  2. $image = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height);
    接着,它创建了一个新的位图(Bitmap)对象,大小正好是整个屏幕的尺寸。

  3. $graphic = [System.Drawing.Graphics]::FromImage($image);
    然后,它从这个空白的全屏大小位图创建了一个绘图(Graphics)对象,准备往上画东西。

  4. $graphic.CopyFromScreen($point, $point, $image.Size);
    这句是核心! CopyFromScreen 方法负责从屏幕上抓取图像。它有三个主要参数:

    • 第一个 $point (值为 0,0):表示从屏幕的哪个 位置开始复制。(0,0) 就是屏幕的左上角。
    • 第二个 $point (值为 0,0):表示复制到目标位图($image)的哪个目标 位置。(0,0) 就是位图的左上角。
    • 第三个 $image.Size:表示要复制的区域大小 。因为 $image 是全屏大小的,所以这里复制的是整个屏幕的内容。

简单说,原始脚本的逻辑就是:创建一个跟屏幕一样大的画板,然后把整个屏幕的内容原封不动地“复印”到这个画板上。这就是它截取全屏的原因。

解决方案:动手修改脚本

要实现只截取特定区域,思路就清晰了:我们需要告诉 CopyFromScreen 方法,别从屏幕的 (0,0) 点开始复制整个屏幕了,而是从我们指定的区域左上角开始,只复制我们指定大小的那一块。同时,创建的位图(画板)大小也应该是我们想要的区域大小,而不是整个屏幕那么大。

下面提供几种实现方法:

方法一:改造 PowerShell 代码 (直接有效)

这是最直接的方法,直接修改嵌入在批处理文件里的 PowerShell 代码。

原理:

  1. 我们需要定义目标区域的左上角坐标 (X, Y) 和区域的宽度 (Width)、高度 (Height)
  2. 在 PowerShell 代码里,创建位图对象时,使用指定的 WidthHeight,而不是全屏尺寸。
  3. 调用 CopyFromScreen 时:
    • 第一个参数(源位置)设置为我们指定的区域左上角坐标 (X, Y)
    • 第二个参数(目标位置)仍然是 (0, 0),表示将捕获到的区域画在目标位图的左上角。
    • 第三个参数(复制大小)设置为我们指定的区域 WidthHeight

坐标系说明 (重要!):

需要特别注意,Windows API 和 .NET Framework(包括 System.Drawing)通常使用的屏幕坐标系是:

  • 原点 (0,0) 在屏幕的左上角。
  • X 轴向右增加。
  • Y 轴向下增加。

这和你提到的“左下角是 0,0”可能不同。

如果你想截取的区域,其左上角 在屏幕上的坐标是 (X, Y),区域的宽度Width高度Height,那么:

  • 在 PowerShell 里,你需要创建一个 System.Drawing.Point 对象,其值为 (X, Y),作为 CopyFromScreen 的第一个参数(源位置)。
  • 你需要创建一个 System.Drawing.Size 对象,其值为 (Width, Height),用于创建位图和作为 CopyFromScreen 的第三个参数(复制大小)。

举个例子:
假设你想要截取从屏幕左上角 (100, 100) 开始,宽度为 900 像素,高度为 900 像素的区域(也就是右下角坐标为 (100+900, 100+900) = (1000, 1000) 的矩形区域)。
那么,你需要设置:

  • X = 100
  • Y = 100
  • Width = 900
  • Height = 900

代码实战:

我们可以在批处理脚本的开头设置这些坐标和尺寸变量,然后把它们传递给 PowerShell。

@echo off
Title Get Specific Region ScreenShot with Batch and Powershell
Set CaptureScreenFolder=C:\ScreenCapture\
If Not Exist "%CaptureScreenFolder%" MD "%CaptureScreenFolder%"

REM --- 定义截图区域 ---
Set CaptureX=100       REM 区域左上角的 X 坐标
Set CaptureY=100       REM 区域左上角的 Y 坐标
Set CaptureWidth=900   REM 区域的宽度
Set CaptureHeight=900  REM 区域的高度
REM ---------------------

Set WaitTimeSeconds=5

:Loop
    Call :ScreenShot "%CaptureX%" "%CaptureY%" "%CaptureWidth%" "%CaptureHeight%"
    echo Region ScreenShot (%CaptureWidth%x%CaptureHeight% at %CaptureX%,%CaptureY%) taken on %Date% @ %Time% in Folder "%CaptureScreenFolder%"
    Timeout /T %WaitTimeSeconds% /NoBreak > nul
Goto Loop

::----------------------------------------------------------------------------
:ScreenShot
:: 参数: %1=X, %2=Y, %3=Width, %4=Height
Powershell -Command ^
$captureX = %1;^
$captureY = %2;^
$captureWidth = %3;^
$captureHeight = %4;^
$Path = '%CaptureScreenFolder%';^
Add-Type -AssemblyName System.Windows.Forms;^
Add-Type -AssemblyName System.Drawing;^
^
Try {^
    if ($captureWidth -le 0 -or $captureHeight -le 0) {^
        Write-Error 'Capture width and height must be positive.';^
        Exit 1;^
    }^
    ^
    $sourcePoint = New-Object System.Drawing.Point($captureX, $captureY);^
    $captureSize = New-Object System.Drawing.Size($captureWidth, $captureHeight);^
    ^
    # 创建指定区域大小的位图
    $image = New-Object System.Drawing.Bitmap($captureSize.Width, $captureSize.Height);^
    $graphic = [System.Drawing.Graphics]::FromImage($image);^
    ^
    # 从屏幕指定区域复制到目标位图的 (0,0) 位置
    # CopyFromScreen(Point upperLeftSource, Point upperLeftDestination, Size blockRegionSize)
    $graphic.CopyFromScreen($sourcePoint, (New-Object System.Drawing.Point(0,0)), $captureSize);^
    ^
    # (可选) 如果需要,在截图中绘制鼠标指针
    # 注意:鼠标位置是相对于整个屏幕的,转换到截图区域内坐标可能需要额外计算
    # 这里暂时简化,不绘制鼠标指针以避免复杂性
    # $cursorPosition = [System.Windows.Forms.Cursor]::Position;
    # $cursorSize = [System.Windows.Forms.Cursor]::Current.Size;
    # # 计算鼠标相对于截图区域的位置 (如果鼠标在区域内)
    # $relativeX = $cursorPosition.X - $captureX;
    # $relativeY = $cursorPosition.Y - $captureY;
    # if ($relativeX -ge 0 -and $relativeX -lt $captureWidth -and $relativeY -ge 0 -and $relativeY -lt $captureHeight) {
    #    $cursorBounds = New-Object System.Drawing.Rectangle($relativeX, $relativeY, $cursorSize.Width, $cursorSize.Height);
    #    [System.Windows.Forms.Cursors]::Default.Draw($graphic, $cursorBounds);
    # }
    ^
    $FileName = $((Get-Date).ToString('dd-MM-yyyy_HH_mm_ss')+'_region.jpg');^
    $FilePath = Join-Path $Path $FileName;^
    $FormatJPEG = [System.Drawing.Imaging.ImageFormat]::jpeg;^
    $image.Save($FilePath, $FormatJPEG);^
    $graphic.Dispose();^ # 释放绘图对象
    $image.Dispose();^   # 释放位图对象
    Exit 0;^
} Catch {^
    Write-Error "Error capturing screen region: $($_.Exception.Message)";^
    Exit 1;^
}^

# 检查 PowerShell 是否出错
If ErrorLevel 1 (
    echo ERROR: Failed to take screenshot. Check PowerShell errors above.
    REM 可以选择在这里暂停或退出批处理
    Pause
    Exit /B 1
)
Exit /B 0

主要改动:

  1. 在批处理脚本开头添加了 CaptureX, CaptureY, CaptureWidth, CaptureHeight 变量。
  2. 调用 :ScreenShot 时将这些变量作为参数传递 (%1%4)。
  3. PowerShell 代码现在接收这些参数 ($captureX = %1 等)。
  4. 添加了对 WidthHeight 是否大于 0 的基本检查。
  5. $sourcePoint 使用了传入的 $captureX, $captureY
  6. $captureSize 使用了传入的 $captureWidth, $captureHeight
  7. 创建 $image 时使用了 $captureSize
  8. CopyFromScreen 的参数修改为 $sourcePoint, (New-Object System.Drawing.Point(0,0)), $captureSize
  9. 文件名加了 _region 后缀以区分。
  10. 添加了 Try...Catch 块来捕获 PowerShell 内部的错误,并在批处理中检查 ErrorLevel
  11. 重要: 添加了 $graphic.Dispose()$image.Dispose() 来释放 GDI+ 资源,防止内存泄漏,尤其是在循环运行时。

安全与健壮性:

  • 坐标边界: 这个脚本没有检查你提供的坐标和尺寸是否超出了实际屏幕范围。如果指定的区域部分或全部在屏幕外,CopyFromScreen 可能会失败或截取到空白/异常内容。在 PowerShell 代码中可以加入对屏幕边界的检查,但这会使代码更复杂。
  • 资源释放: 已经添加了 .Dispose() 来释放 GraphicsBitmap 对象,这对于长时间运行的脚本非常重要。
  • 错误处理: 简单的 Try...Catch 可以捕获 PowerShell 级别的错误。批处理通过 ErrorLevel 检查捕获结果。

方法二:借助命令行截图工具 (另辟蹊径)

如果不想搞复杂的 PowerShell,可以考虑使用专门的命令行截图工具。有很多第三方工具可以做到这一点,例如 NirCmd (来自 NirSoft)。

原理:

这类工具通常提供了丰富的命令行参数,允许你直接指定截图区域、文件名、格式等。批处理脚本只需要负责调用这个外部工具即可。

工具介绍 (NirCmd):

NirCmd 是一个免费的多功能命令行工具,可以执行很多有用的 Windows 任务,包括截取屏幕区域。你需要先从 NirSoft 网站下载 nircmd.exe 并将其放在系统路径下,或者放在与批处理文件相同的目录下。

命令行指令:

使用 nircmd 截取指定区域的命令通常是:

nircmd.exe savescreenshotregion "文件名" <X> <Y> <Width> <Height>
  • 文件名: 保存截图的完整路径和文件名。
  • <X>, <Y>: 区域左上角的坐标。
  • <Width>, <Height>: 区域的宽度和高度。

代码实战:

下面是使用 nircmd 的批处理脚本示例:

@echo off
Title Get Specific Region ScreenShot with NirCmd
Set CaptureScreenFolder=C:\ScreenCapture\
If Not Exist "%CaptureScreenFolder%" MD "%CaptureScreenFolder%"

REM --- 确认 nircmd.exe 的路径 ---
Set NirCmdPath=nircmd.exe  REM 假设 nircmd.exe 在系统 PATH 或当前目录
REM 如果不在,请指定完整路径,例如: Set NirCmdPath=C:\Tools\nircmd.exe

REM --- 定义截图区域 ---
Set CaptureX=100
Set CaptureY=100
Set CaptureWidth=900
Set CaptureHeight=900
REM ---------------------

Set WaitTimeSeconds=5

:Loop
    REM 生成带时间戳的文件名
    For /f "tokens=1-4 delims=/:." %%a in ("%TIME%") do Set NowTime=%%a%%b%%c%%d
    For /f "tokens=1-3 delims=/-" %%a in ("%DATE%") do Set NowDate=%%a%%b%%c
    REM 注意:日期格式可能因系统区域设置而异,格式可能需要调整
    Set FileName=%CaptureScreenFolder%screenshot_%NowDate%_%NowTime%.png

    echo Taking screenshot region with NirCmd...
    REM 调用 NirCmd 进行区域截图
    "%NirCmdPath%" savescreenshotregion "%FileName%" %CaptureX% %CaptureY% %CaptureWidth% %CaptureHeight%

    REM 检查 NirCmd 是否成功执行 (NirCmd 通常不设置 ErrorLevel, 这里简化处理)
    If Exist "%FileName%" (
        echo Region ScreenShot (%CaptureWidth%x%CaptureHeight% at %CaptureX%,%CaptureY%) saved to "%FileName%"
    ) Else (
        echo ERROR: Failed to save screenshot using NirCmd. Check NirCmd path and parameters.
        Pause
    )

    Timeout /T %WaitTimeSeconds% /NoBreak > nul
Goto Loop

安全建议:

  • 从官方或可信赖的来源下载 nircmd.exe。NirSoft 是一个比较知名的开发者,但任何时候下载可执行文件都要小心。

优缺点:

  • 优点: 批处理脚本大大简化,易于理解和维护。nircmd 可能还支持其他截图选项(如包含/排除鼠标)。
  • 缺点: 增加了一个外部依赖项 (nircmd.exe),需要确保它在脚本运行时可用。

方法三:彻底拥抱 PowerShell (更专业的玩法)

既然核心截图逻辑已经是 PowerShell 了,干脆把整个任务(包括循环和延时)都用 PowerShell 来写,生成一个 .ps1 脚本。这样代码更清晰,错误处理也更强大。

原理:

将批处理的循环、延时、文件路径处理等逻辑都迁移到 PowerShell 脚本内部。使用 PowerShell 的 Start-Sleep cmdlet 代替 Timeout,用 PowerShell 的 while 循环代替批处理的 Goto Loop

代码实战 (CaptureRegion.ps1):

param(
    [Parameter(Mandatory=$true)]
    [int]$CaptureX,

    [Parameter(Mandatory=$true)]
    [int]$CaptureY,

    [Parameter(Mandatory=$true)]
    [int]$CaptureWidth,

    [Parameter(Mandatory=$true)]
    [int]$CaptureHeight,

    [Parameter(Mandatory=$true)]
    [string]$OutputPath,

    [Parameter(Mandatory=$false)]
    [int]$IntervalSeconds = 5
)

# 确保输出目录存在
if (-not (Test-Path -Path $OutputPath -PathType Container)) {
    try {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        Write-Host "Created output directory: $OutputPath"
    } catch {
        Write-Error "Failed to create output directory '$OutputPath'. Error: $($_.Exception.Message)"
        Exit 1
    }
}

# 检查宽度和高度
if ($CaptureWidth -le 0 -or $CaptureHeight -le 0) {
    Write-Error "Capture width and height must be positive."
    Exit 1
}

# 加载必要的 .NET 程序集
try {
    Add-Type -AssemblyName System.Windows.Forms
    Add-Type -AssemblyName System.Drawing
} catch {
    Write-Error "Failed to load .NET Assemblies. Error: $($_.Exception.Message)"
    Exit 1
}

Write-Host "Starting screen capture loop..."
Write-Host "Region: X=$CaptureX, Y=$CaptureY, Width=$CaptureWidth, Height=$CaptureHeight"
Write-Host "Saving to: $OutputPath"
Write-Host "Interval: $IntervalSeconds seconds"
Write-Host "Press Ctrl+C to stop."

while ($true) {
    $startTime = Get-Date
    $image = $null
    $graphic = $null

    try {
        $sourcePoint = New-Object System.Drawing.Point($CaptureX, $CaptureY)
        $captureSize = New-Object System.Drawing.Size($CaptureWidth, $CaptureHeight)

        # 创建位图
        $image = New-Object System.Drawing.Bitmap($captureSize.Width, $captureSize.Height)
        $graphic = [System.Drawing.Graphics]::FromImage($image)

        # 核心截图操作
        $graphic.CopyFromScreen($sourcePoint, (New-Object System.Drawing.Point(0,0)), $captureSize)

        # 生成文件名并保存
        $timeStamp = Get-Date -Format 'dd-MM-yyyy_HH_mm_ss'
        $fileName = "screenshot_region_${timeStamp}.jpg"
        $filePath = Join-Path $OutputPath $fileName
        $formatJPEG = [System.Drawing.Imaging.ImageFormat]::Jpeg
        $image.Save($filePath, $formatJPEG)

        Write-Host ("{0:HH:mm:ss} - Screenshot saved: {1}" -f (Get-Date), $filePath)

    } catch {
        Write-Warning "Error during capture or save: $($_.Exception.Message)"
        # 可以在这里决定是否继续循环
    } finally {
        # 确保资源被释放,即使发生错误
        if ($graphic -ne $null) { $graphic.Dispose() }
        if ($image -ne $null) { $image.Dispose() }
    }

    # 计算下次运行时间,并等待
    $elapsed = (Get-Date) - $startTime
    $waitTime = $IntervalSeconds - $elapsed.TotalSeconds
    if ($waitTime -gt 0) {
        Start-Sleep -Seconds $waitTime
    }
}

如何运行:

  1. 将上面的代码保存为 .ps1 文件,例如 C:\Scripts\CaptureRegion.ps1
  2. 打开 PowerShell 控制台,运行:
    powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\CaptureRegion.ps1 -CaptureX 100 -CaptureY 100 -CaptureWidth 900 -CaptureHeight 900 -OutputPath C:\ScreenCapture -IntervalSeconds 5
    
    或者,如果你已经在 PowerShell 环境中:
    .\CaptureRegion.ps1 -CaptureX 100 -CaptureY 100 -CaptureWidth 900 -CaptureHeight 900 -OutputPath C:\ScreenCapture -IntervalSeconds 5
    

执行策略提醒:

如果你的系统禁止运行 PowerShell 脚本,你可能需要调整执行策略。用管理员权限打开 PowerShell,运行 Set-ExecutionPolicy RemoteSignedSet-ExecutionPolicy Unrestricted (后者安全性较低,请谨慎)。或者像上面例子一样,在运行时临时使用 -ExecutionPolicy Bypass

优缺点:

  • 优点: 代码结构清晰,参数化良好,错误处理更完善,易于扩展(比如添加日志、更复杂的鼠标绘制逻辑等)。完全利用 PowerShell 的能力。
  • 缺点: 不再是原始的批处理文件,需要用户对 PowerShell 有一定了解。如果必须保持为 .bat 文件,此方法不适用。

选择哪种方法取决于你的具体需求、对 PowerShell 的熟悉程度,以及是否介意引入外部工具。对于直接修改原始批处理脚本的需求,方法一是最贴切的。