Below is a solution i have used for creating a tabbed browser where every browser was running in a separate thread. I have to warn you that it is not an easy solution and that this kind of windows hacking will cause a lot of minor interface and threading problems. (All solvable, but take some time for testing your application)
The solution consists of a container panel (BrowserPanel) and an embedded web browser panel (BrowserForm). Creating a BrowserPanel will start a web browser inside itself in a separate thread.
This is far from a complete solution but hopefully it will help you start.
1) Create a separate class file for the user32.dll methods (you might already have these)
public static class User32
{
[DllImport("user32.dll")]
public static extern IntPtr SetParent(IntPtr wnd, IntPtr parent);
[DllImport("user32.dll")]
public static extern bool SetWindowPos(IntPtr wnd, IntPtr parent, int x, int y, int w, int h, uint flags);
[DllImport("user32.dll")]
public static extern IntPtr SetFocus(IntPtr wnd);
}
2) Create the container control
Create a new class file and name it BrowserPanel.
This control will live on the main UI thread and acts as a placeholder for the web browser form below.
public class BrowserPanel : Panel
{
}
3) Create the web browser form
Create a new form and place the webbrowser control on it. Name the form BrowserForm.
This form will have its own seperate thread.
The form must have a link to the BrowserPanel. (see source code below)
public partial class BrowserForm : Form
{
public BrowserPanel Panel { get; private set; }
public BrowserForm(BrowserPanel panel)
{
Panel = panel;
InitializeComponent();
FormBorderStyle = FormBorderStyle.None;
TopLevel = false;
}
}
3) Add creation code
Add the following code to the BrowserPanel class.
public class BrowserPanel : Panel
{
public BrowserForm Browser { get; private set; }
private IntPtr _threadownerhandle;
private IntPtr _threadformhandle;
private Thread _thread;
private AutoResetEvent _threadlock;
public BrowserPanel(): Panel
{
Resize += OnResize;
ThreadCreate();
}
public void ThreadCreate()
{
// The following line creates a window handle to the BrowserPanel
// This has to be done in the UI thread, but the handle can be used in an other thread
_threadownerhandle = Handle;
// A waiting lock
_threadlock = new AutoResetEvent(false);
// Create the thread for the BrowserForm
_thread = new Thread(ThreadStart);
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
// Let's wait until the Browser object has been created
_threadlock.WaitOne();
}
private void ThreadStart()
{
// This a NOT the UI thread
try
{
// Create the BrowserForm in a new thread
Browser = new BrowserForm(this);
// Get the handle of the form. This has to be done in the creator thread
_threadformhandle = Browser.Handle;
// The magic. The BrowserForm is added to the BrowserPanel
User32.SetParent(_threadformhandle, _threadownerhandle);
// Make the BrowserForm the same size as the BrowserPanel
ThreadWindowUpdate();
}
finally
{
// Notify the BrowserPanel we are finished with the creation of the Browser
_threadlock.Set();
}
try
{
// With the next line a (blocking) message loop is started
Application.Run(Browser);
}
finally
{
Browser.Dispose();
}
}
private void OnResize(object sender, EventArgs e)
{
// Resizing the BrowserPanel will resize the BrowserForm too
if (Browser != null) ThreadWindowUpdate();
}
public void ThreadWindowUpdate()
{
if (Browser == null) return;
User32.SetWindowPos(_threadformhandle, IntPtr.Zero, 0, 0, Width, Height, 0);
}
}
4) Add some more logic to the BrowserPanel class
public void Focus()
{
// normal focus will not work
User32.SetFocus(_threadformhandle);
}
Are we done yet. No!
Calling the browser control methods from the main UI thread can cause thread exceptions. For many WebBrowser methods you have to make a wrapper in the BrowserForm like the one below. Some WebBrowser methods can be called from another thread without problems. Find out by trial and error.
public void BrowserPrint()
{
if (InvokeRequired)
BeginInvoke(new MethodInvoker(() => { webBrowser1.Print(); }));
else
webBrowser1.Print();
}
public void BrowserClose()
{
Browser.DialogResult = DialogResult.Cancel; // or whatever
if (InvokeRequired)
BeginInvoke(new MethodInvoker(() => { this.Close(); }));
else
this.Close();
}
The same for WebBrowser events calling the main UI thread. For example:
In BrowserForm:
private void webBrowser1_StatusTextChange(object sender, StatusTextChangeEventArgs e)
{
Panel.EventStatusTextChange(e.text);
}
In BrowserPanel:
public void EventStatusTextChange(string text)
{
if (_statustext == text) return;
_statustext s = text;
if (InvokeRequired)
BeginInvoke(new MethodInvoker(() => { Owner.StateChanged(this); }));
else
Owner.StateChanged(this);
}
Some special thing you have to take care of:
Use BeginInvoke instead of Invoke when possible. Deadlock will occur if a call to the WebBrowser control with a blocking Invoke will cause a callback event with another blocking Invoke
A popup menu created in the main UI thread wil not disappear when clicking on a WebBrowser control in another window. (Solve this by catching the onclick event of the WebBrowser control and route this back to the main form)