拦截和管理由托管在 C# 应用程序中的第三方组件创建的 Windows 窗口






4.94/5 (17投票s)
拦截、填充和关闭 C# 应用程序中托管的第三方组件生成的窗口。例如,考虑使 WebBrowser 控件静默。

引言
有时您可能会遇到以下问题:嵌入到您的应用程序中的第三方二进制组件或控件会显示窗口(通常是消息框),直到它们被关闭,否则会挂起您的代码。
如果您没有该二进制文件的源代码,并且该二进制文件没有好的 API 来以编程方式改变其不良行为,那么使用它可能会非常令人头疼。此类二进制文件的著名示例是 .NET 中的 WebBrowser
控件。
可能会出现以下类型的错误
- 您代码中使用的组件会抛出大量错误消息,并让用户在这些消息中摸索才能使应用程序继续运行。这可能是最令人恼火的情况。
- 组件会显示一个对话框并等待在那儿输入数据。
在这两种情况下,您的代码执行都会被挂起,并且令人沮丧的窗口会出现在桌面上,而您可能希望您的代码能够平稳、静默地运行,而不会出现障碍或需要人工干预。
要解决这些问题,您必须从代码中管理这些窗口:关闭它们,甚至用数据填充它们并提交。
重要说明
在本文中,我们将考虑管理嵌入到您的应用程序中的 WebBrowser
控件生成的窗口。这是一个相对常见的情况。但实际上,本文中考虑的解决方案可以应用于您的进程使用的任何控件或二进制文件。
代码是 C# 的,但它包含通过互操作导入的 Win32 函数。所以实际上解决方案是用 C++ 编写的,如果需要,可以轻松地将附带的代码翻译成 C++。
WebBrowser 打开的对话框
使用 WebBrowser
控件,您可能会面临以下挑战:WebBrowser
下载的网页包含抛出错误消息的 JavaScript。这是 WebBrowser
可以显示消息框的原因之一。当浏览器询问如何处理导航的文件(打开或保存)时,也可能会出现对话框。浏览器还可以请求凭据或显示安全警报等。
问题在于,如果打开了这样的对话框,WebBrowser
对象就无法导航到下一页,并且会一直处于瘫痪状态,直到对话框关闭。让我们看看如何以编程方式解决这个问题。
解决方案
我们将遵循最完整、最可靠的方法来完成任务,该方法包括以下步骤
- 拦截对应于窗口创建的窗口消息
- 获取需要管理的窗口的句柄
- 关闭窗口或填充并提交它(单击确定)
拦截对话框窗口
我们要做的第一件事就是从代码中监视窗口的创建。
这可以通过 Win32 API 函数 SetWindowsHookEx
来完成,该函数将调用我们的回调方法。该函数可以跟踪 Windows 中的多个事件,包括窗口的创建。为此,我们将使用标志 WH_CALLWNDPROCRET
调用它,该标志安装一个钩子过程,该过程在消息被目标窗口过程处理后对其进行监视。
每次调用我们的回调函数时,我们只需要过滤 WM_SHOWWINDOW
消息,当窗口即将隐藏或显示时,会向该窗口发送此消息。
请查看下面的代码来实现其余功能
/// <summary>
/// Intercept creation of window and call a handler
/// </summary>
public class WindowInterceptor
{
IntPtr hook_id = IntPtr.Zero;
Win32.Functions.HookProc cbf;
/// <summary>
/// Delegate to process intercepted window
/// </summary>
/// <param name="hwnd"></param>
public delegate void ProcessWindow(IntPtr hwnd);
ProcessWindow process_window;
IntPtr owner_window = IntPtr.Zero;
/// <summary>
/// Start dialog box interception for the specified owner window
/// </summary>
/// <param name="owner_window">owner window; if IntPtr.Zero,
/// any windows will be intercepted</param>
/// <param name="process_window">custom delegate to process intercepted window
/// </param>
public WindowInterceptor(IntPtr owner_window, ProcessWindow process_window)
{
if (process_window == null)
throw new Exception("process_window cannot be null!");
this.process_window = process_window;
this.owner_window = owner_window;
cbf = new Win32.Functions.HookProc(dlg_box_hook_proc);
//notice that win32 callback function must be a global variable within class
//to avoid disposing it!
hook_id = Win32.Functions.SetWindowsHookEx(Win32.HookType.WH_CALLWNDPROCRET,
cbf, IntPtr.Zero, Win32.Functions.GetCurrentThreadId());
}
/// <summary>
/// Stop intercepting. Should be called to calm unmanaged code correctly
/// </summary>
public void Stop()
{
if (hook_id != IntPtr.Zero)
Win32.Functions.UnhookWindowsHookEx(hook_id);
hook_id = IntPtr.Zero;
}
~WindowInterceptor()
{
if (hook_id != IntPtr.Zero)
Win32.Functions.UnhookWindowsHookEx(hook_id);
}
private IntPtr dlg_box_hook_proc(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode < 0)
return Win32.Functions.CallNextHookEx(hook_id, nCode, wParam, lParam);
Win32.CWPRETSTRUCT msg = (Win32.CWPRETSTRUCT)Marshal.PtrToStructure
(lParam, typeof(Win32.CWPRETSTRUCT));
//filter out create window events only
if (msg.message == (uint)Win32.Messages.WM_SHOWWINDOW)
{
int h = Win32.Functions.GetWindow(msg.hwnd, Win32.Functions.GW_OWNER);
//check if owner is that is specified
if (owner_window == IntPtr.Zero || owner_window == new IntPtr(h))
{
if (process_window != null)
process_window(msg.hwnd);
}
}
return Win32.Functions.CallNextHookEx(hook_id, nCode, wParam, lParam);
}
}
请注意一个重要事项:我们只过滤那些由特定窗口拥有的窗口的消息。这是必需的技巧,因为我们自己的代码也可以打开窗口,我们不希望它们被自动拦截和关闭。由控件(在本例中为 WebBrowser
)生成的所有窗口默认都由放置它们的窗体拥有。因此,窗口的所有者是区分 WebBrowser
打开的窗口与其他窗口的一种方式。请注意,为了使此功能正常工作,我们应该为自己的窗口分配另一个所有者。
区分不需要的窗口与您的窗口的另一种方法是通过它们的标题文本进行过滤。窗口的标题可以通过 Win32 函数 GetWindowText
获取。这也很有效,因为通常您会提前知道您的窗口将拥有什么标题。
还请记住,从非托管代码调用的回调函数(代码中的 Win32.Functions.HookProc cbf
)必须是类中的全局变量,以避免被释放。
每次拦截到的窗口的所有者是指定的那个时,回调函数就会调用一个代理方法,我们在其中定义了如何处理拦截到的窗口,并将窗口的句柄传递给它。
通过句柄管理窗口
太棒了!获取了窗口的句柄后,我们就可以对其进行任何我们想做的事情了。
关闭对话框
为了演示我们现在的能力,我创建了一个包含 WebBrowser
的窗体以及一个带有 JavaScript 的小型 HTML 页面。加载此窗体后,WebBrowser
将导航到此页面
<HTML>
<BODY>
HERE IS STUMBLING PAGE...
<script>
ERROR HERE!
</script>
<script>
alert("Error");
</script>
<br>OK NOW!
</BODY>
</HTML>
这将强制我们的 WebBrowser
显示一个警报消息,并在警报框关闭之前暂停 WebBrowser
。
我们想从代码中关闭它吗?现在没问题。为此,我们创建一个 WindowInterceptor
实例并将一个窗口句柄代理传递给它
//form with WebBrowser
Form1 form = new Form1();
//Start intercepting all dialog boxes owned by form
WindowInterceptor d = new WindowInterceptor(form.Handle, ProcessWindow1);
form.Show();
form.Navigate("https:///test1.htm");
这是将关闭对话框窗口的窗口句柄
static void ProcessWindow1(IntPtr hwnd)
{
//close window
Win32.Functions.SendMessage(hwnd, (uint)Win32.Messages.WM_CLOSE, 0, 0);
}
以上就是如何以一种简单而优雅的方式让 WebBrowser
静默下来。
提交对话框
现在让我们考虑一个更复杂的情况:当您需要单击拦截窗口中的某个按钮时。为了演示目的,我创建了另一个 HTML
<HTML>
<BODY>
HERE IS STUMBLING PAGE #2...
<script>
if(confirm("Do you really want to go here?"))
navigate("http://www.google.com");
</script>
</BODY>
</HTML>
假设我们要在此确认对话框中单击“确定”。下面的代码执行此操作
static void ProcessWindow2(IntPtr hwnd)
{
//looking for button "OK" within the intercepted window
IntPtr h =
(IntPtr)Win32.Functions.FindWindowEx(hwnd, IntPtr.Zero, "Button", "OK");
if (h != IntPtr.Zero)
{
//clicking the found button
Win32.Functions.SendMessage
((IntPtr)h, (uint)Win32.Messages.WM_LBUTTONDOWN, 0, 0);
Win32.Functions.SendMessage
((IntPtr)h, (uint)Win32.Messages.WM_LBUTTONUP, 0, 0);
}
}
填充对话框内的控件
如果对话框中的某些字段需要填充数据(例如用户名和密码),则可以使用 Win32 函数 SetWindowText
来设置它们。所需控件的句柄可以与查找“确定”按钮的方式相同。
某些控件(如下拉框)没有文本标题可以帮助我们查找它们。相反,这些控件的句柄可以通过传递给 FindWindowEx
的类名来找到。
但是我们如何找出隐藏的类名或标题呢?为了轻松做到这一点,请使用有用的工具,如 VS Spy++ 或 Winspector - 它们可以显示有关特定窗口中托管的控件的所有信息。
此代码会在拦截的窗口中查找一个编辑框,并用文本填充它
static void ProcessWindow3(IntPtr hwnd)
{
//looking for edit box control within intercepted window
IntPtr h =
(IntPtr)Win32.Functions.FindWindowEx(hwnd, IntPtr.Zero, "Edit", null);
if (h != IntPtr.Zero)
{
//setting text
Win32.Functions.SetWindowText((IntPtr)h, "qwerty");
}
//looking for button "OK" within the intercepted window
IntPtr h = (IntPtr)Win32.Functions.FindWindowEx(hwnd, IntPtr.Zero, "Button", "OK");
if (h != IntPtr.Zero)
{ //clicking the found button
Win32.Functions.SendMessage
((IntPtr)h, (uint)Win32.Messages.WM_LBUTTONDOWN, 0, 0);
Win32.Functions.SendMessage
((IntPtr)h, (uint)Win32.Messages.WM_LBUTTONUP, 0, 0);
}
else
{ //close window if OK was not found
Win32.Functions.SendMessage(hwnd, (uint)Win32.Messages.WM_CLOSE, 0, 0); }
}
}
一些说明
当然,这些示例仅用于测试目的。在实际生活中,这种方法帮助我解决了两个任务:使 WebBrowser
静默并提交登录网站之前出现的凭据对话框,从而使 WebBrowser
能够完全自动化。
为了完整起见,上述情况也可以通过其他方式解决(任何问题都有不止一种解决方案)。其中一些可以通过将自编写的 JavaScript 嵌入下载的页面来解决。其中一些可以通过更改注册表中的 Internet Explorer 用户设置来解决。可以通过在我们的代码中使用 Internet Explorer API 设置凭据来避免凭据对话框等。但这些方法很狭窄,仅适用于特殊情况,而考虑的解决方案不依赖于第三方软件的使用,因此可以编写可靠且简单的代码。顺便说一句,在我研究这个问题时,就 WebBrowser
而言,即使使用 Internet Explorer API(许多复杂接口),也无法抑制浏览器在某些情况下可能出现的对话框。
此外,控件的行为可能因版本而异。作为版本不兼容的一个例子,您可能会注意到 WebBrowser
的属性 Silent
。我猜它在几年前的某些 Internet Explorer 版本中有效,但在 WinXP+SP2 和 Internet Explorer 6 下似乎无效。结果,对该属性充满信心地开发的应用程序在 Internet Explorer 在计算机上升级时可能会遇到问题。
结论
当您无法以合法方式解决第三方组件生成的窗口时,描述的解决方案可能会成功。此外,与使用这些组件的 API(如果它们有 API 的话)相比,它可能具有以下优点
- 它很简单
- 它很可靠
- 它不依赖于不同版本的第三方软件
- 它保持第三方软件设置不变
Using the Code
在附带的代码中,您可以找到
WindowInterceptor
类,它监视出现的对话框窗口并调用自定义代理方法。此类可以作为 DLL 从您的代码中使用。- 带有
WebBrowser
控件的Test
项目 - test1.html 和 test2.html 带有 JavaScript。请记住将它们放在您的 www 目录中,并在
Test
项目中指定它们的 URL。
祝您开心!