在WPF窗口中托管外部应用程序

45
我们正在开发一个 WPF 布局管理器,其中视口可以被用户移动/调整大小等。视口通常通过由我们在布局管理器中控制的提供程序填充数据(图片/电影等)。我的工作是检查是否也可能在视口中托管任何外部 Windows 应用程序(即记事本、计算器、Adobe Reader 等)。我遇到了一些问题。
大部分资源都指向使用 HwndHost 类。我正在尝试使用 Microsoft 自身的这个演示文稿:http://msdn.microsoft.com/en-us/library/ms752055.aspx 我已将列表框替换为来自外部应用程序的 Windows 句柄。有人能帮我回答以下问题吗:
  1. 演示文稿添加了一个额外的静态子窗口,在其中放置了 ListBox。我认为对于外部应用程序我不需要那个。如果我省略它,我必须将外部应用程序设置为子窗口(使用 user32.dll 中的 Get/SetWindowLong 设置 GWL_STYLEWS_CHILD)。但如果我这样做,应用程序的菜单栏会消失(因为 WS_CHILD 样式)并且它不再接收输入。
  2. 如果我使用子窗口,并使外部应用程序成为其子窗口,事情就会比较顺利,但有时外部应用程序无法正常绘制。
  3. 此外,我需要子窗口调整大小以适应视口。这是可能的吗?
  4. 当外部应用程序生成一个子窗口(即记事本->帮助->关于),该窗口未托管到 HwndHost(因此可以移动到视口之外)。有没有办法防止这种情况发生?
  5. 由于我不需要外部应用程序和布局管理器之间的进一步交互,我是否正确地假设我不需要捕获和转发消息?(演示文稿向子窗口添加了 HwndSourceHook 来捕获列表框中的选择更改。)
  • 当您运行(未修改的)VS2010示例并关闭窗口时,VS2010看不到程序已结束。如果使用“Break-all”,则会进入没有源代码的汇编中。发生了一些可疑的事情,但我找不到原因。
  • 这个演练本身似乎编码很松散,但我没有找到关于此主题的任何更好的文档。还有其他示例吗?
  • 另一种方法是不使用HwndHost而使用WindowsFormHost,如此处所讨论的(链接)。它有效(并且更简单!),但我无法控制应用程序的大小?此外,WinFormHost并不真正适用于此?
  • 感谢任何指向正确方向的指针。


    嗨,我肯定会选择第8点。 - Davide Piras
    6个回答

    36

    如果这个问题在20年前提出,答案可能是:“当然可以,看看‘OLE’吧!” 这里有一个链接介绍了什么是“对象链接和嵌入”:

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

    如果你阅读这篇文章,你会发现该规范定义了多少接口,这并不是因为作者觉得这很有趣,而是由于在普通情况下实现它非常困难。

    事实上,一些应用程序(主要是微软的应用程序,因为微软几乎是OLE的唯一赞助商)仍然支持它。

    你可以使用称为DSOFramer的东西来嵌入这些应用程序(参见SO上的这些链接:MS KB311765和DsoFramer已经从Microsoft网站消失),这是一种允许你在应用程序中直观地托管OLE服务器(即作为另一个进程运行的外部应用程序)的组件。 它是微软几年前发布的某种大型黑客,已经不再得到支持,以至于其二进制文件非常难以找到!

    它(可能)仍然适用于简单的OLE服务器,但我认为我曾经在某个地方读到过它甚至无法针对像Word 2010这样的新版Microsoft应用程序运行。

    所以,你可以在支持它的应用程序中使用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“传统”黑客技巧。您还可以按照此处所述的方法删除不喜欢的项目菜单(如何从表单的控制菜单框中删除菜单项)。

    您还可以将“notepad.exe”替换为“winword.exe”,看起来它也可以工作。但是,这种方法有其局限性(键盘、鼠标、焦点等)。

    祝您好运!


    4
    等等,.NET程序员认为WINApi调用是“黑客行为”吗? o_O - Tamás Szelei
    1
    @TamásSzelei - API是被支持的,但你不应该玩弄别人的应用程序,比如更改其标题、框架和父子关系。这很可能会导致目标应用程序崩溃。 - Simon Mourier
    1
    好的。顺便说一下,MainWindowHandle经常无法使用(例如最新的IE、Chrome、Firefox)。更可靠的方法是使用FindWindow和窗口类来获取hwnd。除此之外,您知道如何在单击外部应用程序时保持窗口焦点吗? - Tamás Szelei
    这种方法适用于托管QT应用程序吗?我无法使其工作。 - ricky
    @ricky - 我不知道,但是没有什么是保证的,你应该问另一个问题。 - Simon Mourier
    显示剩余2条评论

    9

    Simon Mourier的回答写的非常好。不过,当我试着将其用于我自己制作的winform应用程序时,它失败了。

    _process.WaitForInputIdle();
    

    可以被替换为

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

    一切顺利进行。

    感谢你的好问题和所有人的回答。


    3
    我认为使用Thread.Sleep(1)阻塞UI线程不是一个好主意 - 这将破坏使用默认同步上下文的WPF或WinForms中的异步代码的性能。例如,您可以将Thread.Sleep(1)替换为Application.DoEvents();await Task.Yield() - Dai

    7
    在阅读了本主题中的答案并进行了一些试验后,我最终得到了一个相当好用的东西,但对于特殊情况,需要您注意一些事项。
    我使用HwndHostEx作为我的宿主类的基类,您可以在此处找到它:http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/69631#1034035 示例代码:
    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);
    
            const int GWL_STYLE = -16;
            const int BORDER = 0x00800000;
            const int DLGFRAME = 0x00400000;
            const int WS_CAPTION = BORDER | DLGFRAME;
            const int WS_THICKFRAME = 0x00040000;
            const int WS_CHILD = 0x40000000;
    
            int style = GetWindowLong(notepadHandle, GWL_STYLE);
            style = style & ~WS_CAPTION & ~WS_THICKFRAME; // Removes Caption bar and the sizing border
            style |= 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)。

    只需在 WPF 窗口中将 NotepadHwndHost 添加为子元素,即可在此处托管记事本窗口。


    2
    解决方案非常复杂,需要大量的代码。以下是一些提示。
    首先,你走在了正确的轨道上。
    你必须使用HwndHost和HwndSource。如果不这样做,你会得到视觉伪影,比如闪烁。警告:如果你不使用宿主和源,它看起来像可以工作,但最终它不会工作——它将有随机的小错误。
    查看这个链接以获取一些提示。它不完整,但会帮助你朝着正确的方向前进。 http://microsoftdwayneneed.codeplex.com/SourceControl/changeset/view/50925#1029346 你需要进入Win32来控制你所问的许多内容。你确实需要捕获并转发消息。你确实需要控制哪些窗口“拥有”子窗口。
    经常使用Spy++。

    2

    我在一个 WPF 应用程序中将这个东西投入生产,目前来看一切都很好。请确保从拥有 window 的 UI 线程调用 SetNativeWindowInWPFWindowAsChild()

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

    1

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接