Search code examples
c#wpfasync-awaitscreenshotcefsharp

Wait asynchronously for a synchronous event/ operation with Task while taking a screenshot from the web browser/ web page using CefSharp


What I'm trying to do is the following:

I've got a CefSharp ChromiumWebBrowser (WPF control), and I would like to take a screenshot of the webpage in that browser. The on-screen ChromiumWebBrowser has no method for taking screenshots. But I can obtain the rendering by attaching an event handler to the OnPaint event of the browser. This way I get a Bitmap that is the screenshot. The process is based on this answer: https://stackoverflow.com/a/54236602/2190492

Now I'm creating a class CefSharpScreenshotRecorder that should be responsible for taking screenshots. It should accept a browser instance, attaches an event handler to the OnPaint event, and gets the bitmap. All the state of this process should be encapsulated in that CefSharpScreenshotRecorder class. I would like to be able to use my class asynchronously. Since we have to wait until the OnPaint event is triggered. When that event is triggered (and event handler called), a Bitmap is available in the event handler. Then this Bitmap should be the result of the asynchronous method that was originally called (like CefSharpScreenshotRecorder.TakeScreenshot(...cefBrowserInstance...). Everything must happen without blocking/lagging the UI of course.

I'm not very familiar with asynchronous programming in C#. The problem I have is that I can't find a way to make an awaitable method, that only returns on behalf of the OnPaint event handler when it is called. I don't even know if any code features exist to create this logic.


Solution

  • This can be achieved using TaskCompletionSource. This way you can wrap synchronous (e.g. event-driven) code into an asynchronous method without using 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();
        }
      }
    }
    

    Usage example (working):

    MainWindow.xaml

    <Window>
      <StackPanel>
        <Button Click="TakeScreenshot_OnClick" Height="50" Content="Take Screenshot"/>
        <ChromiumWebBrowser x:Name="ChromiumWebBrowser"
                            Width="500"
                            Height="500"
                            Address="https://stackoverflow.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);
    }
    

    Edit

    In case you are just interested in taking a snapshot from a web page then take a look at CefSharp.OffScreen (available via the 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 on GitHub.

    Example:

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

    Usage example:

    public async Task CreateScreenShotAsync(ChromiumWebBrowser browserInstance, string url)
    {
      var recorder = new CefSharpScreenshotRecorder();   
      System.Drawing.Bitmap screenshot = await recorder.TakeScreenshotAsync(browserInstance, url);
    }