Cocoaはやっぱり!
インターネットにアクセスしよう
Web Kit #8 : リンク先を表示

今回のテーマ

今回のテーマは、「リンク先の表示」です。技術的には、WebUIDelegateというプロトコルの解説になりますので、このプロトコルにまつわる内容も扱います。

推奨環境 この解説は、以下の環境を前提に作成し、動作確認等を行っています。ご確認ください。

改版履歴

サンプルの概要

前回までで作成したWebブラウザでは、マウスが指しているリンクのリンク先が表示されていませんでした。これを、ステータスエリアに表示するようにしてみます。画面のデザインやインスタンスの構成は前回と同じです。

デリゲートを設定

今回使用するのは WebUIDelegate です。UIというのはユーザインターフェイスのことですが、マウスがWebView上で動いたり、コンテキストメニューを表示する直前などに呼ばれるデリゲートです。

MyWebViewDelegateのインスタンスをWebUIDelegateに設定するには、WebViewの setUIDelegate : メソッドを使用します。これを前回のアプリケーション起動時のところに組み込みます。

デリゲートの設定: MyWebViewDelegate.m
- (void) applicationDidFinishLaunching : (NSNotification *) aNotification { // デリゲートの設定 [ vWeb setFrameLoadDelegate : self ]; [ vWeb setResourceLoadDelegate : self ]; [ vWeb setUIDelegate : self ]; // (1) デリゲートの設定 // ユーザインターフェイスの更新 [ self updateUserInterface ]; }

(1) setUIDelegate : メソッドで、自分自身をデリゲートに設定しています。

リンク先の表示

では、マウスの指しているリンク先の表示の処理を見ていきましょう。WebViewは、WebView内でマウスが動く度に、デリゲートの webView : mouseDidMoveOverElement : modifierFlags : メソッドを繰り返し呼び出します。

マウス移動時の処理 : MyWebViewDelegate
- (void) webView : (WebView *) sender mouseDidMoveOverElement : (NSDictionary *) elementInfo modifierFlags : (unsigned int ) modifierFlags { // 1. リンク先のURLを取得 NSString *sLinkUrl = [ elementInfo objectForKey : WebElementLinkURLKey ]; // 2. リンク先のURLを表示 if ( sLinkUrl ) [ vStatus setStringValue : sLinkUrl ]; else [ vStatus setStringValue : @"" ]; }

(1) パラメータのelementInfoには、マウスカーソルの場所にあるHTMLの要素 ( Element ) についての情報が辞書形式で格納されています。これは、WebPolicyDelegateで解説した項目情報と同じものです。この辞書から WebElementLinkURLKey のキーで辞書からリンク先のURL文字列を取得できます。

(2) URLが得られたら表示を行います。vStatusは、ステータスを表示するためのNSTextFieldを示しているアウトレットです。 URLが得られなかった場合は、リンクがマウスの下には無いので表示を消します。

マウスのホイールをまわしたときも、呼ばれるので、コントロールキーを押しているときは、文字サイズをホイールによって変えられるようにしようかと思ったのですが、実現できませんでした。

パラメータのmodifierFlagsは、シフトキーなどのモディファイキーが押されているかどうかを知ることが出来ますので、コントロールキーの状態も調べることが出来ますが、ホイールの情報を得ることはどうも難しい模様。仮に得られたとして、文字サイズを変えられてもスクロールを止めることはこのメソッド内ではできません。

そこで、WebViewのサブクラスを作ってそっちでなんとかしようかと思いきや、mouseMoved : はひったくられているようで、サブクラスのmouseMoved : は呼ばれません。さらに、HTML中の文字列が選択されていると、サブクラスのmouseMoved : が呼ばれるようになり、デリゲートのメソッドが呼ばれなくなるという現象も発生。

JavaScriptのステータスエリアのメッセージ表示に対応

JavaScriptでステータスエリアにメッセージを表示しているページをたまに見かけます。以下のようなスクリプトを実行すると、ステータスエリアに文字列が表示されます。

window.status = "メッセージ";

Web Kitでは、上記のJavaScriptを実行してもメッセージはどこにも表示されません。WebViewは、コンテンツは表示しますが、ステータスエリアがどこにあるか知らないためです。このため、WebViewは、WebUIDelegateに対してステータスエリアのメッセージが書き込まれたことを通知して、デリゲート側に表示を任せています。

webView : setStatusText : メソッドが呼ばれますので、そこで、自分で用意したNSTextFieldなどに文字列を表示させればよいです。

JavaScriptのステータスエリアのメッセージ表示時の処理 :
- (void) webView : (WebView *) sender setStatusText : (NSString *) text { [ vStatus setStringValue : text ]; // (1) ステータスエリアに文字列を表示 }

第2パラメータに表示すべき文字列が渡ってきますので、これをそのままvStatusにセットします。

このように、JavaScriptのスクリプトの実行によって、WebUIDelegateが呼ばれるケースは他にも沢山あります。

コンテキストメニューを変更する

WebViewは、特に何もしなくてもコンテキストメニューが自動的に表示されます。ただし、標準で用意されているものが表示されるだけで、必要の無いものがメニューにあったり、デリゲートを用意していないと実行できないものがあったりという状態です。コンテキストメニューの中身は、WebUIDelegateを使って変更が出来ます。いらないものは削り、必要なものを追加することができるのです。

コンテキストメニューを表示する直前には、毎度、WebUIDelegateの webView : contextMenuItemsForElement : defaultMenuItems : メソッドが呼ばれます。毎度呼ばれるのは、マウスカーソルでクリックしているものによってメニューの中身が変わるからです。リンクをクリックしている場合は、「リンク先をダウンロード」だったり、画像だったら「画像をクリップボードにコピー」だったりするわけです。Webページ上で選択された文字列を右クリックした場合は「コピー」だったりもします。ということで、毎度何がクリックされているかを見て、適切なメニューを作成することになります。

ここではサンプルとして、「新しいウィンドウを開くメニューの削除」と「Googleのサイトへのジャンメニューの追加」を行います。

コンテキストメニュー表示直前 : MyWebViewDelegate.m
- (NSArray *) webView : (WebView *) sender contextMenuItemsForElement : (NSDictionary *) element defaultMenuItems : (NSArray *) defaultMenuItems { // (1) defaultMenuItemsをNSMutableArrayへ複製 NSMutableArray *modifiedMenuItems = [ defaultMenuItems mutableCopy ]; NSMenuItem *mItem; // (2) 不要なメニューを削除 int i = 0; while( i < [ modifiedMenuItems count ] ) { mItem = [ modifiedMenuItems objectAtIndex : i ]; switch( [ mItem tag ] ) { case WebMenuItemTagOpenLinkInNewWindow : // リンクを新ウィンドウに case WebMenuItemTagOpenImageInNewWindow : // 画像を新ウィンドウに case WebMenuItemTagOpenFrameInNewWindow : // フレームを新ウィンドウに [ modifiedMenuItems removeObject : mItem ]; break; default: i++; break; } } // (3) メニューを作成 mItem = [ [ NSMenuItem alloc ] init ]; [ mItem setTitle : @"Go to Google" ]; [ mItem setTarget : self ]; [ mItem setAction : @selector( goToGoogle : ) ]; // (4) メニューを追加 [ modifiedMenuItems addObject : mItem ]; [ mItem release ]; return( modifiedMenuItems ); }

このメソッドの第3パラメータdefaultMenuItemsには、WebViewが準備しているデフォルトのメニューが渡ってきます。配列クラスになっていますが、メニュー項目( NSMenuItem )の配列になっています。このメソッドは、表示させたいメニュー項目の配列を返す仕様になっていますので、defaultMenuItemsをベースに修正する方法をここでは採っています。

(1) 第3パラメータdefaultMenuItemsをNSMutableArrayにコピーします。mutableCopyメソッドを使うとNSArrayからNSMutableArrayの複製を作ることができます。

(2) 複製した配列の中から不要なメニューを削除していきます。各メニューをobjectAtIndex : でNSMenuItemが取り出せます。デフォルトで用意されているメニューにはタグがついていまして、それぞれの値で識別をすることが出来ます。tagメソッドで取得して、不要なものをremoveObjectで配列から削除していきます。

(3) 追加するメニューはNSMenuItemをallocしてinitした後に、タイトルとターゲットとアクションを設定すれば最低限の動きをします。

(4) メニューを追加するには、配列にこのNSMenuItemを追加するだけです。配列内の順番のままメニューになりますが、今回はaddObject : を使っていますので末尾に追加されます。最後に、この配列を戻り値として返します。

このメニューアイテムのアクションとして、goToGoogle : メソッドを指定していましたので、そのメソッドも実装しておく必要があります。

Googleへジャンプ : MyWebViewDelegate.m
- (void) goToGoogle : (id) sender { // (1) リクエストを作成 NSURL *url = [ NSURL URLWithString : @"http://www.google.com/" ]; NSURLRequest *req = [ NSURLRequest requestWithURL : url ]; // (2) メインフレームにリクエストを通知 [ [ vWeb mainFrame ] loadRequest : req ]; }

(1) Googleへジャンプするためのリクエストを作成します。

(2) loadRequest : メソッドを使って、メインフレームをGoogleへジャンプさせます。

クリックしたものによってコンテキストメニューを変えたい場合は、第2パラメータのelementに項目情報が入っていますので、以下のようにして処理を分岐させて、メニューを追加していきます。

クリックしたものによってメニューを変える : MyWebViewDelegate.m
if ( [ element objectForKey : WebElementImageURLKey ] ) { // (1) 画像をクリック mItem = [ [ NSMenuItem alloc ] init ]; [ mItem setTitle : @"Copy URL of Image to Clipboard" ]; [ mItem setTarget : self ]; [ mItem setAction : @selector( copyImageUrl : ) ]; [ modifiedMenuItems addObject : mItem ]; [ mItem release ]; } if ( [ element objectForKey : WebElementLinkURLKey ] ) { // (2) リンクをクリック mItem = [ [ NSMenuItem alloc ] init ]; [ mItem setTitle : @"Add Link to Bookmark" ]; [ mItem setTarget : self ]; [ mItem setAction : @selector( addLinkToBookmark : ) ]; [ modifiedMenuItems addObject : mItem ]; [ mItem release ]; }

(1) クリックしたものが画像の場合は、その画像のURLが項目情報に入ってきますので、 WebElementImageURLKey をキーにして情報を取り出します。

(2) クリックしたものがリンク場合は、そのリンク先のURLが項目情報に入ってきますので、 WebElementLinkURLKey をキーにして情報を取り出します。

さて、自分が追加したメニューについては、直接アクションメソッドを呼び出してくれますが、デフォルトのメニューはどうなるかです。「テキストやURLや画像やをクリップボードにコピーする」系のものは、特にデリゲートで何もしなくても全て自動的に処理されます。「新規ウィンドウに表示」系のものは、リンクをクリックしたときと同じように色んなデリゲートが呼ばれてページの読み込み処理が開始されます。「ダウンロード」系のものは、NSURLDownloadがダウンロードを開始しますので、ダウンロードのデリゲートを用意しておけば、ダウンロードが行われます。