Home > Latest topics

Latest topics > Split Browser開発のよもやま話(3):縦横に「分割」するための入れ子構造

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

宣伝2。Firefox Hacks Rebooted発売中。本書の1/3を使って、再起動不要なアドオンの作り方のテクニックや非同期処理の効率のいい書き方などを解説しています。既刊のFirefox 3 Hacks拡張機能開発チュートリアルと併せてどうぞ。

Firefox Hacks Rebooted ―Mozillaテクノロジ徹底活用テクニック
浅井 智也 池田 譲治 小山田 昌史 五味渕 大賀 下田 洋志 寺田 真 松澤 太郎
オライリージャパン

Split Browser開発のよもやま話(3):縦横に「分割」するための入れ子構造 - Jan 17, 2007

Split Browserの話の続き。間が空いてしまったけど。

そもそも何故「分割」なのか? いわゆるMDIアプリケーションのように普通にブラウザをたくさん置けないのか? という疑問は結構多くの人が思うものだと思う。

これはXULの仕様上の制約による部分が大きい。XULの場合は基本的に「内容が横に並ぶボックス(hbox:horizontal box)」と「内容が縦に並ぶボックス(vbox:vertical box)」という縦横の箱の集まりとしてGUIの構造を定義するようになっていて、要素の重ね合わせというのはイレギュラーな事態だからだ。

まあ、この制限を乗り越えてボックスを好きな位置に表示するテクニックというのはいくつかあって、タブのドラッグ&ドロップの時に出てくる矢印や、フィッシング詐欺警告とかのように、Firefox本体でもそんなテクニックが使われていたりはする。しかし現状では、タブの切り替えやbrowser要素等と重なったときに色々と予期しない結果になってしまうこともあって、あくまで補助的にしか使えないかなーというのが僕の印象です。Split Browserでもこのテクニックを使っている部分があるけど、これはまた後述します。

あと、さらに余談だけど、Firefox 3だか4だかではSVGのサポートが改善されてSVGのforeignObject要素の中にインラインフレームを置けるようになるそうで、これを使うともっと自由にブラウザを表示できるようになるのかもしれない。

話を戻す。

XULのボックスを「分割」するというのは、先の図を見れば分かるけれども、実態としては、親要素として用意された水平(hbox)または垂直(vbox)のコンテナ要素に対して、その中にどんどん要素を追加していくという事に他ならない。例えば、


<vbox>
  <hbox>
     <browser id="content"/>
  </hbox>
</vbox>

こんな要素構造になっているのであれば、browser#contentを「右に分割」するというのは、


<vbox>
  <hbox>
     <tabbrowser id="content"/>
     <splitter/>
     <subbrowser id="new-content"/>
  </hbox>
</vbox>

こういうこと。「上に分割」するのなら、


<vbox>
  <subbrowser id="new-content"/>
  <splitter/>
  <hbox>
     <tabbrowser id="content"/>
  </hbox>
</vbox>

という具合だ。

じゃあ、「右に分割」した後で、さらに新しいブラウザ(browser#new-content)を「下に分割」しようと思ったら、どうすればいいか? この場合、そのままだとどう足掻いても無理なので、親要素としてもう一段ボックスをかませてやる必要がある。


<vbox>
  <hbox>
     <tabbrowser id="content"/>
     <splitter/>
     <vbox>
       <subbrowser id="new-content"/>
       <splitter/>
       <subbrowser id="new-content2"/>
     </vbox>
  </hbox>
</vbox>

メモリ効率やらなんやらを考えると、こうしてその都度ボックスを追加してDOMツリーをダイナミックに改変していくのがいいのかもしれないけど……実際やると結構面倒そうだというのは、コードを書いてみなくても何となく想像がつくと思う。今の要素の前後にスプリッタがあるかどうかとか、コンテナが既にあるかどうかとか、条件分岐が異常にめんどくさくなるんじゃなかろうか。

そもそも、めんどくさいとかどうとかいう以前に、tabbrowser(browser)要素を一度でもDOMツリーから取り外すのは絶対に避けないといけない。tabbrowserもbrowserもその機能のほとんどはバインディングで実現されているけれども、バインディングはDOMツリーの中に埋め込まれた瞬間に初期化され、ツリーから取り外された瞬間に破棄される。ということは、既に動いているbrowserをDOMツリー内で「付け替え」ようとすると、そのbrowserをぶっ壊してしまうことになるからだ。

そんなこんなで悩ましいことしきりなので、こういうものはできれば、appendChild()あたりでぽこぽこと要素を追加していくだけで済むようにしておきたい。

そこでSplit Browserでは、新しいsubbrowser要素を追加するときは、あらかじめ必ずコンテナをかませておくという方向で対処してみることにした。このための要素型として用意したのが、「subbrowser-container」要素。これもまた実態はバインディングで挙動と匿名内容を定義した要素型だ。


<binding id="container" extends="#contents-in-container">
  <content orient="vertical">
    <xul:vbox anonid="vertical-container" flex="1">
      <xul:hbox anonid="horizontal-container" flex="1"/>
    </xul:vbox>
  </content>

  <implementation>
    <property name="hContainer" readonly="true"
      onget="try {
        return document.getAnonymousElementByAttribute(this, 'anonid', 'horizontal-container');
      } catch(e) {
        return null;
      }"/>
    <property name="vContainer" readonly="true"
      onget="try {
        return document.getAnonymousElementByAttribute(this, 'anonid', 'vertical-container');
      } catch(e) {
        return null;
      }"/>
    <property name="parentContainer" readonly="true">
      <getter><![CDATA[
        var container = this.parentNode;
        while (container.localName != 'subbrowser-container' &&
               container.parentNode) {
               container = container.parentNode;
        }
        return (container.localName == 'subbrowser-container') ? container : null ;
      ]]></getter>
    </property>
  </implementation>
</binding>

プロパティの定義に登場しているgetAnonymousElementByAttribute()というのは、匿名内容を取得するためのメソッドの一つだ。メソッドの名前をよーく見ると分かるけど、XULDocumentで使えるgetElementsByAttribute()と違って単数形なので、返り値はノードリストではない事に注意が必要。ここでは、これを使っていつでも匿名内容の水平コンテナと垂直コンテナにアクセスできるようにしてある。

vbox[anonid="vertical-container"]が一見すると無駄に思えるけれども、これはどうしても必要。何故かというと、匿名内容とそうでない要素とが同じコンテナの中に混在した時、要素の並び順がおかしくなったりと訳の分からんバグが発生しがちだから。匿名内容は匿名内容だけで、そうでない物はそうでない物だけで固めておくのが、僕の経験上では安全っぽいです。

ともかく、subbrowser要素を追加する時には必ずこのsubbrowser-container要素の子として埋め込むようにして(container.hContainer.appendChild(subbrowser)といった感じ)、ボックス構成に冗長性を持たせておくようにした。あとはこれを入れ子にしさえすれば、いつでも必要に応じて、appendChild()insertBefore()だけで縦横に自在に「分割」していける。

こんな風にバインディングを使った要素を入れ子にしていった場合、DOMツリーのネストが深くなってしまうのが難点といえば難点だけれども、それよりも、上から下まで全部同じ構造の繰り返しになることによるメリットの方が実は大きい。再帰処理を書く場合なんかがいい例だ。実際Split Browserでも、この構造を活かして、分割の状態を保存・復元する処理はどちらも再帰で書いている(これもまた後述するつもり)。

無限に分割できるというのは、本当のところを言うと「目的」ではなくて、再起を活かした設計にしたことによってもたらされた副次的なメリットに過ぎない。だいいち、実用性を考えれば実際使うときは2~3程度に分割するくらいまでしか使わないと思う。しかし、だからといって「ブラウザは2つ」と決め打ちして設計してしまうと、いざ「3つに増やしたい」と思った時に、またコードをいじらないといけなくなる。そこが問題なんですよね。

一つの物を二つに増やすのは、割と大変だけれども、実りも大きい。二つの物が競合せずに動くようにする過程で、設計の抽象度が高まって、その後のメンテナンス性や拡張性が向上するというメリットがある。でも、二つに増やしたものを三つに増やすのは、作る側には何もいいことがない。二つに増やした時のコードをコピーして書き換えるだけだから、作業は簡単だろう、って? 確かに簡単だけど、そうすることで全く同じコードがプログラムの中に何度も繰り返し登場するようになってしまい、結果、一つに不具合が見つかったら全てのコピーに手を入れないといけなくなる。こんな無駄な作業をするくらいなら、三つでも四つでも無限に増やせるように設計して、定義は多少面倒でも一回だけで済ませた方が、後々のメンテナンス性や拡張性はずっと高く保てる。

再帰的な構造に限らず、物を作るときは抽象度を高くしておくのがいい。XML(XUL)では、XBLを使えばそれがしやすくなる。XULやるときはXBLも一緒に憶えておくといいですよ。……という話なのでした。

まだ続く。

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

Comments/Trackbacks

trunk では…

バインディングを使ったネスト、確かに便利なんだけども、trunk では
https://bugzilla.mozilla.org/show_bug.cgi?id=350754
Bug 350754 -- Crash [@ ntdll.dll][@ nsFrameManager::GetPrimaryFrameFor] with xbl testcase
が入ったせいで、同じバインディングを使ったネストはできない。
無限ループを防止するのが目的の修正なので、ある制限値を設けてそこまではネストを許してくれても良さそうなものなのにそこまでは考えてくれなかったようで、全くネストできない。
ということで、trunk + 現状の Split Browser では、分割したブラウザをさらに分割することはできないです。
trunk を常用してるので、trunk で使おうかと思いましたがこれであきらめました…

ということで、
> あと、さらに余談だけど、Firefox 3だか4だかではSVGのサポートが改善されてSVGのforeignObject要素の中にインラインフレームを置けるようになるそうで、これを使うともっと自由にブラウザを表示できるようになるのかもしれない。
これ激しくキボンヌ

Commented by at 2007/01/18 (Thu) 02:16:37

ぇぇぇぇー

ってことはtabboxの中にさらにtabboxを入れることもできないんでしょうか?

Commented by Piro at 2007/01/18 (Thu) 10:11:37

no title

ああ、同じバインディングを持った要素のネスト、なら大丈夫ですよ。
あいまいでしたね、すいません。

要素-(バインディング)->匿名内容-(さっきと同じバインディング) とか、
要素-(バインディング)->匿名内容->匿名内容-(さっきと同じバインディング) とかはダメ、

要素-(バインディング)->children=匿名じゃない内容-(さっきと同じバインディング) なら大丈夫です。

Commented by at 2007/01/19 (Fri) 02:08:48

TrackBack ping me at


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

Post a comment

writeback message: Ready to post a comment.

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

Powered by blosxom 2.0 + starter kit
Home

カテゴリ一覧

過去の記事

1999.2~2005.8

最近のつぶやき

オススメ

Mozilla Firefox ブラウザ無料ダウンロード