(似乎这篇文章被垃圾评论认准了,所以无奈关评)
问题
对于浏览器而言,Context Menu使用起来是用户操作页面元素最直接、同时也是对页面执行各种操作中路径最短的。下面这个是系统自带浏览器的效果:
可惜如大家所见,WebBrowser控件不支持这个功能。原本以为会有个OnLinkPressed或者OnImageSelected之类的回调供开发者使用,结果到了Mango一看,还是没有。但前两天下载了UC浏览器,发现它实现了这个功能:
从效果来看,很显然微软没有给UC开小灶,而是UC的工程师凭借自己努力搞出来的,原因有二:
- 这个菜单只有在页面完全载入后才能按出来
- 菜单的触发灵敏程度不稳定,有的时候长按N秒没有反应;有的时候手指移动到链接上动一下就出来了。
实现
据此猜测,UC浏览器用的是注入Javascript的方式实现上下文菜单的互动的。OK,在(我力所能及的)其他方法行不通的情况下,那我们跟也根据这个思路去实现这个功能。既然目标明确,那么需要实现的具体功能也很明确:
- 用户长按页面链接(暂且只支持链接,图片之类可以举一反三)时,WebBrowser可以通知code behind事件发生
- 事件通知的同时还需要带上所点击链接的文本和Url属性
- 在用户所长按位置出现Context Menu,点击具体的Menu Item则利用文本和Url进行后续操作(例如新页面打开、加入收藏等)
和桌面的.net 平台一样,WP7的WebBrowser控件也是通过Navigate方法和Navigated回调进行页面控制的。对于前端页面事件回调,自然率先想到的就是WebBrowser::ScriptNotify()回调,试一下如何?在页面里面写入这样的内容(刚发现blog没装代码高亮插件……):
<a href="#" onclick="javascript:window.external.notify('hi')";>你好</a>
后台绑定方法:
private void webBrowser_ScriptNotify(object sender, NotifyEventArgs e) {
MessageBox.Show( e.value.ToString() );
}
效果不错。继续考虑实现,注入Script则使用InvokeScript。这个方法有两个版本,分别带有1个和2个参数,前者直接执行页面中的函数;后者则是带有参数执行。很显然我们要操作的是外部的网页,并非自己的HTML页面,显然外部的页面是不会如此好心帮你给每一个<a>加上需要的代码执行事件,那么此时就需要Javascript一个特殊的函数eval。eval函数带有一个参数,可以将传入的字符串当作代码执行。很好,于是我们可以用InvokeScript两个参数的版本:
// 函数声明不写了,我把这个函数命名为InjectScripts()
String script =
@"
window.onLinkPressed = function() {
var elem = event.srcElement;
if ( elem != null ) {
window.external.notify( elem.getAttribute('href') + '|' + elem.innerHTML );
}
return false;
};
window.doBindLinks = function() {
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
var elem = elems[i];
elem.attachEvent('onmousedown', onLinkPressed);
}
};";
webBrowser.InvokeScript( "eval", script );
webBrowser.InvokeScript( functionName );
// 可以在页面初始化时候绑定好,页面载入后注入脚本
webBrowser.Navigated += ( s, e ) => {
if ( e.Uri.OriginalString.StartsWith( "http" ) ) {
InjectScripts();
}
};
第一次InvokeScript时候用eval函数执行我们在代码中写好的JS脚本,将需要的方法attach到window节点上。而第二次InvokeScript直接执行需要的方法,为每一个<a>绑定点击事件处理函数。
运行后达到了我们的预期效果。对于ScriptNotify而言,在Mango版本前,从外部通过eval注入的方法调用window.external.notify()是不好用的。也就是说,只有页面中静态的Javascript才有能力回调code behind。而谢天谢地谢M$,Mango里面终于可以这样做了,省了不少事情。另外这里需要注意的是attachEvent方法所绑定的事件为onmousedown,而不是onclick。
如果希望在Windows Phone 7.0上实现同样的效果,可以走前辈给的指路:在Understanding Web Browser Control of Windows Phone 7 – Part 1一文中的实现方法为注入的Script保留一个window节点上面的全局变量,保存链接后得到的属性。code behind内则有一个每1秒执行一次的Timer去不停地抓取这个属性,发现值不为空情况下则当作用户对链接进行了操作。
从这一点也可以见得别看今天我们费了很大力气才能实现这么个功能,到下一个版本微软一边说“嗟,来用”一边开放了相关接口,实现相同的功能可能就是分分钟的事情了。
接下来只剩下Context Menu的UI展现,参考WP7 ContextMenu in depth | Part1: key concepts and API,下载Silverlight Tookit for Windows Phone,导入程序集,将ContextMenu扔到WebBrowser元素里面:
<phone:WebBrowser x:Name="webBrowser" IsScriptEnabled="False">
<toolkit:ContextMenuService.ContextMenu>
<toolkit:ContextMenu x:Name="mnuContext" IsZoomEnabled="False" >
<toolkit:MenuItem Header="新窗口中打开" Click="MenuItem_Click" />
<toolkit:MenuItem Header="链接文本" Click="MenuItem_Click_1" />
</toolkit:ContextMenu>
</toolkit:ContextMenuService.ContextMenu>
</phone:WebBrowser>
后台绑定好Menu Item的事件,直接拿到在ScriptNotify里面保存的链接信息即可,至此功能完成。看看效果:
实现比较简单,代码就不扔了。题外话,利用InvokeScript还可以做很多事情,例如去掉干扰页面正常打开的target=”_blank”等。同样对于本文的应用而言,可以将不同的链接绑定不同的Context Menu,实现更加丰富的功能。
参考资料