I was having a deep discussion with a friend the other day, about how WPF and Winforms controls can’t overlap on the same surface. It’s not really a big limitation, since WPF is so rich you’d never need a Winform Control on your surface, right?
What if you really , really wanted a nice Google map or a Live! map on your WPF form? well I guess you’re S.O.L. then.
Well, up until now!
Yep, that’s a Microsoft Live map right there on your WPF form. Ok, I know its not overlapping , so you’re thinking I could have faked it.., but I haven’t honest, its just in a <StackPanel> as an example.
Anyway, here’s how it works.
Behind the scenes, there’s a standard Winform window that houses the browser. The browser shows a simple HTML form with a bit of JavaScript to allow us to interface and control the map programmatically.
here’s the HTML file.
1: <html>
2: <head>
3: <title>Virtual Earth example by Rob Hill</title>
4: <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5: <script type="text/javascript" src="http://dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=6"></script>
6: <script type="text/javascript" >
7: theMap = null;
8: function load() {
9: theMap = new VEMap('map');
10: theMap.LoadMap();
11: theMap.HideDashboard();
12: zoomTo(53.3862, -1.4345, 14);
13: }
14: function resizeMap(w, h) {
15: theMap.Resize(w, h);
16: }
17: function panMap(dx, dy) {
18: theMap.Pan(dx, dy);
19: window.external.onContentChange();
20: }
21: function centerAndZoom(lat, lon, zoomDelta) {
22: theMap.SetCenterAndZoom(new VELatLong(lat, lon), theMap.GetZoomLevel() + zoomDelta);
23: window.external.onContentChange();
24: }
25: function zoomTo(lat, lon, zoomLevel) {
26: theMap.SetCenterAndZoom(new VELatLong(lat, lon), zoomLevel);
27: }
28: function _onMapError(e) {
29: window.external.onError();
30: }
31: function pixelToLatLon(x, y) {
32: m = theMap.PixelToLatLong(new VEPixel(x,y));
33: return m.Latitude + ',' + m.Longitude;
34: }
35: function latLonToPixel(lat, lon) {
36: m = theMap.LatLongToPixel(new MapLocation(new VELatLong(lat, lon)));
37: return m.X + ',' + m.Y;
38: }
39: </script>
40: </head>
41: <body style="margin: 0px" onload="load();">
42: <div style="overflow: hidden; width:100%; height:100%" id="map"></div>
43: </body>
44: </html>
As you can see, its pretty simple. It loads up the map , and has a few public JavaScript functions.
The next step is to get the browser to render itself somewhere other than the window the control is on. After quite a bit of digging it seems theres an interface that is available on all the System.Windows.Forms.HtmlElement‘s that you retrieve from the DOM called IHTMLElementRender. This interface is declared with a bit couple of attributes :
1: Guid("3050f669-98b5-11cf-bb82-00aa00bdce0b"),
2: InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
3: ComVisible(true),
4: ComImport]
5: interface IHTMLElementRender {
6: void DrawToDC([In] IntPtr hDC);
7: }
So, once you have an element you’ve extracted from the DOM, you can get Internet Explorer to render that Node (and its children) into a Device Context! Neat!
Here’s example using the interface – saving the browser window (the map) to a bitmap file.
1: public void Snapshot(string filename) {
2: HtmlDocument doc = theBrowser.Document;
3: if (doc != null) {
4: HtmlElement element = doc.Body;
5: if (element != null) {
6: IHTMLElementRender render = (IHTMLElementRender)element.DomElement;
7: if (render != null) {
8: Graphics g = Graphics.FromImage(content);
9: render.DrawToDC(g.GetHdc());
10: g.ReleaseHdc();
11: content.Save(filename);
12: }
13: }
14: }
15: }
Ok, so now we can coax IE to render its window to a device context, the next step is getting that into WPF’s space. To do this, we allocate a WPF Image, and then use a method from the System.Windows.Interop.Imaging to render the element into a WPF Image.
1: private readonly Image theMap = new Image();
2:
3: // [SNIP]...
4:
5: public void OnContentChanged(VEWindow window, ContentChangedArgs cca)
6: {
7: theMap.Source = System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
8: cca.Content.GetHbitmap(),IntPtr.Zero,Int32Rect.Empty,
9: System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
10: }
We can then use that image on the form. With a few added events to intercept the mouse clicks and moves, which we then pass on back to the browser we can ‘fake’ having a Live Map on our WPF form.
Download the complete VS2008 Solution
Usual disclaimers about example code destroying all life on the planet apply!