Home > Latest topics

Latest topics > 画面の描画内容を一時的にロックしておいて、裏であれこれして最後にまとめて描画させる方法

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

画面の描画内容を一時的にロックしておいて、裏であれこれして最後にまとめて描画させる方法 - Dec 18, 2009

ツリー型タブとJetpackが同時にインストールされているとコンテンツ表示領域に何も表示されなくなってしまう、という問題の原因がやっと分かった。そこからさらに調査をして、表題のような「画面の再描画を任意に停止・再開させる」方法が見つかった。

先にやり方だけ書いとくと、こうするとできる。

var rootContentViewer = window.top
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocShell)
                          .contentViewer;
rootContentViewer.hide(); // これで画面の描画が止まる

gBrowser.addTab(); // これによって起こる変化は画面上に現れない
gBrowser.addTab(); // この変化も画面上に現れない
gBrowser.addTab(); // 同上

rootContentViewer.show(); // ここでやっと描画が再開される

24日追記。この方法には重大な弊害があることが分かりました。使用を検討している人はより安全な方法を使うようにして下さい。

以下は、これに辿り着いた経緯のお話。

前提(今まではどうしてたか)

ツリー型タブには「タブバーを自動的に隠す」機能がある。これを有効にすると、タブバーが小さく折り畳まれる。その状態でタブバーの近くにポインタを持って行くと、タブバーがコンテンツ表示領域に覆い被さるような形で展開される。という機能を実現するにあたって、タブバーの幅を動的に増やす→その分コンテンツ表示領域が縮まるので、幅が変わらないようにコンテンツ表示領域を画面外にネガティブマージンで広げる→表示がズレるので、コンテンツ領域の描画位置をgBrowser.markupDocumentViewer.move(xOffset, yOffset)でずらす という事をやっているのだけれども、何も考えずにやるとこの工程が全部見えてしまうため、タブバーの表示・非表示が切り替わる瞬間に画面がチラついてだいぶ見苦しいことになる。

そこでこのチラつきをなんとかしたくて、以前、ウィンドウ全体を覆い尽くすHTML Canvasを一番手前に表示してその裏でゴニョゴニョして最後にCanvasを非表示にすることで画面のチラつきを隠すfullScreenCanvas.xulというライブラリというのを作った。せっかくだから他にもいくつかのアドオンで流用した。

今までのやり方の問題

しかしよく調べてみた所、このライブラリのやっていることが原因で、冒頭のような衝突が起こっているらしいということが分かった。

  • JetpackはSlidebar機能を実現するにあたって、tabbrowser要素全体と同じ大きさのbrowser要素を、tabbrowser要素に重なるような形で、opacity: 0の状態でウィンドウ内に配置している。
    • 通常時は、画面描画はSlidebar用のbrowser要素→tabbrowser要素内のbrowser要素という順番で行われる。なので何も問題は無い。
  • fullScreenCanvas.xulはCSSのposition: fixedでcanvasをウィンドウ全体を覆い隠せる位置に配置する。
    • しかしFirefox 3.0以降では、この方法で配置したcanvasよりも上(手前)にtabbrowser要素内のbrowser要素の内容が描画されてしまう。
    • browser要素の内容よりも上に描画されるのは、そのbrowser要素よりも後に生成されてDOMツリーに挿入されたフレームだけのようだ。
    • なので、fullScreenCanvas.xulではFirefox 3.0以降においては動的にbrowser要素を生成してその中にcanvasを置くようにしていた。
  • この時期待される描画順は、JetpackのSlidebarのbrowser要素→tabbrowser要素内のbrowser要素→fullScreenCanvas.xulのcanvasを格納しているbrowser要素 という順番である。
    • しかし実際には、fullScreenCanvas.xulのbrowser要素を非表示にした段階で上記の描画順が狂ってしまい、tabbrowser要素内のbrowser要素→JetpackのSlidebarのbrowser要素 という順番で描画が行われるようになってしまっていた。
    • その結果、tabbrowser要素の上に完全に透明なbrowser要素が乗っかる形となり、Slidebarの透明なbrowser要素とtabbrowser要素のbrowser要素が重なった部分が透明になってしまっていた(背景色が透けて見えていた=何も見えなくなっていた)。

Firefox 3以降ならpanel要素が使えるのだからそれで代用してみようか(panel要素はポップアップウィンドウ扱いなので、他の部分に影響を与えないで一番手前に任意の内容を表示できる)、と思ってまずはそれを試してみた(browser要素の中にcanvasを置くのではなくpanel要素の中にcanvasを置くようにした)のだけれども、Windows環境ではそれなりの速度で動いたものの、Ubuntu上ではpanelが表示されたり消えたりするときにものすごいチラつきが発生して、却って逆効果にすらなってしまっていた。

そもそもの問題として、全部のフレームを走査してそれぞれをdrawWindow()で描画するということをやっていたからその分重くなっていたというのも、無視できないポイントだったと思う。

やりたかった事

似たようなことをやっていて似たような悩みを抱えていたであろうアドオンの中で、一番理想に近い事をしていたのがAutohideだった。Autohideではそのものズバリ「描画を停止する」「描画を再開する」といった機能を提供するC++製のXPCOMコンポーネントのバイナリを含んでいて、低層の部分にアクセスして実現しているようだ。しかしソースは公開されていないので、結局何をどうすればいいのかは分からずじまいだった。

nsIContentViewer

調べてみた所nsIContentViewerインターフェースにenableRenderingという真偽値のプロパティがあることが分かった。これをfalseにすれば再描画が一時的に行われなくなるのではないか? と思って、やってみた。

そのビューのnsIContentViewerには、nsIDocShellのcontentViewerプロパティでアクセスできる。また、nsIDOMWindowから辿ってそのフレームのnsIDocShellにもアクセスできる。ということでこれを組み合わせて、トップレベルのウィンドウのnsIContentViewerのenableRenderingをfalseにしてみた。

var rootContentViewer = window.top
                          .QueryInterface(Ci.nsIInterfaceRequestor)
                          .getInterface(Ci.nsIWebNavigation)
                          .QueryInterface(Ci.nsIDocShell)
                          .contentViewer;
rootContentViewer.enableRendering = false;

しかしこれは、期待に反して何の効果もなかった。というかそもそもこのプロパティが本来何を行うためのものなのか自体が分かってなかったんだけど。SetEnableRendering等のキーワードでC++のコードの部分も検索してみたけど、何がどうなってるのかは結局分からなかった。

それでもうだいぶ諦め気分になって、せめて「内容がズレて表示される→元の位置に表示し直す」という処理がガタついて見えるのだけでもなんとかごまかしたい、と思ってnsIContentViewerの他のメソッドを試していた。gBrowser.markupDocumentViewer.hide()とやると(gBrowser.markupDocumentViewergBrowser.docShell.contentViewerへのエイリアス)、確かに名前の通りフレームの内容が描画されなくなる(=グレー1色で塗りつぶされたような状態になる)。そういえばこれはまだ試してなかったな、と思ってトップレベルのフレームのnsIContentViewerのhide()も試してみた。「ウィンドウ全体がグレー1色で塗りつぶされたようになるのかなー」とか考えてた。

そしたら今度は予想に反して、その瞬間に描画されていた内容が保たれたままで、それ以降の再描画だけが止まった。さらに、show()メソッドを呼んだら、再描画が止まっていた間の変化が一気に画面上に現れた。おおお!!! 一番やりたかった事があっさりできてるじゃないか!!!!!!

ということで、冒頭に書いたようなコードで描画の一時停止と再開が簡単にできる事が分かった。少なくとも、Windows VistaとUbuntuで試した限りではこういう動作になった。

なんで今まで気付かなかったのか……と思って実験してみたら、Firefox 3.0やFirefox 3.5やMinefield 3.7a1preでは前述のような動作になったんだけど、Firefox 2ではhide()を呼んだ後にDOMツリーに何か変更があると勝手に描画が再開されてしまった(Firefox 3以降でも、ウィンドウのフォーカスが変わるとかウィンドウ内をクリックするとかのトリガーで描画が再開される)。一番最初に再描画の事を調べた頃はまだ最新版はFirefox 2だったと思うので、それで気付かなかったんだろう。当然Firefox 2にも対応するアドオンではこの方法は使えないという事にもなるんだけど、Firefox 3以降のみをサポートする方針にしたおかげで、今ならこの方法をためらいなく使える。

ということで、今までfullScreenCanvas.xulを使ってた所は全部これで置き換えることにした。無駄な処理が大幅に減ったので、「自動で隠す」等のもたつきがだいぶ小さくなって実用的になったと思う。自分がツリー型タブの「自動で隠す」をあまり使ってなかった(自分で実装したくせに……)のはfullScreenCanvas.xulによるもたつきにイライラしてしまったからだったんだけど、今後は安心して使えるようになるだろうか。

……と書いてましたが、冒頭にも追記したとおり、この方法には重大な弊害があります。弊害の内容、および弊害のない安全なやり方は新しいエントリで紹介していますのでそちらも併せてご覧下さい。

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

Comments/Trackbacks

TrackBack ping me at


の末尾に2020年11月30日時点の日本の首相のファミリーネーム(ローマ字で回答)を繋げて下さい。例えば「noda」なら、「2009-12-18_stop-rendering.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

最近のコメント

最近のつぶやき