2014-12-26 回答
wpf中自带一个webbrowser控件,当我们使用它打开一个网页,例如百度,然后点击它其中的链接时,如果这个链接是会弹出一个新窗口的,那么它会生生的弹出一个ie窗口来,而不是在内部跳到该链接。
如果使用winform的webbrowser控件,我们可以监听它的newwindow事件,在这个事件中做一些处理,例如,在新建一个tab来打开,或者控制它在当前webbrowser中跳转。很不幸的是,wpf的webbrowser没有这个事件。
说到底,winform的wb或者是wpf的wb都是在调用ie的一个控件,因此,winform能加上的,我们wpf一定也有办法加上。如此,那我们就请出神器reflector,研究一把。
首先,我们打开winform的webbrowser,找到触发newwindow事件的代码:
protected virtual void onnewwindow(canceleventargs e)
{
if (this.newwindow != null)
{
this.newwindow(this, e);
}
}
它是在onnewwindow方法中触发的。那么,是谁调用了这个onnewwindow呢?接着搜索,最后在一个叫webbrowserevent的类里面发现这么一段:
public void newwindow2(ref object ppdisp, ref bool cancel)
{
canceleventargs e = new canceleventargs();
this.parent.onnewwindow(e);
cancel = e.cancel;
}
我们接着搜newwindow2,却发现没有地方显式地调用它了。既然从方法入手没找到,那我们就来研究一下定义这个方法的webbrowserevent,看看是谁在使用它。
仔细搜索一遍,最后发现在webbrowser的createsink方法中有这么一段:
代码
protected override void createsink()
{
object activexinstance = base.activexinstance;
if (activexinstance != null)
{
this.webbrowserevent = new webbrowserevent(this);
this.webbrowserevent.allownavigation = this.allownavigation;
this.cookie = new axhost.connectionpointcookie(activexinstance, this.webbrowserevent, typeof(unsafenativemethods.dwebbrowserevents2));
}
}
注意这句话:
this.cookie = new axhost.connectionpointcookie(activexinstance, this.webbrowserevent, typeof(unsafenativemethods.dwebbrowserevents2));
很显然,这句话是关键。axhost.connectionpointcookie类的作用是:“将一个activex 控件连接到处理该控件的事件的客户端”。
上面的调用中有一个很奇怪的类型:dwebbrowserevents2,熟悉com的童鞋应该马上能想到,这其实是一个com类型的定义。
代码
[comimport, typelibtype(typelibtypeflags.fhidden), interfacetype(cominterfacetype.interfaceisidispatch), guid("34a715a0-6587-11d0-924a-0020afc7ac4d")]
public interface dwebbrowserevents2
{
......
}
实际上,我们再去看webbrowserevent的定义,它恰恰是实现了这个接口的。
[classinterface(classinterfacetype.none)]
private class webbrowserevent : standardolemarshalobject, unsafenativemethods.dwebbrowserevents2
{
......
}
因此,上面这句话不难理解,就是定义一个实现了特定com接口的类型,让浏览器控件的事件能够转发到这个类型实例去处理。因此,newwindow2其实是浏览器控件去调用的。
winform的webbrowser我们搞清楚了,下面我们来看wpf的。其实,打开wpf的webbrowser代码之后,我们会发现它跟winform的webbrowser机制是一样的。一个似曾相识的createsink方法映入眼中:
代码
[securitytreatassafe, securitycritical]
internal override void createsink()
{
this._cookie = new connectionpointcookie(this._axiwebbrowser2, this._hostingadaptor.createeventsink(), typeof(unsafenativemethods.dwebbrowserevents2));
}
这儿也有一个connectionpointcookie,但是它的访问权限是internal的:(
第二个参数,_hostingadapter.createeventsink返回的是什么呢:
代码
[securitycritical]
internal virtual object createeventsink()
{
return new webbrowserevent(this._webbrowser);
}
[classinterface(classinterfacetype.none)]
internal class webbrowserevent : internaldispatchobject, unsafenativemethods.dwebbrowserevents2
{
......
}
仍然是一个webbrowserevent!悲剧的是,这个wpf的webbrowserevent,并没有触发newwindowevent:
public void newwindow2(ref object ppdisp, ref bool cancel)
{
}
现在知道为什么wpf的wb控件没有newwindow事件了吧?微软的童鞋压根儿就没写!
既然微软的童鞋不写,那我们就自己折腾一把,反正原理已经搞清楚了。
首先,我们也得定义一个dwebbrowserevents2接口,这个我们直接通过reflector复制一份就好了。代码就不贴上来了。
接着,我们再仿造一个webbrowserevent,关键是要触发newwindow事件:
代码
public partial class webbrowserhelper
{
private class webbrowserevent : standardolemarshalobject, dwebbrowserevents2
{
private webbrowserhelper _helperinstance = null;
public webbrowserevent(webbrowserhelper helperinstance)
{
_helperinstance = helperinstance;
}
......
public void newwindow2(ref object pdisp, ref bool cancel)
{
_helperinstance.onnewwindow(ref cancel);
}
......
}
}
最后,我们需要仿造framework中的代码,也来createsink一把(我承认,用了反射来取webbrowser内部的东东,谁让这些类型都是internal的呢):
代码
private void attach()
{
var axiwebbrowser2 = _webbrowser.reflectgetproperty("axiwebbrowser2");
var webbrowserevent = new webbrowserevent(this);
var cookietype = typeof(webbrowser).assembly.gettype("ms.internal.controls.connectionpointcookie");
_cookie = activator.createinstance(
cookietype,
reflectionservice.bindingflags,
null,
new[] { axiwebbrowser2, webbrowserevent, typeof(dwebbrowserevents2) },
cultureinfo.currentuiculture);
}
最后的使用:
var webbrowserhelper = new webbrowserhelper(webbrowser);
......
webbrowserhelper.newwindow += webbrowseronnewwindow;
【效果图】
初始网页:
点击一个链接,默认情况下,将是弹出一个ie窗口,现在是在新的tab中打开: