Thursday, November 13, 2008

Putting Google Earth into a WPF window

This may end up being fruitless, but I was inspired by this multi-channel version of Microsoft Virtual Earth. It looks like they linked multiple version of Virtual Earth with different camera settings, and I wanted to try something similar with Google Earth for our Global Immersion dome. This four-projector rig needs a viewport and four camera views to work right. Can I create a full-screen app that has these four viewports, and have four synchronized version of Google Earth running? I don't know, but the first step was to see if I could create a WPF app that had Google Earth embedded in the WPF window. You can at least do that, and that's interesting in itself, because I can add a Google Earth widget to my InfoMesa/Collage experiments described here...

Anyhow, here's the window in all its (yawn) glory:



It was a bit of a slog to get it right, and I'll share the code that worked. First, I had to get Google Earth, which gives you this COM SDK. I did this using C#, Vis Studio 2008. I added a project ref to the COM Google Earth library, and created a class that extended HwndHost.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Interop;
using System.Runtime.InteropServices;
using EARTHLib;

namespace GeTest
{
class MyHwndHost : HwndHost
{


[DllImport("user32.dll")]
static extern int SetParent(int hWndChild, int hWndParent);
IApplicationGE iGeApp;


[DllImport("user32.dll", EntryPoint = "GetDC")]
public static extern IntPtr GetDC(IntPtr ptr);



[DllImport("user32.dll", EntryPoint = "GetWindowDC")]
public static extern IntPtr GetWindowDC(Int32 ptr);

[DllImport("user32.dll", EntryPoint = "IsChild")]
public static extern bool IsChild(int hWndParent, int hwnd);


[DllImport("user32.dll", EntryPoint = "ReleaseDC")]
public static extern IntPtr ReleaseDC(IntPtr hWnd, IntPtr hDc);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
public extern static bool SetWindowPos(int hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);

[DllImport("user32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr PostMessage(int hWnd, int msg, int wParam, int lParam);

//PInvoke declarations
[DllImport("user32.dll", EntryPoint = "CreateWindowEx", CharSet = CharSet.Auto)]
internal static extern IntPtr CreateWindowEx(int dwExStyle,
string lpszClassName,
string lpszWindowName,
int style,
int x, int y,
int width, int height,
IntPtr hwndParent,
IntPtr hMenu,
IntPtr hInst,
[MarshalAs(UnmanagedType.AsAny)] object pvParam);


readonly IntPtr HWND_BOTTOM = new IntPtr(1);
readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
readonly IntPtr HWND_TOP = new IntPtr(0);
readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
static readonly UInt32 SWP_NOSIZE = 1;
static readonly UInt32 SWP_NOMOVE = 2;
static readonly UInt32 SWP_NOZORDER = 4;
static readonly UInt32 SWP_NOREDRAW = 8;
static readonly UInt32 SWP_NOACTIVATE = 16;
static readonly UInt32 SWP_FRAMECHANGED = 32;
static readonly UInt32 SWP_SHOWWINDOW = 64;
static readonly UInt32 SWP_HIDEWINDOW = 128;
static readonly UInt32 SWP_NOCOPYBITS = 256;
static readonly UInt32 SWP_NOOWNERZORDER = 512;
static readonly UInt32 SWP_NOSENDCHANGING = 1024;
static readonly Int32 WM_CLOSE = 0xF060;
static readonly Int32 WM_QUIT = 0x0012;

private IntPtr GEHrender = (IntPtr)0;
private IntPtr GEParentHrender = (IntPtr)0;

internal const int
WS_CHILD = 0x40000000,
WS_VISIBLE = 0x10000000,
LBS_NOTIFY = 0x00000001,
HOST_ID = 0x00000002,
LISTBOX_ID = 0x00000001,
WS_VSCROLL = 0x00200000,
WS_BORDER = 0x00800000;



public ApplicationGEClass googleEarth;

protected override HandleRef BuildWindowCore(HandleRef hwndParent)
{

// start google earth
googleEarth = new ApplicationGEClass();

int ge = googleEarth.GetRenderHwnd();

IntPtr hwndControl = IntPtr.Zero;
IntPtr hwndHost = IntPtr.Zero;
int hostHeight = 200;
int hostWidth = 300;

// create a host window that is a child of this HwndHost. I'll plug this HwndHost class as a child of
// a border element in my WPF app,

hwndHost = CreateWindowEx(0, "static", "",
WS_CHILD | WS_VISIBLE,
0, 0,
hostHeight, hostWidth,
hwndParent.Handle,
(IntPtr)HOST_ID,
IntPtr.Zero,
0);


// set the parent of the Google Earth window to be the host I created here
int oldPar = SetParent(ge, (int) hwndHost);
// check to see if I'm now a child, for my own amusement
if (IsChild(hwndHost.ToInt32(), ge)) {
System.Console.WriteLine("now a child");
}

// return a ref to the hwndHost, which should now be the parent of the google earth window
return new HandleRef(this, hwndHost);
}

protected override void DestroyWindowCore(HandleRef hwnd)
{
throw new NotImplementedException();
}
}
}



The main window of my WPF app, in its constructor for the Window, just plugs this HwndHost as a child of a Border control:

public Window1()
{

InitializeComponent();
MyHwndHost hwndHost = new MyHwndHost();
border1.Child = hwndHost;


}


And you are off to the races! I found a lot of different approaches to this all over the web, but none of them seemed to work, as is often the case. Maybe this will work for you, or maybe it adds to the confusion.

UPDATE: can't run more than one Google Earth, so it's a bust, but still has use in my InfoMesa/Collage project. I wonder about Virtual Earth?

1 comment:

Anonymous said...

it's work