如何在工作线程上创建和使用WebBrowser控件?

3
我正在创建一个应用,使用以下方法对网站进行屏幕截图:http://pietschsoft.com/post/2008/07/C-Generate-WebPage-Thumbmail-Screenshot-Image.aspx 我尝试将应用程序设置为多线程,但是遇到了以下错误:
[ActiveX控件“8856f961-340a-11d0-a96b-00c04fd705a2”无法实例化,因为当前线程不在单线程公寓中。]
你有什么建议来解决这个问题吗?我的代码基本如下:
List<string> lststrWebSites = new List<string>();
lststrWebSites.Add("http://stackoverflow.com");
lststrWebSites.Add("http://www.cnn.com");
foreach (string strWebSite in lststrWebSites)
{
  System.Threading.ThreadStart objThreadStart = delegate
  {
    Bitmap bmpScreen = GenerateScreenshot(strWebSite, -1, -1);
    bmpScreen.Save(@"C:\" + strWebSite + ".png", 
      System.Drawing.Imaging.ImageFormat.Png);
  };
  new System.Threading.Thread(objThreadStart).Start();
}

GenerateScreenShot()函数的实现是从上述URL中复制的:

public Bitmap GenerateScreenshot(string url)
{
  // This method gets a screenshot of the webpage
  // rendered at its full size (height and width)
  return GenerateScreenshot(url, -1, -1);
}

public Bitmap GenerateScreenshot(string url, int width, int height)
{
  // Load the webpage into a WebBrowser control
  WebBrowser wb = new WebBrowser();
  wb.ScrollBarsEnabled = false;
  wb.ScriptErrorsSuppressed = true;
  wb.Navigate(url);
  while (wb.ReadyState != WebBrowserReadyState.Complete) 
    { Application.DoEvents(); }


  // Set the size of the WebBrowser control
  wb.Width = width;
  wb.Height = height;

  if (width == -1)
  {
    // Take Screenshot of the web pages full width
    wb.Width = wb.Document.Body.ScrollRectangle.Width;
  }

  if (height == -1)
  {
    // Take Screenshot of the web pages full height
    wb.Height = wb.Document.Body.ScrollRectangle.Height;
  }

  // Get a Bitmap representation of the webpage as it's rendered in 
  // the WebBrowser control
  Bitmap bitmap = new Bitmap(wb.Width, wb.Height);
  wb.DrawToBitmap(bitmap, new Rectangle(0, 0, wb.Width, wb.Height));
  wb.Dispose();

  return bitmap;
} 

我有点困惑,你为什么要在非GUI线程中创建和操作浏览器控件。有没有可能将繁重的工作与控件交互分开,只将繁重的工作委托给工作线程? - Tormod
嗯,我试过做那个,但卡住了。 - Karim
3个回答

6

WebBrowser,像许多ActiveX控件一样,具有严格的线程要求。创建它的线程必须使用Thread.SetApartmentState()进行初始化以将其切换到STA。并且该线程必须循环消息队列,可以通过Application.Run()获取。

这使得与浏览器交互非常棘手。以下是让您入门的代码。请注意,Completed回调在后台线程上运行。不要忘记调用Dispose()来关闭线程。

using System;
using System.Threading;
using System.ComponentModel;
using System.Windows.Forms;

class WebPagePump : IDisposable {
  public delegate void CompletedCallback(WebBrowser wb);
  private ManualResetEvent mStart;
  private SyncHelper mSyncProvider;
  public event CompletedCallback Completed;

  public WebPagePump() {
    // Start the thread, wait for it to initialize
    mStart = new ManualResetEvent(false);
    Thread t = new Thread(startPump);
    t.SetApartmentState(ApartmentState.STA);
    t.IsBackground = true;
    t.Start();
    mStart.WaitOne();
  }
  public void Dispose() {
    // Shutdown message loop and thread
    mSyncProvider.Terminate();
  }
  public void Navigate(Uri url) {
    // Start navigating to a URL
    mSyncProvider.Navigate(url); 
  }
  void mSyncProvider_Completed(WebBrowser wb) {
    // Navigation completed, raise event
    CompletedCallback handler = Completed;
    if (handler != null) handler(wb);
  }
  private void startPump() {
    // Start the message loop
    mSyncProvider = new SyncHelper(mStart);
    mSyncProvider.Completed += mSyncProvider_Completed;
    Application.Run(mSyncProvider);
  }
  class SyncHelper : Form {
    WebBrowser mBrowser = new WebBrowser();
    ManualResetEvent mStart;
    public event CompletedCallback Completed;
    public SyncHelper(ManualResetEvent start) {
      mBrowser.DocumentCompleted += mBrowser_DocumentCompleted;
      mStart = start;
    }
    public void Navigate(Uri url) {
      // Start navigating
      this.BeginInvoke(new Action(() => mBrowser.Navigate(url)));
    }
    void mBrowser_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e) {
      // Generated completed event
      Completed(mBrowser);
    }
    public void Terminate() {
      // Shutdown form and message loop
      this.Invoke(new Action(() => this.Close()));
    }
    protected override void SetVisibleCore(bool value) {
      if (!IsHandleCreated) {
        // First-time init, create handle and wait for message pump to run
        this.CreateHandle();
        this.BeginInvoke(new Action(() => mStart.Set()));
      }
      // Keep form hidden
      value = false;
      base.SetVisibleCore(value);
    }
  }
}

与该问题相关,特别是这个答案,我曾遇到类似的问题,因此编写了一个包装器,用于.NET WebBrowser以抽象化信息循环问题。对于其他任何对.NET而言寻求相对简单的无头浏览器的人,我已在GitHub上发布了代码,并通过NuGet提供下载。有关更多信息,请访问https://github.com/LeastOne/WebBrowserWaiter。 - LeastOne
请查看页面底部的许可条款。 - Hans Passant
@LeastOne 我在你的github上提出了问题,请你看一下。 - jkyadav

5
尝试设置托管浏览器控件的线程的 ApartmentState
var thread = new Thread(objThreadStart);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();

2
这个可以运行。但我需要将ApartmentState设置为STA而不是MTA。thread.SetApartmentState(ApartmentState.STA); - Karim

0
改变你的Main方法上的属性从STAThreadMTAThread有帮助吗?
例子:
[STAThread]
public static void Main()
{

更改为:

[MTAThread]
public static void Main()
{

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接