AwtとSwingにおける描画処理
翻訳元:Painting in AWT and Swing By Amy Fowler
一般的にグラフィカルシステムにおけるウィンドウツールキットは、比較的簡単にスクリーン上の正確なピクセルに正確なタイミングでGUIを描画するためのフレームワークを提供しています。AWT(abstract window toolkit)とSwingは共にそのようなフレームワークを提供していますが、何人かの開発者はその実装方法をあまり理解していません。--まるでプログラム(AWT,SWING)が役立たずであるかのように思ってしまうことが問題です。
この記事はAWTとSwingの描画メカニズムの詳細を説明しています。その目的は開発者が、正確且つ効率的なGUIの描画コードを書くことを助けることです。この記事は一般的な描画メカニズム(どこで、何時描画するか等)をカバーするもので、「SwingのグラフィックスAPIで精細な出力をする方法」などは含みません。美麗なグラフィックスの描き方を学びたければ、Java 2DのWEBサイトを見てください。
この記事のメイントピックス:
描画システムの進化
元々 AWT APIはJDK1.0の為に開発され、重量級コンポーネントしか存在しませんでした。("重量級"が意味するものはコンポーネント自身が不透明なネイティブウィンドウであるということです)。これは個々のネイティブプラットホームが持つ描画サブシステムに深く依存してしまっています。この制限によりダメージ検出、領域計算、重なり順のような詳細に注意を払うことを余儀なくされました。軽量コンポーネント(軽量コンポーネントはヘビー級コンポーネントを親にもちそれを再利用するコンポーネントです。)の始まりはJDK1.1からですが、AWTは共通のコードに軽量コンポーネントの為の描画処理を実装する必要があった為、重量コンポーネントと軽量コンポーネントには描画処理の動作に微妙な違いが存在します。
AWTにおける描画処理
AWTの描画APIがどのように動作するかを理解するには、ウィンドウツールキットの環境における描画のきっかけを知ることです。AWTでは、描画のきっかけが2種類(system-triggered、app-triggeredの描画)存在します。
"system-triggered"な描画
"system-triggered"な描画では、通常は以下のような理由で、システムがコンポーネントに対してコンテンツの描画をリクエストします。:
- 初めてそのコンポーネントがスクリーン上に表示された
- コンポーネントがリサイズされた
- コンポーネントの描画領域が破壊され、修復が必要(例:コンポーネントをおおい隠していた別のコンポーネント及びウィンドウが動き、おおい隠された部分が露出した 等々).
"app-triggered"な描画
内部状態が変化することで、コンテンツの更新が必要だと判断したコンポーネントが行う描画が、"app-triggered"な描画です。(例:ボタンコンポーネントがマウスによる押下と開放を検出した時に、押下状態から開放されたことを表現する)
The Paint Method
描画リクエストのきっかけに関係なく、AWTは描画の仕組みとしてコールバックを利用します。 この仕組みは、重量コンポーネント、軽量コンポーネントにかかわらず共通です。 これは、描画プログラムをオーバーライドされた特定のメソッド内に書かなければならない事を意味します。 ツールキットは描画のタイミングでそのメソッドを実行します。下記はそのオーバーライドすべきjava.awt.Component
のメソッド:
public void paint(Graphics g)
AWTがこのメソッドを実行したとき、そのパラメータであるGraphics
オブジェクトは、事前にそのコンポーネントを描画するための適切な状態に設定されています。以下はその属性:
Graphics
オブジェクトの色はコンポーネントのforeground
プロパティがセットされますGraphics
オブジェクトのフォントはコンポーネントのfont
プロパティがセットされますGraphics
オブジェクトの座標変換の起点はコンポーネントの左上隅の座標が(0,0)として設定されますGraphics
オブジェクトの矩形領域はコンポーネントの再描画に必要な矩形領域が設定されます
プログラムが出力を描画する為には、このGraphics
オブジェクト (もしくはそのオブジェクトが提供するもの)を使用する必要があります。必要であれば、Graphics
オブジェクトの状態は自由に変更することが出来ます。
ここに、単純な例として、コンポーネントの矩形領域いっぱいに塗りつぶされた円を描画してみます:
public void paint(Graphics g) {
// 動的なサイズ計算
Dimension size = getSize();
// 直径
int d = Math.min(size.width, size.height);
int x = (size.width - d)/2;
int y = (size.height - d)/2; // 円を描画(色はコンポーネントの前景にセットされている)
g.fillOval(x, y, d, d);
g.setColor(Color.black);
g.drawOval(x, y, d, d);
}
AWTに不慣れな開発者はPaintDemo exampleを覗いてみてください。実行可能な例としてAWTの描画コールバックの利用方法が提供されています。
一般的には、描画コールバックメソッドのスコープ外で実行される可能性のある場所に描画コードを置くべきではありません。
なぜかというと、そのようなコードはコンポーネントが表示される前や、Graphics
オブジェクトが適切に初期化されていない状態など、そのコンポーネントインスタンスにとって、描画するべきでないタイミングで実行される可能性があるからです。同様に、paint()
メソッドを外部のプログラムから直接実行することもお勧めできません。
AWTはアプリケーションから描画をコントロールする為に、下記のような非同期な描画リクエストを可能にするメソッドを提供しています。:
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()
を使用した単純な例です。マウスボタンのオンとオフがきっかけで、トグルボタンをON/OFFしています。:
MouseListener l = new MouseAdapter() {
public void mousePressed(MouseEvent e) {
MyButton b = (MyButton)e.getSource();
b.setSelected(true);
b.repaint();
} public void mouseReleased(MouseEvent e) {
MyButton b = (MyButton)e.getSource();
b.setSelected(false);
b.repaint();
}
};
複雑な描画出力が必要なコンポーネントは、更新が必要な領域だけを更新するように、更新する矩形領域を定義する引数を持つrepaint()
を使うべきです。常に引数なしのrepaint()
を使って必要のない描画処理を頻繁に行ってしまうのはよくある間違いのひとつです。
paint() vs. update()
なぜ"システム起因"と"アプリケーション起因"のような描画の区別を作ったのか?それは、AWTが、重量級コンポーネント(軽量コンポーネントの場合については後述します)における上記の2つの場合にそれぞれ動作が少し違うことを気遣ってのことですが、残念なことにこれが大きな混乱の元となりました。
重量級コンポーネントを扱うために、2種類の描画方法が、2種類の異なった方式で行われます。それは、システム起因かアプリケーション起因かによって区別されています。
"system-triggered"な描画
下記はどのように"system-triggered"な描画が行われるかを示しています:
- AWTが、そのコンポーネントの全てあるいは一部分を描画する必要があることを検出します。
- AWTがイベントを起こし、イベントを振り分けているスレッドが、そのコンポーネントの
paint()
メソッドを実行します。
"app-triggered"な描画
下記はどのようにアプリケーション起因の描画が行われるかを示しています:
- プログラムがなんらかの内部状態の変化によってコンポーネントの全部、もしくは一部分の再描画が必要だと判断します。
- プログラムがそのコンポーネントの
repaint()
メソッドを実行し、非同期で、AWTにコンポーネントの再描画リクエストを登録します。
- AWTがイベントを起こし、イベントを振り分けるスレッドがそのコンポーネントの
update()
メソッドを実行します。
NOTE: コンポーネントが初期描画される前に複数回の
repaint()
メソッドが
コールされると、複数回の描画リクエストは一回のupdate()
呼び出しにまとめられるかも知れません。
このアルゴリズムで、複数回の描画リクエストがまとめられるかどうかは実装依存です。
もし複数のリクエストがまとめられた場合、更新される矩形領域は、まとめられたリクエスト郡に
含まれる矩形領域の和と同じになります。
- もしそのコンポーネントが
update()
をオーバーライドしなかった場合、標準の実装が、コンポーネントの背景をクリア(軽量コンポーネントではない場合)した後、単にpaint()
が呼ばれるだけです。
既定の動作の場合、update()
とpaint()
の最終的な結果に違いがないので、多くの人がupdate()
メソッドがpaint()
とは別の目的で存在することを理解していません。update
の標準実装が、どこかでpaint()
を呼ぶのは本当ですが、この"update"は、別のケースのアプリケーション起因による描画処理を行う場合に、プログラムから描画のハンドリングをすることをを可能にする為の"フック"として置かれています。paint()
メソッドは、その矩形領域が破壊(上書き)されている場合に、それを完全に再描画する為にあり、update()
は描画処理を付加する為にあることを認識しなければなりません。
この描画処理の付加は、コンポーネントの最前面に追加レイヤを描画するようなプログラムを書く場合に有効です。このUpdateDemo exampleはupdate()
を有効に利用した描画処理の付加を行っているデモプログラムです。
現実には、ほとんどのGUIのコンポーネントは描画処理の付加を必要としませんので、現状ではコンポーネントを描画をプするプログラムは、paint()
をオーバーライドするだけで、update()
を無視することができます。これはほとんどのコンポーネント実装にとって、"system-triggered"と"app-triggered"な描画処理に、違いは無いということを意味しています。
描画と軽量コンポーネント
アプリケーション開発者の視点からすると、重量級と軽量級の描画APIは基本的に同じに見えます。(あなたは単に、paint()
をオーバーライドし、repaint()
を実行するだけです)けれども、全てがピュアなJavaコードで記述された軽量コンポーネントでは、そのメカニズムの実装方法に、軽量であるための微妙な違いが存在します。
軽量コンポーネントはどのように描画されるか?
軽量であるためには、描画する領域を確保するためにそのコンポーネントのコンテナである上位階層のどこかにある重量コンポーネントを必要とします。この重量級の祖先がそのウィンドウを描画する時、その描画命令は全ての子孫に対する命令に置き換えられる必要があります。それはjava.awt.Container
のpaint()
メソッドで行われ、そのコンテナの子である表示可能で描画される矩形領域と交わる全ての軽量級コンポーネントのpaint()
を呼び出します。
よって、すべてのContainer
のサブクラス達(軽量級、重量級ともに)は下記のようにpaint()
をオーバーライドする必要があります:
public class MyContainer extends Container {
public void paint(Graphics g) {
// 軽量級コンポーネントが確実に描画されるように
// 最初にコンテナのコンテンツを描画...
super.paint(g);
}
}
super.paint()
を忘れると、そのコンテナに含まれる軽量コンポーネントは表示されません。(JDK1.1が最初に軽量コンポーネントを導入した時からの非常に一般的な問題です)
Container.update()
は子孫である軽量コンポーネントのupdate()
もしくはpaint()
の再帰的な呼び出しを行わない事に注意する必要があります。これは、update()
をつかって描画を付加しているContainer
のサブクラスが、子孫の軽量級コンポーネントの再帰的な再描画を確実に行う必要があるということを意味します。幸いなことに、描画の付加を必要とする重量級コンポーネントは少なく、この問題はほとんどのプログラムにとって影響がありません。
軽量コンポーネントと、"system-triggered"な描画
軽量コンポーネントのフレームワークは、全てがJavaで記述された軽量コンポーネントの為にウィンドウの振る舞い(表示、隠す、移動、リサイズ、etc.)の実装を提供します。機能のJavaによる実装は、頻繁にいろいろな軽量コンポーネントに描画を指示しなければなりません(たとえそれがもはやネイティブシステムが起因するものではないとしても、これが"system-triggered"な描画の本質です)。##つまり、軽量コンポーネントの"system-triggered"な描画とは、ネイティブシステムが行う描画指示だけではなく、AWTのフレームワークが行う描画指示も存在するということ。
しかし、軽量フレームワークはコンポーネント郡の描画に、repaint()
を使います。直接paint()
を使う代わりにupdate()
を
使った結果は前述したとおりです。従って、軽量コンポーネントには下記のように2通りの
"system-triggered"の描画が存在します:
- ネイティブシステムがきっかけで、
paint()
が直接呼び出される描画リクエスト(例:その軽量コンポーネントの祖先の重量級コンポーネントが最初に描画される等)。
- 軽量コンポーネントフレームワークがきっかけで、
update()
が呼び出される描画リクエスト(例:軽量級コンポーネントがリサイズされた等)。update()
の標準実装はpaint()
を呼び出しています。
簡単に言うと、軽量コンポーネントにおけるupdate()
とpaint()
は区別されないこと、さらに言うと、描画の付加というテクニックが軽量コンポーネントにおいて使われるべきでないことを意味しています。
軽量コンポーネントと透明性
軽量コンポーネントが、上位の重量級コンポーネントからスクリーン上の領域を"借りる"ようになってから、透明性をサポートするようになりました。ぜそのようになるのかというと、背面から前面に向けて描画される軽量コンポーネント郡が、もし描画されなかった部分をそのまま残すとしたら、その背面のコンポーネントが"透けて見える"からです。これは、update()
のデフォルト実装が、そのコンポーネントが軽量である場合に背景をクリアしない理由でもあります。
このLightweightDemo サンプルは、軽量コンポーネントの透明性をデモンストレーションするプログラムです。
"スマート"な描画
AWTは、可能な限り効率的にコンポーネントを描画しようとしますが、コンポーネントのpaint()
の実装しだいで、その全ての性能に大きな影響を与える事ができてしまいます。性能に影響を与える鍵は下記の2つです。:
もしあなたのコンポーネントが単純なものなら -- 例:ボタン等 -- それは描画領域と交差する部分を描画するだけなので、描画処理を分割する価値はないでしょう。ただ単にコンポーネント全体を描画することで、適切な描画領域が使われることは望ましいことです。しかし、あなたの作ろうとしているコンポーネントがテキストコンポーネントのような複雑な出力を生成するものなら、あなたのコードが使う描画領域を狭くする事が非常に重要となります。
さらに言うと、もしあなたの書いている複雑な軽量コンテナが多数のコンポーネントを包含する可能性があれば、そのコンポーネント、もしくはそのコンポーネントのレイアウトマネージャが持つレイアウトに関する情報は、どの子コンポーネントが描画されるべきなのかを決定する事をよりスマートに実現することができるので、利用する価値があります。Container.paint()
の標準実装は、単に子コンポーネントの可視性と、描画領域との交わりを順番にチェックしているだけです。--あるレイアウトにとって、その操作は不要で非効率かもしれません。例として、コンポーネントを100×100の格子状に配置するコンテナの場合、そのグリッドのレイアウト情報を使ってその時に描画が必要な部分を決定する方が、10,000ものコンポーネントと描画領域との交点を調査するよりも早いでしょう。
AWT における描画処理のガイドライン
AWTはコンポーネントを描画するためのシンプルなコールバックAPIを提供しています。
利用する際は、下記のガイドラインを適用してください。:
- ほとんどのプログラムの描画処理は、コンポーネントの
paint()
メソッドに実装すべきです。
- プログラムが
repaint()
を呼び出すことでpaint()
が実行されるきっかけとなるものの、プログラムから直接paint()
を呼ぶべきではありません。
- 複雑な描画出力をするコンポーネントは、コンポーネント全体の再描画を行う引数が無いバージョンの
repaint()
よりも、更新が必要な領域を指定する引数を持ったrepaint()
を実行するべきです。
repaint()
呼び出しによってupdate()
がよびだされた結果、標準の実装ではpaint()
が呼び出されます。重量級コンポーネントでは、描画の付加が必要な場合にupdate()
をオーバーライドできます(軽量コンポーネントでは描画の付加テクニックはサポートされません)
java.awt.Container
の拡張コンポーネントは子コンポーネントを確実に描画するため、paint()
をオーバーライドし、super.paint()
を常に実行するべきです。
- 複雑な出力をするコンポーネントは、その描画領域を効率的に利用し、描画処理の対象をそのコンポーネントの矩形領域と交差する矩形領域のみに絞り込むべきです。
Swingにおける描画処理
SwingはAWTの基本的な描画モデルを基盤とし、さらなる性能の最大化、拡張性の改善のために拡張されています。SwingはAWTのように、描画コールバックとrepaint()
による更新をサポートしています。さらにSwingにはSwingの追加構造(ボーダー、UI delegateのような)として"double-buffering"のサポートが組み込まれました。そしてSwingはさらに進んだ描画メカニズムのカスタマイズをする為のRepaintManager
APIを提供しています。
"double-buffering"のサポート
Swingの特筆すべき特長のひつととして、ツールキットに正確な"double-buffering"
のサポートが組み込まれていることです。それらは、javax.swing.JComponent
の"doubleBufferd"属性によって
サポートされています。:
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)
Swingの"double-buffering"の仕組みは、"double-buffering"が有効となっているコンポーネントを含む階層(通常は最上位のウィンドウ)ごとに単一のオフスクリーンバッファを使って実現されます。この属性は基本的にコンポーネントごとに設定可能ですが、結果として、個々のコンポーネントが持つ"doubleBuffered"属性の値に関係なく、そのコンポーネントのコンテナにセットされた"doubleBuffered"属性が有効であればそのコンテナの下位に属する全てのコンポーネントがオフスクリーンバッファに描画される事となります。
全てのSwingコンポーネントのこの属性の既定値は、true
に設定されています。しかし本当に重要なのは、JRootPane
の設定です。なぜなら、この最上位のSwingコンポーネントに対する設定は、下位に属するすべてのコンポーネントの"double-buffering"の切替に影響するからです。Swingプログラムのほとんどの部分では"double-buffering"に関してonかoff以外に特別な対応は必要ありません。(あなたがスムーズなGUIの描画を望むならONにするでしょう)
Swingは全てのコンポーネントが描画するのに必要な適切な型のGraphics
オブジェクト(通常のGraphics
オブジェクトではなく、"double-buffering"の為のオフスクリーンのGraphics
オブジェクト)をコンポーネントの描画コールバックメソッドに確実に渡します。このメカニズムのさらなる詳細はこの議事の後ろのほうのセクション描画処理で説明します。
追加された描画プロパティ
Swingは内部の描画アルゴリズムの性能を改善するためにJComponent
に2つの追加属性を導入しました。これらのプロパティは軽量コンポーネントの高負荷な処理である下記の2つの問題に対処するために導入されました:
- 透明性: 軽量コンポーネントを描画する場合、一部、もしくは全部が透明である場合、コンポーネントに関連する全てのピクセルを描画しないようにする事が可能です。この特性は、再描画されるたびに背面のオブジェクトが最初に再描画されなければならない事を示しており、最初にコンポーネント階層の中から最背面に位置する祖先である重量級コンポーネントを見つけだして、背面から前面に向けて、順番に描画処理を開始することをシステムにリクエストしています。
- コンポーネントの重なり: 軽量コンポーネントを描画する場合、そのコンポーネントに対して、部分的に他の軽量コンポーネントを重ねる事が可能です。これは、元々の軽量コンポーネントが描画されるときは、必ずそのコンポーネントに重なった全てのコンポーネントの一部分(その描画領域と交差して重なる領域)が再描画されなければならない事を示しており、描画操作ごとにコンポーネントの重なりをチェックする為に包含階層をスキャンすることをシステムにリクエストしています。
不透明性
Swingは、一般的なケースでの不透明なコンポーネントの性能を改善するため、javax.swing.JComponent
に読み書き可能なopaque
属性を追加しました:
public boolean isOpaque()
public void setOpaque(boolean o)
設定値:
コンポーネント実装における最も一般的なミスの一つは、opaque
属性の既定値を"true"に設定したにもかかわらず、全てのピクセルを完全に描画せずに、たまたま描画されなかったピクセルが捨てられてしまうことです。コンポーネントの設計時は、描画時の負荷が高い"透過"を慎重に利用し、描画システムとの契約を守ることを確実に行うように、opaque
属性の扱いを注意深く考えるべきです。
opaque
属性の意味は良く間違って理解されます。時々"コンポーネントの背景を透明にする"と取られたりしますが、それはSwingの不透明性に対する厳密な解釈ではありません。ボタンのようないくつかのコンポーネントは、矩形ではない形をコンポーネントに与えるために、もしくは、フォーカスインジケータのように、一時的にコンポーネントの余白を作る為に、opaque属性をfalseに設定するかもしれません。これらの場合、コンポーネントは不透明ではありませんが、背景の主要な部分は塗りつぶされたままです。*1
前述したopaque属性の定義は、再描画システムとの主な契約の一つです。もし、コンポーネントが、opaque属性を、どのようにコンポーネントの表現を透明にするかということに使う場合は、属性の使い方をドキュメントに明示すべきです。(いくつかのコンポーネントは、別の属性を定義して、コンポーネントの表現にどのように透明性を適用するかをコントロールしたほうがよいかも知れません。例:javax.swing.AbstractButton
はContantAreaFilled
属性をこの目的の為に提供しています。)
他のささいな問題としては、Swingコンポーネントのborder
属性と、opacity属性がどのように関係するかというものがあります。コンポーネントのBorder
オブジェクトによって描画される領域はコンポーネントの描画領域の一部であると考えられます。これは、コンポーネントが不透明な場合、そのコンポーネントのborderが占める領域を描画するのはそのコンポーネントの役割であることを意味します(borderは、最前面の不透明コンポーネントの上に重ねて描画されるだけです)。
あなたのコンポーネントを、背面のコンポーネントのborderが透けて見えるようにしたければ、-- isBorderOpaque()
がfalse
を返し、透過をサポートしているボーダーの場合 -- コンポーネント自身を不透明でないように定義し、そのborderの領域を描画しないようにしなればなりません。
描画処理の"最適化"
コンポーネントの重なり問題はより複雑です。たとえそのコンポーネントに重なっているコンポーネントが直系の兄弟でなく、祖先でない親戚(従兄弟や、叔父)だとしてもそれらはいつでも重ねる事ができます。このような場合の再描画処理は、複雑な階層構造を持つコンポーネントを確実に描画する為に多数のツリーを渡り歩く必要ありました。不必要なオブジェクトのスキャンを減らすために、Swingはjavax.swing.JComponentに読み取り専用の
isOptimizedDrawingEnabled
プロパティを追加しました。:
public boolean isOptimizedDrawingEnabled()
設定値:
true
:このコンポーネントに直系の子孫が重なる事はありません。
false
:このコンポーネントが直系の子孫と重ならないかどうかは保証しません。
isOptimizedDrawingEnabled
属性を調べることで、Swingは描画時のコンポーネントの重なりをより速く検出することが出来ます。
isOptimizedDrawingEnabled
は読み取り専用なので、この値を変える方法は、サブクラス化して、望む値を返すようにオーバーライドするしかありません。SwingのJLayeredPane, JDesktopPane
とJViewPort
を除く全ての標準コンポーネントのisOptimizedDrawingEnabled
属性はtrue
を返します。
The Paint Methods
Swingが、paint()
の呼び出しをさらに分割して、下記の3つに分割されたメソッドが実行されることを除いては、AWTの軽量コンポーネントに適用されるルール -- 描画時にpaint()
が呼び出れる -- はSwingのコンポーネントにも当てはまります:
protected void paintComponent(Graphics g)
protected void paintBorder(Graphics g)
protected void paintChildren(Graphics g)
Swingのプログラムではpaint()
オーバーライドする代わりにpaintComponent()
をオーバーライドすべきです。APIはpaintBorder()
及び、paintComponents()
をオーバーライドすることを許していますが、一般的に、これらをオーバーライドする理由はありません(もしオーバーライドする場合、あなたは自分が何をしようとしているのか確実に把握してください!)。この分割によって、拡張が必要な部分の描画処理のみをオーバーライドすることが簡単になりました。そして、これらの改良はsuper.paint()
の呼び出し場所を間違えると子の軽量コンポーネントは表示されないというAWTの問題を解決しました。
このSwingPaintDemoはSwingのpaintComponent()
コールバックの単純な使い方をデモンストレーションするサンプルプログラムです。
描画処理とUI Delegate
Swingコンポーネントが持つLook&Feelのなかで最も標準的なものは、SwingのプラッガブルなLook&Feelという特徴を生かすために、分離されたlook-and-feelオブジェクト("UI delegate"といいます)で実装されています。これは、標準のコンポーネント郡が描画処理をUI deletateに委譲していることを意味します。方法は下記です:
paint()
がpaintComponent()
を実行します。ui
属性がnullで無いならpaintComponent()
がui.update()
を実行- コンポーネントの
opaque
属性がtrueの時,ui.udpate()
はコンポーネントの背景をその背景色で塗りつぶし、ui.paint()
を実行します。 ui.paint()
がコンポーネントのコンテンツを描画します。
これは"UI delagate"を持つSwingコンポーネントのサブクラス(JComponent
のサブクラスを除く)については、オーバーライドしたpaintComponent
内で、super.paintComponent()
を実行するべきであることを意味します:
public class MyPanel extends JPanel {
protected void paintComponent(Graphics g) {
// UI delegate の描画を先に行う
// (不透明な場合の背景の塗りつぶしを含む)
super.paintComponent(g);
// 次に自身のコンテンツを描画する...
}
}
拡張したコンポーネントで"UI delegate"に描画を委譲したくない理由がある場合(例:完全にそのコンポーネントの見た目を変えたい場合)は、super.paintComponent()
の呼び出しをスキップしてもかまいませんが、そのコンポーネントのopaque
が、true
の場合は、opaque
属性のセクションで論じているように、確実にコンポーネント自身の描画領域を塗りつぶす必要があります。
描画処理
Swingは"repaint"リクエストの処理をAWTとは少し違う方法で処理しますが、アプリケーションプログラマにとっては本質的な違いはありません。 -- paint()
が実行される。Swingはパフォーマンスのみならず、RepaintManager
API(後述)をサポートする為に"repaint"リクエストの処理方法が変更されています。Swingで下記2種類の方法をサポートしています:
(A)最初の祖先である重量級コンポーネント上ではじまった描画リクエスト(一般的にはJFrame,JDialog,JWindow,
もしくは
JApplet
):
- イベントディスパッチスレッドがその祖先の
paint()
を実行
Container.paint()
の標準実装が、全ての子である軽量コンポーネントpaint()
を再帰的に実行
- 最初のSwingコンポーネントに到達したとき、
JComponent.paint()
の標準実装が下記の処理を実行:
- コンポーネントの
doubleBuffered
属性がtrue
で、RepaintManager
の"double-buffering"が有効な場合、Graphics
オブジェクトをオフスクリーン用のgraphicsへ変換 paintComponent()
を実行("double-buffering"が有効ならオフスクリーンgraphicsを渡す)paintBorder()
を実行("double-buffering"が有効ならオフスクリーンgraphicsを渡す)paintChildren()
を実行("double-buffering"が有効ならオフスクリーンgraphicsを渡す)。その描画領域情報とopaque
属性とoptimizedDrawingEnabled
属性を使って、その子孫らのpaint()
を再帰的に実行する必要があるかどうかを正確に決定します。- コンポーネントの
doubleBuffered
属性がtrue
で、RepaintManager
の"double-buffering"が有効な場合、オフスクリーンのイメージをコンポーネントが使うオリジナルのオンスクリーンGraphics
オブジェクトにコピー
Note:
paint()
の再帰呼び出し中(ステップ#4で説明しているpaintChildren()
)のJComponent.paint()
はステップ#1と#5を省略します、なぜなら、Swingの全ての軽量コンポーネントのウィンドウ階層は、同じ"double-buffering"用オフスクリーンイメージを共有しているからです。 - コンポーネントの
(B)javax.swing.JComponent
を拡張したコンポーネントからの描画リクエスト:
JComponent.repaint()
は非同期の再描画リクエストをコンポーネントのRepaintManager
に登録します。RepaintManager
はinvokeLater()
を使って、そのリクエストを後で処理する為にRunnable
としてイベントディスパッチスレッド上にキューイングします。
- イベントディスパッチスレッド上で実行されたRunnableは、そのコンポーネントの
RepaintManager
にそのコンポーネントのpaintImmediately()
を実行させます。それは下記のように実行されます:
- 描画領域情報と、
opaque
属性と、optimizedDrawingEnabled
属性を使って、描画を開始しなければならない"root"コンポーネントを決定します(コンポーネントの透過と重なりに対応するため)。 - ルートコンポーネントの
doubleBuffered
属性がtrue
且つ、ルートコンポーネントのRepaitnManager
の"double-buffering"が有効である場合、Graphics
オブジェクトをオフスクリーンのgraphicsオブジェクトに変換 - ルートコンポーネントの
paint()
を実行することで、そのルートコンポーネントの描画領域と交差する下位のコンポーネントを全て描画 - ルートコンポーネントの
doubleBuffered
属性がtrue
でルートコンポーネントのRepaintManager
の"double-buffering"が有効な場合、オフスクリーンのイメージをコンポーネントの描画領域のオンスクリーンGraphics
オブジェクトにコピー
NOTE:コンポーネントもしくは、Swingの祖先であるコンポーネント全てについて、再描画リクエストが完了するまでに、複数の
repaint()
呼び出しが起こった場合、それらのリクエストはrepaint()
を実行したコンポーネントの中で、"階層の最上位"のSwingコンポーネントのpaintImmediately()
に対する1回のコールバックにまとめられる可能性があります。例:JTabbedPane
が、JTable
を包含しており、その階層の前回の再描画リクエストが完了一つも完了していないときに、repaint()
が呼び出された結果、JTabbedPane
上のpaintImmediately()
に対する1回の呼び出しが行われ、それぞれのコンポーネントのpaint
が実行された。 - 描画領域情報と、
これはSwingコンポーネントのupdate
が絶対に実行されないことを意味しています。
repaint()
を実行すると、結果としてpaintImmediately()
が呼び出されますが、それは描画の"callback"と見なされません。そして描画プログラムはpaintImmediately()
の中で書くべきではありません。事実として、paintImmediately()
をオーバーライドする一般的な理由は全くありません。
描画処理の同期
前のセクションで説明したように、paintImmediately()
が一つのSwingコンポーネントに対して自身の描画を指示する為の開始点となる事で、全ての必要な描画処理の実行が適切に行われています。このメソッドはその名が意味するとおりに、同期的な描画リクエストを作成するために使われるかもしれません。同期的な描画リクエストは、内部状態と表示をリアルタイムで"同期"する必要のあるコンポーネント(例:JScrollPane
のスクロール操作)に必要とされます。
プログラムは、リアルタイムの描画が本当に必要で無い限り、このメソッドを実行すべきではありません。なぜなら、非同期なrepaint()
が、重複したリクエストを効率的にまとめるのに対し、paintImmediately()
を直接呼び出した場合は、そのようにならないからです。
もっと言うと、このメソッドは、イベントディスパッチスレッドから呼び出さなければならないという"仕様"なのです。このAPIはあなたのマルチスレッドな描画プログラムには対応していないのです!。これらSwingのシングルスレッドモデルについての更なる詳細については、次の記事を見てください。"Threads and Swing."
The RepaintManager
SwingのRepaintManager
クラスの目的は、Swingの包含階層における再描画処理の能力を最大化する事と、Swingに"revalidation"の仕組みを実装することです(後者は別の記事の題材とします)。それは、Swingコンポーネントの全ての再描画リクエストをインターセプト(AWTによるその処理はそんなに長くかかりません)し、更新が必要である描画領域("dirty regions"として知られています)の状態を管理することで、再描画の仕組みを実装しています。最終的には、"描画処理"のセクションの(b)で説明したように、リクエストディスパッチスレッド上でリクエストを処理するためinvokeLater()
を使います。
ほとんどのプログラムにとって、RepaintManager
はSwingの内部システムの一部にしか見えませんし、実質的には無視できます。しかし、これらのAPIはプログラムに、描画処理のある側面における、より詳細な制御を行う事を可能にしています。
The "Current" RepaintManager
RepaintManager
は既定では単一のインスタンスですが、動的に組み込めるように設計されています。RepaintManager
にある、下記のstaticメソッドは"現在"のRepaintManager
の取得と変更を許可しています。:
public static RepaintManager currentManager(Component c)
public static RepaintManager currentManager(JComponent c)
public static void setCurrentManager(RepaintManager aRepaintManager)
The "Current" RepaintManagerの差し替え
RepaintManager
を拡張し、グローバルなRepaintManager
を差し替えようとするプログラムは、下記のようなコードでしょう
RepaintManager.setCurrentManager(new MyRepaintManager());
RepaintManager
のインストールについてのシンプルで実行可能な例としてRepaintManagerDemoを見てもよいでしょう。この例では再描画の情報をコンソールに出力する、RepaintManager
に差し替えています。
RepaintManager
を拡張し、差し替えるさらに、興味深い理由は、再描画リクエストの処理方法を帰ることでしょう。現在、標準のRepaintManager
が"dirty regions"を記録する為に使っている内部領域は、プライベートで、サブクラスからのアクセスもできません。しかし、下記のメソッドをオーバーライドすることで"dirty regions"の記録や、リクエストの統合の為に、独自の機構を実装することが出来るかもしれません:
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()
をよびださない)場合は、適切なコンポーネントのpaintImmediately()
を実行するRunnnable
をEventQueue
上に配置する為に、invokeLater()
を実行する事が自身の責任となります。(要するに:心臓の弱い方はご遠慮ください)
グローバルなDouble-Bufferingの制御
RepaintManager
グローバルにdouble-bufferingのonとoffを切り替えるためのAPIを提供します:
public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()
この属性は、レンダリングにオフスクリーンバッファを使うかどうかを決定する為、描画処理の間、JComponent
の中でチェックされます。この属性の既定値はtrue
ですが、下記のようにすることで、全てのSwingコンポーネントのdouble-bufferingを無効にすることができます:
RepaintManager.currentManager(mycomponent)
.setDoubleBufferingEnabled(false);
Note: Swingの標準実装では、単一のRepaintManager
をインスタンス化するので、上記のmycomponentという引数は意味をなしていません。
Swing における描画処理のガイドライン
Swingで、描画処理のプログラムを書く時は以下のガイドラインを理解すべきです:
- Swingコンポーネントでは、"system-triggered"と"app-triggered"の描画リクエスト両方で、常に
paint()
が実行されます。update()
はSwingコンポーネントで実行されることはありません。
repaint()
の実行がpaint()
の呼び出しのきっかけになる事がありますが、プログラムから直接paint()
を呼ぶべきではありません。
- 複雑な出力を伴うコンポーネントでは、多数のコンポーネントの再描画を行う可能性のある引数の無い
repaint()
では無く、更新が必要な領域を定義する引数と共にrepaint()
を使うべきです。
- Swingの
paint()
実装は下記の3つのコールバックメソッドの呼び出しに分解されています。:
paintComponent()
paintBorder()
paintChildren()
paintComponent()
メソッドのスコープ内に配置されるべきです(paint()
ではなく )。
- Swingは描画能力を最大化するため、下記2つのプロパティを導入しています:
opaque
:このコンポーネントが全てのピクセルを塗りつぶすかどうか?optimizedDrawingEnabled
: コンポーネントの子が重なる可能性があるか?- Swingコンポーネントの
opaque
属性がtrue
にセットされている場合、そのコンポーネントは描画領域に含まれるピクセルを全て塗りつぶす事に同意したことになり(これは自身の背景をpaintConponent()
の中でクリアする事も含まれます)、そうしなかった場合は塗りつぶさなかったスクリーンが捨てられるかもしれません。 コンポーネントの
opaque
もしくは、optimizedDrawingEnabled属性のどちらかを
false
に設定すると、描画操作により多くの処理が必要となるので、透過と、コンポーネントの重なりについては慎重に利用することを進めます。UI delegatesを持つSwingコンポーネント(
JPanel
を含む)を拡張したほとんどのコンポーネントは、そのpaintComponent()
を実装するときに、super.paintComponent()
を実行すべきです。UI delegateは不透明なコンポーネントの背景をクリアする責務を持ち持ちますので、このセクションの#5に注意してください。Swingは
JComponent
のdoubleBuffered
属性で、組み込みのdouble-bufferingをサポートしており、既定で全てのSwingコンポーネントがtrue
に設定されていますが、個々コンポーネントの設定値に関係なくSwingのコンテナの設定値をtrue
に設定すれば、そのコンテナの子である全てのSwingコンポーネントのdoule-bufferingが有効になります。全てのSwingコンポーネントにおいてdouble-bufferingを有効にすることを強く勧めます。
-
複雑な出力を描画するコンポーネントは、描画の処理の範囲をそれらが交差する矩形領域に狭めるように、その描画領域情報を活用するべきです。
Summary
AWTとSwingは、プログラムがコンテンツを正しく描画することを簡単にするAPI郡を提供しています。ほとんどのGUIにSwingを使うことをお勧めしますが、AWTの描画メカニズムを理解することも役に立ちます。なぜなら、Swingは、その上に構築されているからです。このAPIを使ってアプリケーションプログラムがベストパフォーマンスを得るには、このドキュメントを参照し、ガイドラインに従ってプログラムを書くことにも責任を持たなければなりません。
*1:opaqueは"完全に不透明"であることをツールキットに知らせる事が目的なので、完全に透明という意味は持っていない。という意味。