PAYATHON 2026

WPF Process.Start(url) 如何获取当前 URL 或检测窗口关闭

支付小周

结论

通过 Process.Start(new ProcessStartInfo(url)) 调用系统默认浏览器后,WPF 应用通常无法可靠读取浏览器当前访问的 URL,也无法准确判断刚打开的浏览器窗口或标签页何时关闭。

这是因为 Process.Start 只是把 URL 交给操作系统处理:

  • URL 可能在已有浏览器进程的新标签页中打开。
  • 返回的 Process 不一定对应具体的窗口或标签页。
  • 浏览器可能采用多进程架构。
  • System.Diagnostics.Process 不会向调用方提供浏览器的导航内容。
  • Process.Exited 最多只能说明某个进程已经退出,不能说明支付标签页已经关闭。

在支付场景中,更合适的做法是让支付平台在支付完成后回调业务服务器,再由 WPF 应用通过轮询、长连接或本地回调获取最终状态。浏览器重定向可以改善用户体验,但不能作为确认支付成功的唯一依据。

为什么无法从 Process 获取当前 URL

典型调用方式如下:

var process = Process.Start(new ProcessStartInfo
{
    FileName = url,
    UseShellExecute = true
});

这段代码启动的是系统为 http 或 https 协议注册的处理程序。系统可能会:

  1. 启动一个新的浏览器进程;
  2. 把 URL 发送给已经运行的浏览器;
  3. 先启动一个中间进程,再由它把 URL 转交给浏览器;
  4. 在已有窗口中创建新标签页。

Process 对象只能提供进程级信息,例如进程 ID、退出状态和主窗口句柄。它不包含浏览器标签页的导航状态,也没有通用 API 可用于读取地址栏。

在受控环境中,可以尝试通过 UI Automation、浏览器扩展或特定浏览器的调试协议读取地址栏。不过,这些方法依赖浏览器类型、版本、权限和启动方式,不适合通用支付流程。

能否检测浏览器关闭

如果 Process.Start 确实创建了独立进程,可以监听该进程退出:

var startInfo = new ProcessStartInfo
{
    FileName = url,
    UseShellExecute = true
};

var process = Process.Start(startInfo);

if (process != null)
{
    process.EnableRaisingEvents = true;
    process.Exited += (_, _) =>
    {
        Application.Current.Dispatcher.Invoke(() =>
        {
            // 这里只能说明该进程退出,不能证明支付完成。
        });
    };
}

但这种方式无法可靠检测某个浏览器窗口或标签页是否关闭。例如,URL 在已有的 Chrome 或 Edge 进程中打开时:

  • 返回值可能是 null;
  • 返回的可能是一个很快就会退出的启动进程;
  • 浏览器窗口关闭后,后台进程可能仍在运行;
  • 用户关闭支付标签页时,浏览器主进程通常不会退出。

所以,不能用 Process.Exited 判断支付结果。

推荐方案:通过服务器回调确认支付状态

支付系统通常会提供两类地址:

  • 用户完成支付后,浏览器访问的返回地址,例如 return_url;
  • 支付平台直接通知业务服务器的回调地址,例如 notify_url、Webhook 或 callback URL。

较可靠的流程如下:

  1. WPF 应用请求业务服务器创建交易;
  2. 业务服务器生成交易引用并返回支付 URL;
  3. WPF 使用默认浏览器打开支付 URL;
  4. 支付完成后,支付平台调用业务服务器的 Webhook;
  5. 业务服务器验证签名、金额、币种、订单号和支付状态;
  6. WPF 查询服务器,或由服务器主动通知 WPF;
  7. WPF 根据服务器保存的最终状态更新界面。

保留数据库轮询并不意味着设计有问题。对于桌面程序,采用短时间、有限次数并带有退避策略的状态轮询,通常比监视外部浏览器更可靠。

示例:

private async Task WaitForPaymentAsync(
    string transactionId,
    CancellationToken cancellationToken)
{
    var delay = TimeSpan.FromSeconds(2);
    var deadline = DateTimeOffset.UtcNow.AddMinutes(10);

    while (DateTimeOffset.UtcNow < deadline)
    {
        cancellationToken.ThrowIfCancellationRequested();

        PaymentStatus status =
            await paymentApi.GetStatusAsync(transactionId, cancellationToken);

        switch (status)
        {
            case PaymentStatus.Succeeded:
                ShowPaymentSucceeded();
                return;

            case PaymentStatus.Failed:
            case PaymentStatus.Cancelled:
                ShowPaymentFailed();
                return;
        }

        await Task.Delay(delay, cancellationToken);

        // 可根据实际负载逐步延长间隔,并设置合理上限。
        delay = TimeSpan.FromSeconds(
            Math.Min(delay.TotalSeconds * 1.5, 10));
    }

    ShowPaymentPending();
}

轮询接口应返回业务服务器已经验证过的状态,桌面应用不应直接信任浏览器传回的参数。

支付完成后自动返回 WPF

如果希望用户完成支付后自动切回应用,可以注册自定义 URI Scheme,例如:

myapp://payment-completed?transactionId=12345

业务服务器确认支付结果后,支付完成页面再跳转到这个地址。操作系统收到该 URL 后,会启动或激活 WPF 应用。

WPF 应用可以解析启动参数:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    string? callbackUrl = e.Args.FirstOrDefault();

    if (callbackUrl != null &&
        Uri.TryCreate(callbackUrl, UriKind.Absolute, out var uri) &&
        uri.Scheme.Equals("myapp", StringComparison.OrdinalIgnoreCase) &&
        uri.Host.Equals(
            "payment-completed",
            StringComparison.OrdinalIgnoreCase))
    {
        var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
        var transactionId = query["transactionId"];

        if (!string.IsNullOrWhiteSpace(transactionId))
        {
            // 再向业务服务器查询并确认最终支付状态。
            _ = RefreshPaymentStatusAsync(transactionId);
        }
    }
}

自定义协议需要在操作系统中注册。如果应用已经在运行,还需要处理单实例通信。不要在回调 URL 中携带密钥、支付凭证,或任何可以直接改变订单状态的可信数据。

另一种方式是在 WPF 中临时监听一个随机的本机回环地址,例如:

http://127.0.0.1:随机端口/callback

支付返回页面重定向到该地址后,应用接收请求并重新查询服务器。使用这种方式时,需要处理端口冲突、防火墙、请求伪造、超时,以及应用退出后的清理问题。

如果必须直接观察导航地址

只有当网页显示在应用自己控制的浏览器组件中时,监听导航地址才比较方便。例如,可以使用 WebView2:

await webView.EnsureCoreWebView2Async();

webView.CoreWebView2.NavigationStarting += async (_, e) =>
{
    if (!Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri))
        return;

    if (uri.Scheme == "myapp" &&
        uri.Host == "payment-completed")
    {
        e.Cancel = true;

        // 不直接相信 URL 中的“成功”标志,
        // 而是向业务服务器查询该交易的最终状态。
        await RefreshPaymentStatusAsync();
    }
};

webView.CoreWebView2.Navigate(paymentUrl);

不过,部分支付服务会因为安全、兼容性或身份验证要求而限制嵌入式浏览器。采用 WebView2 前,需要先确认支付平台是否允许这种集成方式。

安全注意事项

浏览器跳转到”支付成功”页面,并不能证明款项已经到账。用户可以手动构造回调 URL,网络延迟也可能导致页面已经跳转,但服务器尚未确认支付结果。

最终支付状态必须由可信服务端确认,至少需要校验:

  • 支付平台签名;
  • 商户和订单标识;
  • 交易引用;
  • 金额及币种;
  • 支付状态;
  • 回调是否重复;
  • 订单是否已经处理。

外部浏览器的 URL 和关闭状态最多只能作为界面提示信号,不能代替服务器回调或经过验证的支付状态查询。

备注:内容仅供参考。