使用Win32 SetParent()在非WPF应用程序中托管WPF应用程序

10
我有一个 WPF 应用程序,我希望它看起来像是运行在另一个非 WPF 应用程序中。在现实生活中,这个非 WPF 应用程序是 Internet Explorer 中的 ActiveX,但是为了说明问题,我使用了一个简单的 Windows Forms 应用程序。
我使用了 Windows API 函数 SetParent,关于它已经有很多线程了。然而,我找不到任何关于我的确切问题的解决方案:WPF 应用程序的右下角有一小部分没有被绘制在非 WPF 应用程序的窗口内
WPF 窗口自己运行时的截图:
alt text WPF 窗口作为 WinForm 应用程序的父窗口时的截图:
alt text 如果我使用 WinForms 应用程序或纯 Win32 应用程序(如记事本)替换 WPF 应用程序,则不会出现此问题。
WinForm 代码如下:
private void Form1_Load(object sender, EventArgs e)
    {
        // Start process
        var psi = new ProcessStartInfo("c:\\wpfapp\\wpfapp\\bin\\Debug\\wpfapp.exe");
        psi.WindowStyle = ProcessWindowStyle.Normal;
        _process = Process.Start(psi);

        // Sleep until new process is ready
        Thread.Sleep(1000);

        // Set new process's parent to this window
        SetParent(_process.MainWindowHandle, this.Handle);

        // Remove WS_POPUP and add WS_CHILD window style to child window
        const int GWL_STYLE = -16;
        const long WS_POPUP = 0x80000000;
        const long WS_CHILD = 0x40000000;
        long style = GetWindowLong(_process.MainWindowHandle, GWL_STYLE);
        style = (style & ~(WS_POPUP)) | WS_CHILD;
        SetWindowLong(_process.MainWindowHandle, GWL_STYLE, style);

        // Move and resize child window to fit into parent's
        MoveWindow(_process.MainWindowHandle, 0, 0, this.Width, this.Height, true);
    }

注意:我知道使用SetParent不一定是推荐的做法,但我想要并且需要找出如何以这种方式实现它,所以请允许我 :)


我应该补充说明,我认为MoveWindow引起了“裁剪”。在此之前一切都很好,但是WPF显然有错误的位置和大小。 - jonsb
使用Silverlight不是更容易吗? - TeaDrivenDev
我没有看到您在哪里设置父窗口的大小,而且它显然太小了。您需要调整父窗口的大小,以便在减去非客户区域后,客户区域的大小符合您 WPF 应用程序所需的大小。 - John Knoeller
John Knoeller:我尝试了不同的窗体启动大小,包括最大化,但没有任何区别。WPF的内容总是会被绘制在外面。(我理解上面小窗体的大小可能会误导) - jonsb
GCATNM:不,这是一个完整的.NET应用程序,但客户想要它在浏览器内。我尝试了常规的通过ActiveX进行主机托管方式,但IE进程变得太大并崩溃(而且微软不建议这样做)。XBAP可能是长期解决方案,但我还需要一个快速的短期解决方案,不需要太多重写。 - jonsb
3个回答

6
我找到了一个相当好的解决办法:在SetParent之前调用MoveWindow函数:
private void Form1_Load(object sender, EventArgs e)
{
     // Start process            
     var psi = new ProcessStartInfo("C:\\WpfApp\\WpfApp.exe");
     psi.WindowStyle = ProcessWindowStyle.Normal;
     _process = Process.Start(psi);

     // Sleep until new process is ready
     Thread.Sleep(3000);

     // Move and resize child window to fit into parent's
     Rectangle rect;
     GetClientRect(this.Handle, out rect);
     MoveWindow(_process.MainWindowHandle, 0, 0, rect.Right - rect.Left, rect.Bottom - rect.Top, true);

     // Set new process's parent to this window
     SetParent(_process.MainWindowHandle, this.Handle);

     // Remove WS_POPUP and add WS_CHILD window style to child window
     long style = GetWindowLong(_process.MainWindowHandle, GWL_STYLE);
     style = (style & ~(WS_POPUP) & ~(WS_CAPTION)) & ~(WS_THICKFRAME) | WS_CHILD;
     SetWindowLong(_process.MainWindowHandle, GWL_STYLE, style);
}           

在 SizeChanged 事件处理程序中,我将子级从父级中取出,调用 MoveWindow 并将其移回到父级。为了减少闪烁,我在执行这些操作时隐藏窗口:
private void Form1_SizeChanged(object sender, EventArgs e)
{
     ShowWindow(_process.MainWindowHandle, SW_HIDE);

     var ptr = new IntPtr();
     SetParent(_process.MainWindowHandle, ptr);

     Rectangle rect;
     GetClientRect(this.Handle, out rect);

     MoveWindow(_process.MainWindowHandle, 0, 0, rect.Right - rect.Left, rect.Bottom - rect.Top, true);            
     SetParent(_process.MainWindowHandle, this.Handle);
     ShowWindow(_process.MainWindowHandle, SW_SHOW);
}

这并没有解释为什么在SetParent之后MoveWindow无法工作,但它解决了我的问题,所以除非有更好的解决方法,否则我会将其标记为答案。


最好使用execProcess.WaitForInputIdle()来等待进程运行并处于空闲状态,而不是使用Thread.Sleep(3000)。 - Spawnrider

3

你可能需要调整 WPF 窗口的大小,使其适应其新父窗口的 客户端区域

[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

[DllImport("user32.dll")]
static extern bool GetClientRect(IntPtr hWnd, out RECT lpRect);

// Move and resize child window to fit into parent's client area.
RECT rc = new RECT();
GetClientRect(this.Handle, out rc);
MoveWindow(_process.MainWindowHandle, 0, 0, rc.Right - rc.Left,
    rc.Bottom - rc.Top, true)

谢谢,你说得对,关于客户区域的问题。我按照上述方法操作后,现在在父窗口之外绘制的区域比以前少了一些。似乎右侧绘制在父窗口之外的区域等于左右父边框的宽度,底部绘制在父窗口之外的区域等于顶部和底部父标题栏和边框的高度。如果我将父窗口的边框设置为“无”,那么就可以解决这个问题。这是怎么回事? - jonsb
@jonsb,奇怪的是客户区域不应该考虑窗口装饰。你能否p/invoke GetWindowRect()this.Handle上并确认结果的宽度和高度与GetClientRect()相同? - Frédéric Hamidi
是的,GetClientRect() 函数没有将窗口装饰考虑在内,导致窗口大小不正确。GetWindowRect() 返回的宽度和高度也比实际大。然而,WPF 窗口的内容却绘制在其自身窗口之外。 - jonsb

0
我使用了一个带有面板的表单/用户控件。然后,我将该面板用作新父级,并使用SetWindowPos设置子窗口的新位置和大小。
注意:微软建议在使用SetWindowLong(或SetWindowLongPtr)激活新样式后再使用SetWindowPos。

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