Windows Phone上为WebBrowser增加链接的Context Menu

(似乎这篇文章被垃圾评论认准了,所以无奈关评)

问题

对于浏览器而言,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,实现更加丰富的功能。

参考资料

《ちはやふる》(歌牌情缘)

《ちはやふる》这部连名字都不好翻译的今年10月新番看过后,实在是刺激人更新Blog的欲望,以便进行布教。对于获得了2009年漫画大赏以及2010年“这部漫画好厉害!”第一名的同名原作的漫画分类来说,网上的评论多数倾向于“披着少女们漫画皮的热血漫”。老实说是看到翻译有偏差的台版译名《花牌情缘》,作为花牌Fans才决定点进去看一集再说。结果看了半天,发现不是花牌(花札),而是以“小仓百人一首”作为游戏基础的“歌牌”。

虽然花札和歌牌在现代日语中都可以使用来自于葡萄牙语“纸牌”的Carta进行指代,写作“かるた”,但实际上从牌面类型到游戏玩法则是完全不同的。歌牌的另外一个叫法就是“百人一首”,也就是上面说过的“小仓百人一首”。这本是日本镰仓时代的藤原定家收集和编撰的“和歌”集,说白了就是诗集,里面收集了100首从《古今和歌集》等古诗集中挑选的作品。后来到了浮世绘流行的年代,有人将这个集子配上了符合意境的画作,渐渐演变成了纸牌的形式——甚至加入了双人、多人对抗的元素:两方随机分到一半牌面按照自己的喜好排列,听到旁白念出的上半句之后去抢对应的下半句所在的牌面,最终拿到更多牌的人人获胜。因为有的时候抢牌的动作可能非常大,百人一首又有个“榻榻米上的格斗技(畳の上の格闘技)”的称号。

《ちはやふる》的主线推动道具,就是这种双人对抗的古老纸牌游戏。其他动画、漫画作品中也曾有百人一首的出场,例如《幸运星》中大小姐提到自家虽然是偏向西式风格,但在日本新年时候反而以和式方式庆祝,其中就有百人一首的歌牌游戏。另外《寒蝉鸣泣之时》中也有一集用相当的篇幅描绘主角们进行歌牌游戏的场景。

回到本作的剧情上来:女主千早成立的“竞技歌牌部”没人来,但无意中发现小学同学、曾经一起玩百人一首的太一和自己同高中,于是决定拉他入部。故事讲到这里突然拉回到小学时候,出现了另外一个被称为百人一首魔王天才的人物绵谷新。也是拜他所赐,女主终于知道百人一首还可以杀人这么燃,对战起来不啻于简单的听音找牌游戏。对于剧中人物来说,这可能是几十分钟的回忆,但要说清楚来龙去脉又不让观众觉得“啊上来就回忆杀,有没有搞错”,监督实在是太大胆了。

第一集观赏完毕后我在豆瓣上给了3分,觉得不看好剧情发展,对于故事后续展开存疑。结果从第二集开始,这样白痴的评价就被连续打脸:故事不仅充实,而且转折多,避重就轻进行人物描画,女主、太一和新三个人的形象越来越明显,同时在最后三人分离时候煽情了一把的同时还不忘为后续高中时代的故事做铺垫,这个剧的原作、监督、脚本和构成真是不简单!监督浅香守生指导过的作品包括《NANA》、《人间失格》剧场版、《魔卡少女樱》、《青之文学》、《人形电脑天使心》等,可以说偏向于少女漫画改编作品。

而《ちはやふる》的原作末次由紀更要提一下,因为她的经历相当坎坷。虽然09-11年靠着《ちはやふる》拿奖拿到手软,但之前在05年因为旧作《エデンの花》被指控抄袭他人(这个他人名气比较大,井上雄彦……而且竟然抄的是《SLAM DUNK》和《リアル》,末次老师在想什么)作品构图后,因为证据确凿,末次老师承认抄袭并且在杂志刊登谢罪文,正在连载的作品也被暂停,而已发行的单行本全部回收。直到07年才复出进行新作的创作。一年后开始连载《ちはやふる》,可以说是末次老师从少女漫画慢慢转向受众面更大类型漫画的尝试,虽然画风还是标准的少女漫,但在视觉元素下面铺设的却是竞技、热血的故事。所以虽然讲谈社漫画奖上这部作品拿到的还是“少女部门”奖项,但个人认为这仅仅是因为所刊登杂志类型的影响,让人相当期待作者后续作品。在这里想到之前新浪微博上被指认抄袭他人作品的某几位漫画创作者及其支持者的态度,天差地别,虽然最后还是道歉了事,但完全没有为之前的抄袭付出代价。

最后说一说这部作品的音响部分。MadHouse对于第一主角千早大胆起用了新人瀬戸麻沙美,而男性角色方面则选择了已经成名的宮野真守和細谷佳正。而同样在今年因为《那朵花》出名的茅野愛衣也在阵容之内,到目前为止可以说CV表现太好了(或者说没有棒读系就已经很好了)。OP和ED也非常好听,属于看片时候绝对不能跳过的类型。

Update:剧中女主的“神卡”所写的诗句为在原业平所作“ちはやぶる神代もきかず竜田川からくれなゐに水くくるとは”。

很明显,作品名即来源于此,但包括一些专业的百人一首讲解网站以及书籍内都可以看到,这句的开头是“ちはやぶる”,其中第四个假名为浊音,并非作品名中的“ふ”。这个网页内则是两者皆有。之所以如此变化的原因在OP中可以看到,是因为Chihaya Full的谐音,应当表达的是“千早、全力”之意:

顺带一提,这一首的刘德润译本为

悠悠神代事,黯黯不曾问。枫染龙田川,潺潺流水深。

流云字幕组所采用的各集标题译文均应为刘德润版本。