PAYATHON 2026

如何将 System.Windows.Forms.WebBrowser 添加到 WPF StackPanel

支付小周

结论

System.Windows.Forms.WebBrowser 是 WinForms 控件,并不继承 System.Windows.UIElement,所以不能直接添加到 WPF 的 StackPanel.Children 中。

需要用 System.Windows.Forms.Integration.WindowsFormsHost 承载它:

var host = new WindowsFormsHost
{
    Child = browser
};

stackPanel.Children.Add(host);

如果没有使用自定义的 WinForms WebBrowserSite,也可以换成 WPF 自带的 System.Windows.Controls.WebBrowser。不过,现有的 MyWebBrowser 继承自 System.Windows.Forms.WebBrowser,使用 WindowsFormsHost 改动最少。

使用 WindowsFormsHost

项目需要引用以下程序集,或者启用相应的桌面框架支持:

  • WindowsFormsIntegration
  • System.Windows.Forms

完整示例:

using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Forms.Integration;

private void OpenBrowser(PaymentViewModel viewModel, Uri uri)
{
    var window = new WithoutCloseButton
    {
        Width = 1000,
        Height = 700,
        Title = "Payment"
    };

    var browser = new MyWebBrowser
    {
        Dock = System.Windows.Forms.DockStyle.Fill,
        ScriptErrorsSuppressed = false
    };

    var host = new WindowsFormsHost
    {
        Child = browser,
        HorizontalAlignment = HorizontalAlignment.Stretch,
        VerticalAlignment = VerticalAlignment.Stretch
    };

    var panel = new StackPanel
    {
        Orientation = Orientation.Vertical
    };

    panel.Children.Add(host);

    window.Content = panel;
    window.Closed += (_, _) =>
    {
        // WinForms 控件持有本机资源,应显式释放。
        host.Child = null;
        browser.Dispose();
        host.Dispose();
    };

    viewModel.BrowserWindow = window;

    browser.Navigate(uri);
    window.Show();
}

这段代码能够通过编译,是因为添加到 StackPanel.Children 中的是 WindowsFormsHost。它是 WPF 元素,可以作为 UIElement 参与布局。真正的 WinForms 控件则放在它的 Child 属性中。

更适合浏览器的布局方式

如果窗口里只有浏览器,一般不需要使用 StackPanel,直接将 WindowsFormsHost 设为窗口内容即可:

var browser = new MyWebBrowser
{
    Dock = System.Windows.Forms.DockStyle.Fill
};

var host = new WindowsFormsHost
{
    Child = browser
};

viewModel.BrowserWindow.Content = host;

如果窗口里还要放工具栏或提示文字,Grid 通常比纵向 StackPanel 更合适:

var grid = new Grid();

grid.RowDefinitions.Add(new RowDefinition
{
    Height = GridLength.Auto
});

grid.RowDefinitions.Add(new RowDefinition
{
    Height = new GridLength(1, GridUnitType.Star)
});

var message = new TextBlock
{
    Text = "正在处理付款,请不要关闭窗口。",
    Margin = new Thickness(10)
};

var host = new WindowsFormsHost
{
    Child = browser
};

Grid.SetRow(message, 0);
Grid.SetRow(host, 1);

grid.Children.Add(message);
grid.Children.Add(host);

viewModel.BrowserWindow.Content = grid;

StackPanel 会沿排列方向为子元素提供近似无限的可用空间,因此承载 WinForms 控件时可能需要显式设置 Height。使用 Grid 的星号行高,浏览器可以自然填满剩余空间。

在特定页面阻止关闭窗口

如果只是想防止用户在付款过程中关闭窗口,界面层更可靠的处理方式是在 WPF 窗口的 Closing 事件中取消关闭,而不是隐藏 X 按钮。

可以为窗口添加一个状态属性:

public class PaymentBrowserWindow : Window
{
    public bool CanUserClose { get; set; } = true;

    protected override void OnClosing(CancelEventArgs e)
    {
        if (!CanUserClose)
        {
            e.Cancel = true;

            MessageBox.Show(
                this,
                "付款正在确认,请等待处理完成。",
                "无法关闭",
                MessageBoxButton.OK,
                MessageBoxImage.Information);

            return;
        }

        base.OnClosing(e);
    }
}

再根据浏览器当前打开的页面更新状态:

var window = new PaymentBrowserWindow();

browser.DocumentCompleted += (_, e) =>
{
    // DocumentCompleted 也可能由 frame 触发,因此先确认主文档 URL。
    if (browser.Url == null || e.Url != browser.Url)
        return;

    window.CanUserClose = IsSafeToClose(browser.Url);
};

判断 URL 时,应明确检查域名和路径:

private static bool IsSafeToClose(Uri url)
{
    if (url == null)
        return false;

    return string.Equals(
               url.Host,
               "payments.example.com",
               StringComparison.OrdinalIgnoreCase)
           && url.AbsolutePath.StartsWith(
               "/payment/result",
               StringComparison.OrdinalIgnoreCase);
}

实际项目中,不要只用 url.ToString().Contains("success") 判断支付是否完成。查询参数、重定向地址或恶意页面都可能让这种判断被误触发。

如果程序要在支付成功后主动关闭窗口,可以先允许关闭:

window.CanUserClose = true;
window.Close();

如果必须禁用标题栏的 X 按钮

可以通过 Win32 系统菜单禁用 SC_CLOSE。这种做法只会改变标题栏按钮的状态,仍应保留 Closing 事件作为最后的保护,因为窗口也可能通过 Alt+F4、任务栏菜单或其他方式收到关闭请求。

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

public class PaymentBrowserWindow : Window
{
    private const uint SC_CLOSE = 0xF060;
    private const uint MF_BYCOMMAND = 0x00000000;
    private const uint MF_ENABLED = 0x00000000;
    private const uint MF_GRAYED = 0x00000001;

    public bool CanUserClose { get; private set; } = true;

    [DllImport("user32.dll")]
    private static extern IntPtr GetSystemMenu(
        IntPtr hWnd,
        bool bRevert);

    [DllImport("user32.dll")]
    private static extern bool EnableMenuItem(
        IntPtr hMenu,
        uint uIDEnableItem,
        uint uEnable);

    [DllImport("user32.dll")]
    private static extern bool DrawMenuBar(IntPtr hWnd);

    public void SetCloseAllowed(bool allowed)
    {
        CanUserClose = allowed;

        var handle = new WindowInteropHelper(this).Handle;
        if (handle == IntPtr.Zero)
            return;

        var systemMenu = GetSystemMenu(handle, false);
        if (systemMenu == IntPtr.Zero)
            return;

        EnableMenuItem(
            systemMenu,
            SC_CLOSE,
            MF_BYCOMMAND | (allowed ? MF_ENABLED : MF_GRAYED));

        DrawMenuBar(handle);
    }

    protected override void OnClosing(CancelEventArgs e)
    {
        if (!CanUserClose)
        {
            e.Cancel = true;
            return;
        }

        base.OnClosing(e);
    }
}

这段代码必须在窗口句柄创建后调用,例如:

window.SourceInitialized += (_, _) =>
{
    window.SetCloseAllowed(false);
};

之后可以根据页面状态重新启用:

window.SetCloseAllowed(true);

项目配置

现代 SDK 风格的 Windows 桌面项目需要同时启用 WPF 和 WinForms:

<PropertyGroup>
  <TargetFramework>net8.0-windows</TargetFramework>
  <UseWPF>true</UseWPF>
  <UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>

目标框架应与现有项目保持一致,不需要为了使用 WindowsFormsHost 强行升级。

重要注意事项

System.Windows.Forms.WebBrowser 使用旧版 Internet Explorer 浏览器引擎。打开第三方支付页面时,可能遇到 TLS、JavaScript、Cookie、SameSite 或页面兼容性问题。如果支付服务不再支持旧版 IE 引擎,应考虑迁移到 WebView2。

禁止用户关闭窗口也不能保证支付结果可靠。程序崩溃、断网、断电或进程被终止,都可能导致窗口消失。支付系统应以后端通知为准,同时配合支付状态查询、幂等处理和对账机制,不能只依赖浏览器重定向。

现有代码中的 OnSecurityProblem() 无条件返回 S_OK,等于忽略浏览器遇到的部分安全问题。支付页面涉及敏感数据,不应绕过证书或 HTTPS 错误。除非已经明确掌握相关 COM 返回值的含义及其安全影响,否则应删除这段忽略错误的处理。

备注:内容仅供参考。