教程:批处理结合PowerShell定时截取屏幕区域
2025-04-22 21:09:23
定时截取屏幕指定区域?用批处理和 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 代码:
-
$screen = [System.Windows.Forms.Screen]::PrimaryScreen.Bounds;
这一行获取的是主显示器 的边界信息,包含了整个屏幕的宽度 ($screen.Width
) 和高度 ($screen.Height
)。 -
$image = New-Object System.Drawing.Bitmap($screen.Width, $screen.Height);
接着,它创建了一个新的位图(Bitmap)对象,大小正好是整个屏幕的尺寸。 -
$graphic = [System.Drawing.Graphics]::FromImage($image);
然后,它从这个空白的全屏大小位图创建了一个绘图(Graphics)对象,准备往上画东西。 -
$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 代码。
原理:
- 我们需要定义目标区域的左上角坐标 (X, Y) 和区域的宽度 (Width)、高度 (Height) 。
- 在 PowerShell 代码里,创建位图对象时,使用指定的
Width
和Height
,而不是全屏尺寸。 - 调用
CopyFromScreen
时:- 第一个参数(源位置)设置为我们指定的区域左上角坐标
(X, Y)
。 - 第二个参数(目标位置)仍然是
(0, 0)
,表示将捕获到的区域画在目标位图的左上角。 - 第三个参数(复制大小)设置为我们指定的区域
Width
和Height
。
- 第一个参数(源位置)设置为我们指定的区域左上角坐标
坐标系说明 (重要!):
需要特别注意,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
主要改动:
- 在批处理脚本开头添加了
CaptureX
,CaptureY
,CaptureWidth
,CaptureHeight
变量。 - 调用
:ScreenShot
时将这些变量作为参数传递 (%1
到%4
)。 - PowerShell 代码现在接收这些参数 (
$captureX = %1
等)。 - 添加了对
Width
和Height
是否大于 0 的基本检查。 $sourcePoint
使用了传入的$captureX
,$captureY
。$captureSize
使用了传入的$captureWidth
,$captureHeight
。- 创建
$image
时使用了$captureSize
。 CopyFromScreen
的参数修改为$sourcePoint
,(New-Object System.Drawing.Point(0,0))
,$captureSize
。- 文件名加了
_region
后缀以区分。 - 添加了
Try...Catch
块来捕获 PowerShell 内部的错误,并在批处理中检查ErrorLevel
。 - 重要: 添加了
$graphic.Dispose()
和$image.Dispose()
来释放 GDI+ 资源,防止内存泄漏,尤其是在循环运行时。
安全与健壮性:
- 坐标边界: 这个脚本没有检查你提供的坐标和尺寸是否超出了实际屏幕范围。如果指定的区域部分或全部在屏幕外,
CopyFromScreen
可能会失败或截取到空白/异常内容。在 PowerShell 代码中可以加入对屏幕边界的检查,但这会使代码更复杂。 - 资源释放: 已经添加了
.Dispose()
来释放Graphics
和Bitmap
对象,这对于长时间运行的脚本非常重要。 - 错误处理: 简单的
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
}
}
如何运行:
- 将上面的代码保存为
.ps1
文件,例如C:\Scripts\CaptureRegion.ps1
。 - 打开 PowerShell 控制台,运行:
或者,如果你已经在 PowerShell 环境中:powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\CaptureRegion.ps1 -CaptureX 100 -CaptureY 100 -CaptureWidth 900 -CaptureHeight 900 -OutputPath C:\ScreenCapture -IntervalSeconds 5
.\CaptureRegion.ps1 -CaptureX 100 -CaptureY 100 -CaptureWidth 900 -CaptureHeight 900 -OutputPath C:\ScreenCapture -IntervalSeconds 5
执行策略提醒:
如果你的系统禁止运行 PowerShell 脚本,你可能需要调整执行策略。用管理员权限打开 PowerShell,运行 Set-ExecutionPolicy RemoteSigned
或 Set-ExecutionPolicy Unrestricted
(后者安全性较低,请谨慎)。或者像上面例子一样,在运行时临时使用 -ExecutionPolicy Bypass
。
优缺点:
- 优点: 代码结构清晰,参数化良好,错误处理更完善,易于扩展(比如添加日志、更复杂的鼠标绘制逻辑等)。完全利用 PowerShell 的能力。
- 缺点: 不再是原始的批处理文件,需要用户对 PowerShell 有一定了解。如果必须保持为
.bat
文件,此方法不适用。
选择哪种方法取决于你的具体需求、对 PowerShell 的熟悉程度,以及是否介意引入外部工具。对于直接修改原始批处理脚本的需求,方法一是最贴切的。