I have some WinForms applications that would benefit from having images and thumbnails (sometimes called thumbshots) of internet web pages and so I decided yesterday to have a look at how to approach this. As you would expect, I went Googling. C# web page thumbnail, C# web site thumbnail, and others give plenty of links about how to make "websites of thumbnails" rather than "thumbnails of websites"...
Along the way, I found Display Web Page ThumbShots without Hosting Images which discusses two sites in particular (thumbshots.org and alexa.com) which provide web capabilites for thumbnails by serving up images. An example this can be seen at Search CSS which combines Google results with images from thumbshots.org, which I think is a cool piece of application integration.
If you want to write a web application, these services may be very useful for you. They might also be useful if you want to to write certain types of windows applications (for example, smart client apps on PocketPC where storage space is at a premium). However, they really don't meet my needs because I want to generate the images myself.
My next thought was to see if I could use Internet Explorer to get the images I needed. IE is provided as a component by the WebBrowser control. To add this control to your project, simply select "Add/Remove Items" from the Toolbox context menu and check "Microsoft Web Browser" on the "COM Components" tab of the "Customize Toolbox" dialog.
Once added, the control can be treated as any normal control and placed on a form. Unfortunately, the WebBrowser control does not provide an Image property (life just isn't that simple). The challenge is to obtain the rendered image. Usefully, the WebBrowser control does provide the CreateGraphics() method. This is useful because, in turn, the System.Drawing.Graphics class provides the GetHdc() method which returns the handle to the device context. System.Drawing (aka GDI+) is a powerful library that does an excellent job of encapsulating the GDI32 library, but it is not comprehensive and with a device context handle we can call the Win32 API directly to achieve what we need.
The following code snippet is for the event triggered by the WebBrowser control when document completes after a call to the Navigate() method, which isn't shown. Also not shown is the GDI32 class which contains DllImport static methods and enumerations. These can be obtained easily from pinvoke.net.
C#
private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
using (Graphics srcGraphics = this.axWebBrowser1.CreateGraphics())
{
using (Graphics destGraphics = this.pictureBox1.CreateGraphics())
{
IntPtr hdcDest = destGraphics.GetHdc();
IntPtr hdcSrc = srcGraphics.GetHdc();
GDI32.BitBlt(
hdcDest,
0, 0,
this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height,
hdcSrc,
0, 0,
(int) GDI32.TernaryRasterOperations.SRCCOPY
);
srcGraphics.ReleaseHdc(hdcSrc);
destGraphics.ReleaseHdc(hdcDest);
}
}
}
VB.NET
Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
' Using
Dim srcGraphics As Graphics = Me.axWebBrowser1.CreateGraphics
Try
' Using
Dim destGraphics As Graphics = Me.pictureBox1.CreateGraphics
Try
Dim hdcDest As IntPtr = destGraphics.GetHdc
Dim hdcSrc As IntPtr = srcGraphics.GetHdc
GDI32.BitBlt(hdcDest, 0, 0, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height, hdcSrc, 0, 0, CType(GDI32.TernaryRasterOperations.SRCCOPY, Integer))
srcGraphics.ReleaseHdc(hdcSrc)
destGraphics.ReleaseHdc(hdcDest)
Finally
CType(destGraphics, IDisposable).Dispose()
End Try
Finally
CType(srcGraphics, IDisposable).Dispose()
End Try
End Sub
The code above copies the rendered image from the WebBrowser to the PictureBox. However, there is a problem. The problem is that the BitBlt operation will only copy the visible client area of the WebBrowser. This means that part if the WebBrowser window is not visible (by being off-screen, for example) then it will not be copied. If it is obscured by another window then the copy will part of the other window. The workaround for this issue is to use interfaces in the Microsoft.mshtml library as this provides a mean of having the control draw onto a device context of your choosing. To do this, click "Add Reference" on your project, and select "Microsoft.mshtml" from the .NET tab.
There is a gotcha, which I found on Ryan Farber's weblog, which means that you need to describe the IHTMLElementRender interface:
C#
[Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
ComVisible(true),
ComImport]
interface IHTMLElementRender
{
void DrawToDC([In] IntPtr hDC);
void SetDocumentPrinter([In, MarshalAs(UnmanagedType.BStr)] string bstrPrinterName, [In] IntPtr hDC);
};
VB.NET
<Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), ComVisible(True), ComImport()> _
Interface IHTMLElementRender
Sub DrawToDC( <In()> _
ByVal hDC As IntPtr)
Sub SetDocumentPrinter( <In(), MarshalAs(UnmanagedType.BStr)> _
ByVal bstrPrinterName As String, <In()> _
ByVal hDC As IntPtr)
End Interface
Once you have done this, then the event handler can be amended to:
C#
private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
IHTMLDocument2 document = (IHTMLDocument2) this.axWebBrowser1.Document;
if (document != null)
{
IHTMLElement element = (IHTMLElement) document.body;
if (element != null)
{
IHTMLElementRender render = (IHTMLElementRender) element;
if (render != null)
{
using (Graphics graphics = this.pictureBox1.CreateGraphics())
{
IntPtr hdc = graphics.GetHdc();
render.DrawToDC(hdc);
}
}
}
}
}
VB.NET
Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
Dim document As IHTMLDocument2 = CType(Me.axWebBrowser1.Document, IHTMLDocument2)
If Not (document Is Nothing) Then
Dim element As IHTMLElement = CType(document.body, IHTMLElement)
If Not (element Is Nothing) Then
Dim render As IHTMLElementRender = CType(element, IHTMLElementRender)
If Not (render Is Nothing) Then
' Using
Dim graphics As Graphics = Me.pictureBox1.CreateGraphics
Try
Dim hdc As IntPtr = graphics.GetHdc
render.DrawToDC(hdc)
Finally
CType(graphics, IDisposable).Dispose()
End Try
End If
End If
End If
End Sub
Now the copy operation performs better. If the WebBrowser control is obscured in some fashion then you still get the original copy. There is, unfortunately, a further problem which is that the copy made is not persistent. This means that if the PictureBox which contains the copy is obscured in some fashion, then the copy needs to be made a second time. Furthermore, the copied image is not available via the Image property of the PictureBox which means that you cannot call the Save() method, for example, to save the image to disk. In order to set the Image property, we need to create and set a persistent bitmap. To do this we need to resort to the Win32 API again and change the code:
C#
private void OnDocumentComplete(object sender, AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent e)
{
IHTMLDocument2 document = (IHTMLDocument2) this.axWebBrowser1.Document;
if (document != null)
{
IHTMLElement element = (IHTMLElement) document.body;
if (element != null)
{
IHTMLElementRender render = (IHTMLElementRender) element;
if (render != null)
{
using (Graphics graphics = this.pictureBox1.CreateGraphics())
{
IntPtr hdcDestination = graphics.GetHdc();
render.DrawToDC(hdcDestination);
IntPtr hdcMemory = GDI32.CreateCompatibleDC(hdcDestination);
IntPtr bitmap = GDI32.CreateCompatibleBitmap(
hdcDestination,
this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height
);
if (bitmap != IntPtr.Zero)
{
IntPtr hOld = (IntPtr) GDI32.SelectObject(hdcMemory, bitmap);
GDI32.BitBlt(
hdcMemory,
0, 0,
this.axWebBrowser1.ClientRectangle.Width, this.axWebBrowser1.ClientRectangle.Height,
hdcDestination,
0, 0,
(int) GDI32.TernaryRasterOperations.SRCCOPY
);
GDI32.SelectObject(hdcMemory, hOld);
GDI32.DeleteDC(hdcMemory);
graphics.ReleaseHdc(hdcDestination);
this.pictureBox.Image = Image.FromHbitmap(bitmap);
}
}
}
}
}
}
VB.NET
Private Sub OnDocumentComplete(ByVal sender As Object, ByVal e As AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEvent)
Dim document As IHTMLDocument2 = CType(Me.axWebBrowser1.Document, IHTMLDocument2)
If Not (document Is Nothing) Then
Dim element As IHTMLElement = CType(document.body, IHTMLElement)
If Not (element Is Nothing) Then
Dim render As IHTMLElementRender = CType(element, IHTMLElementRender)
If Not (render Is Nothing) Then
' Using
Dim graphics As Graphics = Me.pictureBox1.CreateGraphics
Try
Dim hdcDestination As IntPtr = graphics.GetHdc
render.DrawToDC(hdcDestination)
Dim hdcMemory As IntPtr = GDI32.CreateCompatibleDC(hdcDestination)
Dim bitmap As IntPtr = GDI32.CreateCompatibleBitmap(hdcDestination, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height)
If Not (bitmap = IntPtr.Zero) Then
Dim hOld As IntPtr = CType(GDI32.SelectObject(hdcMemory, bitmap), IntPtr)
GDI32.BitBlt(hdcMemory, 0, 0, Me.axWebBrowser1.ClientRectangle.Width, Me.axWebBrowser1.ClientRectangle.Height, hdcDestination, 0, 0, CType(GDI32.TernaryRasterOperations.SRCCOPY, Integer))
GDI32.SelectObject(hdcMemory, hOld)
GDI32.DeleteDC(hdcMemory)
graphics.ReleaseHdc(hdcDestination)
Me.pictureBox.Image = Image.FromHbitmap(bitmap)
End If
Finally
CType(graphics, IDisposable).Dispose()
End Try
End If
End If
End If
End Sub
I'm not going to go into detail about the GDI operations (as they are so well documented elsewhere) and I have sacrified good practice like exception handling for clarity, but the code above will set a persistent Image of the web page loaded in the WebBrowser control which can be saved or manipulated with the GDI+ library. For example, the following code sample takes the Image created above and scales it to a thumbnail PictureBox:
C#
private void DrawThumbnail()
{
using (Graphics graphics = this.thumbnailPictureBox.CreateGraphics())
{
graphics.DrawImage(this.pictureBox1.Image, this.thumbnailPictureBox.ClientRectangle);
}
}
VB.NET
Private Sub DrawThumbnail()
' Using
Dim graphics As Graphics = Me.thumbnailPictureBox.CreateGraphics
Try
graphics.DrawImage(Me.pictureBox1.Image, Me.thumbnailPictureBox.ClientRectangle)
Finally
CType(graphics, IDisposable).Dispose()
End Try
End Sub
Comments