AWTとSwingのペイント(paint)の仕組み | Java API javadoc改善運動! | Javaの手帖 | Javaの手帖--GUI分冊 | /ひとつ上をゆくJavaの教科書 | 「JavaServer Faces完全ガイド」補遺 | |||||||
javacのエラーメッセージ解説 | 「Javaの哲学」の恥かき | 「Javaによるテキスト処理」の恥かき | 「モア・サーブレット」の恥かき | 恥かきのホームページ | フォーラム「JavaBeginner」 | |||||||
UIDefaultsのkeyとデフォルト値 | 「コア・サーブレット」の恥かき | comp.lang.java.guiのFAQ日本語訳 | 「入門 正規表現」 | 「Javaネットワークプログラミングの真髄」 | 「Java並行処理プログラミング」 |
|
AWTをまだ使い慣れていないかたは、このサンプル・プログラムを見てください:PaintDemo example。AWTのプログラムの中でpaint()コールバックをどうやって使うのか、それが、このプログラムを実際に動かしてみると、お分かりいただけるでしょう。
描画を行うコードを、paint()
の中や、paint()
から呼び出されるメソッド以外の場所(paint()
のスコープ内にない場所)に書くのは禁物です。なぜなら、そのような描画コードは、ペイントすべき正しいタイミングで呼び出されることが保証されないからです。たとえば、コンポーネントがまだ表示されていないのに呼び出されたり、有効なGraphics
オブジェクトにアクセスできない状態で呼び出されたりするかもしれません。また、プログラムが直接paint()
を呼び出すことも、すべきではありません。
〔訳注:コンポーネントのgetGraphics()メソッドから取得したGraphicsオブジェクトを使用する描画コードが、相当古いJava教科書によく見られます。これもまた、paint()のスコープ外の場所で行われる“禁物”のペイントの典型です。そのような描画コードを書いてはいけません。paint()と無縁な“禁物”のペイントは、上で述べているタイミングの問題以外に、描画の履歴記憶や管理が不可能という深刻な欠陥も抱えます。参考記事:[comp.lang.java.gui FAQ], Q3.4。 〕
アプリケーションが起動するペイントを実現するために、java.awt.Component
クラスは次のようなメソッドを用意しています。プログラムはこれらのメソッドを呼び出すことによって、ペイント操作を非同期(asynchronous)に※リクエストできます:
public void repaint()
public void repaint(long tm)
public void repaint(int x, int y, int width, int height)
public void repaint(long tm, int x, int y,
int width, int height)
〔※訳注:アプリケーションがrepaint()を呼び出すと、ペイントリクエストがAWTのキューに入れられる。つまり、repaint()をコールしてすぐにpaint()が実行されるわけではないので“非同期”。〕
次のコードは、マウスリスナのメソッドの最後にrepaint()
を呼び出して、マウスが押されたときと離されたときにMyButtonコンポーネントの描画更新を行わせます:
|
複雑な描画を行うコンポーネントは、引数のあるrepaint()
を呼び出して、更新が必要な部分だけが再描画されるようにすべきです。いつでもどこでもおかまいなく、引数のないrepaint()
を呼び出すのは、よくやることですが間違いです。引数のないrepaint()
はコンポーネント全体をリペイント(再描画)するので、無駄なペイントをすることが多くなります。
「システムが起動するペイント」と「アプリケーションが起動するペイント」は、どこがどう違うのでしょうか?。重量コンポーネントのペイントでは、この両者にわずかな違いがあります(軽量コンポーネントについては後述します)。しかし、両者に違いがあることが、大きな混乱の原因になっています。
重量コンポーネントでは、ペイント操作をシステムが起動したか、アプリケーションが起動したかの違いによって、ペイントの方法が次のように違います。
システムが起動するペイントは、次のように行われます:
paint()
を呼び出させる.アプリケーションが起動するペイント操作は、次のように行われます:
repaint()
を呼び出し,このコンポーネントの再描画(リペイント)が必要であるという非同期のリクエストをAWTに登録する.
update()
を呼び出させる.
注:一つのコンポーネントに対して、最初のリペイントリクエストが処理されるまでに複数のrepaint()
呼び出しが行われたら、それらの複数のリクエストに対して一回のupdate()
呼び出しが行われる。どこからどこまでのリクエストをその一回のupdate()
に対応させるかを決めるアルゴリズムは、実装次第である。このように複数のリクエストがひとつにまとめられるときには、コンポーネントの矩形領域の更新結果(表示)は、複数のリクエストの各更新結果の論理和となる.
update()
をオーバライドしなかったら, update()
のデフォルトの実装は(軽量コンポーネントでない場合)コンポーネントのバックグラウンドをクリア※してからpaint()
を呼び出すだけである.
〔※訳注:ただし、抽象クラスであるjava.awt.Componentのupdate()は背景塗りつぶしをやらずに、ただ単にpaint()を呼び出しています。〕
結局のところ、リペイントの目的はpaint()
を呼び出して再描画を行わせることですから、そこにupdate()
のような一見“余計なもの”が介在することを、不思議に思う人が多くいます。しかし、java.awt.Component
のupdate()
のデフォルトの実装はたしかにpaint()
を呼び出しているだけですが、update()
は一種の"フック(hook, 何らかのアプリケーション目的の処理コードをそこにひっかけておくことのできる鉤のようなもの)"ですから、update()
をオーバライドすることによってアプリケーションが起動するペイント操作の内容をプログラムがカスタマイズ(独自化)できます。実際、java.awt.Containerをはじめ、Componentのサブクラスの多くが、update()をオーバライドして毎回の呼び出しでコンポーネントを背景色で塗りつぶし、毎回フレッシュな描画をやらせています(後述の連続描画の逆をやっている)。
システムが起動するpaint()
の呼び出しは、当然、Graphicsオブジェクトのクリップ矩形の領域が"損傷している"ので再描画が必要だという指示ですが、アプリケーションが(repaint()
を介して)呼び出すupdate()
には、そんな暗黙の再描画指示は含まれません。このことを利用すると(update()をオーバライドし、上で述べた背景色塗りつぶしをやめることによって)、連続描画(incremental drawing)(paint()の前回の描画結果を背景塗りつぶしによって消去しないで、前回の結果も画面上に残っていく--プロッターの動作のような--描画)を行うペイントができます。
連続描画は、コンポーネントの既存のビット(それまでのペイント結果)の上にさらに何かを描画していきたいとき、便利に利用できます。サンプルプログラムUpdateDemoは、コード中でオーバライドしているupdate()
を生かす(アンコメントする)と、連続描画の状態になります。〔訳注:この方法で実現するAWT重量コンポーネント上の連続描画は、コンポーネントをリサイズすると、システムが起動するペイントにより消去されます。〕
しかし、連続描画が必要なGUIコンポーネントはめったにないので、通常はupdate()
メソッドのことを忘れて、paint()
をオーバライドするだけで十分です。つまりコンポーネントの現在の状態だけをペイントすれば十分です。そしてその場合は、ほとんどのコンポーネントの実装にとって、システムが起動する描画とアプリケーションが起動する描画は基本的に同じ結果(各回で最初に背景色による塗りつぶしが行われるので連続描画にならない)です。
アプリケーションのデベロッパにとっては、軽量コンポーネントも重量コンポーネントもペイントのAPIは基本的には同じです(つまりpaint()
をオーバライドしてrepaint()
を呼び出し、表示の更新を行わせます)。ただしAWTの軽量コンポーネントはすべてJavaで書かれているので、ペイントの実装の細部には違いがあります。
軽量コンポーネントは、自分がペイントされる場所として、コンポーネントの収容階層のどこか上のほうに重量コンポーネントが親ウィンドウとして必要です。その重量コンポーネントが自分のウィンドウをペイントするときには、子の軽量コンポーネントたちのペイントも呼び出します。そういうペイントは、java.awt.Container
のpaint()
メソッドが行います。Container
のpaint()
メソッドは、これからペイントする矩形領域に含まれていて、現在実際に表示されている、子の軽量コンポーネントたちのpaint()
を呼び出します。したがってContainer
のサブクラスはどれも(軽量でも重量でも)、paint()
メソッドを次のようにオーバライドしていることが重要です:
|
super.paint()
の呼び出しがないと、このコンテナの子の軽量コンポーネントたちがペイントされません(軽量コンポーネントが初めて導入されたJDK 1.1のころは、この問題がよく起きました)。
なお、 Container.update()
のデフォルトの実装は子の軽量コンポーネントたちのupdate()
やpaint()
を再帰的に呼び出していません。 したがって、重量Container
のサブクラスがupdate()
をオーバライドして連続描画を行う場合、子の軽量コンポーネントたちの表示も必要なら、それらを確実に再帰的にリペイントすることが重要です。ただし連続描画を行う重量コンテナを作る機会はあまりありませんから、この点が問題になるプログラムはそんなに多くないでしょう。
ウィンドウの動作(表示、非表示、移動、リサイズ、など)を実装する軽量コンポーネント(軽量コンテナ)のコードも、すべてJavaで書かれています。それらのJavaコードの中で、AWTがいろんな軽量コンポーネントたちを明示的にペイントしなければならないことがよくあります(それは、もはやネイティブのシステムからではないとはいえ、やはりシステムが起動するペイントです)。しかし軽量コンポーネントではrepaint()
を呼び出してコンポーネントをペイントするので、その場合は、前にも説明したように、paint()
が直接呼び出されるのではなくupdate()
が呼び出されます。したがって、軽量コンポーネントの場合は、システムが起動するペイントの実行のされ方が二通りあることになります:
paint()
が直接呼び出される。
update()
が呼び出され、デフォルトではupdate()
がpaint()
を呼び出す。
以上をまとめると、軽量コンポーネントでは要するにupdate()
とpaint()
の違いが実質的にない、ということになります。したがって、軽量コンポーネントでは連続描画のテクニックを使ってはいけない※、ということにもなります。
〔※訳注:軽量コンポーネントのサブクラスを作るときは、この原則により、update()をオーバライドしてはいけないので、連続描画は別の方法で行う…BufferedImageに描画をしていき、それをpaint()(SwingのコンポーネントならpaintComponent())の中で毎回drawImage()する、あるいは描画パラメータの全履歴をArrayListなどに記録し、それを使って毎回のpaint()やpaintComponent()で全描画履歴を再描画する、など。)
軽量コンポーネントは親の重量コンポーネントの表示スペースを"間借りして"いるので、透明性という性質を持っています。軽量コンポーネントは早くペイントしたものが先に表示され、あとからのコンポーネントはその上に重なって表示されます。そこで、上になるほうのコンポーネントのビットの一部またはすべてを無色にすると、下のコンポーネントが"透けて見えます"。軽量コンポーネントで、update()
のデフォルトの実装が背景をクリアしないのも、このような透明性をサポートするためです。
サンプルプログラムLightweightDemoを動かして、軽量コンポーネントの透明性をご覧になってください。
(この節の話題は、軽量コンポーネントに限定されません。)
AWTがコンポーネントを描画する処理はかなり効率的に書かれていますが、実際に描画の実行性能を左右するのはそのコンポーネントのpaint()
の実装です。中でもとくに重要なのは次の2点です:
ボタンのような簡単なコンポーネントの場合は、わざわざクリップ領域を指定するまでもなく、単純にコンポーネント全体をペイントすればグラフィクス(Graphicsオブジェクト,グラフィクスコンテキスト)が適切なクリッピングを行います。しかしテキストコンポーネントのような複雑なコンポーネントを作ったときは、クリップ領域を明示的に指定して描画の量を抑えることがきわめて重要です。
さらに、軽量のコンテナを作ってその中にたくさんのコンポーネントを収める場合は、コンテナやそのレイアウトマネージャがレイアウトに関する情報を持っているので、その情報を利用して、次はどのコンポーネントを描画するのかを特定できます。Container.paint()
のデフォルトの実装は、子のコンポーネントを順に見ていって各コンポーネントの可視性やほかのコンポーネントとの重なり具合などをチェックします。これは、複雑なレイアウトではかなりの時間--しかも無駄な時間--を費やします。そこでたとえば、コンテナがコンポーネントを100 x 100のグリッドにレイアウトしているなら、そのグリッドの情報を利用して、10,000のコンポーネントのうちのどれとどれがクリップ領域に含まれているので、それらをペイントすればよい、といった判断ができるようになります。
AWTのコンポーネントをペイントするためには、単純なコールバックAPI(システムの側が呼び出すAPI)を使います。その場合、次の点に注意してください:
paint()
メソッドのスコープ内に書いてください。
repaint()
を呼び出して, paint()
の呼び出しをシステムにリクエストします。そしてシステムは未来のある時点に、適切なタイミングでpaint()
を呼び出します。プログラムが直接paint()
を呼び出してはいけません。
repaint()
を呼び出して、更新が必要な矩形領域だけをリペイントすべきです。引数のないrepaint()
を呼び出すと、コンポーネント全体がリペイントされます。
repaint()
を呼び出すとupdate()
が呼ばれ、デフォルトではそこからpaint()
が呼ばれます。したがって重量コンポーネントは、必要ならupdate()
をオーバライドして連続描画ができます(軽量コンポーネントではこのやり方による連続描画はできません)。[参考注記]
paint()
をオーバライドしたjava.awt.Container
のサブクラスは、必ずsuper.paint()
を呼び出して、そのコンテナの子コンポーネントたちを確実にペイントしてください。
〔訳注:AWTと軽量コンポーネントについて -- 現実的なプログラミングの知恵としては、AWTのアプリケーション(たとえばApplet)の中では、軽量コンポーネントの作成や使用をしないほうが良い。そのほうが、paint()をめぐる問題に足をすくわれずに済む。軽量コンポーネントはSwingのコンポーネントだと割り切り、逆にSwingのアプリケーション中ではAWTの重量コンポーネントの併用をしないよう、気をつけよう。うっかり併用すると、不可解な表示トラブルに見舞われることがよくあります。〕
SwingはAWTのベーシックなペイント方式を拡張して、実行性能と拡張性の向上を実現しています。AWTと同じくSwingもpaint()
によるコールバック方式を採用し、 repaint()
が描画の更新をリクエストします。それに加えてSwingは最初からダブルバッファリングを実装しており、さらに、ボーダー(コンポーネントの外枠線)やUIデリゲイト(UI delegate)(自由に差し替えて使用できる多様なUIデザインをサポート)など新しい機能と、それらの新機能を可能にするための変更(AWTとは異なるコンポーネントの設計)を導入しています。さらにSwingは、ペイントの仕組みを独自にカスタマイズしたいプログラムのために、RepaintManager
APIを提供しています。
Swingの重要な特長のひとつが、コンポーネントがデフォルトでダブルバッファリングをサポートしていることです。Swingのコンポーネントの共通のスーパークラスであるjavax.swing.JComponent
には、ダブルバッファリングをon/offするためのプロパティ、"doubleBuffered"があります:
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)
ダブルバッファリングがonのときは、それぞれのトップのウィンドウが自分とすべての子を描画するためのオフスクリーンバッファ(offscreen buffer, 直接表示されない作業用イメージバッファ)を持ちます。そして、あるコンテナの"doubleBuffered"プロパティをセットすると、そのコンテナ内に表示されるすべての軽量子コンポーネントも、オフスクリーンバッファにまず描画されます(個々のコンポーネントの"doubleBuffered"プロパティの値は無視される)。
Swingのコンポーネントはすべて、デフォルトではこのプロパティがtrue
です。しかし実際に重要なのはJRootPane
に対する設定です。このレベルでダブルバッファリングをonにすると、最上位のフレームウィンドウとその中に表示される軽量コンポーネントのすべてがダブルバッファリングされます。アプリケーションプログラムはダブルバッファリングに関して、それをon/offすること以外には、ほかにやることがあまりありません(そしてもちろん、GUIの表示をなめらかにするためには当然onにするでしょう)。Swingはコンポーネントのpaint()コールバックに適切なタイプの Graphics
オブジェクトを渡します(ダブルバッファリング用にはオフスクリーンイメージのGraphics
、それ以外の場合は通常のGraphics
)。コンポーネントはただ、そのGraphics
を使って描画を行うだけです。以上の仕組みは、この先のSwingのペイントの処理過程のところで詳しく説明します。
〔訳注:ダブルバッファリングは、グラフィクスシステムが本来持っているバッファメモリに加えて、プログラムがバッファメモリを用意すること。プログラムは自前のバッファメモリに描画し、メモリの内容を一気にグラフィクスシステムにロードする(高速でちらつきのない表示が行われる)[参考記事]。AWTの時代には、アニメなどで画面のちらつきを抑えるためにダブルバッファリングをプログラマが実装した。今日でもフルスクリーン(全画面)モードのアプリケーションでは、ダブルバッファリングをプログラマが明示的に実装する必要がある。〕
Swingは、ペイントのアルゴリズムの効率を良くするために、JComponent
(Swingのコンポーネントの最上位共通スーパークラス)に2つの新しいプロパティを導入しました。これらのプロパティは、軽量コンポーネントのペイントの足を引っ張りかねない次の二つの問題に対応します:
(以下は、上の二つの問題に対応するJComponent
の2つのプロパティの説明)
コンポーネントが不透明であるという、いちばんふつうの場合の実行性能を良くするために、javax.swing.JComponent
にはプログラマが自由に設定できるopaque
プロパティがあります:
public boolean isOpaque()
public void setOpaque(boolean o)
このプロパティのtrue/falseの値の意味は、次のとおりです:
true
: コンポーネントはその矩形領域の全体をペイントされる.
false
: コンポーネントの矩形領域の中にペイントされない部分(透明な部分)がある.(たとえばJLabelはデフォルトで背景が透明、JPanelなどはsetOpaque(false)で背景が透明になる。)
opaque
プロパティによってSwingは、コンポーネントのリペイントリクエストがあったときに、その下のコンポーネントもペイントすべきか否かを判断します。Swingの各標準コンポーネントのopaque
プロパティのデフォルトの値は、使用するUIオブジェクトが決めていますが、通常はほとんどのコンポーネントがtrue
です。
コンポーネントの実装でよく犯す間違いが、opaque
プロパティのデフォルトの値をtrue
にしておきながら、コンポーネントの全矩形領域を完全に描画しないことです。そうすると、描画しなかった部分にはゴミが表示されるでしょう。コンポーネントを設計するときにはopaque
プロパティの扱い方によく注意する必要があります。それは、処理費用が高くなりがちな透明性を上手に使うためと、ペイントシステムとの契約を守るためです。(opaque
プロパティの値の正しい意味を遵守した実装をせよ。ゴミが表示されても、それはシステムのせいじゃない…。)
opaque
プロパティの意味が、よく誤解されます。“falseはコンポーネントの背景を透明にすることだ”という説がありますが、それはちがいます。たとえばボタンなどのコンポーネントでは、矩形以外の形を表示するためにopaque
プロパティをfalse
にすることがあります。あるいは、コンポーネントの周囲を透明にして、そこに(つまり透明部分の下に)別の画像など(たとえばフォーカスの有無を表すサイン)を表示することもあります。このような場合にはコンポーネントのopaque
プロパティはfalse
ですが、背景のかなりの部分が描画されます。
上で述べたように、opaque
プロパティはそもそも、リペイントを行うシステムとの契約です。コンポーネントのopaque
プロパティに(つまりそのコンポーネントの透明性の扱い方に)実装上の独特の意味があるのなら、その点を文書化すべきです。(そのコンポーネント独自のプロパティを新たに加えて、独自の透明性の意味をより有効に制御するほうがよいでしょう。たとえばjavax.swing.AbstractButton
には、そのためにContentAreaFilled
というプロパティを持っています。)
Swingのコンポーネントのborder
プロパティも、不透明性と密接な関係があります。コンポーネントにセットしたBorder
オブジェクトは、そのコンポーネントの領域に属していますから、opaque
プロパティがtrue
なら(コンポーネントが不透明なら)、ボーダーによって占められる部分もしっかり描画すべきです(そしてボーダーは不透明なコンポーネントの上に描画されます)。
isBorderOpaque()
がfalse
を返すボーダー、すなわち透明部分のあるボーダーを使うなら、それはコンポーネントの背景色が透けて見えるという意味ではなく、そのコンポーネントの下のコンポーネントや画像が透けて見えるという意味でしょうから、コンポーネントのほうもsetOpaque(false)にして、ボーダーの部分を透明にしてやるべきです。
コンポーネントの重なりという問題は、ちょっと面倒です。同一の親コンテナの中で単純に上下に隣接しているコンポーネント同士だけでなく、何層も離れていてまた親が共通でもない、たとえば"従兄弟同士"や"叔母"と"甥姪"に相当するコンポーネントが重なることもあります。そんな複雑なコンテナ〜コンポーネント階層の中で、特定の一つのコンポーネントを‘正しく’ペイントするためには、ツリーの探索を大量に行う必要があります。その場合、不必要なツリー走査をさせないために、javax.swing.JComponent
にはisOptimizedDrawingEnabled
というリードオンリー(自由な設定不可)のプロパティが用意されています:
public boolean isOptimizedDrawingEnabled()
このプロパティのtrue/falseの意味は、以下のとおりです:
true
: このコンポーネントの直接の子コンポーネントたちは、どれも重なっていない.(ふつうの簡単なGUIはこのケースに該当する。)false
: このコンポーネントの直接の子コンポーネントたちが重なっているかいないかを保証できない.
Swingはリペイントをするとき、このisOptimizedDrawingEnabled
プロパティをチェックして、重なっているコンポーネントを探索する範囲を素早く狭めます。
isOptimizedDrawingEnabled
プロパティはリードオンリーなので、コンポーネントがデフォルトの値を変える唯一の方法は、サブクラスを作ってこのメソッドをオーバライドし、必要な値を返させることです。Swingの標準コンポーネントはほとんどすべて、このプロパティの値としてtrue
を返します。例外はJLayeredPane, JDesktopPane
, そしてJViewPort
です。
AWTの軽量コンポーネントに適用されるルールが、Swingのコンポーネントにも適用されます--たとえば、描画(ペイント)をするときにはpaint()
が呼ばれます--ただし、Swingではpaint()
がさらに、次のような3つのメソッドを、下に書かれている順に呼び出します:
protected void paintComponent(Graphics g)
protected void paintBorder(Graphics g)
protected void paintChildren(Graphics g)
Swingのプログラムでは、paint()
ではなく、必ずpaintComponent()
をオーバライドしてください。paintBorder()
とpaintChildren()
もオーバライドできますが、通常はこの2つのメソッドをオーバライドする必要はまったくありません(オーバライドするなら、これらのメソッドの意味と機能を十分に理解したうえでオーバライドしてください)。paint()
の中身をこのように3分割したことによって、プログラマはコンポーネントのペイントに必要な部分だけをオーバライドすればよいことになりました。AWTではオーバライドしたpaint()
の中で必ずsuper.paint()
を呼ぶ必要があり、それを忘れると子の軽量コンポーネントが描画されませんでしたが、Swingではそういう問題がありません。
SwingのpaintComponent()
コールバックの使い方の例を、サンプルプログラムSwingPaintDemoで見てください。
Swingの標準コンポーネントの多くが、ルック&フィールを独立のルック&フィールオブジェクトによって実装しています(このような差し替え自由(pluggable)なルック&フィールオブジェクトのことをUIデリゲイト(delegate)--UI委譲--と呼びます)。したがって標準コンポーネントのペイント操作はUIデリゲイトに“委譲”され、次のような処理が行われます:
paint()
がpaintComponent()
を呼び出す.
ui
プロパティがnullでなければ、paintComponent()
はui.update()を呼び出す.
opaque
プロパティがtrueなら、 ui.udpate()
はコンポーネントの背景をコンポーネントの背景色で塗り、ui.paint()
を呼び出す.
ui.paint()
がコンポーネントの内容を描画する.
UIデリゲイトのあるSwingのコンポーネントのサブクラス(JComponent
の直接のサブクラスでない場合)で以上の処理が実行されるためには、オーバライドしたpaintComponent()
メソッドの中でsuper.paintComponent()
を呼び出してください:
|
UIデリゲイトにペイントさせたくない場合(たとえばコンポーネントの視覚表現を完全にカスタマイズしたいとき)は、super.paintComponent()
の呼び出しを省略できますが、しかしそのコンポーネントのopaque
プロパティがtrue
なら、opaque
プロパティの説明のところで述べたように、背景の塗りつぶしを自分でやる必要があります。
Swingが行う"リペイント"リクエストの処理は、AWTとやや違います。ただし、アプリケーションプログラムにとって、その最終結果はどちらも基本的に同じです--つまりpaint()
が呼び出されます。SwingはRepaintManager
API(後述)を使ってリペイントリクエストを処理し、ペイントの実行性能を上げています。Swingのペイントは、次のような二つの過程をたどります:
(A)ペイントリクエストが最初の親(最外側のウィンドウコンテナ)である重量コンポーネント(通常はJFrame, JDialog, JWindow,
またはJApplet
)に来たとき:
paint()
を呼び出す
Container.paint()
のデフォルトの実装が子の軽量コンポーネントのpaint()
を再帰的に呼び出す
JComponent.paint()
のデフォルトの実装が次の処理を行う:
doubleBuffered
プロパティがtrue
で、そのコンポーネントのRepaintManager
がダブルバッファリングをonにしていたら、Graphics
オブジェクトをオフスクリーンのグラフィクスに変換する.
paintComponent()
を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して)
paintBorder()
を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して)
paintChildren()
を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して). これはクリップ領域とopaque
およびoptimizedDrawingEnabled
を使って、paint()
を再帰的に呼び出すべき子のコンポーネントを判断します.
doubleBuffered
プロパティがtrue
で、そのコンポーネントのRepaintManager
がダブルバッファリングonなら、元のオンスクリーンのGraphics
オブジェクトを使ってオフスクリーンの画像をコピーする.注: JComponent.paint()
のステップ#1と#5は、paint()
の再帰的な呼び出し(上のステップ#4で述べているpaintChildren()
からの呼び出し)では行われません。ダブルバッファリングでは、一つの同じオフスクリーンイメージを一つのウィンドウ階層中のすべての軽量コンポーネントが共用するからです。
(B)ペイントリクエストがjavax.swing.JComponent
のサブクラスへのrepaint()
呼び出しから来たとき:
JComponent.repaint()
は非同期のリペイントリクエストをコンポーネントのRepaintManager
に登録し、RepaintManager
はinvokeLater()
を使ってRunnable
をキューに入れ、あとでそのリクエストをイベントディスパッチスレッドの上で処理する.
Runnable
オブジェクトがイベントディスパッチスレッドの上で実行されると、コンポーネントのRepaintManager
がそのコンポーネントのpaintImmediately()
を呼び出す. このメソッドは次のことを行う:opaque
およびoptimizedDrawingEnabled
プロパティを使って、ペイント操作を開始する‘ルート’のコンポーネントを判断する(透明性とコンポーネントの重なりを正しく扱うため).
doubleBuffered
プロパティがtrue
で、ルートのRepaintManager
の上でダブルバッファリングがonなら、Graphics
オブジェクトをオフスクリーンのグラフィクスに変換する.
paint()
を呼び出す(それがさらに、上の(A)のJComponent.paint()
のステップ#2-4を実行する)--これにより、ルートの中にあってクリップ矩形に含まれるものすべてがペイントされる.
doubleBuffered
プロパティがtrue
で、ルートのRepaintManager
の上でダブルバッファリングがonなら、オフスクリーンの画像(オフスクリーンイメージ)を元のオンスクリーンのGraphics
オブジェクトを使ってコンポーネントにコピーする. 注: 一つのコンポーネントやその親のどれかに対して複数のrepaint()
呼び出しが行われると、これらの複数の呼び出しが、今repaint()
が呼び出されている最上位(いちばん上の親)のコンポーネントのpaintImmediately()
への一回のコールバックにまとめられることがあります。たとえばJTabbedPane
の中にJTable
があって、この収容階層のそのほかのリペイントの処理中に両者が共にrepaint()
を呼び出すと、この2つのrepaint()
呼び出しはJTabbedPane
の一回のpaintImmediately()
呼び出しとして処理され、そこから、両コンポーネントのpaint()
が実行されます。
したがってSwingのコンポーネントでは、update()
は呼び出されません。
repaint()
を呼び出すとpaintImmediately()
が呼び出されますが、このメソッドはペイント"コールバック"ではないので、ペイントを行うコードをpaintImmediately()
の中に書いてはいけません。というより、ふつう、paintImmediately()
をオーバライドするような状況はほとんどありません。
前の節で述べたように、paintImmediately()
はSwingのコンポーネントに自分をペイントせよと命じるための入り口となるメソッドです。ここから始まって、すべての必要なペイントが最後まで確実に行われます。このメソッドを使って、その名前どおりの同期ペイントを行わせることもできます。同期ペイントは、コンポーネントの内部ステート(状態)の変化と、その変化に伴って必要な視覚的変化を、リアルタイムで同期させるペイントです。ステートが変わったら即座に表示も変わらなければならない、そんなときに同期ペイントを使います。たとえばスクロール操作をしているときのJScrollPane
は、そんなペイントを必要とします。
リアルタイムのペイントをしなければならない正当な理由がないかぎり、プログラムが直接、このメソッドを呼び出してはいけません。非同期のrepaint()
呼び出しは複数の呼び出しを一つにまとめて効率化できますが、直接のpaintImmediately()
呼び出しはそれができません。また、このメソッドを呼び出すときには必ず、イベントディスパッチスレッドから呼び出さないといけません。したがって、このメソッドを使ってペイントのコードをマルチスレッド化することは困難です。Swingのシングルスレッドモデルについては、この保存記事:"Threads and Swing."を読んでください。(Swingとスレッドに関する比較的新しい記事は:Concurrency in Swing。)
SwingのRepaintManager
クラスの仕事は、Swingのコンテナ〜コンポーネントの収容階層のリペイント処理の効率を上げることと、Swingの‘更新確認(revalidate)’の仕組みを実装することです(後者については別の記事で説明する予定です)。このクラスが実装しているリペイントの仕組みは、Swingのコンポーネントたちに対するすべてのリペイントリクエストを捕捉します(したがってもはやAWTは処理に介入しません)。また、更新を要するコンポーネントや領域を“汚れた区域(dirty region)”と呼ばれる、このクラス独自のプロパティで管理します。そしてこのクラス(のオブジェクト)は、実行を待っているリクエストをイベントディスパッチスレッドの上でinvokeLater()
を使って処理します。それについては、上の“Swingのペイントの処理過程”の節の(B)の項で説明しました。
RepaintManager
はSwingの内部的なシステムの一環なので知る必要も使う必要もない、というプログラムが実際には多いでしょう。しかしプログラムがこのAPIを使うと、ペイント操作のいくつかの側面を細かく制御できます。
RepaintManager
は動的に(ランタイムに)差し替えできますが、何もしなくても最初からデフォルトのインスタンスが提供されています。以下のstaticメソッドを使って、プログラムは"現在の(current)"RepaintManager(カレントリペイントマネージャ)をget/setできます:
public static RepaintManager currentManager(Component c)
public static RepaintManager currentManager(JComponent c)
public static void
setCurrentManager(RepaintManager aRepaintManager)
RepaintManager
のサブクラスを作って、そのオブジェクトをデフォルトのリペイントマネージャと置き換えることができます:
RepaintManager.setCurrentManager(new MyRepaintManager());
このサンプルプログラムRepaintManagerDemoでRepaintManager
をインストールするやり方とが分かるでしょう。このプログラムは、リペイントの過程に関する情報をプリントします。
RepaintManager
のサブクラスを作ってそのオブジェクトを新たなカレントリペイントマネージャとしてsetすることに意味があるのは、リペイントリクエストの処理を制御したい場合です。現在のデフォルトの実装では、汚れた区域(dirty region)を管理するための内部的ステートがprivateなので、サブクラスからアクセスできません。しかしサブクラスが以下のメソッドをオーバライドすれば、汚れた区域の追跡管理や複数のリクエストを一つにまとめる処理をカスタマイズ(自前化)できます:
public synchronized void
addDirtyRegion(JComponent c, int x, int y, int w, int h)
public Rectangle getDirtyRegion(JComponent aComponent)
public void markCompletelyDirty(JComponent aComponent)
public void markCompletelyClean(JComponent aComponent)
addDirtyRegion()
はSwingのコンポーネントのrepaint()
が呼ばれたときに呼び出されるメソッドなので、すべてのリペイントリクエストを捕捉するフックとして使えます。このメソッドをオーバライドしたら(そしてsuper.addDirtyRegion()
を呼ばなければ)、invokeLater()
を使ってRunnable
をEventQueue
に置くことも、あなたが書かなければなりません。そのRunnable
は目的のコンポーネントのpaintImmediately()
を呼び出します(つまり、この部分を書くのは難しいです)。
RepaintManager
には、ダブルバッファリングをグローバルにon/offするAPIがあります:
public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()
このプロパティをJComponent
がペイント操作の間にチェックして、オフスクリーンのバッファを使って描画をするのか否かを判断します。デフォルトではtrue
ですが、Swingのすべてのコンポーネントのダブルバッファリングをグローバルにoffにしたいときは、次のようにします:
RepaintManager.currentManager(mycomponent).
setDoubleBufferingEnabled(false);
注: Swingのデフォルトの実装ではRepaintManager
のインスタンスは一つですから、mycomponent
のように引数を指定するのは本当は無意味です。
Swingのプログラムでペイントのコードを書くときには、次の点に注意してください:
paint()
が呼び出されます。Swingのコンポーネントでは、update()
は呼び出されません。
repaint()
を呼び出して、システムが近い将来paint()
を呼び出してくれるようリクエストします。プログラムが直接、paint()
を呼び出してはいけません。
repaint()
を呼び出して、更新を要する矩形領域を指定すべきです。引数のないrepaint()
は、コンポーネントの全領域をリペイントさせます。
paint()
の実装は、次の3つのコールバックに分かれています:
paintComponent()
paintBorder()
paintChildren()
paint()
)ではなくpaintComponent()
をオーバライドしてください。
opaque
: は、コンポーネントのすべてのビットをペイントするのか、それともペイントしない部分(透明な部分)があるのか、を指定します[参照]。
optimizedDrawingEnabled
: は、このコンポーネントの子が重なっているかいないかを指定します[参照]。
opaque
プロパティをtrue
にセットすると、それは、コンポーネントの全領域の全ビットをペイントするという、システムに対する約束(契約)です。それにはpaintComponent()
)が毎回、自分の領域を背景色で塗りつぶすことも含まれます。約束を守らないと、画面にゴミが表示されることもあります。 〔訳注:UIデリゲイトがあり、opaque==trueを継承するSwingコンポーネント-Japanelなど-のサブクラスは、paintComponent()の冒頭でsuper.paintComponent(g)を呼び出してこの約束(背景塗りつぶし)を守るべきである。この項と、下の8.を見よ。〕
opaque
やoptimizedDrawingEnabled
をfalse
にすると、ペイントの処理の負担が増えますから、透明部分のあるコンポーネントや、複数のコンポーネントが重なるような描画を、濫用しないでください。
JPanel
など)のサブクラスは、オーバライドした paintComponent()
の冒頭でsuper.paintComponent()
を呼び出してください。UIデリゲイトが、不透明なコンポーネントの背景のクリア(塗りつぶし)を担当します。このことによって、上の6.の条件が満たされます。 〔上6の訳注の続き:opaqueをtrueにセットしただけでは、コンポーネントの不透明性は実現しない。この[重要なフォーラム・スレッド]を読んでみましょう。〕
JComponent
のdoubleBuffered
プロパティを使って、すべてのコンポーネントがダブルバッファリングを内部的に実装しています(このプロパティはデフォルトではtrue
です)。Swingのコンテナに対してこのプロパティをtrue
にすると、そのコンテナ内に表示されるすべての軽量コンポーネントが、同じくダブルバッファリングonになります(個々の設定は無視されます)。
複雑な描画を行うコンポーネントは、クリップ矩形(clip rectangle)を上手に使って、リペイントの範囲を必要最小限に狭めてください(上の3.を見てください)。
AWTとSwingはどちらも、デベロッパのみなさんがGUIのプログラムを比較的簡単に書けるように、グラフィクスのコンテンツを画面に正しく描くための使いやすいAPI提供しています。通常のGUIにはSwingをおすすめしますが、それでも、AWTのペイントの仕組みを理解していることは有意義です。Swingも、その土台はAWTですから。
これらのAPIから最高の実行性能を引き出すためには、本稿で示したガイドラインを正しく守っているアプリケーションを書いてください。
|