螢幕擷取
從 Windows 10 版本 1803 開始,Windows.Graphics.Capture 命名空間提供 API 來從顯示器或應用程式視窗取得畫面、建立視訊串流或快照以建立協作和互動體驗。
使用螢幕擷取時,開發人員會叫用安全的系統 UI,讓使用者挑選要擷取的顯示或應用程式視窗,而系統會在主動擷取的專案周圍繪製黃色通知框線。 在多個同時擷取工作階段的情況下,會在每個正在擷取的專案周圍繪製黃色框線。
注意
只有 Windows 裝置和 Windows Mixed Reality 沉浸式頭戴裝置才支援螢幕擷取 API。
本文說明擷取顯示或應用程式視窗的單一影像。 如需從畫面擷取到視訊檔案之編碼畫面的資訊,請參閱螢幕擷取至影片
新增螢幕擷取功能
在 Windows.Graphics.Capture 命名空間中找到的 API 需要在應用程式的資訊清單中宣告一般功能:
- 在方案總管中,開啟 Package.appxmanifest。
- 選取 [功能] 索引標籤。
- 檢查圖形擷取。
啟動系統 UI 以啟動螢幕擷取
啟動系統 UI 之前,您可以檢查您的應用程式目前是否能夠擷取螢幕快照。 您的應用程式可能無法使用螢幕擷取有幾個原因,包括裝置是否符合硬體需求,或針對擷取封鎖螢幕擷取的目標應用程式。 使用 GraphicsCaptureSession 類別中的 IsSupported 方法來判斷是否支援 UWP 螢幕擷取:
// This runs when the application starts.
public void OnInitialization()
{
if (!GraphicsCaptureSession.IsSupported())
{
// Hide the capture UI if screen capture is not supported.
CaptureButton.Visibility = Visibility.Collapsed;
}
}
Public Sub OnInitialization()
If Not GraphicsCaptureSession.IsSupported Then
CaptureButton.Visibility = Visibility.Collapsed
End If
End Sub
確認支援螢幕擷取之後,請使用 GraphicsCapturePicker 類別來叫用系統選擇器 UI。 使用者使用此 UI 來選取要擷取螢幕快照的顯示或應用程式視窗。 選擇器會傳回用來建立 GraphicsCaptureSession 的 GraphicsCaptureItem:
public async Task StartCaptureAsync()
{
// The GraphicsCapturePicker follows the same pattern the
// file pickers do.
var picker = new GraphicsCapturePicker();
GraphicsCaptureItem item = await picker.PickSingleItemAsync();
// The item may be null if the user dismissed the
// control without making a selection or hit Cancel.
if (item != null)
{
// We'll define this method later in the document.
StartCaptureInternal(item);
}
}
Public Async Function StartCaptureAsync() As Task
' The GraphicsCapturePicker follows the same pattern the
' file pickers do.
Dim picker As New GraphicsCapturePicker
Dim item As GraphicsCaptureItem = Await picker.PickSingleItemAsync()
' The item may be null if the user dismissed the
' control without making a selection or hit Cancel.
If item IsNot Nothing Then
StartCaptureInternal(item)
End If
End Function
因為這是 UI 程式碼,所以必須在 UI 執行緒上呼叫它。 如果您要從應用程式頁面的程式碼後置呼叫它 (例如 MainPage.xaml.cs),這會自動為您完成,但如果不是,您可以強制它以下列程式碼在 UI 執行緒上執行:
CoreWindow window = CoreApplication.MainView.CoreWindow;
await window.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
await StartCaptureAsync();
});
Dim window As CoreWindow = CoreApplication.MainView.CoreWindow
Await window.Dispatcher.RunAsync(CoreDispatcherPriority.Normal,
Async Sub() Await StartCaptureAsync())
建立擷取畫面集區和擷取工作階段
使用 GraphicsCaptureItem,您將使用您的 D3D 裝置建立 Direct3D11CaptureFramePool、支援的像素格式(DXGI_FORMAT_B8G8R8A8_UNORM)、所需的畫面數目 (可以是任何整數),以及畫面大小。 GraphicsCaptureItem 類別的 ContentSize屬性可用來做為畫面的大小:
注意
在啟用 Windows HD 色彩的系統上,內容像素格式不一定是 DXGI_FORMAT_B8G8R8A8_UNORM。 若要避免擷取 HDR 內容時像素過度剪裁 (亦即擷取的內容看起來已清除),請考慮針對擷取管線中的每個元件使用 DXGI_FORMAT_R16G16B16A16_FLOAT,包括 Direct3D11CaptureFramePool,例如 CanvasBitmap 的目標目的地。 視需要而定,可能需要其他處理,例如儲存至 HDR 內容格式或 HDR 到 SDR 音調對應。 本文將著重於 SDR 內容擷取。 有關詳細資訊,請參閱將 DirectX 與高動態範圍顯示器和進階色彩配合使用。
private GraphicsCaptureItem _item;
private Direct3D11CaptureFramePool _framePool;
private CanvasDevice _canvasDevice;
private GraphicsCaptureSession _session;
public void StartCaptureInternal(GraphicsCaptureItem item)
{
_item = item;
_framePool = Direct3D11CaptureFramePool.Create(
_canvasDevice, // D3D device
DirectXPixelFormat.B8G8R8A8UIntNormalized, // Pixel format
2, // Number of frames
_item.Size); // Size of the buffers
}
WithEvents CaptureItem As GraphicsCaptureItem
WithEvents FramePool As Direct3D11CaptureFramePool
Private _canvasDevice As CanvasDevice
Private _session As GraphicsCaptureSession
Private Sub StartCaptureInternal(item As GraphicsCaptureItem)
CaptureItem = item
FramePool = Direct3D11CaptureFramePool.Create(
_canvasDevice, ' D3D device
DirectXPixelFormat.B8G8R8A8UIntNormalized, ' Pixel format
2, ' Number of frames
CaptureItem.Size) ' Size of the buffers
End Sub
接下來,將 GraphicsCaptureItem 傳遞至 CreateCaptureSession 方法,以取得 Direct3D11CaptureFramePool 的 GraphicsCaptureSession 類別執行個體:
_session = _framePool.CreateCaptureSession(_item);
_session = FramePool.CreateCaptureSession(CaptureItem)
一旦使用者明確同意擷取應用程式視窗或顯示在系統 UI 中,GraphicsCaptureItem 就可以與多個 CaptureSession 物件相關聯。 如此一來,您的應用程式可以選擇針對各種體驗擷取相同的專案。
若要同時擷取多個專案,您的應用程式必須為每個要擷取的專案建立擷取工作階段,這需要針對要擷取的每個專案叫用選擇器 UI。
取得擷取畫面
建立畫面集區和擷取工作階段之後,請在 GraphicsCaptureSession 執行個體上呼叫 StartCapture 方法,以通知系統開始將擷取畫面傳送至您的應用程式:
_session.StartCapture();
_session.StartCapture()
若要取得這些擷取畫面,也就是 Direct3D11CaptureFrame 物件,您可以使用 Direct3D11CaptureFramePool.FrameArrived 事件:
_framePool.FrameArrived += (s, a) =>
{
// The FrameArrived event fires for every frame on the thread that
// created the Direct3D11CaptureFramePool. This means we don't have to
// do a null-check here, as we know we're the only one
// dequeueing frames in our application.
// NOTE: Disposing the frame retires it and returns
// the buffer to the pool.
using (var frame = _framePool.TryGetNextFrame())
{
// We'll define this method later in the document.
ProcessFrame(frame);
}
};
Private Sub FramePool_FrameArrived(sender As Direct3D11CaptureFramePool, args As Object) Handles FramePool.FrameArrived
' The FrameArrived event is raised for every frame on the thread
' that created the Direct3D11CaptureFramePool. This means we
' don't have to do a null-check here, as we know we're the only
' one dequeueing frames in our application.
' NOTE Disposing the frame retires it And returns
' the buffer to the pool.
Using frame = FramePool.TryGetNextFrame()
ProcessFrame(frame)
End Using
End Sub
如果 FrameArrived 可能的話,建議您避免使用 UI 執行緒,因為每次有新的畫面可用時都會引發此事件,這將會很頻繁。 如果您選擇在 UI 執行緒上接聽 FrameArrived,請留意每次引發事件時要執行的工作。
或者,您可以使用 Direct3D11CaptureFramePool.TryGetNextFrame 方法手動提取畫面,直到取得您需要的所有畫面。
Direct3D11CaptureFrame 物件包含 ContentSize、Surface 和 SystemRelativeTime 屬性。 SystemRelativeTime 是 QPC (QQueryPerformanceCounter) 時間,可用來同步處理其他媒體元素。
處理擷取畫面
呼叫 TryGetNextFrame 時,會取出 Direct3D11CaptureFramePool 中的每個畫面,並根據 Direct3D11CaptureFrame 物件的存留期重新簽入。 針對原生應用程式,釋放 Direct3D11CaptureFrame 物件就足以檢查畫面集區中的畫面。 針對受控應用程式,建議使用 Direct3D11CaptureFrame.Dispose (C++中的 Close) 方法。 Direct3D11CaptureFrame 會實作 IClosable 介面,其投影為 C# 呼叫端的 IDisposable。
應用程式不應該儲存 Direct3D11CaptureFrame 使用者的參考,也不應該在簽回畫面之後儲存基礎 Direct3D 介面的參考。
處理畫面時,建議應用程式在與 Direct3D11CaptureFramePool 物件相關聯的相同裝置上取得 ID3D11Multithread 鎖定。
基礎 Direct3D 表面一律是建立 Direct3D11CaptureFramePool 時所指定的大小。 如果內容大於畫面,則會將內容裁剪為畫面的大小。 如果內容小於畫面,則畫面的其餘部分會包含未定義的資料。 建議應用程式使用 Direct3D11CaptureFrame 的 ContentSize 屬性來複製子內容,以避免顯示未定義的內容。
拍攝螢幕擷取畫面
在我們的範例中,我們會將每個 Direct3D11CaptureFrame 轉換成 CanvasBitmap,這是 Win2D APIs 的一部分。
// Convert our D3D11 surface into a Win2D object.
CanvasBitmap canvasBitmap = CanvasBitmap.CreateFromDirect3D11Surface(
_canvasDevice,
frame.Surface);
一旦我們有 CanvasBitmap,就可以將它儲存為影像檔。 在下列範例中,我們會將它儲存為使用者 [已儲存的圖片] 資料夾中的 PNG 檔案。
StorageFolder pictureFolder = KnownFolders.SavedPictures;
StorageFile file = await pictureFolder.CreateFileAsync("test.png", CreationCollisionOption.ReplaceExisting);
using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite))
{
await canvasBitmap.SaveAsync(fileStream, CanvasBitmapFileFormat.Png, 1f);
}
回應擷取專案重設大小或裝置遺失
在擷取過程中,應用程式可能會想要變更其 Direct3D11CaptureFramePool 的各個層面。 這包括提供新的 Direct3D 裝置、變更畫面緩衝區的大小,或甚至變更集區內的緩衝區數目。 在這些案例中,Direct3D11CaptureFramePool 物件上的 Recreate 方法是建議的工具。
呼叫 Recreate 時,會捨棄所有現有的畫面。 這是為了避免將基礎 Direct3D 表面所屬的畫面傳送到應用程式可能無法再存取的裝置。 基於這個理由,在呼叫 Recreate 之前,處理所有擱置的畫面可能是明智的。
融會貫通
下列程式碼片段是如何在UWP應用程式中實作螢幕擷取的端對端範例。 在此範例中,前端有兩個按鈕:一個呼叫 Button_ClickAsync,另一個呼叫 ScreenshotButton_ClickAsync。
注意
此程式碼片段使用 Win2D,這是 2D 圖形轉譯的程式庫。 如需如何為您的項目設定的相關資訊,請參閱其檔。
using Microsoft.Graphics.Canvas;
using Microsoft.Graphics.Canvas.UI.Composition;
using System;
using System.Numerics;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Graphics;
using Windows.Graphics.Capture;
using Windows.Graphics.DirectX;
using Windows.Storage;
using Windows.UI;
using Windows.UI.Composition;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Hosting;
namespace ScreenCaptureTest
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
// Capture API objects.
private SizeInt32 _lastSize;
private GraphicsCaptureItem _item;
private Direct3D11CaptureFramePool _framePool;
private GraphicsCaptureSession _session;
// Non-API related members.
private CanvasDevice _canvasDevice;
private CompositionGraphicsDevice _compositionGraphicsDevice;
private Compositor _compositor;
private CompositionDrawingSurface _surface;
private CanvasBitmap _currentFrame;
private string _screenshotFilename = "test.png";
public MainPage()
{
this.InitializeComponent();
Setup();
}
private void Setup()
{
_canvasDevice = new CanvasDevice();
_compositionGraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(
Window.Current.Compositor,
_canvasDevice);
_compositor = Window.Current.Compositor;
_surface = _compositionGraphicsDevice.CreateDrawingSurface(
new Size(400, 400),
DirectXPixelFormat.B8G8R8A8UIntNormalized,
DirectXAlphaMode.Premultiplied); // This is the only value that currently works with
// the composition APIs.
var visual = _compositor.CreateSpriteVisual();
visual.RelativeSizeAdjustment = Vector2.One;
var brush = _compositor.CreateSurfaceBrush(_surface);
brush.HorizontalAlignmentRatio = 0.5f;
brush.VerticalAlignmentRatio = 0.5f;
brush.Stretch = CompositionStretch.Uniform;
visual.Brush = brush;
ElementCompositionPreview.SetElementChildVisual(this, visual);
}
public async Task StartCaptureAsync()
{
// The GraphicsCapturePicker follows the same pattern the
// file pickers do.
var picker = new GraphicsCapturePicker();
GraphicsCaptureItem item = await picker.PickSingleItemAsync();
// The item may be null if the user dismissed the
// control without making a selection or hit Cancel.
if (item != null)
{
StartCaptureInternal(item);
}
}
private void StartCaptureInternal(GraphicsCaptureItem item)
{
// Stop the previous capture if we had one.
StopCapture();
_item = item;
_lastSize = _item.Size;
_framePool = Direct3D11CaptureFramePool.Create(
_canvasDevice, // D3D device
DirectXPixelFormat.B8G8R8A8UIntNormalized, // Pixel format
2, // Number of frames
_item.Size); // Size of the buffers
_framePool.FrameArrived += (s, a) =>
{
// The FrameArrived event is raised for every frame on the thread
// that created the Direct3D11CaptureFramePool. This means we
// don't have to do a null-check here, as we know we're the only
// one dequeueing frames in our application.
// NOTE: Disposing the frame retires it and returns
// the buffer to the pool.
using (var frame = _framePool.TryGetNextFrame())
{
ProcessFrame(frame);
}
};
_item.Closed += (s, a) =>
{
StopCapture();
};
_session = _framePool.CreateCaptureSession(_item);
_session.StartCapture();
}
public void StopCapture()
{
_session?.Dispose();
_framePool?.Dispose();
_item = null;
_session = null;
_framePool = null;
}
private void ProcessFrame(Direct3D11CaptureFrame frame)
{
// Resize and device-lost leverage the same function on the
// Direct3D11CaptureFramePool. Refactoring it this way avoids
// throwing in the catch block below (device creation could always
// fail) along with ensuring that resize completes successfully and
// isn’t vulnerable to device-lost.
bool needsReset = false;
bool recreateDevice = false;
if ((frame.ContentSize.Width != _lastSize.Width) ||
(frame.ContentSize.Height != _lastSize.Height))
{
needsReset = true;
_lastSize = frame.ContentSize;
}
try
{
// Take the D3D11 surface and draw it into a
// Composition surface.
// Convert our D3D11 surface into a Win2D object.
CanvasBitmap canvasBitmap = CanvasBitmap.CreateFromDirect3D11Surface(
_canvasDevice,
frame.Surface);
_currentFrame = canvasBitmap;
// Helper that handles the drawing for us.
FillSurfaceWithBitmap(canvasBitmap);
}
// This is the device-lost convention for Win2D.
catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
{
// We lost our graphics device. Recreate it and reset
// our Direct3D11CaptureFramePool.
needsReset = true;
recreateDevice = true;
}
if (needsReset)
{
ResetFramePool(frame.ContentSize, recreateDevice);
}
}
private void FillSurfaceWithBitmap(CanvasBitmap canvasBitmap)
{
CanvasComposition.Resize(_surface, canvasBitmap.Size);
using (var session = CanvasComposition.CreateDrawingSession(_surface))
{
session.Clear(Colors.Transparent);
session.DrawImage(canvasBitmap);
}
}
private void ResetFramePool(SizeInt32 size, bool recreateDevice)
{
do
{
try
{
if (recreateDevice)
{
_canvasDevice = new CanvasDevice();
}
_framePool.Recreate(
_canvasDevice,
DirectXPixelFormat.B8G8R8A8UIntNormalized,
2,
size);
}
// This is the device-lost convention for Win2D.
catch (Exception e) when (_canvasDevice.IsDeviceLost(e.HResult))
{
_canvasDevice = null;
recreateDevice = true;
}
} while (_canvasDevice == null);
}
private async void Button_ClickAsync(object sender, RoutedEventArgs e)
{
await StartCaptureAsync();
}
private async void ScreenshotButton_ClickAsync(object sender, RoutedEventArgs e)
{
await SaveImageAsync(_screenshotFilename, _currentFrame);
}
private async Task SaveImageAsync(string filename, CanvasBitmap frame)
{
StorageFolder pictureFolder = KnownFolders.SavedPictures;
StorageFile file = await pictureFolder.CreateFileAsync(
filename,
CreationCollisionOption.ReplaceExisting);
using (var fileStream = await file.OpenAsync(FileAccessMode.ReadWrite))
{
await frame.SaveAsync(fileStream, CanvasBitmapFileFormat.Png, 1f);
}
}
}
}
Imports System.Numerics
Imports Microsoft.Graphics.Canvas
Imports Microsoft.Graphics.Canvas.UI.Composition
Imports Windows.Graphics
Imports Windows.Graphics.Capture
Imports Windows.Graphics.DirectX
Imports Windows.UI
Imports Windows.UI.Composition
Imports Windows.UI.Xaml.Hosting
Partial Public NotInheritable Class MainPage
Inherits Page
' Capture API objects.
WithEvents CaptureItem As GraphicsCaptureItem
WithEvents FramePool As Direct3D11CaptureFramePool
Private _lastSize As SizeInt32
Private _session As GraphicsCaptureSession
' Non-API related members.
Private _canvasDevice As CanvasDevice
Private _compositionGraphicsDevice As CompositionGraphicsDevice
Private _compositor As Compositor
Private _surface As CompositionDrawingSurface
Sub New()
InitializeComponent()
Setup()
End Sub
Private Sub Setup()
_canvasDevice = New CanvasDevice()
_compositionGraphicsDevice = CanvasComposition.CreateCompositionGraphicsDevice(Window.Current.Compositor, _canvasDevice)
_compositor = Window.Current.Compositor
_surface = _compositionGraphicsDevice.CreateDrawingSurface(
New Size(400, 400), DirectXPixelFormat.B8G8R8A8UIntNormalized, DirectXAlphaMode.Premultiplied)
Dim visual = _compositor.CreateSpriteVisual()
visual.RelativeSizeAdjustment = Vector2.One
Dim brush = _compositor.CreateSurfaceBrush(_surface)
brush.HorizontalAlignmentRatio = 0.5F
brush.VerticalAlignmentRatio = 0.5F
brush.Stretch = CompositionStretch.Uniform
visual.Brush = brush
ElementCompositionPreview.SetElementChildVisual(Me, visual)
End Sub
Public Async Function StartCaptureAsync() As Task
' The GraphicsCapturePicker follows the same pattern the
' file pickers do.
Dim picker As New GraphicsCapturePicker
Dim item As GraphicsCaptureItem = Await picker.PickSingleItemAsync()
' The item may be null if the user dismissed the
' control without making a selection or hit Cancel.
If item IsNot Nothing Then
StartCaptureInternal(item)
End If
End Function
Private Sub StartCaptureInternal(item As GraphicsCaptureItem)
' Stop the previous capture if we had one.
StopCapture()
CaptureItem = item
_lastSize = CaptureItem.Size
FramePool = Direct3D11CaptureFramePool.Create(
_canvasDevice, ' D3D device
DirectXPixelFormat.B8G8R8A8UIntNormalized, ' Pixel format
2, ' Number of frames
CaptureItem.Size) ' Size of the buffers
_session = FramePool.CreateCaptureSession(CaptureItem)
_session.StartCapture()
End Sub
Private Sub FramePool_FrameArrived(sender As Direct3D11CaptureFramePool, args As Object) Handles FramePool.FrameArrived
' The FrameArrived event is raised for every frame on the thread
' that created the Direct3D11CaptureFramePool. This means we
' don't have to do a null-check here, as we know we're the only
' one dequeueing frames in our application.
' NOTE Disposing the frame retires it And returns
' the buffer to the pool.
Using frame = FramePool.TryGetNextFrame()
ProcessFrame(frame)
End Using
End Sub
Private Sub CaptureItem_Closed(sender As GraphicsCaptureItem, args As Object) Handles CaptureItem.Closed
StopCapture()
End Sub
Public Sub StopCapture()
_session?.Dispose()
FramePool?.Dispose()
CaptureItem = Nothing
_session = Nothing
FramePool = Nothing
End Sub
Private Sub ProcessFrame(frame As Direct3D11CaptureFrame)
' Resize and device-lost leverage the same function on the
' Direct3D11CaptureFramePool. Refactoring it this way avoids
' throwing in the catch block below (device creation could always
' fail) along with ensuring that resize completes successfully And
' isn't vulnerable to device-lost.
Dim needsReset As Boolean = False
Dim recreateDevice As Boolean = False
If (frame.ContentSize.Width <> _lastSize.Width) OrElse
(frame.ContentSize.Height <> _lastSize.Height) Then
needsReset = True
_lastSize = frame.ContentSize
End If
Try
' Take the D3D11 surface and draw it into a
' Composition surface.
' Convert our D3D11 surface into a Win2D object.
Dim bitmap = CanvasBitmap.CreateFromDirect3D11Surface(
_canvasDevice,
frame.Surface)
' Helper that handles the drawing for us.
FillSurfaceWithBitmap(bitmap)
' This is the device-lost convention for Win2D.
Catch e As Exception When _canvasDevice.IsDeviceLost(e.HResult)
' We lost our graphics device. Recreate it and reset
' our Direct3D11CaptureFramePool.
needsReset = True
recreateDevice = True
End Try
If needsReset Then
ResetFramePool(frame.ContentSize, recreateDevice)
End If
End Sub
Private Sub FillSurfaceWithBitmap(canvasBitmap As CanvasBitmap)
CanvasComposition.Resize(_surface, canvasBitmap.Size)
Using session = CanvasComposition.CreateDrawingSession(_surface)
session.Clear(Colors.Transparent)
session.DrawImage(canvasBitmap)
End Using
End Sub
Private Sub ResetFramePool(size As SizeInt32, recreateDevice As Boolean)
Do
Try
If recreateDevice Then
_canvasDevice = New CanvasDevice()
End If
FramePool.Recreate(_canvasDevice, DirectXPixelFormat.B8G8R8A8UIntNormalized, 2, size)
' This is the device-lost convention for Win2D.
Catch e As Exception When _canvasDevice.IsDeviceLost(e.HResult)
_canvasDevice = Nothing
recreateDevice = True
End Try
Loop While _canvasDevice Is Nothing
End Sub
Private Async Sub Button_ClickAsync(sender As Object, e As RoutedEventArgs) Handles CaptureButton.Click
Await StartCaptureAsync()
End Sub
End Class
錄製影片
如果您想要錄製應用程式的影片,您可以遵循螢幕擷取至影片一文中所示的逐步解說。 或者,您可以使用 Windows.Media.AppRecording 命名空間。 這是桌面延伸模組 SDK 的一部分,因此它只能在 Windows 桌面上運作,而且需要從專案新增參考。 有關更多資訊,請參閱使用延伸模組 SDK 進行程式設計。