Host a process window in WPF

What follows is a code snippet to embed a window from another process to a WPF window.

C#

 
/************************************************************************/
/*                  Engine42 - Editor Project (c) 2010                  */
/************************************************************************/
 
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Security;
using System.Threading;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using AvalonDock;
using Engine42.Editor.Native;
using System.Windows.Input;
 
namespace Engine42.Editor
{
  public partial class Viewport : DocumentContent
  {
    private Boolean mViewportLoadingFinished = false;
    private GameViewportSession mGAVSession = null;
    private Thread mGAVLoadingThread = null;
 
    /// <summary>
    /// Indicates if the widget has been disposed.
    /// </summary>
    private bool IsDisposed { set; get; }
 
    /// <summary>
    /// Construct the game viewport widget.
    /// </summary>
    public Viewport()
    {
      InitializeComponent();
 
      this.IsFloatingAllowed = false;
      this.Closed += new EventHandler( OnViewportClose );
 
      //DataContext = this;
    }
 
    /// <summary>
    /// Called when the user dispose of the widget, by closing it for example.
    /// We make sure the hosted process is terminated.
    /// </summary>
    public void Dispose()
    {
      if ( IsDisposed )
        return;
 
      mGAVSession.Dispose();
      mGAVSession = null;
 
      IsDisposed = true;
    }
 
    /// <summary>
    /// Called when the system ask the widget if it can be closed safely.
    /// </summary>
    /// <returns>Always true for now I guess.</returns>
    public bool RequestClose()
    {
      return true;
    }
 
    // Generic delegates for N variables signatures.
    private delegate void NoArgDelegate();
    private delegate void OneArgDelegate(String arg);
 
    /// <summary>
    /// Called when the user control is initialized.
    /// </summary>
    private void OnInit(object sender, EventArgs e)
    {
      if ( !System.ComponentModel.DesignerProperties.GetIsInDesignMode( this ) )
      {
        // Launch the loading of the GAV and 
        // the creation of the host control in another thread.
        mGAVLoadingThread = new Thread( LoadGameViewportSessionThread );
        mGAVLoadingThread.SetApartmentState( System.Threading.ApartmentState.STA );
        mGAVLoadingThread.Start();
 
        // Launch another thread to display the loading of the GAV
        new Thread( DisplayViewportLoadingProgressionThread ).Start();
 
        // Bind event control events.
        HideButton.Click += new RoutedEventHandler( OnHideButtonClicked );
 
        this.KeyUp += new KeyEventHandler( Viewport_KeyUp );
        this.KeyDown += new System.Windows.Input.KeyEventHandler( Viewport_KeyDown );
        this.PreviewKeyDown += new System.Windows.Input.KeyEventHandler( Viewport_PreviewKeyDown );
      }
    }
 
    void Viewport_KeyUp(object sender, KeyEventArgs e)
    {
    }
 
    void Viewport_KeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
    }
 
    void Viewport_PreviewKeyDown(object sender, System.Windows.Input.KeyEventArgs e)
    {
    }
 
    /// <summary>
    /// Hides/Shows the hosted window.
    /// </summary>
    void OnHideButtonClicked(object sender, RoutedEventArgs e)
    {
      if ( HostControlParent.Visibility == Visibility.Collapsed )
      {
        HostControlParent.Visibility = Visibility.Visible;
        HideButton.Content = "Hide";
      }
      else
      {
        HostControlParent.Visibility = Visibility.Collapsed;
        HideButton.Content = "Show";
      }
    }
 
    /// <summary>
    /// Called when the size of the viewport changes.
    /// We update the host control size to make sure it is synced with the Horizon 
    /// </summary>
    private void OnHostSizeChanged(object sender, SizeChangedEventArgs e)
    {
      if ( mGAVSession != null && mGAVSession.HwndHost != null )
      {
        mGAVSession.HwndHost.UpdateWindowPos();
        mGAVSession.HwndHost.UpdateLayout();
      }
    }
 
    /// <summary>
    /// Called when the viewport gets closed. We make sure the GAV session is terminated.
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    void OnViewportClose(object sender, EventArgs e)
    {
      mGAVSession.Dispose();
      mGAVSession = null;
      mGAVLoadingThread.Abort();
    }
 
    /// <summary>
    /// Sets the loading label in the UI thread.
    /// </summary>
    /// <param name="label"></param>
    private void SetLoadingLabel(string label)
    {
      Debug.Assert( this.Dispatcher.Thread == Thread.CurrentThread );
 
      LoadingControl.Text = label;
    }
 
    /// <summary>
    /// Thread entry point to display the loading process to the user.
    /// </summary>
    /// <remarks>TEMPORARY CODE, could be change with an image animation.</remarks>
    private void DisplayViewportLoadingProgressionThread()
    {
      int dots = 0;
      int dir = 1;
 
      while ( !mViewportLoadingFinished )
      {
        string label = String.Format( "{2}{1}{0}Loading viewport{0}{1}{2}",
          dots > 0 ? "." : "", dots > 1 ? "." : "", dots > 2 ? "." : "" );
 
        Thread.Sleep( 750 );
 
        this.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
              new OneArgDelegate( SetLoadingLabel ),
              label );
 
        dots += dir;
        if ( 1 > dots || dots > 2 )
          dir *= -1;
      }
 
      this.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
              new OneArgDelegate( SetLoadingLabel ),
              "Viewport loaded" );
    }
 
    /// <summary>
    /// Creates and show the host control.
    /// </summary>
    /// <remarks>Should only be called in the UI thread.</remarks>
    private void CreateHostedControl(GameViewportSession hs)
    {
      Debug.Assert( mGAVSession == null );
      Debug.Assert( this.Dispatcher.Thread == Thread.CurrentThread );
 
      // Associate the hosted session with the this viewport widget.
      mGAVSession = hs;
 
      // Dynamically create the host control.
      GameViewportHost newHostedControl = new GameViewportHost( hs );
      newHostedControl.Focusable = true;
      newHostedControl.Focus();
 
      // Save the control host for the GAV session.
      hs.HwndHost = newHostedControl;
 
      // Embed the control.
      HostControlParent.Child = newHostedControl;
      HostControlParent.SizeChanged += new SizeChangedEventHandler( OnHostSizeChanged );
    }
 
    /// <summary>
    /// Launches a process and grab its main window.
    /// </summary>
    /// <param name="param"></param>
    private void LoadGameViewportSessionThread()
    {
      const string kWindowClassName = "GTLGAME";
      const string kProcessName = @"game.exe";
      const string kProcessArgs = "-hooked";
      const int kTimeToWaitAfterProcessLaunch = 0;
 
      // Create the hosted session.
      GameViewportSession hs = new GameViewportSession(
        kProcessName, kProcessArgs, kWindowClassName, kTimeToWaitAfterProcessLaunch );
 
      hs.Host = this;
 
      // Prepare the hosted control in the UI thread.
      this.Dispatcher.BeginInvoke(
        (Action)(() => CreateHostedControl( hs )),
        System.Windows.Threading.DispatcherPriority.Normal );
 
      // Indicate that we are done loading the viewport.
      mViewportLoadingFinished = true;
    }
  }
 
  /// <summary>
  /// WPF control to embed the game process main window.
  /// The control needs the process to be already loaded through the GAV session.
  /// </summary>
  public class GameViewportHost : HwndHost
  {
    private GameViewportSession mHostedSession = null;
    private W32.HookProc mHookProc = null;
    private IntPtr mHookID;
 
    /// <summary>
    /// Construct the game viewport host.
    /// </summary>
    /// <param name="hostedSession"></param>
    public GameViewportHost(GameViewportSession hostedSession)
    {
      Debug.Assert( hostedSession != null );
      mHostedSession = hostedSession;
    }
 
    /// <summary>
    /// Grabbed the host process's main window.
    /// </summary>
    /// <param name="hwndParent"></param>
    /// <returns></returns>
    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
      IntPtr childWindow = mHostedSession.WindowHandle;
 
      // Set the client window style and new parent.
      // This steps is necessary so we can manage that externally created window.
      W32.SetParent( childWindow, hwndParent.Handle );
      W32.SetWindowLong( childWindow, (int)W32.GetWindowLongConst.GWL_STYLE,
        (int)W32.WindowStyles.WS_CHILD | (int)W32.WindowStyles.WS_VISIBLE |
        (int)W32.WindowStyles.WS_CLIPCHILDREN | (int)W32.WindowStyles.WS_CLIPSIBLINGS );
 
      // The the parent (being the host) of the client window.
      mHostedSession.Parent = hwndParent.Handle;
 
      // Make sure we have a window in our hands.
      Debug.Assert( childWindow != IntPtr.Zero && W32.IsWindow( childWindow ) );
 
      IntPtr hModule = W32.GetModuleHandle( mHostedSession.Process.MainModule.ModuleName );
      mHookProc = new W32.HookProc( MyCallbackFunction );
      mHookID = W32.SetWindowsHookEx( W32.HookType.WH_KEYBOARD_LL, mHookProc, hModule, 0 );
      return new HandleRef( this, childWindow );
    }
 
    /// <summary>
    /// Clean up any resources that the hosted window might of created. Right now? Nothing.
    /// </summary>
    /// <param name="hwnd"></param>
    protected override void DestroyWindowCore(HandleRef hwnd)
    {
      W32.UnhookWindowsHookEx( mHookID );
    }
 
    private int MyCallbackFunction(int code, IntPtr wParam, IntPtr lParam)
    {
      if ( code < 0 )
      {
        //you need to call CallNextHookEx without further processing
        //and return the value returned by CallNextHookEx
        return (int)W32.CallNextHookEx( IntPtr.Zero, (int)code, wParam, lParam );
      }
 
      W32.KBDLLHOOKSTRUCT kbd = (W32.KBDLLHOOKSTRUCT)Marshal.PtrToStructure( lParam, typeof( W32.KBDLLHOOKSTRUCT ) );
 
      if ( (uint)wParam == (uint)W32.WM.KEYUP && W32.GetForegroundWindow() == mHostedSession.WindowHandle )
      {
        PresentationSource source = PresentationSource.FromVisual( this );
        KeyEventArgs e = new KeyEventArgs( Keyboard.PrimaryDevice, source, 0, (Key)kbd.vkCode );
        e.RoutedEvent = Keyboard.KeyUpEvent;
        mHostedSession.Host.RaiseEvent( e );
        //InputManager.Current.ProcessInput( e );
      }
      //return the value returned by CallNextHookEx
      return (int)W32.CallNextHookEx( IntPtr.Zero, (int)code, wParam, lParam );
    }
  }
}

C++
—-

//////////////////////////////////////////////////////////////////////////////////
///
/// Acquires the focus. 
/// To be called when we click in the window. WM_?MOUSEDOWN?
///
//////////////////////////////////////////////////////////////////////////////////
void Application::AcquireFocus()
{
  if ( mCreationFlags & CW_HOOKED )
  {
    ::SetFocus( mWindowHandle );
    ::SetForegroundWindow( mWindowHandle );
  }
}
This entry was posted in Coding, WPF and tagged , . Bookmark the permalink.

2 Responses to Host a process window in WPF

  1. Toon says:

    Hi,

    Nice piece of code. I need something like this to get work a delphi form (from a dll) hosted in a wpf form. The focus, tab and keys don’t work well. Do you have a sample solution of this to download? I have some problems with the W32 calls

    thanks and regards,

    Toon

  2. Jonathan says:

    Hi,

    You could always try to reroute events from the delphi form to your WPF app. See the method BuildWindowCore below.

    ///

    /// Grabbed the host process’s main window.
    ///

    /// ///
    protected override HandleRef BuildWindowCore(HandleRef hwndParent)
    {
    IntPtr childWindow = mHostedSession.WindowHandle;

    // Set the client window style and new parent.
    // This steps is necessary so we can manage that externally created window.
    W32.SetParent( childWindow, hwndParent.Handle );
    W32.SetWindowLong( childWindow, (int)W32.GetWindowLongConst.GWL_STYLE,
    (int)W32.WindowStyles.WS_CHILD );

    // The the parent (being the host) of the client window.
    mHostedSession.Parent = hwndParent.Handle;

    // Make sure we have a window in our hands.
    Debug.Assert( childWindow != IntPtr.Zero && W32.IsWindow( childWindow ) );

    // Attach game’s input to the editor’s window.
    mClientThreadID = W32.GetWindowThreadProcessId( childWindow, IntPtr.Zero );
    mHostThreadID = W32.GetWindowThreadProcessId( hwndParent.Handle, IntPtr.Zero );
    W32.AttachThreadInput( mClientThreadID, mHostThreadID, true );

    return new HandleRef( this, childWindow );
    }

    If this doesn’t work. You can always try to override :

    virtual bool TabInto(TraversalRequest request)
    virtual bool TranslateAccelerator(MSG msg, ModifierKeys modifiers)
    etc. (see base classes of HwndHost)

Leave a Reply

Your email address will not be published. Required fields are marked *