Microsoft LIVE! Maps, WPF and Winforms.

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!

Microsoft Live Maps in WPF!

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!

This entry was posted in General. Bookmark the permalink.

1 Response to Microsoft LIVE! Maps, WPF and Winforms.

  1. Martin Randall says:

    Funnily enough, I’m working at a company at the moment that has a LBS asset tracking application and I’m building a WPF only version of Virtual Earth using their web service. Also abstracting out the tile generating stuff so we can use Open Source Streetmaps, Yahoo, etc. as per the DeepEarth project (http://deepearth.soulsolutions.com.au)

Leave a Reply