PowerShellでスクリーンショットを撮る方法とその応用

PowerShellで画面をPNGへ保存するには、System.DrawingのBitmapとGraphicsを作り、Graphics.CopyFromScreenで画面上の指定範囲をコピーします。複数モニターをまとめて撮影する場合は、VirtualScreenの幅・高さだけでなく、負になることもある開始座標X・Yを使います。この記事では、範囲と保存先の確認、1回の撮影、保存後の検査を順に説明します。

対象は、本人が操作しているWindowsの対話デスクトップです。例はWindows PowerShell 5.1を基準にしています。System.Windows.FormsとSystem.Drawingを使うため、Linux・macOS向けの汎用PowerShellスクリプトではありません。PowerShell 7でもWindows上の必要なアセンブリ・実行環境を確認してください。通常の自分のデスクトップ撮影のために管理者権限へ昇格する必要はありません。

これは画面録画や特定アプリの内部データの取得ではなく、その時点で画面に表示された領域のコピーです。全画面を指定すると他のモニター、通知、メールや個人情報も含まれます。撮影してよい範囲だけを表示し、保存先と共有先を決めてから実行してください。

目次

撮影範囲とDPIを決める

VirtualScreenは複数モニター全体を囲む長方形を返します。プライマリ画面の左や上に別モニターがあれば、XまたはYは負になります。コピー元にはそのLocationを渡し、コピー先のBitmap内では左上を0,0にします。モニター配置が長方形を埋め尽くしていない場合、モニターのない隙間も外接長方形に含まれます。

DPIと表示倍率には別の検証が必要です。PowerShellのホストやプロセス・スレッドのDPI認識、モニターごとの100%・150%などの倍率によって、座標やサイズの見え方が変わる場合があります。この簡易コードはDPI認識を設定せず、混在DPI環境で常に全物理ピクセルを欠けなく撮れると保証するものではありません。

  • Windowsの表示設定で、モニターの配置・解像度・各表示倍率を確認します。
  • 実行するPowerShellの版とホスト(コンソール、ISEなど)を記録します。同じコードでも別ホストで結果が変わる可能性があります。
  • 機密情報を含まない画面で、各モニターの四隅と境界が入るかを試します。保存したPNGの寸法一致だけで、正しく撮影できたとは判定しません。
  • ドッキング、解像度変更、RDP再接続などで表示構成が変わったら、範囲取得からやり直します。

厳密な物理ピクセル単位の撮影が必要なら、DPI認識を起動時から適切に構成した専用プロセスや対応するキャプチャー実装で検証します。起動済みのPowerShellへDPI変更APIを足すだけで、すべてのホストを正しくできるとは限りません。Bitmap.SetResolutionでPNGのDPI値だけを変更しても、コピー元座標のずれは直りません。

保存先が既存かを確認

以下の例はデスクトップを保存先にしますが、実際の絶対パスを撮影前に表示します。デスクトップがOneDriveなどへ同期される設定なら、保存した時点でクラウドへ同期される可能性があります。機密画面では、必要に応じて組織が許可する非共有・非同期の既存フォルダーへ$outputDirectoryを変更してください。

ファイル名は日時とランダム値で衝突を減らし、保存時にはFileMode.CreateNewを使います。Test-Pathによる事前確認だけでは、確認後に同じパスが作られる競合を防げません。CreateNewは既存ファイルがあれば失敗するため、既存の画像を上書きしないことを保存時にも守れます。

System.Drawing型を読み込む

次の3つのコードブロックは、同じWindows PowerShellセッションで順番に使います。まず準備のブロックだけを実行し、表示された範囲と保存先を確認してください。この段階では画面のコピーやファイルの作成をしません。エラーが出たら先へ進まず、その原因を解決します。

$captureSaved = $false
Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
Add-Type -AssemblyName System.Drawing -ErrorAction Stop

$bounds = [System.Windows.Forms.SystemInformation]::VirtualScreen
$outputDirectory = [Environment]::GetFolderPath('Desktop')
if ([string]::IsNullOrWhiteSpace($outputDirectory) -or
    -not [System.IO.Directory]::Exists($outputDirectory)) {
    throw '保存先の既存フォルダーを確認してください。'
}
if ($bounds.Width -le 0 -or $bounds.Height -le 0) {
    throw 'VirtualScreenの寸法が無効です。'
}
$fileName = 'capture-{0}-{1}.png' -f (Get-Date -Format 'yyyyMMdd-HHmmss-fff'), ([guid]::NewGuid().ToString('N'))
$outputPath = [System.IO.Path]::GetFullPath((Join-Path $outputDirectory $fileName))
if (Test-Path -LiteralPath $outputPath -ErrorAction Stop) {
    throw "既存ファイルを上書きしません: $outputPath"
}
[pscustomobject]@{
    X = $bounds.X; Y = $bounds.Y
    Width = $bounds.Width; Height = $bounds.Height
    Output = $outputPath
    PowerShellVersion = $PSVersionTable.PSVersion.ToString()
    HostName = $Host.Name
}

この表示は撮影予定の値です。保存済みを意味せず、表示倍率を自動測定した結果でもありません。各モニターの倍率は先ほど確認した表示設定を別途記録します。Bitmapのメモリー使用量は幅×高さに応じて増えるため、非常に広い仮想画面では必要な1画面に絞ることも検討してください。

CopyFromScreenで画像を作って保存する

範囲と出力先が正しいことを確認してから、次を実行します。確認でCAPTUREと入力すると3秒後に撮影するので、その間に撮りたい画面を前面へ出します。Read-Hostの確認はこの例が用意したもので、CopyFromScreen自体に-WhatIfや確認パラメーターがあるわけではありません。

$captureSaved = $false
if ((Read-Host '撮影範囲と保存先を確認しましたか。撮影する場合はCAPTUREと入力') -cne 'CAPTURE') {
    throw '撮影を中止しました。'
}
Start-Sleep -Seconds 3

$bitmap = $null
$graphics = $null
$stream = $null
try {
    # 既存パスなら、撮影前に失敗する
    $stream = [System.IO.File]::Open(
        $outputPath,
        [System.IO.FileMode]::CreateNew,
        [System.IO.FileAccess]::Write,
        [System.IO.FileShare]::None
    )
    $bitmap = [System.Drawing.Bitmap]::new($bounds.Width, $bounds.Height)
    $graphics = [System.Drawing.Graphics]::FromImage($bitmap)
    $graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
    $bitmap.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
} catch {
    throw
} finally {
    try {
        if ($null -ne $graphics) { $graphics.Dispose() }
    } finally {
        try {
            if ($null -ne $bitmap) { $bitmap.Dispose() }
        } finally {
            if ($null -ne $stream) { $stream.Dispose() }
        }
    }
}
if (-not (Test-Path -LiteralPath $outputPath -PathType Leaf -ErrorAction Stop)) {
    throw 'PNGが保存されませんでした。'
}
$captureSaved = $true

CopyFromScreenのコピー元は$bounds.Location、コピー先はBitmap内の0,0、コピー範囲は$bounds.Sizeです。コピー元を固定の0,0にすると、左や上にあるモニターの領域を取りこぼします。$boundsの値は撮影直前まで同じ構成で使い、途中でモニターの接続や配置を変えたら準備からやり直してください。

CreateNewでファイルを開いた後に撮影や保存が失敗すると、空または不完全なPNGが残る場合があります。例外を隠して成功扱いせず、$captureSavedがFalseのままなら検査や共有へ進みません。残ったファイルは内容・パスを確認して保管規程に従い扱い、次回は準備から新しいファイル名を作ります。例では自動削除を行いません。

PNG保存後に寸法を再取得

保存できたら、別のImageオブジェクトでPNGを開き直し、幅と高さを取得します。これでPNGとして読み取れることと、指定したBitmapサイズで保存されたことを確認できます。再読込用のImageもファイルを保持するため、finallyで必ず破棄します。

if (-not $captureSaved) {
    throw '撮影・保存が完了していません。前のエラーを確認してください。'
}
$image = $null
try {
    $image = [System.Drawing.Image]::FromFile($outputPath)
    $actualWidth = $image.Width
    $actualHeight = $image.Height
} finally {
    if ($null -ne $image) { $image.Dispose() }
}
if ($actualWidth -ne $bounds.Width -or $actualHeight -ne $bounds.Height) {
    throw ("保存寸法が不一致です: {0}x{1}" -f $actualWidth, $actualHeight)
}
$hash = Get-FileHash -LiteralPath $outputPath -Algorithm SHA256 -ErrorAction Stop
[pscustomobject]@{
    Path = $outputPath
    Width = $actualWidth; Height = $actualHeight
    ExpectedWidth = $bounds.Width; ExpectedHeight = $bounds.Height
    SourceX = $bounds.X; SourceY = $bounds.Y
    Sha256 = $hash.Hash
    ExactDimensions = $true
    VisualReview = '未確認。画像を開いて範囲と機密情報を確認してください。'
}

ExactDimensionsがTrueでも、撮影内容の正しさは未確認です。Bitmapを$boundsのサイズで作っているので、黒い画像やDPIのずれで欠けた画像でも寸法は一致し得ます。SHA256も、そのファイルの同一性を後で比較するための値であり、画面が正しく写った証明ではありません。最後にPNGを開き、四隅、画面境界、文字、必要範囲と写り込みを目視確認します。

複数モニターと負の座標

例として、プライマリ画面が1920×1080で、その左に同じサイズのモニターがある場合、倍率などの条件が一致していればVirtualScreenはX=-1920、Y=0、Width=3840、Height=1080になります。コピー元の-1920,0をBitmapの0,0へ写すため、左側の画面も含まれます。上に配置したモニターならYが負になる場合もあります。

画面一覧を確認する

次は各画面の識別名、プライマリかどうか、境界を読み取るだけの例です。名前の順番だけで「2番目が目的のモニター」と決めず、表示設定の配置とBoundsを照合してください。再接続によって識別名や並びが変わることがあります。

[System.Windows.Forms.Screen]::AllScreens |
    Select-Object DeviceName, Primary, Bounds

プライマリ画面だけを撮影する

全画面が不要なら、準備ブロックの$boundsへの代入を次に置き換えます。これは範囲選択の代替であり、全画面撮影に続けて追加実行する手順ではありません。範囲を選んだ後は、同じ寸法確認・保存処理を使います。

$primary = [System.Windows.Forms.Screen]::PrimaryScreen
if ($null -eq $primary) { throw 'プライマリ画面を取得できませんでした。' }
$bounds = $primary.Bounds

特定のモニターを選ぶ

一覧で目的のDeviceNameを確認できた場合は、その名前で1件に限定します。以下のDISPLAY2は例なので、実際に表示された名前へ置き換えます。選択後は準備ブロックの保存先・サイズ確認へ進み、範囲を変えたまま古い出力パスへ再保存しないでください。

$deviceName = '\\.\DISPLAY2'  # 一覧で確認した名前に置き換える
$selected = @([System.Windows.Forms.Screen]::AllScreens |
    Where-Object { $_.DeviceName -eq $deviceName })
if ($selected.Count -ne 1) { throw '対象モニターを1件に特定できませんでした。' }
$bounds = $selected[0].Bounds

WorkingAreaはタスクバーなどを除いた作業領域です。画面全体を撮る目的ではBoundsを選びます。どの範囲を使っても、混在DPIの制約は残るため、対象の四隅まで入ることを最初に確認してください。

GraphicsとBitmapを必ず破棄

GraphicsとBitmapはネイティブの描画リソースを使用します。撮影を繰り返す処理でDisposeを省くと、メモリーやGDIリソースの消費が積み重なる可能性があります。$nullを代入するだけでは即座に解放されるとは限りません。正常時だけでなく、コピー・PNG保存・読込に失敗したときにもfinallyを通して解放します。

例ではGraphics、Bitmap、書込みストリームをそれぞれ破棄し、その後でPNGを読み直します。確認用Imageも破棄するため、検査後にファイルを開いたまま保持しません。既存の別処理が所有するオブジェクトではなく、このコード自身が作ったオブジェクトだけを解放します。

撮れない・欠ける・黒くなる場合の切り分け

症状確認すること
Add-Typeで型を読み込めないWindows環境か、PowerShellの版とアセンブリが対応しているか。ポリシーを無断で迂回しない。
保存先エラー・GDI+の保存エラー既存フォルダー、書込み権限、空き容量、同名ファイル、他プロセスの使用状況を確認する。
左や上のモニターが欠けるコピー元が0,0固定になっていないか、VirtualScreenの負のX・Yを保っているかを確認する。
倍率が違うモニターでずれるホストのDPI認識と各画面の表示倍率を確認する。寸法一致だけを成功条件にしない。
ファイルはできたが黒い・内容が想定と違うセッション状態、ロック、RDP接続状態、保護された映像などを切り分ける。画像の存在だけで成功としない。
メモリー不足や大きすぎる画像外接長方形の面積を確認し、必要なモニターだけへ範囲を狭める。

CopyFromScreenは画面コピーの失敗時に例外を返すことがあります。RDPの切断・最小化、ロック中、サービスや無人タスクなどの状態では、通常の対話デスクトップと同じ結果を期待できません。黒い画像や失敗を権限不足と決めつけて管理者化せず、本人が操作している通常セッションで機密情報を含まない画面から確認します。

この方法は、覆われたウィンドウや最小化したアプリの内部画面を必ず取得できるものではありません。ロック画面、UACのセキュアデスクトップ、保護コンテンツを回避して撮る用途にも使いません。エラー本文、保存先、実行ホスト、範囲、表示構成を残すと、撮影範囲・保存・セッションのどこで失敗したかを切り分けやすくなります。

機密画面を写さない運用

  • 全モニターを撮る前に、メール、チャット通知、認証情報、個人情報が表示されていないか確認します。可能なら必要な1モニターへ限定します。
  • 撮影は本人や関係者の同意と組織のルールに従います。定期撮影へ拡張する場合は、対象、頻度、保存期間、閲覧権限を明確にします。
  • 共有前にPNGを開いて確認し、誤撮影ファイルは共有しません。すでに共有した場合は、保管規程に従って共有先や管理者へ対応を相談します。
  • この例は既存ファイルやWindowsの表示設定を変更しませんが、撮影したPNGは新たに残ります。スクリプトを止めても保存済みのファイルや同期済みのコピーが自動で消えるわけではありません。

自動撮影を追加するときも、最初は同じユーザー・同じ対話セッションで1回だけ検証します。無人で動かすために確認を削るだけでは、表示状態やDPIの問題は解消しません。成功した保存、撮影前の中止、コピー失敗、保存失敗、目視未確認を分け、正常なPNGの存在を業務画面の正常性と取り違えないでください。

完了条件:意図した新規PNGへ保存できること、別Imageで指定寸法を読み取れること、実際に開いて必要な画面の四隅と内容を確認できること、不要な機密情報がないことをそろえて確認します。DPIやセッションの条件が変わったら、この確認をやり直します。

公式情報・参考資料

確認日:2026年10月20日。座標、リソース解放、Windows対応、保存方式、DPIの制約をMicrosoftの一次資料で確認しました。OS・PowerShell・ホストの更新後は、使用環境で改めて検証してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次