如何拦截WebBrowser控件中的onbeforeunload事件?

发布于 2024-12-27 07:14:44 字数 4774 浏览 0 评论 0原文

我有一个 WinForms 应用程序,在其中托管了一个网页 WebBrowser 控件。

该网页的内容如下:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
  <title>onbeforeunload test</title>
  <meta charset="utf-8">
</head>
<body>

<a href="#" onclick="window.location.reload();">Test</a>

<script type="text/javascript">
    window.onbeforeunload = function () {
        return 'Are you sure you want to leave this page?';
    };
</script>
</body>
</html>

如您所见,我已订阅了onbeforeunload 事件允许在离开此页面之前显示确认对话框。当我单击重新加载页面的锚点时,效果很好。将显示确认框,用户可以取消页面的重新加载。这在 WinForms 托管控件中工作得很好。

现在,我遇到的困难是当用户关闭 WinForms 应用程序时(例如通过单击 X 按钮)拦截并执行此事件。

我能够在 WinForms 应用程序中获取此函数的内容,但无论我尝试什么,我都无法获取此函数返回的字符串的内容,以便稍后我可以在用户使用它时使用它来伪造 MessageBox尝试关闭应用程序:

webBrowser1.Navigated += (sender, e) =>
{
    webBrowser1.Document.Window.Load += (s, ee) =>
    {
        // In order to get the IHTMLWindow2 interface I have referenced
        // the Microsoft HTML Object Library (MSHTML) COM control
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;

        // the bu variable contains the script of the onbeforeunload event
        var bu = window.onbeforeunload();

        // How to get the string that is returned by this function
        // so that I can subscribe to the Close event of the WinForms application
        // and show a fake MessageBox with the same text?
    };
};
webBrowser1.Navigate("file:///c:/index.htm");

我已尝试 window.execScript 方法不可用:

// returns null
var script = string.Format("({0})();", bu);
var result = window.execScript(script, "javascript");

我也尝试了以下方法,但它也返回 null:

var result = window.execScript("(function() { return 'foo'; })();", "javascript");

作为最后的手段,我可​​以使用第三方 javascript 解析器输入这个函数的主体,它将执行它并给我返回值,但这确实是最后的手段。如果有一种更原生的方法可以使用 Microsoft 的 MSHTML 库来执行此操作,我会很高兴。


更新:

由于@Hans 提供的优秀答案,现在这个问题已经解决了。由于某种原因,我无法让他的解决方案在我的测试机上运行(Win7 x64、.NET 4.0 Client Profile、IE9、en-US 区域设置),并且我总是在IDispatch.Invoke 调用。因此,我将 IDispatch P/Invoke 签名修改为以下内容(此签名不需要将 c:\windows\system32\stdole2.tlb 的引用添加到项目中):

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
public interface IDispatch
{
    [PreserveSig]
    int GetTypeInfoCount(out int Count);
    [PreserveSig]
    int GetTypeInfo(
        [MarshalAs(UnmanagedType.U4)] int iTInfo,
        [MarshalAs(UnmanagedType.U4)] int lcid, 
        out ITypeInfo typeInfo
    );

    [PreserveSig]
    int GetIDsOfNames(
        ref Guid riid,
        [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgsNames, 
        int cNames, 
        int lcid, 
        [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId
    );

    [PreserveSig]
    int Invoke(
        int dispIdMember, 
        ref Guid riid, 
        uint lcid, 
        ushort wFlags,
        ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, 
        out object pVarResult,
        ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo, 
        IntPtr[] pArgErr
    );
}

然后我已经订阅了表单的 Closing 事件,并且能够获取 onbeforeunload 事件返回的消息并提示用户:

protected override void OnFormClosing(FormClosingEventArgs e)
{
    var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
    var args = new System.Runtime.InteropServices.ComTypes.DISPPARAMS();
    var result = new object();
    var except = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
    var idisp = window.onbeforeunload as IDispatch;
    if (idisp != null)
    {
        var iid = Guid.Empty;
        var lcid = (uint)CultureInfo.CurrentCulture.LCID;
        int hr = idisp.Invoke(0, ref iid, lcid, 1, ref args, out result, ref except, null);
        if (hr == 0)
        {
            var msgBox = MessageBox.Show(
                this,
                (string)result,
                "Confirm",
                MessageBoxButtons.OKCancel
            );
            e.Cancel = msgBox == DialogResult.Cancel;
        }
    }
    base.OnFormClosing(e);
}

I have a WinForms application in which I have hosted a web page inside a WebBrowser control.

The contents of the web page is the following:

<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
  <title>onbeforeunload test</title>
  <meta charset="utf-8">
</head>
<body>

<a href="#" onclick="window.location.reload();">Test</a>

<script type="text/javascript">
    window.onbeforeunload = function () {
        return 'Are you sure you want to leave this page?';
    };
</script>
</body>
</html>

As you can see I have subscribed to the onbeforeunload event which allows to show a confirmation dialog before navigating away from this page. This works fine when I click on the anchor that reloads the page. The confirmation box is shown and the user can cancel the reload of the page. This works fine inside the WinForms hosted control.

Now, what I am having difficulties with is intercepting and executing this event when the user closes the WinForms application (by clicking on the X button for example).

I am able to fetch the contents of this function in the WinForms application but no matter what I tried I wasn't able to get the contents of the string that this function returns so that I can use it later to fake a MessageBox when the user attempts to close the application:

webBrowser1.Navigated += (sender, e) =>
{
    webBrowser1.Document.Window.Load += (s, ee) =>
    {
        // In order to get the IHTMLWindow2 interface I have referenced
        // the Microsoft HTML Object Library (MSHTML) COM control
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;

        // the bu variable contains the script of the onbeforeunload event
        var bu = window.onbeforeunload();

        // How to get the string that is returned by this function
        // so that I can subscribe to the Close event of the WinForms application
        // and show a fake MessageBox with the same text?
    };
};
webBrowser1.Navigate("file:///c:/index.htm");

I have tried the window.execScript method to no available:

// returns null
var script = string.Format("({0})();", bu);
var result = window.execScript(script, "javascript");

I have also tried the following but it also returned null:

var result = window.execScript("(function() { return 'foo'; })();", "javascript");

As a final resort I could use a third party javascript parser to which I can feed the body of this function and it will execute it and give me the return value but that would really be a last resort. I would be happy if there was a more native way to do this using Microsoft's MSHTML library.


UPDATE:

This is now solved thanks to the excellent answer that @Hans provided. For some reason I couldn't make his solution work on my test machine (Win7 x64, .NET 4.0 Client Profile, IE9, en-US locale) and I was always getting hr = -2147024809 after the IDispatch.Invoke call. So I have modified the IDispatch P/Invoke signature to the following (this signature doesn't require a reference to c:\windows\system32\stdole2.tlb to be added to the project):

using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("00020400-0000-0000-C000-000000000046")]
public interface IDispatch
{
    [PreserveSig]
    int GetTypeInfoCount(out int Count);
    [PreserveSig]
    int GetTypeInfo(
        [MarshalAs(UnmanagedType.U4)] int iTInfo,
        [MarshalAs(UnmanagedType.U4)] int lcid, 
        out ITypeInfo typeInfo
    );

    [PreserveSig]
    int GetIDsOfNames(
        ref Guid riid,
        [MarshalAs(UnmanagedType.LPArray, ArraySubType = UnmanagedType.LPWStr)] string[] rgsNames, 
        int cNames, 
        int lcid, 
        [MarshalAs(UnmanagedType.LPArray)] int[] rgDispId
    );

    [PreserveSig]
    int Invoke(
        int dispIdMember, 
        ref Guid riid, 
        uint lcid, 
        ushort wFlags,
        ref System.Runtime.InteropServices.ComTypes.DISPPARAMS pDispParams, 
        out object pVarResult,
        ref System.Runtime.InteropServices.ComTypes.EXCEPINFO pExcepInfo, 
        IntPtr[] pArgErr
    );
}

and then I have subscribed to the Closing event of the Form and was able to fetch the message returned by the onbeforeunload event and prompt the user:

protected override void OnFormClosing(FormClosingEventArgs e)
{
    var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
    var args = new System.Runtime.InteropServices.ComTypes.DISPPARAMS();
    var result = new object();
    var except = new System.Runtime.InteropServices.ComTypes.EXCEPINFO();
    var idisp = window.onbeforeunload as IDispatch;
    if (idisp != null)
    {
        var iid = Guid.Empty;
        var lcid = (uint)CultureInfo.CurrentCulture.LCID;
        int hr = idisp.Invoke(0, ref iid, lcid, 1, ref args, out result, ref except, null);
        if (hr == 0)
        {
            var msgBox = MessageBox.Show(
                this,
                (string)result,
                "Confirm",
                MessageBoxButtons.OKCancel
            );
            e.Cancel = msgBox == DialogResult.Cancel;
        }
    }
    base.OnFormClosing(e);
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

白馒头 2025-01-03 07:14:44

IHtmlWindow2.onbeforeunload 本身不显示对话框。它仅返回一个字符串,然后主机必须在消息框中使用该字符串。由于您的 Winforms 应用程序是主机,因此它必须使用 MessageBox.Show()。调用 onbeforeunload 很困难,它是一个 IDispatch 指针,其默认成员(dispid 0)返回字符串。添加对 c:\windows\system32\stdole2.tlb 的引用并粘贴此代码:

using System.Runtime.InteropServices;
...
        [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00020400-0000-0000-C000-000000000046")]
        public interface IDispatch {
            int dummy1();
            int dummy2();
            int dummy3();
            [PreserveSig]
            int Invoke(int dispIdMember, ref Guid riid, int lcid, int dwFlags, 
                [In, Out] stdole.DISPPARAMS pDispParams, 
                [Out, MarshalAs(UnmanagedType.LPArray)] object[] pVarResult, 
                [In, Out] stdole.EXCEPINFO pExcepInfo, 
                [Out, MarshalAs(UnmanagedType.LPArray)] IntPtr[] pArgErr);
        }

您将像这样使用它:

    protected override void OnFormClosing(FormClosingEventArgs e) {
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
        var args = new stdole.DISPPARAMS();
        var result = new object[1];
        var except = new stdole.EXCEPINFO();
        var idisp = (IDispatch)window.onbeforeunload;
        var iid = Guid.Empty;
        int hr = idisp.Invoke(0, ref iid, 1033, 1, args, result, except, null);
        if (hr == 0) {
            if (MessageBox.Show(this, (string)result[0], "Confirm",
                MessageBoxButtons.OKCancel) == DialogResult.Cancel) e.Cancel = true;
        }
        base.OnFormClosing(e);                                                       
    }

IHtmlWindow2.onbeforeunload does not itself display the dialog. It merely returns a string which the host must then use in a message box. Since your Winforms app is the host it must use MessageBox.Show(). Calling onbeforeunload is difficult, it is an IDispatch pointer whose default member (dispid 0) returns the string. Add a reference to c:\windows\system32\stdole2.tlb and paste this code:

using System.Runtime.InteropServices;
...
        [ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("00020400-0000-0000-C000-000000000046")]
        public interface IDispatch {
            int dummy1();
            int dummy2();
            int dummy3();
            [PreserveSig]
            int Invoke(int dispIdMember, ref Guid riid, int lcid, int dwFlags, 
                [In, Out] stdole.DISPPARAMS pDispParams, 
                [Out, MarshalAs(UnmanagedType.LPArray)] object[] pVarResult, 
                [In, Out] stdole.EXCEPINFO pExcepInfo, 
                [Out, MarshalAs(UnmanagedType.LPArray)] IntPtr[] pArgErr);
        }

You'll use it like this:

    protected override void OnFormClosing(FormClosingEventArgs e) {
        var window = (IHTMLWindow2)webBrowser1.Document.Window.DomWindow;
        var args = new stdole.DISPPARAMS();
        var result = new object[1];
        var except = new stdole.EXCEPINFO();
        var idisp = (IDispatch)window.onbeforeunload;
        var iid = Guid.Empty;
        int hr = idisp.Invoke(0, ref iid, 1033, 1, args, result, except, null);
        if (hr == 0) {
            if (MessageBox.Show(this, (string)result[0], "Confirm",
                MessageBoxButtons.OKCancel) == DialogResult.Cancel) e.Cancel = true;
        }
        base.OnFormClosing(e);                                                       
    }
停滞 2025-01-03 07:14:44

我刚刚处理了类似的问题。这是一个迟到的答案,但希望它可以帮助某人。该解决方案基于这篇 MSKB 文章。它也适用于网页处理 onbeforeunload 事件通过 attachEventaddEventListener 实现。

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    // this code depends on SHDocVw.dll COM interop assembly,
    // generate SHDocVw.dll: "tlbimp.exe ieframe.dll",
    // and add as a reference to the project

    var activeX = this.webBrowser.ActiveXInstance;
    object arg1 = Type.Missing;
    object arg2 = true;
    ((SHDocVw.WebBrowser)activeX).ExecWB(SHDocVw.OLECMDID.OLECMDID_ONUNLOAD, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref arg1, ref arg2);
    if (!(bool)arg2)
    {
        e.Cancel = true;
    }
}

上面的代码适用于 WinForms 版本的 WebBrowser 控件。对于WPF版本,应首先通过反射获取ActiveXInstance

 var activeX = this.WB.GetType().InvokeMember("ActiveXInstance",
                    BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                    null, this.WB, new object[] { }) as SHDocVw.WebBrowser;

I just dealt with a similar problem. This is a late answer, but hopefully it may help someone. The solution is based on this MSKB article. It also works for cases when the web page handles onbeforeunload event via attachEvent or addEventListener.

void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    // this code depends on SHDocVw.dll COM interop assembly,
    // generate SHDocVw.dll: "tlbimp.exe ieframe.dll",
    // and add as a reference to the project

    var activeX = this.webBrowser.ActiveXInstance;
    object arg1 = Type.Missing;
    object arg2 = true;
    ((SHDocVw.WebBrowser)activeX).ExecWB(SHDocVw.OLECMDID.OLECMDID_ONUNLOAD, SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref arg1, ref arg2);
    if (!(bool)arg2)
    {
        e.Cancel = true;
    }
}

The above code is for WinForms version of WebBrowser control. For WPF version, ActiveXInstance should be first obtained via reflection:

 var activeX = this.WB.GetType().InvokeMember("ActiveXInstance",
                    BindingFlags.GetProperty | BindingFlags.Instance | BindingFlags.NonPublic,
                    null, this.WB, new object[] { }) as SHDocVw.WebBrowser;
飘然心甜 2025-01-03 07:14:44

如果您愿意提前执行事件代码(可以是任何内容),则以下内容会在您的 Window.Load 中为我捕获字符串;

Object[] args = { @"(" + bu + ")();" };
string result = webBrowser1.Document.InvokeScript("eval", args).ToString();

If your happy to prematurely execute the event code (which could be anything) the following captures the string for me in your Window.Load;

Object[] args = { @"(" + bu + ")();" };
string result = webBrowser1.Document.InvokeScript("eval", args).ToString();
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文