使用 CefSharp 从 Web 浏览器/网页截取屏幕截图时,异步等待与 Task 同步的事件/操作
Wait asynchronously for a synchronous event/ operation with Task while taking a screenshot from the web browser/ web page using CefSharp
我想做的是:
我有一个 CefSharp ChromiumWebBrowser
(WPF 控件),我想截取该浏览器中的网页。屏幕上ChromiumWebBrowser
没有截屏方法。但是我可以通过将事件处理程序附加到浏览器的 OnPaint
事件来获得渲染。
这样我就得到了一个作为屏幕截图的位图。该过程基于此答案:
现在我正在创建一个class CefSharpScreenshotRecorder
,它应该负责截屏。它应该接受浏览器实例,将事件处理程序附加到 OnPaint
事件,并获取位图。这个过程的所有状态都应该封装在CefSharpScreenshotRecorder
class中。
我希望能够异步使用我的 class。因为我们必须等到 OnPaint 事件被触发。当触发该事件(并调用事件处理程序)时,事件处理程序中将提供一个位图。那么这个Bitmap应该是原来调用的异步方法的结果(比如CefSharpScreenshotRecorder.TakeScreenshot(...cefBrowserInstance...)
。一切都必须在没有blocking/lagging当然UI的情况下发生。
我对 C# 中的异步编程不是很熟悉。
我遇到的问题是我找不到制作可等待方法的方法,该方法在调用时仅代表 OnPaint 事件处理程序 returns 。
我什至不知道是否存在任何代码功能来创建此逻辑。
这可以使用 TaskCompletionSource
来实现。通过这种方式,您可以将同步(例如事件驱动)代码包装到异步方法中,而无需使用 Task.Run
.
class CefSharpScreenshotRecorder
{
private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }
public Task<System.Drawing.Bitmap> TakeScreenshotAsync(
ChromiumWebBrowser browserInstance,
TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
{
this.TaskCompletionSource = new TaskCompletionSource<System.Drawing.Bitmap>(optionalTaskCreationOptions);
browserInstance.Paint += GetScreenShotOnPaint;
// Return Task instance to make this method awaitable
return this.TaskCompletionSource.Task;
}
private void GetScreenShotOnPaint(object sender, PaintEventArgs e)
{
(sender as ChromiumWebBrowser).Paint -= GetScreenShotOnPaint;
System.Drawing.Bitmap newBitmap = new Bitmap(e.Width, e.Height, 4 * e.Width, PixelFormat.Format32bppPArgb, e.Buffer);
// Optional: save the screenshot to the hard disk "MyPictures" folder
var screenshotDestinationPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"CefSharpBrowserScreenshot.png");
newBitmap.Save(screenshotDestinationPath);
// Create a copy of the bitmap, since the underlying buffer is reused by the library internals
var bitmapCopy = new System.Drawing.Bitmap(newBitmap);
// Set the Task.Status of the Task instance to 'RanToCompletion'
// and return the result to the caller
this.TaskCompletionSource.SetResult(bitmapCopy);
}
public BitmapImage ConvertToBitmapImage(System.Drawing.Bitmap bitmap)
{
using(var memoryStream = new MemoryStream())
{
bitmap.Save(memoryStream, ImageFormat.Png);
memoryStream.Position = 0;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
}
}
}
用法示例(有效):
MainWindow.xaml
<Window>
<StackPanel>
<Button Click="TakeScreenshot_OnClick" Height="50" Content="Take Screenshot"/>
<ChromiumWebBrowser x:Name="ChromiumWebBrowser"
Width="500"
Height="500"
Address="" />
<Image x:Name="ScreenshotImage" />
</StackPanel>
</Window>
MainWindow.xaml.cs
private async void TakeScreenshot_OnClick(object sender, RoutedEventArgs e)
{
var cefSharpScreenshotRecorder = new CefSharpScreenshotRecorder();
System.Drawing.Bitmap bitmap = await cefSharpScreenshotRecorder.TakeScreenshotAsync(this.ChromiumWebBrowser);
this.ScreenshotImage.Source = cefSharpScreenshotRecorder.ConvertToBitmapImage(bitmap);
}
编辑
如果您只是对从网页截取快照感兴趣,请查看 CefSharp.OffScreen(可通过 NuGet package manager). The ChromiumWebBrowser
class exposes a ScreenshotAsync
method that returns a ready to use System.Drawing.Bitmap
. Here is an example from the project repository 在 GitHub.
示例:
class CefSharpScreenshotRecorder
{
private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }
public async Task<System.Drawing.Bitmap> TakeScreenshotAsync(
ChromiumWebBrowser browser,
string url,
TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
{
if (!string.IsNullOrEmpty(url))
{
throw new ArgumentException("Invalid URL", nameof(url));
}
this.TaskCompletionSource = new TaskCompletionSource<Bitmap>(optionalTaskCreationOptions);
// Load the page. In the loaded event handler
// take the snapshot and return it asynchronously it to caller
return await LoadPageAsync(browser, url);
}
private Task<System.Drawing.Bitmap> LoadPageAsync(IWebBrowser browser, string url)
{
browser.LoadingStateChanged += GetScreenShotOnLoadingStateChanged;
browser.Load(url);
// Return Task instance to make this method awaitable
return this.TaskCompletionSource.Task;
}
private async void GetScreenShotOnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
browser.LoadingStateChanged -= GetScreenShotOnLoadingStateChanged;
System.Drawing.Bitmap screenshot = await browser.ScreenshotAsync(true);
// Set the Task.Status of the Task instance to 'RanToCompletion'
// and return the result to the caller
this.TaskCompletionSource.SetResult(screenshot);
}
}
用法示例:
public async Task CreateScreenShotAsync(ChromiumWebBrowser browserInstance, string url)
{
var recorder = new CefSharpScreenshotRecorder();
System.Drawing.Bitmap screenshot = await recorder.TakeScreenshotAsync(browserInstance, url);
}
您真的不需要单独的 class 来保存状态。您可以使用 local function (or an Action<object, PaintEventArgs>
delegate) and the compiler will generate a class for you to hold the state, if there is any state. These hidden classes are known as closures.
public static Task<Bitmap> TakeScreenshotAsync(this ChromiumWebBrowser source)
{
var tcs = new TaskCompletionSource<Bitmap>(
TaskCreationOptions.RunContinuationsAsynchronously);
source.Paint += ChromiumWebBrowser_Paint;
return tcs.Task;
void ChromiumWebBrowser_Paint(object sender, PaintEventArgs e)
{
source.Paint -= ChromiumWebBrowser_Paint;
using (var temp = new Bitmap(e.Width, e.Height, 4 * e.Width,
PixelFormat.Format32bppPArgb, e.Buffer))
{
tcs.SetResult(new Bitmap(temp));
}
}
}
选项TaskCreationOptions.RunContinuationsAsynchronously
确保任务的继续不会运行在UI线程中同步。当然,如果您在 WPF 应用程序的上下文中 await
没有 configureAwait(false)
的任务,则继续
然后将在 UI 线程中重新安排到 运行,因为 configureAwait(true)
是默认值。
As a general rule, I would say any usage of TaskCompletionSource should specify TaskCreationOptions.RunContinuationsAsynchronously. Personally, I think the semantics are more appropriate and less surprising with that flag.
免责声明:创建位图的代码部分是从复制过来的,然后修改(见评论),但没有经过测试。
我想做的是:
我有一个 CefSharp ChromiumWebBrowser
(WPF 控件),我想截取该浏览器中的网页。屏幕上ChromiumWebBrowser
没有截屏方法。但是我可以通过将事件处理程序附加到浏览器的 OnPaint
事件来获得渲染。
这样我就得到了一个作为屏幕截图的位图。该过程基于此答案:
现在我正在创建一个class CefSharpScreenshotRecorder
,它应该负责截屏。它应该接受浏览器实例,将事件处理程序附加到 OnPaint
事件,并获取位图。这个过程的所有状态都应该封装在CefSharpScreenshotRecorder
class中。
我希望能够异步使用我的 class。因为我们必须等到 OnPaint 事件被触发。当触发该事件(并调用事件处理程序)时,事件处理程序中将提供一个位图。那么这个Bitmap应该是原来调用的异步方法的结果(比如CefSharpScreenshotRecorder.TakeScreenshot(...cefBrowserInstance...)
。一切都必须在没有blocking/lagging当然UI的情况下发生。
我对 C# 中的异步编程不是很熟悉。 我遇到的问题是我找不到制作可等待方法的方法,该方法在调用时仅代表 OnPaint 事件处理程序 returns 。 我什至不知道是否存在任何代码功能来创建此逻辑。
这可以使用 TaskCompletionSource
来实现。通过这种方式,您可以将同步(例如事件驱动)代码包装到异步方法中,而无需使用 Task.Run
.
class CefSharpScreenshotRecorder
{
private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }
public Task<System.Drawing.Bitmap> TakeScreenshotAsync(
ChromiumWebBrowser browserInstance,
TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
{
this.TaskCompletionSource = new TaskCompletionSource<System.Drawing.Bitmap>(optionalTaskCreationOptions);
browserInstance.Paint += GetScreenShotOnPaint;
// Return Task instance to make this method awaitable
return this.TaskCompletionSource.Task;
}
private void GetScreenShotOnPaint(object sender, PaintEventArgs e)
{
(sender as ChromiumWebBrowser).Paint -= GetScreenShotOnPaint;
System.Drawing.Bitmap newBitmap = new Bitmap(e.Width, e.Height, 4 * e.Width, PixelFormat.Format32bppPArgb, e.Buffer);
// Optional: save the screenshot to the hard disk "MyPictures" folder
var screenshotDestinationPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyPictures),
"CefSharpBrowserScreenshot.png");
newBitmap.Save(screenshotDestinationPath);
// Create a copy of the bitmap, since the underlying buffer is reused by the library internals
var bitmapCopy = new System.Drawing.Bitmap(newBitmap);
// Set the Task.Status of the Task instance to 'RanToCompletion'
// and return the result to the caller
this.TaskCompletionSource.SetResult(bitmapCopy);
}
public BitmapImage ConvertToBitmapImage(System.Drawing.Bitmap bitmap)
{
using(var memoryStream = new MemoryStream())
{
bitmap.Save(memoryStream, ImageFormat.Png);
memoryStream.Position = 0;
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = memoryStream;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
bitmapImage.Freeze();
}
}
}
用法示例(有效):
MainWindow.xaml
<Window>
<StackPanel>
<Button Click="TakeScreenshot_OnClick" Height="50" Content="Take Screenshot"/>
<ChromiumWebBrowser x:Name="ChromiumWebBrowser"
Width="500"
Height="500"
Address="" />
<Image x:Name="ScreenshotImage" />
</StackPanel>
</Window>
MainWindow.xaml.cs
private async void TakeScreenshot_OnClick(object sender, RoutedEventArgs e)
{
var cefSharpScreenshotRecorder = new CefSharpScreenshotRecorder();
System.Drawing.Bitmap bitmap = await cefSharpScreenshotRecorder.TakeScreenshotAsync(this.ChromiumWebBrowser);
this.ScreenshotImage.Source = cefSharpScreenshotRecorder.ConvertToBitmapImage(bitmap);
}
编辑
如果您只是对从网页截取快照感兴趣,请查看 CefSharp.OffScreen(可通过 NuGet package manager). The ChromiumWebBrowser
class exposes a ScreenshotAsync
method that returns a ready to use System.Drawing.Bitmap
. Here is an example from the project repository 在 GitHub.
示例:
class CefSharpScreenshotRecorder
{
private TaskCompletionSource<System.Drawing.Bitmap> TaskCompletionSource { get; set; }
public async Task<System.Drawing.Bitmap> TakeScreenshotAsync(
ChromiumWebBrowser browser,
string url,
TaskCreationOptions optionalTaskCreationOptions = TaskCreationOptions.None)
{
if (!string.IsNullOrEmpty(url))
{
throw new ArgumentException("Invalid URL", nameof(url));
}
this.TaskCompletionSource = new TaskCompletionSource<Bitmap>(optionalTaskCreationOptions);
// Load the page. In the loaded event handler
// take the snapshot and return it asynchronously it to caller
return await LoadPageAsync(browser, url);
}
private Task<System.Drawing.Bitmap> LoadPageAsync(IWebBrowser browser, string url)
{
browser.LoadingStateChanged += GetScreenShotOnLoadingStateChanged;
browser.Load(url);
// Return Task instance to make this method awaitable
return this.TaskCompletionSource.Task;
}
private async void GetScreenShotOnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e)
{
browser.LoadingStateChanged -= GetScreenShotOnLoadingStateChanged;
System.Drawing.Bitmap screenshot = await browser.ScreenshotAsync(true);
// Set the Task.Status of the Task instance to 'RanToCompletion'
// and return the result to the caller
this.TaskCompletionSource.SetResult(screenshot);
}
}
用法示例:
public async Task CreateScreenShotAsync(ChromiumWebBrowser browserInstance, string url)
{
var recorder = new CefSharpScreenshotRecorder();
System.Drawing.Bitmap screenshot = await recorder.TakeScreenshotAsync(browserInstance, url);
}
您真的不需要单独的 class 来保存状态。您可以使用 local function (or an Action<object, PaintEventArgs>
delegate) and the compiler will generate a class for you to hold the state, if there is any state. These hidden classes are known as closures.
public static Task<Bitmap> TakeScreenshotAsync(this ChromiumWebBrowser source)
{
var tcs = new TaskCompletionSource<Bitmap>(
TaskCreationOptions.RunContinuationsAsynchronously);
source.Paint += ChromiumWebBrowser_Paint;
return tcs.Task;
void ChromiumWebBrowser_Paint(object sender, PaintEventArgs e)
{
source.Paint -= ChromiumWebBrowser_Paint;
using (var temp = new Bitmap(e.Width, e.Height, 4 * e.Width,
PixelFormat.Format32bppPArgb, e.Buffer))
{
tcs.SetResult(new Bitmap(temp));
}
}
}
选项TaskCreationOptions.RunContinuationsAsynchronously
确保任务的继续不会运行在UI线程中同步。当然,如果您在 WPF 应用程序的上下文中 await
没有 configureAwait(false)
的任务,则继续
然后将在 UI 线程中重新安排到 运行,因为 configureAwait(true)
是默认值。
As a general rule, I would say any usage of TaskCompletionSource should specify TaskCreationOptions.RunContinuationsAsynchronously. Personally, I think the semantics are more appropriate and less surprising with that flag.
免责声明:创建位图的代码部分是从