从 Windows 10 版本 1803 开始, Windows.Graphics.Capture 命名空间提供用于从显示或应用程序窗口获取帧的 API,以创建视频流或快照以生成协作和交互式体验。
通过屏幕捕获,开发者调用安全系统 UI,让最终用户选择要捕获的显示或应用程序窗口,并且系统在正在被捕获的项目周围绘制黄色通知边框。 对于多个同时进行的捕获会话,系统将为正在捕获的每个项绘制黄色边框。
注释
屏幕捕获 API 仅在 Windows 设备和 Windows Mixed Reality 沉浸式头戴显示设备上受支持。
本文介绍如何捕获显示或应用程序窗口的单个图像。 有关将从屏幕捕获的帧编码为视频文件的信息,请参阅 屏幕截图到视频
添加屏幕捕获功能
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 选择要获取屏幕截图的显示窗口或应用程序窗口。 选取器将返回一个 GraphicsCaptureItem,用以创建一个 GraphicsCaptureSession:
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 设备、支持的像素格式(DXGI_FORMAT_B8G8R8A8_UNORM)、所需帧数(可以是任何整数)和帧大小创建 Direct3D11CaptureFramePool。 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 (QueryPerformanceCounter) 时间。
进程捕获帧
调用 TryGetNextFrame时,会签出来自 Direct3D11CaptureFramePool 的每个帧,并根据 Direct3D11CaptureFrame 对象的生存期重新签入。 对于本机应用程序,释放 Direct3D11CaptureFrame 对象足以将帧返回到帧池进行检查。 对于托管应用程序,建议使用 Direct3D11CaptureFrame.Dispose(C++ 中关闭) 方法。 Direct3D11CaptureFrame 实现 IClosable 接口,该接口在 C# 调用中被投影为 IDisposable。
应用程序不应保存对 Direct3D11CaptureFrame 对象的引用,也不应在重新签入帧后保存对基础 Direct3D 图面的引用。
处理帧时,建议应用程序应在与 Direct3D11CaptureFramePool 对象关联的同一设备上获取 ID3D11Multithread 锁。
基础 Direct3D 表面在每次创建(或重新创建)Direct3D11CaptureFramePool时始终保持为指定的大小。 如果内容大于框架,则内容将被剪裁为框架的大小。 如果内容小于帧,则框架的其余部分包含未定义的数据。 建议应用程序使用 ContentSize 属性复制一个子矩形,以避免显示未定义的内容的 Direct3D11CaptureFrame。
拍摄屏幕截图
在我们的示例中,我们将每个 Direct3D11CaptureFrame 转换为 CanvasBitmap,这是 Win2D API 的一部分。
// 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 方法。
调用 “重新创建 ”时,将丢弃所有现有帧。 这是为了防止分发那些其底层 Direct3D 曲面属于应用程序可能再也无法访问的设备的帧。 因此,在调用 重新创建之前,处理所有挂起的帧可能是明智的。
整合所有内容
以下代码片段是如何在 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 进行编程。