【问题标题】:Wait asynchronously for a synchronous event/ operation with Task while taking a screenshot from the web browser/ web page using CefSharp使用 CefSharp 从 Web 浏览器/网页截屏时异步等待同步事件/操作
【发布时间】:2026-01-04 19:55:01
【问题描述】:

我想要做的是:

我有一个 CefSharp ChromiumWebBrowser(WPF 控件),我想在该浏览器中截取网页的屏幕截图。屏幕上的ChromiumWebBrowser 没有截图方法。但是我可以通过将事件处理程序附加到浏览器的OnPaint 事件来获得渲染。 这样我得到一个位图,它是屏幕截图。该过程基于此答案:https://*.com/a/54236602/2190492

现在我正在创建一个类CefSharpScreenshotRecorder,它应该负责截屏。它应该接受一个浏览器实例,将一个事件处理程序附加到OnPaint 事件,并获取位图。这个过程的所有状态都应该封装在CefSharpScreenshotRecorder 类中。 我希望能够异步使用我的课程。因为我们必须等到 OnPaint 事件被触发。当该事件被触发(并调用事件处理程序)时,事件处理程序中会提供一个位图。那么这个 Bitmap 应该是最初调用的异步方法的结果(比如CefSharpScreenshotRecorder.TakeScreenshot(...cefBrowserInstance...)。当然,一切都必须在不阻塞/滞后 UI 的情况下发生。

我对 C# 中的异步编程不是很熟悉。 我遇到的问题是我找不到一种方法来制作可等待的方法,该方法仅在调用时代表 OnPaint 事件处理程序返回。 我什至不知道是否存在任何代码功能来创建此逻辑。

【问题讨论】:

  • 您是如何决定所有这些都必须是异步的?
  • @Andy 也许有人喜欢将 API 设计为异步的而不是事件驱动的。
  • 等待绘制事件被触发,实际上是在等待。你必须为这个过程保持一个状态,我想把它封装到一个单独的类中,只需在该类上调用一个异步方法就可以使用它。这不会阻塞,并且从类外部您不必担心状态或附加/删除事件处理程序。
  • @user2190492 我更新了我的答案,以展示使用CefSharp.OffScreen API 获取屏幕截图的更简单方法。

标签: c# wpf async-await screenshot cefsharp


【解决方案1】:

这可以使用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="https://*.com/a/57695630/3141792" />
    <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 包管理器获得)。 ChromiumWebBrowser 类公开了一个ScreenshotAsync 方法,该方法返回一个可以使用的System.Drawing.BitmapHere 是来自 GitHub 上的 project repository 的示例。

例子:

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);
}

【讨论】:

  • 你可以把它变成一个扩展方法,让它更容易使用。我会使用与github.com/cefsharp/CefSharp/blob/cefsharp/75/… 中概述的类似模式。
  • @amaitland 谢谢你的链接。扩展方法确实让你的代码看起来更好。我同意。但在这种情况下,我想我会远离任何静态方法。将快照逻辑封装在(非静态)类中的优点是它增加了可测试性。在为使用快照类的类编写单元测试时,我可以轻松地模拟底层逻辑(加载网页,将快照转换为位图,等待事件)。将代码耦合到静态代码会降低可测试性,因为您无法模拟(替换)静态依赖项(无需修改代码)。
  • 是否有 WinForms 等价物? e.Width等不可用。
  • @TEK 是的。您只需改用 WinForms 库。检查 "CefSharp.WinForms"NuGet 包管理器 或从 GtiHub 下载它:CefSharp.WinForms(例如:CefSharp.WinForms.Example
  • @TEK 如果您有兴趣拍摄快照,请查看CefSharp.OffScreen。 “ChromiumWebBrowser”类公开了一个ScreenshotAsync 方法,该方法返回一个可以使用的位图。 Here(github.com/cefsharp/CefSharp/blob/master/…) 就是一个例子。只需调用异步方法来获取位图。相当简单的 API。
【解决方案2】:

你真的不需要一个单独的类来保存状态。您可以使用local function(或Action&lt;object, PaintEventArgs&gt; 委托),如果有任何状态,编译器将为您生成一个类来保存状态。这些隐藏类称为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) 是默认值。

作为一般规则,我会说 TaskCompletionSource 的任何用法都应指定 TaskCreationOptions.RunContinuationsAsynchronously。就个人而言,我认为该标志的语义更合适且不那么令人惊讶。 [citation]


免责声明:创建位图的部分代码是从another answer 复制然后修改的(参见 cmets),但未经测试。

【讨论】:

  • 我也更喜欢使用内联方法/委托的简洁性。 CefSharp 只需要 .Net 4.5.2 并且此代码至少需要 .Net 4.6 作为任何希望使用此代码的人的免责声明。您还需要一个更新的支持内联方法的C# 编译器,从内存中您至少需要VS2017Microsoft.Net.Compilers Nuget Package
  • 稍微详细一点的评论,PixelFormat 应该是PixelFormat.Format32bppPArgb,因为它支持透明度。缓冲区不应该在Paint 事件的范围之外使用,它经常在内部重用,甚至可能被释放。应该复印一份。
  • @amaitland 我根据您的建议更新了位图创建代码。你觉得可以吗?我还没有真正测试过,因为我还没有安装 CefSharp 库。之前的代码是从另一个答案复制粘贴的。 :-)
  • 答案应该有一个免责声明,它实际上没有经过测试。理论上看起来没问题,我还没有机会自己运行它。
  • @amaitland 同意了。 :-)