web-dev-qa-db-fra.com

Hébergement d'une application externe dans la fenêtre WPF

Nous développons actuellement un gestionnaire de disposition dans WPF avec des fenêtres pouvant être déplacées/redimensionnées/etc. par un utilisateur. Les fenêtres sont généralement remplies de données (images/films/etc.) via des fournisseurs sous notre contrôle dans le gestionnaire de disposition. Mon travail consiste à examiner s’il est également possible d’héberger toute application Windows externe (bloc-notes, calculateur, lecteur Adobe, etc.) dans une fenêtre. Je rencontre un certain nombre de problèmes.

La plupart des ressources indiquent l'utilisation de la classe HwndHost. J'expérimente cette procédure de Microsoft elle-même: http://msdn.Microsoft.com/en-us/library/ms752055.aspx

J'ai adapté cela pour que la liste soit remplacée par le handle de Windows de l'application externe. Quelqu'un peut-il m'aider avec ces questions:

  1. La procédure pas à pas ajoute une sous-fenêtre statique supplémentaire dans laquelle la variable ListBox est placée. Je ne pense pas avoir besoin de cela pour les applications externes. Si je l'omets, je dois faire de l'application externe une fenêtre enfant (à l'aide de Get/SetWindowLong à partir de user32.dll pour définir GWL_STYLE en tant que WS_CHILD). Mais si je le fais, la barre de menus de l'application disparaît (à cause du style WS_CHILD) et elle ne reçoit plus d'entrée. 
  2. Si j'utilise la sous-fenêtre et fais de l'application externe un enfant de ce genre de chose, cela fonctionne raisonnablement, mais parfois l'application externe ne permet pas de peindre correctement. 
  3. En outre, j'ai besoin de la fenêtre enfant pour redimensionner la fenêtre. Est-ce possible? 
  4. Lorsque l'application externe crée une fenêtre enfant (par exemple, le Bloc-notes-> Aide-> À propos de), cette fenêtre n'est pas hébergée par la HwndHost (et peut donc être déplacée en dehors de la fenêtre d'affichage). Y at-il un moyen que je peux empêcher cela? 
  5. Puisque je n'ai plus besoin d'interaction entre l'application externe et le gestionnaire de disposition, ai-je raison de supposer que je n'ai pas besoin de récupérer et de transférer des messages? (la procédure ajoute un HwndSourceHook à la sous-fenêtre pour capturer les changements de sélection dans la liste). 
  6. Lorsque vous exécutez l'exemple VS2010 (non modifié) et fermez la fenêtre, VS2010 ne voit pas que le programme s'est terminé. Si vous casser tout, vous vous retrouvez dans Assembly sans source. Quelque chose de malodorant se passe, mais je ne le trouve pas. 
  7. La procédure en elle-même semble être très codée, mais je n'ai pas trouvé de meilleure documentation sur ce sujet. D'autres exemples?
  8. Une autre approche consiste à ne pas utiliser HwndHost mais WindowsFormHost comme discuté ici . Cela fonctionne (et est beaucoup plus simple!), Mais je n'ai pas de contrôle sur la taille de l'application? En outre, WinFormHost n'est pas vraiment destiné à cela? 

Merci pour tous les conseils dans la bonne direction.

37
Stiggy

Eh bien ... si la question avait été posée il y a 20 ans, on aurait répondu: "Bien sûr, regardez" OLE "!", Voici un lien vers ce qui est "Liaison et incorporation d'objet":

http://en.wikipedia.org/wiki/Object_Linking_and_Embedding

Si vous lisez cet article, vous verrez le nombre d'interfaces définies par cette spécification, non pas parce que son auteur l'a trouvé amusant, mais parce que c'est techniquement difficile à atteindre dans les cas généraux 

Il est en fait toujours pris en charge par certaines applications (principalement celles de Microsoft, car Microsoft était presque le seul sponsor d'OLE ...)

Vous pouvez intégrer ces applications en utilisant quelque chose appelé DSOFramer (voir les liens ici sous SO: MS KB311765 et DsoFramer sont manquants sur le site MS ), un composant qui vous permet d’héberger OLE serveur (c'est-à-dire: applications s'exécutant en tant que processus différent) visuellement à l'intérieur d'une application. C'est une sorte de gros bidouillage laissé par Microsoft il y a quelques années, qui n'est plus supporté au point que les fichiers binaires sont assez difficiles à trouver!

Cela fonctionne toujours pour de simples OLE serveurs, mais je pense avoir lu quelque part que cela ne fonctionne même pas pour les nouvelles applications Microsoft telles que Word 2010 . Vous pouvez donc utiliser DSOFramer pour les applications prenant en charge il. Tu peux l'essayer.

Pour d'autres applications, eh bien, aujourd'hui, dans le monde moderne dans lequel nous vivons, vous n'hébergez pas applications, exécuté en processus externe, vous hébergez composants, et elles sont en général supposées pour exécuter inprocess. C'est pourquoi vous aurez de grandes difficultés à faire ce que vous voulez faire en général. Un des problèmes que vous rencontrerez (et pas des moindres avec les versions récentes de Windows) est la sécurité: comment votre processus auquel je ne fais pas confiance peut légitimement gérer des mes fenêtres et menus créés par mon processus :-)?

Néanmoins, vous pouvez faire pas mal d’application par application, en utilisant divers hack Windows . SetParent est fondamentalement la mère de tous les hacks :-)

Voici un morceau de code qui étend l'exemple que vous avez indiqué, en ajoutant un redimensionnement automatique et la suppression de la zone de légende . Il montre comment supprimer implicitement la zone de contrôle, le menu système, à titre d'exemple:

public partial class Window1 : Window
{
    private System.Windows.Forms.Panel _panel;
    private Process _process;

    public Window1()
    {
        InitializeComponent();
        _panel = new System.Windows.Forms.Panel();
        windowsFormsHost1.Child = _panel;
    }

    [DllImport("user32.dll")]
    private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);

    [DllImport("user32.dll", SetLastError = true)]
    private static extern int GetWindowLong(IntPtr hWnd, int nIndex);

    [DllImport("user32")]
    private static extern IntPtr SetParent(IntPtr hWnd, IntPtr hWndParent);

    [DllImport("user32")]
    private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, int uFlags);

    private const int SWP_NOZORDER = 0x0004;
    private const int SWP_NOACTIVATE = 0x0010;
    private const int GWL_STYLE = -16;
    private const int WS_CAPTION = 0x00C00000;
    private const int WS_THICKFRAME = 0x00040000;

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        button1.Visibility = Visibility.Hidden;
        ProcessStartInfo psi = new ProcessStartInfo("notepad.exe");
        _process = Process.Start(psi);
        _process.WaitForInputIdle();
        SetParent(_process.MainWindowHandle, _panel.Handle);

        // remove control box
        int style = GetWindowLong(_process.MainWindowHandle, GWL_STYLE);
        style = style & ~WS_CAPTION & ~WS_THICKFRAME;
        SetWindowLong(_process.MainWindowHandle, GWL_STYLE, style);

        // resize embedded application & refresh
        ResizeEmbeddedApp();
    }

    protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
    {
        base.OnClosing(e);
        if (_process != null)
        {
            _process.Refresh();
            _process.Close();
        }
    }

    private void ResizeEmbeddedApp()
    {
        if (_process == null)
            return;

        SetWindowPos(_process.MainWindowHandle, IntPtr.Zero, 0, 0, (int)_panel.ClientSize.Width, (int)_panel.ClientSize.Height, SWP_NOZORDER | SWP_NOACTIVATE);
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        Size size = base.MeasureOverride(availableSize);
        ResizeEmbeddedApp();
        return size;
    }
}

C’est fondamentalement tous les bidouilles "traditionnelles" de Windows Vous pouvez également supprimer des menus d'éléments que vous n'aimez pas, comme expliqué ici: http://support.Microsoft.com/kb/110393/en-us (Comment faire pour supprimer des éléments de menu de la zone de menu Contrôle d'un formulaire ).

Vous pouvez également remplacer "notepad.exe" par "winword.exe" et le semble fonctionner. Mais il y a des limites à cela (clavier, souris, focus, etc.).

Bonne chance!

23
Simon Mourier

Après avoir lu les réponses dans ce fil de discussion et fait quelques essais et erreurs moi-même, je me suis retrouvé avec quelque chose qui fonctionne plutôt bien, mais bien sûr, certaines choses nécessiteront votre attention pour des cas particuliers.

J'ai utilisé HwndHostEx comme classe de base pour ma classe Host, vous pouvez le trouver ici: http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/69631#1034035

Exemple de code:

public class NotepadHwndHost : HwndHostEx
{
    private Process _process;

    protected override HWND BuildWindowOverride(HWND hwndParent)
    {
        ProcessStartInfo psi = new ProcessStartInfo("notepad.exe");
        _process = Process.Start(psi);
        _process.WaitForInputIdle();

        // The main window handle may be unavailable for a while, just wait for it
        while (_process.MainWindowHandle == IntPtr.Zero)
        {
            Thread.Yield();
        }

        HWND hwnd = new HWND(_process.MainWindowHandle);

        int style = NativeMethods.GetWindowLong(hwnd, GWL.STYLE);

        style = style & ~((int)WS.CAPTION) & ~((int)WS.THICKFRAME); // Removes Caption bar and the sizing border
        style |= ((int)WS.CHILD); // Must be a child window to be hosted

        NativeMethods.SetWindowLong(hwnd, GWL.STYLE, style);

        return hwnd;
    }

    protected override void DestroyWindowOverride(HWND hwnd)
    {
        _process.CloseMainWindow();

        _process.WaitForExit(5000);

        if (_process.HasExited == false)
        {
            _process.Kill();
        }

        _process.Close();
        _process.Dispose();
        _process = null;
        hwnd.Dispose();
        hwnd = null;
    }
}

Le HWND, NativeMethods et les énumérations viennent également de la bibliothèque DwayneNeed (Microsoft.DwayneNeed.User32).

Ajoutez simplement le NotepadHwndHost en tant qu’enfant dans une fenêtre WPF et vous devriez voir la fenêtre du bloc-notes hébergée ici.

4
TGasdf

La réponse de Simon Mourier est extrêmement bien écrite. Cependant, lorsque j'ai essayé avec une application Winform créée par moi-même, cela a échoué.

_process.WaitForInputIdle();

peut être remplacé par 

while (_process.MainWindowHandle==IntPtr.Zero)
            {
                Thread.Sleep(1);
            }

et tout se passe bien.

Merci pour cette excellente question et merci à vous tous pour vos réponses.

4
PauLEffect

J'ai ce travail en production et jusqu'ici tout va bien dans une application WPF. Assurez-vous d'appeler SetNativeWindowInWPFWindowAsChild() à partir du thread d'interface utilisateur qui possède window.

    public static bool SetNativeWindowInWPFWindowAsChild(IntPtr hWndNative, Window window)
    {
        UInt32 dwSyleToRemove = WS_POPUP | WS_CAPTION | WS_THICKFRAME;
        UInt32 dwExStyleToRemove = WS_EX_DLGMODALFRAME | WS_EX_WINDOWEDGE | WS_EX_CLIENTEDGE | WS_EX_STATICEDGE;

        UInt32 dwStyle = GetWindowLong(hWndNative, GWL_STYLE);
        UInt32 dwExStyle = GetWindowLong(hWndNative, GWL_EXSTYLE);

        dwStyle &= ~dwSyleToRemove;
        dwExStyle &= ~dwExStyleToRemove;

        SetWindowLong(hWndNative, GWL_STYLE, dwStyle | WS_CHILD);
        SetWindowLong(hWndNative, GWL_EXSTYLE, dwExStyle);

        IntPtr hWndOld = SetParent(hWndNative, new WindowInteropHelper(window).Handle);
        if (hWndOld == IntPtr.Zero)
        {
            System.Diagnostics.Debug.WriteLine("SetParent() Failed -> LAST ERROR: " + Marshal.GetLastWin32Error() + "\n");
        }
        return hWndOld != IntPtr.Zero;
    }

Voici l'API Win32 native que j'ai utilisée. (Il y a des extras ici parce que je taille/focalise la fenêtre après son réglage)

        [StructLayout(LayoutKind.Sequential)]
        private struct RECT
        {
            public Int32 left;
            public Int32 top;
            public Int32 right;
            public Int32 bottom;
        }
        [DllImport("user32.dll", SetLastError = true)]
        private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
        [DllImport("user32.dll")]
        private static extern UInt32 SetWindowLong(IntPtr hWnd, int nIndex, UInt32 dwNewLong);
        [DllImport("user32.dll")]
        private static extern UInt32 GetWindowLong(IntPtr hWnd, int nIndex);
        [DllImport("user32.dll")]
        private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
        [DllImport("user32.dll")]
        private static extern IntPtr SetFocus(IntPtr hWnd);
        [DllImport("user32.dll")]
        private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, SetWindowPosFlags uFlags);

        private static int GWL_STYLE = -16;
        private static int GWL_EXSTYLE = -20;

        private static UInt32 WS_CHILD = 0x40000000;
        private static UInt32 WS_POPUP = 0x80000000;
        private static UInt32 WS_CAPTION = 0x00C00000;
        private static UInt32 WS_THICKFRAME = 0x00040000;

        private static UInt32 WS_EX_DLGMODALFRAME = 0x00000001;
        private static UInt32 WS_EX_WINDOWEDGE = 0x00000100;
        private static UInt32 WS_EX_CLIENTEDGE = 0x00000200;
        private static UInt32 WS_EX_STATICEDGE = 0x00020000;

        [Flags]
        private enum SetWindowPosFlags : uint
        {
            SWP_ASYNCWINDOWPOS = 0x4000,
            SWP_DEFERERASE = 0x2000,
            SWP_DRAWFRAME = 0x0020,
            SWP_FRAMECHANGED = 0x0020,
            SWP_HIDEWINDOW = 0x0080,
            SWP_NOACTIVATE = 0x0010,
            SWP_NOCOPYBITS = 0x0100,
            SWP_NOMOVE = 0x0002,
            SWP_NOOWNERZORDER = 0x0200,
            SWP_NOREDRAW = 0x0008,
            SWP_NOREPOSITION = 0x0200,
            SWP_NOSENDCHANGING = 0x0400,
            SWP_NOSIZE = 0x0001,
            SWP_NOZORDER = 0x0004,
            SWP_SHOWWINDOW = 0x0040
        }
        private static readonly IntPtr HWND_TOPMOST = new IntPtr(-1);
        private static readonly IntPtr HWND_NOTOPMOST = new IntPtr(-2);
        private static readonly IntPtr HWND_TOP = new IntPtr(0);
        private static readonly IntPtr HWND_BOTTOM = new IntPtr(1);
1
Andy

La solution est incroyablement complexe. Beaucoup de code. Voici quelques conseils. 

D'abord, vous êtes sur la bonne voie. 

Vous devez utiliser les options HwndHost et HwndSource. Si vous ne le faites pas, vous aurez des artefacts visuels. Comme le scintillement. Un avertissement, si vous n'utilisez pas l'hôte et la source, il semblera que cela fonctionnera, mais ce ne sera finalement pas le cas - il y aura des petits bugs stupides au hasard.

Jetez un coup d'oeil à ceci pour quelques astuces. Ce n'est pas complet, mais cela vous aidera à aller dans la bonne direction . http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/50925#1029346

Vous devez entrer dans Win32 pour contrôler beaucoup de ce que vous demandez. Vous devez attraper et transférer des messages. Vous devez contrôler quelles fenêtres "possèdent" les fenêtres enfants.

Utilisez Spy ++ beaucoup.

1
010110110101

Découvrez ma réponse à: Comment exécuter une application dans l’application wpf?

J'ai réussi à obtenir l'exemple du bloc-notes sans DwayneNeed jiggery. Je viens d'ajouter SetParent () et boom ... elle fonctionne exactement comme dans l'exemple de DwayneNeed.

0
Youngy