Размещение внешнего приложения в окне WPF

Мы разрабатываем менеджер макетов в WPF, который имеет видовые окна, которые могут быть перемещены / изменены по размеру / и т.д. пользователем. Окна просмотра обычно заполняются данными (изображения / фильмы / и т. Д.) Через провайдеров, которые находятся под нашим контролем в менеджере макетов. Моя работа состоит в том, чтобы проверить, возможно ли также разместить какое-либо внешнее приложение Windows (например, блокнот, Calc, Adobe Reader и т. Д.) В области просмотра. Я сталкиваюсь с рядом проблем.

Большинство ресурсов указывают на использование класса HwndHost. Я экспериментирую с этим пошаговым руководством от самой Microsoft: http://msdn.microsoft.com/en-us/library/ms752055.aspx

Я адаптировал это так, чтобы окно списка заменялось дескриптором окна из внешнего приложения. Кто-нибудь может мне помочь с этими вопросами:

  1. Пошаговое руководство добавляет дополнительное статическое подокно, в котором ListBox размещен. Я не думаю, что мне это нужно для внешних приложений. Если я пропущу его, я должен сделать внешнее приложение дочерним окном (используя Get/SetWindowLong из user32.dll для установки GWL_STYLE как WS_CHILD). Но если я это сделаю, строка меню приложения исчезнет (из-за WS_CHILD стиль), и он больше не получает ввод.
  2. Если я использую подокно и сделаю внешнее приложение дочерним, то разумно, но внешнее приложение не рисует нормально.
  3. Кроме того, мне нужно дочернее окно, чтобы изменить размер окна просмотра. Это возможно?
  4. Когда внешнее приложение порождает дочернее окно (т.е. Блокнот-> Справка-> О программе), это окно не размещается HwndHost (и, следовательно, может быть перемещен за пределы области просмотра). Есть ли способ, которым я могу предотвратить это?
  5. Поскольку мне больше не нужно взаимодействие между внешним приложением и менеджером компоновки, правильно ли я считаю, что мне не нужно перехватывать и пересылать сообщения? (пошаговое руководство добавляет HwndSourceHook в подокно, чтобы отследить изменения выбора в списке).
  6. Когда вы запускаете (неизмененный) пример VS2010 и закрываете окно, VS2010 не видит, что программа завершилась. Если вы разбиваете все, вы в конечном итоге в сборке без источника. Что-то вонючее происходит, но я не могу найти это.
  7. Само пошаговое руководство кажется очень неаккуратным, но я не нашел лучшей документации по этому вопросу. Есть еще примеры?
  8. Другой подход не использовать HwndHost но WindowsFormHost как обсуждено здесь. Это работает (и намного проще!), Но у меня нет контроля над размером приложения? Кроме того, WinFormHost не предназначен для этого?

Спасибо за любые указатели в правильном направлении.

6 ответов

Что ж... если бы вопрос был задан примерно 20 лет назад, можно было бы ответить "Конечно, посмотрите на" OLE "!", Вот ссылка на то, что такое "Связывание и внедрение объектов":

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

Если вы прочитаете эту статью, вы увидите количество интерфейсов, определенных этой спецификацией, не потому, что ее автор считал это забавным, а потому, что это технически сложно достичь в общих случаях.

На самом деле он все еще поддерживается некоторыми приложениями (в основном, Microsoft, поскольку Microsoft была почти единственным спонсором OLE...)

Вы можете встроить эти приложения, используя что-то под названием DSOFramer (см. Ссылки здесь на SO: MS KB311765 и DsoFramer отсутствуют на сайте MS), компонент, который позволяет вам визуально размещать OLE-сервер (т.е. внешние приложения, выполняющиеся как другой процесс) внутри приложения, Это своего рода большой взлом, выпущенный Microsoft несколько лет назад, который больше не поддерживается настолько, что найти двоичные файлы довольно сложно!

Он (может) все еще работает для простых OLE-серверов, но я думаю, что где-то читал, что он даже не работает для новых приложений Microsoft, таких как Word 2010. Таким образом, вы можете использовать DSOFramer для приложений, которые его поддерживают. Можешь попробовать.

Что касается других приложений, то в современном мире, в котором мы живем, вы не размещаете приложения, запускаете их во внешних процессах, размещаете компоненты, и они, как правило, должны выполняться в процессе. Вот почему у вас будут большие трудности делать то, что вы хотите делать в целом. Одна из проблем, с которой вы столкнетесь (и не в последнюю очередь с последними версиями Windows), - это безопасность: как ваш процесс, которому я не доверяю, может законно обрабатывать мои окна и меню, созданные моим процессом:-)?

Тем не менее, вы можете сделать довольно много приложений за приложениями, используя различные взломы Windows. SetParent - мать всех хаков:-)

Вот фрагмент кода, который расширяет пример, который вы указали, добавляя автоматическое изменение размера и удаление поля заголовка. Это демонстрирует, как неявно удалить блок управления, системное меню, в качестве примера:

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;
    }
}

Это в основном все Windows "традиционные" хаки. Вы также можете удалить меню элементов, которые вам не нравятся, как описано здесь: http://support.microsoft.com/kb/110393/en-us (Как удалить элементы меню из поля меню элемента управления формы).

Вы также можете заменить "notepad.exe" на "winword.exe", и это, кажется, работает. Но есть ограничения (клавиатура, мышь, фокус и т. Д.).

Удачи!

Ответ Саймона Мурье очень хорошо написан. Однако, когда я попробовал это с приложением winform, сделанным мной, это потерпело неудачу.

_process.WaitForInputIdle();

можно заменить на

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

и все идет гладко.

Спасибо за отличный вопрос и всем вам за ваши ответы.

Прочитав ответы в этой теме и выполнив несколько проб и ошибок, я получил кое-что, что работает довольно хорошо, но, конечно, некоторые вещи потребуют вашего внимания в особых случаях.

Я использовал HwndHostEx в качестве базового класса для своего хост-класса, вы можете найти его здесь: http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/69631

Пример кода:

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;
    }
}

HWND, NativeMethods и перечисления также поступают из библиотеки DwayneNeed (Microsoft.DwayneNeed.User32).

Просто добавьте NotepadHwndHost как дочерний элемент в окне WPF, и вы должны увидеть окно блокнота, размещенное там.

У меня это работает в производстве и пока хорошо работает в приложении WPF. Убедитесь, что вы звоните SetNativeWindowInWPFWindowAsChild() из пользовательского интерфейса потока, который владеет 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;
    }

Вот родной Win32 API, который я использовал. (Здесь есть дополнительные функции, потому что я изменяю размер / фокусирую окно после его установки)

        [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);

Решение невероятно сложное. Много кода. Вот несколько советов.

Во-первых, вы на правильном пути.

Вы должны использовать HwndHost и HwndSource. Если вы этого не сделаете, вы получите визуальные артефакты. Как мерцание. Предупреждение: если вы не используете Host и Source, может показаться, что это сработает, но в итоге это не сработает - у него будут случайные маленькие глупые ошибки.

Посмотрите на это для некоторых подсказок. Это не полный, но это поможет вам идти в правильном направлении. http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/50925

Вы должны войти в Win32, чтобы контролировать многое из того, о чем вы спрашиваете. Вам нужно ловить и пересылать сообщения. Вам нужно контролировать, какие окна "владеют" дочерними окнами.

Используйте Spy++ много.

Проверьте мой ответ на: Как запустить приложение внутри приложения wpf?

Мне удалось получить пример работы с блокнотом без использования DwayneNeed. Я просто добавил SetParent() и boom... она работает так же, как пример DwayneNeed.

Другие вопросы по тегам