Home > Latest topics

Latest topics > 画面上の座標を指定してその位置の要素を取得する、という処理

宣伝。日経LinuxにてLinuxの基礎?を紹介する漫画「シス管系女子」を連載させていただいています。 以下の特設サイトにて、単行本まんがでわかるLinux シス管系女子の試し読みが可能! シス管系女子って何!? - 「シス管系女子」特設サイト

画面上の座標を指定してその位置の要素を取得する、という処理 - May 09, 2007

タブカタログのサムネイル上でリンクをクリックしたらリンク先に飛べるようにしたついでに、リンク上の中クリックでタブも開けるようにしてみたんだけど、これ、ちょっとでもクリック位置がずれたらタブを閉じてしまいかねないなあ。いまリンクの上にポインタがあるのかどうか一目で分かるようにしたいけど、CSSのcursorみたいなことはJavaScriptじゃ実現できないようなあ……と思った末に、現在ポイントしている位置のクリック可能な要素を強調表示するということを思いついた。でも、これで本当に実用的な速度で動くかどうか?というのははなはだ疑問だった。

GomitaさんのTab Scopeでは、DOM2 Tree Walkerによってすべてのクリック可能な要素を頭から順番に走査していき、要素のボックスの範囲がclickイベントの発生した座標を含んでいるかどうかをチェックする、という手順によってcanvas上のクリック位置に対応するリンクやボタンを見つけている。タブカタログもこの実装をそのまま使わせてもらっている。

この方法の問題は、ページの中にリンクやボタンが大量に配置されていたり、ページの末尾近くまでスクロールしていたりすると、クリック位置の要素を見つけるまでにものすごく時間がかかってしまうという点だ。クリック時のみの処理ならまあ我慢できるかもだけど、mousemoveイベントを拾って「今クリック可能な要素をポイントしている」時だけその事を示そうと思ったら、相当悲惨な事になるのは目に見えている。っていうか実際やってみてあまりの重さに死にかけた。

そこで、何年も前に試して諦めたnsIAccessibleを使う方法に再び挑戦してみることにした。

結論から言うと、これのおかげで現在の位置の要素の取得を飛躍的に高速化できて、やっと実用レベルになったと思う。

当該箇所の最終的なコードはこんな感じになった。

getClickableElementFromPoint : function(aWindow, aScreenX, aScreenY) 
{
  var accNode;
  try {
    /*
      クリック位置からアクセシビリティ用のノードを得る
      参考:http://www.mozilla-japan.org/access/architecture.html
    */
    var accService = Components.classes['@mozilla.org/accessibilityService;1']
              .getService(Components.interfaces.nsIAccessibilityService);
    var acc = accService.getAccessibleFor(aWindow.document);
    accNode = acc.getChildAtPoint(aScreenX, aScreenY);
    /* アクセシビリティ用のノードからDOMのノードを得る */
    accNode = accNode.QueryInterface(Components.interfaces.nsIAccessNode).DOMNode;
    /*
      この時点で、得られたノードがクリック可能な要素またはその子孫である場合、
      祖先をたどってクリック可能な要素を返す。
    */
    var clickable = accNode ? this.getParentClickableNode(accNode) : null ;
    if (clickable)
      return this.getImageInLink(clickable) || clickable;
  }
  catch(e) {
  }

  var doc = aWindow.document;
  /*
    アクセシビリティ用のノードから得られたDOMノードがクリック可能な要素または
    その子孫で *なかった* 場合でも、かなり近い位置の祖先ノードは取得できている。
    なので、検索をそこからスタートすれば、相当な高速化になる。
  */
  var startNode = accNode || doc;
  var filter = function(aNode) {
    switch (aNode.localName) {
      case 'A':
        if (aNode.href)
          return NodeFilter.FILTER_ACCEPT;
        break;
      case 'BUTTON':
        return NodeFilter.FILTER_ACCEPT;
        break;
      case 'INPUT':
        if (aNode.type == 'button' || aNode.type == 'submit' || aNode.type == 'image')
          return NodeFilter.FILTER_ACCEPT;
        break;
    }
    return NodeFilter.FILTER_SKIP;
  };
  var img;
  var walker = aWindow.document.createTreeWalker(startNode, NodeFilter.SHOW_ELEMENT, filter, false);
  for (var node = walker.firstChild(); node != null; node = walker.nextNode())
  {
    if (
      node.hasChildNodes() &&
      (img = this.getImageInLink(node))
      )
      node = img;
    var box = doc.getBoxObjectFor(node);
    var l = box.screenX;
    var t = box.screenY;
    var r = l + box.width;
    var b = t + box.height;
    if (l <= aScreenX && aScreenX <= r && t <= aScreenY && aScreenY <= b)
      return node;
  }
  return null;
},

nsIAccessibleServiceを使って取得できるnsIAccessibleというのは、DOMツリーのサブセット的な物で、ユーザが操作可能な要素?のみで構成されたツリーらしい。nsIAccessibleのgetChildAtPoint()メソッドを使うと、画面上の座標から、その位置にあるユーザが操作可能な要素に対応するノード(nsIAccessible)が取得できる。3年前は、ここからDOMノードを取得する方法が分からなくてドツボにハマッていた。nsIAccessibleにはgetDOMNode()というメソッドがあってこれでDOMノードを取れると思いきや、「未実装だYO!」というエラーが起こってにっちもさっちもいかなくなっていた。

ところが、この度情報を求めて検索していてたどり着いたBug 249421 – Remove getDOMNode from nsIAccessible (it is already available in nsIAccessNode)を見て初めて、このメソッドを使わなくてもnsIAccessNodeインターフェースを介せばnsIAccessibleからDOMノードを取得できる(ようにいつの間にかなっていた)ということを知った。というかこのリンク先のバグの修正の結果、そもそもnsIAccessibleからgetDOMNode()メソッド自体が削除されたらしい。

というわけで早速getChildAtPoint()を使ってみたんだけど、実際には期待に反した動作をすることがままあるようだった。リンクをポイントしてもその祖先要素のblockquote要素が取得されてしまったりして……ていうかこれってもしかして文字の隙間からその背景要素を取得してしまってそうなるという話なんだろうか? まあともかく、狙った位置の要素を確実に取得するのは残念ながら無理っぽい。

とはいえ、この方法で取得した要素ノードは、これまでクリック可能な要素の走査の起点にしていたルートノードに比べればずっと、目的としているノードに「近い」位置にあることは間違いない。というわけでフォールバックとしてこれまでの処理も組み合わせてやることで、やっと、意図通りに実用的な速度で動くようになってくれた。

これでXUL/MigemoやCrossFireの動作ももっと高速化できるかも?

分類:Mozilla > XUL, , , , 時刻:04:11 | Comments/Trackbacks (5) | Edit

Comments/Trackbacks

no title

Gecko 1.9以降ですが、nsIDOMWindowUtils::sendMouseEventメソッドという手もあります。
これを使えば一気にスマートになるのですが、互換性維持のためTab Scopeでは採用見送りとしています。

var req = win.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
var winUtils = req.getInterface(Components.interfaces.nsIDOMWindowUtils);
winUtils.sendMouseEvent(aEvent.type, x, y, aEvent.button, 1, 0);

Commented by Gomita at 2007/05/10 (Thu) 04:15:48

うまく使えないものだろうか

Tab Catalogでも、最初は、Gecko 1.9系ではそれを使うようにしようかと思ってたんです。
しかしクリックされた要素によって挙動を変えたい(Tab Catalogでは設定を変えると、クリック可能な要素だった時にはそれに対してクリックイベントを送り、そうでないときはタブを選択、という風に動くようになります)と思ったので、今ではコメントアウトしてあります。

この方法でmousemoveやmouseoverイベントを送ればもしかしたらいいのかもしれませんけど、その場合、他の拡張機能やなんかとかちあうのが怖いですね。

nsIAccessibleのgetChildAt()で狙った位置の要素をピンポイントでとれればいいんですけどねぇ。

Commented by Piro at 2007/05/10 (Thu) 14:54:51

no title

なるほど、sendMouseEventだとその位置にある要素が何であるかによらずとにかくクリックする、ってことになってしまいますね。
もうひとつの望みとしてこのバグに期待したいところですね。
Bug 199692 〓 support document.elementFromPoint(x,y)
https://bugzilla.mozilla.org/show_bug.cgi?id=199692

Commented by Gomita at 2007/05/11 (Fri) 02:03:25

no title

うご。まさにそのものじゃないすか。
早速vote。

Commented by Piro at 2007/05/11 (Fri) 14:33:11

nsIAccessibilityService、リーク

nsIAccessibilityServiceを一度でも取得すると、以降ウィンドウが全て残る。 Tomblooでは、getElementByPosition(x, y)という、特定座標の下にある要素を取得する部分で使用していた(Firefox 3の実装はどうやってるんだろう)。 以下のコードだけで起きた。 Components.class

Trackback from 実用 at 2007/12/03 (Mon) 13:02:22

TrackBack ping me at


の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2007-05-09_screen.trackbacknoda」です。これは機械的なトラックバックスパムを防止するための措置です。

Post a comment

writeback message: Ready to post a comment.

2020年11月30日時点の日本の首相のファミリーネーム(ひらがなで回答)

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のコメント

最近のつぶやき