Rendering an image from an embedded Web Browser (C# WPF application)
How is all started
So this week I was working on an extension for WebMatrix, Luke Sampson of http://StudioStyle.es just integrate a cool piece of code from Matt MCElheny. The news is that the studiostyle.es website now supports converting the over 1,000 themes uploaded for Visual Studio 2010 into the WebMatrix format, and hence we automatically got a very large load of themes to choose from.
Still we aspired for an even better experience, currently the WebMatrix user will have to install the ColorThemeEditor extension, go to the site, find a theme, download it, import it, and so on. Just too many steps. So Luke and some other folks (thanks Justin Beckwith and Scott Hanselman) started chatting. Luke was super fast, two hours later he announced that there is an api to the site.
By hitting:
http://studiostyl.es/api/schemes.xml I could now get a live feed of all the themes available
and for each one he provided a preview, and a download link. So I was ready to write the update to my extension.
The preview came from the following style link http://studiostyl.es//schemes/son-of-obsidian/snippet
This returns an HTML page, that looks like this:
and I was planning to show one per item in a WPF list view. Unfortunately the web browser control is not too happy inside a WPF life (since it’s basically a thin wrapper around native window). So instead I decided to render the browser into a bitmap and then show it on the list. I Spent several hours playing around and searching for a solution online, and couldn’t find any valid solution for my case.
When I came back to work, I chatted with Mikhail Arkhipov and he has some old class that did something very similar.
What doesn’t work
The WPF WebBrowser control is really just a wrapper around the WinFrom WebBrowser control. So at first I thought I’ll load the page off the screen and render it. Well it turns out that if this control is not on the screen it will never actually render. I wasn’t inclined to add it to the view as a hack, but I tried anyways. Well turns out that rendering the image using RenderTargetBitmap doesn’t work either. I guess because it’s an activeX and not a true WPF control.
So what is the solution
First I used a WinForms WebBrowser, so instead of instantiating a System.Windows.Controls.WebBrowser I used System.Windows.Forms.WebBrowser
Things already start to look better because I got the browser to render without going on the screen. I still couldn’t figure out how to render the bitmap. This where mshtml came to help.
The browsers document can be cast into IViewObject like in the code below:
1: var viewObject = wb.Document.DomDocument as IViewObject;
for more info: http://msdn.microsoft.com/en-us/library/windows/desktop/ms680763(v=vs.85).aspx
and now rendering can happen.
In my code I wasn’t hitting the server directly using Navigate and in order to do verify that I actually got the site data back rather than an error page I looked for a <pre tag that I knew Luke is always including in the response and is highly unlikely in an error page.
Of course I could have gotten an HttpWebRequest get the response and then feed the browser with the plain html, except that this page actually has some Jquery links, and it felt way more natural to let the browser deal with it. It’s really up to the implementer to pick what suits his application.
I’m attaching the full code below, here is some explanations on how it works and how to use it.
How to use it:
1. Add a reference to mshtml it’s going to appear in the COM section in VS2010
2. In your code instantiate a WebBrowserUtility utility object
3. Whenever you get a uri you want to render hookup to the WebBrowserImageReady event, and call Navigate with the Uri
1: private WebBrowserUtility _browserUtility;
2:
3: ...
4:
5: _browserUtility = new WebBrowserUtility();
6:
7: _browserUtility.WebBrowserImageReady += ImageReady;
8:
9: _browserUtility.Navigate(PreviewUri);
4. The event will fire back after some time with the image on the eventargs, in my case if the html text did not include “<pre” it will return null.
5. In my case I use CroppedImage to remove the scrollbars and I just render larger than I need (see lines 41, 42), cheap and dirty tricks that works nicely in my case. It’s a bit more complex to remove the scrollbars, but there are plenty of blogs on how to achieve that.
A word of caution: The webbrowserutility might not call back on the event, so you might want to spin up a timer disconnect it and dispose it after a while.
That’s it you are ready to roll.
How it works:
Lines 37-43:
When you instantiate the utility a WinForms browser is generated, that is important for rendering off the screen. The WPF one simply never fires an event back when it’s ready in this case.
Line 45:
I made navigate a separate method to avoid timing issues in hooking up the WebBrowserImageReady event. It can only be called once! The browser is disposed after the first navigation. Of course this can be modified to make the class reusable and disposable.
Line 57+:
When the browser finishes downloading it fires document ready event, at that point I’m getting the Document.Text and doing a simple text search for a <pre (yes it could be better, in my case it’s a sufficient test).
I then create an image and “Cast” or QueryInterface really for the IViewObject. If I got it I’ll create graphics and Draw it on the bitmap (that’s the method signature defined below on line 132+).
Since this is a WPF app, I now convert it back to a WPF Image, and raise the event. If I failed in any way the event fires with a null image.
1: using System;
2: using System.Runtime.InteropServices;
3: using System.Windows;
4: using System.Windows.Controls;
5: using System.Windows.Interop;
6: using System.Windows.Media.Imaging;
7: using mshtml;
8:
9: using Forms = System.Windows.Forms;
10: using Drawing = System.Drawing;
11:
12: namespace ColorThemeManager
13: {
14: internal class WebBrowserImageReadyEvengArgs : EventArgs
15: {
16: public BitmapSource Bms
17: {
18: get;
19: private set;
20: }
21:
22: public WebBrowserImageReadyEvengArgs(BitmapSource b)
23: {
24: Bms = b;
25: }
26: }
27:
28: internal class WebBrowserUtility : IDisposable
29: {
30: public Forms.WebBrowser WebBrowser { get; private set; }
31:
32: public BitmapSource BitmapSource { get; private set; }
33:
34: public event EventHandler<WebBrowserImageReadyEvengArgs>
35: WebBrowserImageReady = null;
36:
37: public WebBrowserUtility()
38: {
39: WebBrowser = new Forms.WebBrowser();
40: WebBrowser.DocumentCompleted += DocumentCompleted;
41: WebBrowser.Width = OnlineTheme.BrowserWidth;
42: WebBrowser.Height = OnlineTheme.BrowserHeight;
43: }
44:
45: public void Navigate(Uri url)
46: {
47: if (WebBrowser != null)
48: {
49: WebBrowser.Navigate(url);
50: }
51: else
52: {
53: throw new InvalidOperationException();
54: }
55: }
56:
57: private void DocumentCompleted
58: (object sender,
59: Forms.WebBrowserDocumentCompletedEventArgs e)
60: {
61: using (var wb = (Forms.WebBrowser)sender)
62: {
63: var text = wb.DocumentText ?? string.Empty;
64: bool validResult = text.Contains("<pre");
65:
66: if (validResult)
67: {
68: var pixelFormat = Drawing.Imaging.PixelFormat.Format24bppRgb;
69:
70: using (var bitmap = new Drawing.Bitmap(wb.Width,
71: wb.Height,
72: pixelFormat))
73: {
74: var viewObject = wb.Document.DomDocument as IViewObject;
75:
76: if (viewObject != null)
77: {
78: var sourceRect = new tagRECT();
79: sourceRect.left = 0;
80: sourceRect.top = 0;
81: sourceRect.right = wb.Width;
82: sourceRect.bottom = wb.Height;
83:
84: var targetRect = new tagRECT();
85: targetRect.left = 0;
86: targetRect.top = 0;
87: targetRect.right = wb.Width;
88: targetRect.bottom = wb.Height;
89:
90: using (var gr = Drawing.Graphics.FromImage(bitmap))
91: {
92: IntPtr hdc = gr.GetHdc();
93:
94: try
95: {
96: int hr = viewObject.Draw(1 /*DVASPECT_CONTENT*/,
97: (int)-1,
98: IntPtr.Zero,
99: IntPtr.Zero,
100: IntPtr.Zero,
101: hdc,
102: ref targetRect,
103: ref sourceRect,
104: IntPtr.Zero,
105: (uint)0);
106: }
107: finally
108: {
109: gr.ReleaseHdc();
110: }
111: }
112:
113: BitmapSource = Imaging.CreateBitmapSourceFromHBitmap(
114: bitmap.GetHbitmap(),
115: IntPtr.Zero,
116: Int32Rect.Empty,
117: BitmapSizeOptions.FromEmptyOptions());
118: }
119: }
120: }
121: }
122:
123: WebBrowser = null;
124:
125: if (WebBrowserImageReady != null)
126: {
127: WebBrowserImageReady(this,
128: new WebBrowserImageReadyEvengArgs(BitmapSource));
129: }
130: }
131:
132: [ComVisible(true), ComImport()]
133: [GuidAttribute("0000010d-0000-0000-C000-000000000046")]
134: [InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
135: private interface IViewObject
136: {
137: [return: MarshalAs(UnmanagedType.I4)]
138: [PreserveSig]
139: int Draw(
140: ////tagDVASPECT
141: [MarshalAs(UnmanagedType.U4)] UInt32 dwDrawAspect,
142: int lindex,
143: IntPtr pvAspect,
144: [In] IntPtr ptd,
145: //// [MarshalAs(UnmanagedType.Struct)] ref DVTARGETDEVICE ptd,
146: IntPtr hdcTargetDev, IntPtr hdcDraw,
147: [MarshalAs(UnmanagedType.Struct)] ref tagRECT lprcBounds,
148: [MarshalAs(UnmanagedType.Struct)] ref tagRECT lprcWBounds,
149: IntPtr pfnContinue,
150: [MarshalAs(UnmanagedType.U4)] UInt32 dwContinue);
151: }
152:
153: public void Dispose()
154: {
155: if (WebBrowser != null)
156: {
157: WebBrowser.Dispose();
158: WebBrowser = null;
159: }
160: }
161: }
162: }
And this it how it looks at the end
Come give it a spin
-> Install webmatrix from here:
http://www.microsoft.com/web/webmatrix/betafeatures.aspx
-> Open any site
-> Click on the gallery icon:
-> Install ColorThemeManager
-> Switch to to the Files workspace, open any file
-> Click on the Theme button
-> Notice the new fun colors in your editor