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とSwingのペイントの仕組み

上質な描画コード(ペイントコード)がアプリケーションの実行性能を支える

By Amy Fowler [原文]
訳:岩谷 宏
(updated: 080709)

(小さな明朝文字の部分は訳者による注記です。)

Painter's palette文字しか使えなかった昔のコンピュータとちがって、グラフィックを駆使する今日のシステムでは、画面のどこを、いつ、何色に塗るかという決定と処理があらゆる瞬間に重要です。それを比較的容易に行い、グラフィックを使うユーザインタフェイス(GUI, graphical user interface)を楽に構築するために、画面の上にウィンドウを作ってそれらのウィンドウを操作するツールキット(windowing toolkit)(小さな部品的なソフトウェアの集まり)が、プログラムを開発するための基本的な枠組みとして使われます。

AWT(abstract windowing toolkit)とSwingはどちらも、そのようなツールキットであり、ソフトウェア開発のための基本的枠組み(フレームワーク)です。しかし一部のデベロッパたちは、この二つのツールキットを実装しているAPI集合について、正しく理解していません。そのため、プログラムが期待通りに動かないという問題がよく起こります。

本稿は、AWTとSwingのペイントの仕組みを詳しく説明します。デベロッパのみなさんは、本稿を読んで、GUIを効率的に正しくペイント(paint)するコード(描画コード)を書けるようになってください。ただし本稿では、ペイントの仕組み(何がどこにいつ描画されるか--whereとwhen)を一般的に説明しますが、AWTやSwingの個々のAPIの使い方(how)には立ち入りません。上質なグラフィクスを作るためのハウツーは、Java 2DのWebサイトで入手してください。

本稿は、次のような話題を取り上げます:

ペイントシステムのこれまでの進化過程

JDK 1.0で登場したAWTには、重量(heavyweight)コンポーネントしかありませんでした(“重量”とは、どのコンポーネントも不透明なネイティブの--つまり各OSのレベルの--ウィンドウを背負ったGUIコンポーネントである、という意味です)。そのためAWTはどうしても、それぞれのネイティブなプラットホーム(各OSとその上のウィンドウシステム)が持っているペイントサブシステムに依存せざるをえません。それによって、ウィンドウの損傷の発見、クリップ領域の計算、z軸上の順序(複数のコンポーネントやウィンドウの重なり具合)といった細かい機能も実現します。

しかしJDK 1.1から軽量(lightweight)コンポーネントが登場したことによって、(“軽量”とは、各コンポーネントが自分からいちばん近いネイティブの親ウィンドウを自分のウィンドウとして利用する…個々に自分専用のネイティブウィンドウは持たない…、という意味です)AWTは軽量コンポーネントのためのペイント処理を、各コンポーネントが共用するJavaコードとして実装することになりました。その結果、重量コンポーネントのペイントと軽量コンポーネントのペイントに、細かい違いがいくつか生じています。

Swingというツールキットが登場したJDK 1.1以降は、コンポーネントをペイントするための独特の方法が導入されました。Swingのペイントの仕組みはAWTと似ている部分やAWTに依存している部分もたいへん多いのですが、違いがいくつかあります。そしてまた、SwingのAPI集合を使うと、アプリケーションがペイントの動作をカスタマイズ(独自化,自前化)しやすくなりました。

〔訳注(基本用語の解説):軽量コンポーネントは、純粋にJavaだけで書かれたグラフィクスコンポーネント(component,部品)、重量コンポーネントはOSのウィンドウシステムを呼び出して自分を表示するグラフィクスコンポーネントです。ただし軽量コンポーネントも、もっとも外側(もっとも下、もっとも根っこ)には、重量コンポーネントの、つまりOSの管理システムに依存する、ウィンドウ※を表示のために必要とします。軽量コンポーネントだけをいきなり画面に表示することは、今日のコンピュータの上ではまず不可能です。※:1つまたは複数のコンポーネントをその中に収めて表示する、容器のような役割のコンポーネントをGUI用語でコンテナ(container,収容器)と呼びます。つまり、軽量コンポーネントも、最根っこのコンテナとしては重量コンポーネントのウィンドウを必要とします。〕


AWTのペイント

AWTのペイントAPIの動作を理解するためには、画面上にウィンドウが描かれたり、GUIのコンテナやコンポーネントがいろんな操作をされるときに、それらの描画を起動する主体(何がペイントを起動するのか)の違いをまず知る必要があります。AWTには、2種類のペイント操作があります。その2種類とは、「システムが起動するペイント」と「アプリケーションが起動するペイント」です。


システムが起動するペイント

システムが起動するペイントは、システム(AWTのランタイムシステム)がコンポーネントに描画をリクエストすることによって行われます。システムは、主に次のような理由でコンポーネントにそのコンポーネントの描画をリクエストします:


アプリケーションが起動するペイント

アプリケーションが起動するペイント操作では、コンポーネントの内部の状態が変わったので表示の更新が必要だ、とコンポーネント自身が判断します(たとえばマウスがボタンを押したことをボタンが検出して「押されたときの色」にペイントしなければならないと判断します。〔以下は訳補記: あるいは弾丸と怪物が衝突したので爆発効果をペイントしなければならないとアプリケーションのコードが判断します。〕


paint()メソッド

ペイントのリクエストを起動したのがシステムでもアプリケーションでもどちらでも、AWTはリクエストに対する「コールバック」方式でペイントを実行します。軽量コンポーネントも重量コンポーネントもどちらも、この点は同じです。ですからプログラムは、ツールキットが呼び出す(コールバックする)メソッドをオーバライドして、そこに描画のコードを書いておけば、ペイントする時にはツールキットがそのメソッドを呼び出します。プログラムがオーバライドする、そのメソッドが、java.awt.Componentクラスの:

    public void paint(Graphics g)

メソッドです。

〔訳注:電話を受けた側から最初にかけてきた側へかけ直すのが“コールバック”--これと似てAWTとSwingのシステムは、受けたリクエストに対して自分からpaint()を呼び出すことによってペイントを行う。ウィンドウ全体のマネージメントの問題などが絡むので、リクエストする側が勝手に自分でペイントのコードを呼び出すことは許されない。〕

AWTがこのメソッドを呼び出すときは、引数のGraphicsオブジェクトが、この特定のコンポーネントを描画するのにふさわしい状態へと構成(コンフィギュレーション)されます。構成の項目は:

プログラムは必ず、このGraphicsオブジェクト(またはそれからの派生オブジェクト)を使う描画コードをpaint()の中に書かなければなりません。Graphicsの状態(たとえばcolor)を必要に応じて変えるのは自由です。

以下は、paint()コールバックの簡単な例です。このpaint()は、コンポーネントのサイズと同じ高さまたは幅の円を描いて、その内部をコンポーネントの前景色で塗りつぶします:


    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のプログラムの中で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コンポーネントの描画更新を行わせます:

        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()を呼び出すのは、よくやることですが間違いです。引数のないrepaint()はコンポーネント全体をリペイント(再描画)するので、無駄なペイントをすることが多くなります。

paint()とupdate()

「システムが起動するペイント」と「アプリケーションが起動するペイント」は、どこがどう違うのでしょうか?。重量コンポーネントのペイントでは、この両者にわずかな違いがあります(軽量コンポーネントについては後述します)。しかし、両者に違いがあることが、大きな混乱の原因になっています。

重量コンポーネントでは、ペイント操作をシステムが起動したか、アプリケーションが起動したかの違いによって、ペイントの方法が次のように違います。

システムが起動するペイント

システムが起動するペイントは、次のように行われます:

  1. コンポーネントの一部をペイントすべきか、それとも全体をペイントすべきかをAWTが判断する.

  2. AWTはイベントディスパッチスレッド(Event Dispatch Thread, EDT, JavaのGUIを動かしているスレッド)にコンポーネントのpaint()を呼び出させる.

アプリケーションが起動するペイント

アプリケーションが起動するペイント操作は、次のように行われます:

  1. コンポーネントの内部の状態変化に応じて、コンポーネントの一部をペイントすべきか、それとも全体をペイントすべきかをプログラムが判断する.

  2. プログラムはコンポーネントのrepaint()を呼び出し,このコンポーネントの再描画(リペイント)が必要であるという非同期のリクエストをAWTに登録する.

  3. AWTはイベントディスパッチスレッド(JavaのGUIを動かしているスレッド)にコンポーネントのupdate()を呼び出させる.

    :一つのコンポーネントに対して、最初のリペイントリクエストが処理されるまでに複数のrepaint()呼び出しが行われたら、それらの複数のリクエストに対して一回のupdate()呼び出しが行われる。どこからどこまでのリクエストをその一回のupdate()に対応させるかを決めるアルゴリズムは、実装次第である。このように複数のリクエストがひとつにまとめられるときには、コンポーネントの矩形領域の更新結果(表示)は、複数のリクエストの各更新結果の論理和となる.

  4. コンポーネントがupdate()をオーバライドしなかったら, update()のデフォルトの実装は(軽量コンポーネントでない場合)コンポーネントのバックグラウンドをクリア※してからpaint()を呼び出すだけである.

〔※訳注:ただし、抽象クラスであるjava.awt.Componentのupdate()は背景塗りつぶしをやらずに、ただ単にpaint()を呼び出しています。〕

結局のところ、リペイントの目的はpaint()を呼び出して再描画を行わせることですから、そこにupdate()のような一見“余計なもの”が介在することを、不思議に思う人が多くいます。しかし、java.awt.Componentupdate()デフォルトの実装はたしかに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.Containerpaint()メソッドが行います。Containerpaint()メソッドは、これからペイントする矩形領域に含まれていて、現在実際に表示されている、子の軽量コンポーネントたちのpaint()を呼び出します。したがってContainerのサブクラスはどれも(軽量でも重量でも)、paint()メソッドを次のようにオーバライドしていることが重要です:

    public class MyContainer extends Container {
        public void paint(Graphics g) {
	    // まず自分をペイントしてから...
	    // 子の軽量コンポーネントたちをペイントする
	    super.paint(g); 
        }
    }

super.paint()の呼び出しがないと、このコンテナの子の軽量コンポーネントたちがペイントされません(軽量コンポーネントが初めて導入されたJDK 1.1のころは、この問題がよく起きました)。

なお、 Container.update()のデフォルトの実装は子の軽量コンポーネントたちのupdate()paint()再帰的に呼び出していません。 したがって、重量Containerのサブクラスがupdate()をオーバライドして連続描画を行う場合、子の軽量コンポーネントたちの表示も必要なら、それらを確実に再帰的にリペイントすることが重要です。ただし連続描画を行う重量コンテナを作る機会はあまりありませんから、この点が問題になるプログラムはそんなに多くないでしょう。

軽量コンポーネントとシステムが起動するペイント

ウィンドウの動作(表示、非表示、移動、リサイズ、など)を実装する軽量コンポーネント(軽量コンテナ)のコードも、すべてJavaで書かれています。それらのJavaコードの中で、AWTがいろんな軽量コンポーネントたちを明示的にペイントしなければならないことがよくあります(それは、もはやネイティブのシステムからではないとはいえ、やはりシステムが起動するペイントです)。しかし軽量コンポーネントではrepaint()を呼び出してコンポーネントをペイントするので、その場合は、前にも説明したように、paint()直接呼び出されるのではなくupdate()が呼び出されます。したがって、軽量コンポーネントの場合は、システムが起動するペイントの実行のされ方が二通りあることになります:

以上をまとめると、軽量コンポーネントでは要するに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のペイントのためのガイドライン

AWTのコンポーネントをペイントするためには、単純なコールバックAPI(システムの側が呼び出すAPI)を使います。その場合、次の点に注意してください:

  1. 通常のプログラム(全画面モード以外のプログラム)では、ペイントするコードはすべて、コンポーネントのpaint()メソッドのスコープ内に書いてください。

  2. プログラムはrepaint()を呼び出して, paint()の呼び出しをシステムにリクエストします。そしてシステムは未来のある時点に、適切なタイミングでpaint()を呼び出します。プログラムが直接paint() を呼び出してはいけません。

  3. 複雑な描画をするコンポーネントに対しては、引数のあるrepaint()を呼び出して、更新が必要な矩形領域だけをリペイントすべきです。引数のないrepaint()を呼び出すと、コンポーネント全体がリペイントされます。

  4. repaint()を呼び出すとupdate()が呼ばれ、デフォルトではそこからpaint()が呼ばれます。したがって重量コンポーネントは、必要ならupdate()をオーバライドして連続描画ができます(軽量コンポーネントではこのやり方による連続描画はできません)。[参考注記]

    〔訳注:update()と連続描画に関するここまでの説明は、複雑すぎるかもしれない。もっとも簡単に言うと、Applet, Canvasなどのサブクラスのupdate()の内容をpaint(g);のたった1行だけにすれば、そのAWT重量コンポーネントでは、ここで言う連続描画が可能になる。update()の中で、背景色による塗りつぶしなどをやらないからである。すべてのコンポーネントの最祖先である抽象クラスjava.awt.Componentのupdate()がまさにpaint(g);のたった1行だけだけど、AWT中の具体クラスのupdate()はそうではない。〕

  5. paint()をオーバライドしたjava.awt.Containerのサブクラスは、必ずsuper.paint()を呼び出して、そのコンテナの子コンポーネントたちを確実にペイントしてください。

  6. 複雑な描画を行うコンポーネントは、クリップ領域の矩形を適切に指定することによって、描画操作をクリップ領域に含まれる部分だけに狭めてください(上記3.を参照)

〔訳注:AWTと軽量コンポーネントについて -- 現実的なプログラミングの知恵としては、AWTのアプリケーション(たとえばApplet)の中では、軽量コンポーネントの作成や使用をしないほうが良い。そのほうが、paint()をめぐる問題に足をすくわれずに済む。軽量コンポーネントはSwingのコンポーネントだと割り切り、逆にSwingのアプリケーション中ではAWTの重量コンポーネントの併用をしないよう、気をつけよう。うっかり併用すると、不可解な表示トラブルに見舞われることがよくあります。〕


Swingのペイント

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つのプロパティの説明)

不透明性(opacity)

コンポーネントが不透明であるという、いちばんふつうの場合の実行性能を良くするために、javax.swing.JComponentにはプログラマが自由に設定できるopaqueプロパティがあります:

    public boolean isOpaque()
    public void setOpaque(boolean o)


このプロパティのtrue/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の意味は、以下のとおりです:

Swingはリペイントをするとき、このisOptimizedDrawingEnabledプロパティをチェックして、重なっているコンポーネントを探索する範囲を素早く狭めます。

isOptimizedDrawingEnabledプロパティはリードオンリーなので、コンポーネントがデフォルトの値を変える唯一の方法は、サブクラスを作ってこのメソッドをオーバライドし、必要な値を返させることです。Swingの標準コンポーネントはほとんどすべて、このプロパティの値としてtrueを返します。例外はJLayeredPane, JDesktopPane, そしてJViewPortです。


Swingのペイントメソッド

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で見てください。

UIデリゲイトとペイント

Swingの標準コンポーネントの多くが、ルック&フィールを独立のルック&フィールオブジェクトによって実装しています(このような差し替え自由(pluggable)なルック&フィールオブジェクトのことをUIデリゲイト(delegate)--UI委譲--と呼びます)。したがって標準コンポーネントのペイント操作はUIデリゲイトに“委譲”され、次のような処理が行われます:

  1. paint()paintComponent()を呼び出す.
  2. uiプロパティがnullでなければ、paintComponent()ui.update()を呼び出す.
  3. コンポーネントのopaqueプロパティがtrueなら、 ui.udpate()はコンポーネントの背景をコンポーネントの背景色で塗り、ui.paint()を呼び出す.
  4. ui.paint()がコンポーネントの内容を描画する.

UIデリゲイトのあるSwingのコンポーネントのサブクラス(JComponentの直接のサブクラスでない場合)で以上の処理が実行されるためには、オーバライドしたpaintComponent()メソッドの中でsuper.paintComponent()を呼び出してください:

    public class MyPanel extends JPanel {
        protected void paintComponent(Graphics g) {
	    // 最初にUIデリゲイトにペイントさせる
	    // (opaque==true==不透明なら、背景を塗りつぶす)
	    super.paintComponent(g); 

	    // 次に自分の内容をペイントする....
        }
    }

UIデリゲイトにペイントさせたくない場合(たとえばコンポーネントの視覚表現を完全にカスタマイズしたいとき)は、super.paintComponent()の呼び出しを省略できますが、しかしそのコンポーネントのopaqueプロパティがtrueなら、opaqueプロパティの説明のところで述べたように、背景の塗りつぶしを自分でやる必要があります。


Swingのペイントの処理過程

Swingが行う"リペイント"リクエストの処理は、AWTとやや違います。ただし、アプリケーションプログラムにとって、その最終結果はどちらも基本的に同じです--つまりpaint()が呼び出されます。SwingはRepaintManager API(後述)を使ってリペイントリクエストを処理し、ペイントの実行性能を上げています。Swingのペイントは、次のような二つの過程をたどります:

(A)ペイントリクエストが最初の親(最外側のウィンドウコンテナ)である重量コンポーネント(通常はJFrame, JDialog, JWindow,またはJApplet)に来たとき:

  1. イベントディスパッチスレッド(event dispatching thread, EDT, JavaのGUIが動いているスレッド)が、その重量コンポーネントのpaint()を呼び出す

  2. Container.paint()のデフォルトの実装が子の軽量コンポーネントのpaint()を再帰的に呼び出す

  3. 最初のSwingコンポーネントに到着したら、JComponent.paint()のデフォルトの実装が次の処理を行う:

    1. コンポーネントのdoubleBufferedプロパティがtrueで、そのコンポーネントのRepaintManagerがダブルバッファリングをonにしていたら、Graphicsオブジェクトをオフスクリーンのグラフィクスに変換する.
    2. paintComponent()を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して)
    3. paintBorder()を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して)
    4. paintChildren()を呼び出す(ダブルバッファリングonならオフスクリーンのグラフィクスを渡して). これはクリップ領域とopaqueおよびoptimizedDrawingEnabledを使って、paint()を再帰的に呼び出すべき子のコンポーネントを判断します.
    5. コンポーネントのdoubleBufferedプロパティがtrueで、そのコンポーネントのRepaintManagerがダブルバッファリングonなら、元のオンスクリーンのGraphicsオブジェクトを使ってオフスクリーンの画像をコピーする.

    注: JComponent.paint()のステップ#1と#5は、paint()再帰的な呼び出し(上のステップ#4で述べているpaintChildren()からの呼び出し)では行われません。ダブルバッファリングでは、一つの同じオフスクリーンイメージを一つのウィンドウ階層中のすべての軽量コンポーネントが共用するからです。

(B)ペイントリクエストがjavax.swing.JComponentのサブクラスへのrepaint()呼び出しから来たとき:

  1. JComponent.repaint()は非同期のリペイントリクエストをコンポーネントのRepaintManagerに登録し、RepaintManagerinvokeLater()を使ってRunnableをキューに入れ、あとでそのリクエストをイベントディスパッチスレッドの上で処理する.

  2. そのRunnableオブジェクトがイベントディスパッチスレッドの上で実行されると、コンポーネントのRepaintManagerがそのコンポーネントのpaintImmediately()を呼び出す. このメソッドは次のことを行う:

    1. クリップ領域とopaqueおよびoptimizedDrawingEnabled プロパティを使って、ペイント操作を開始する‘ルート’のコンポーネントを判断する(透明性とコンポーネントの重なりを正しく扱うため).
    2. ルートコンポーネントのdoubleBufferedプロパティがtrueで、ルートのRepaintManagerの上でダブルバッファリングがonなら、Graphicsオブジェクトをオフスクリーンのグラフィクスに変換する.
    3. ルートコンポーネントのpaint()を呼び出す(それがさらに、上の(A)のJComponent.paint()のステップ#2-4を実行する)--これにより、ルートの中にあってクリップ矩形に含まれるものすべてがペイントされる.
    4. ルートコンポーネントのdoubleBufferedプロパティがtrueで、ルートのRepaintManagerの上でダブルバッファリングがonなら、オフスクリーンの画像(オフスクリーンイメージ)を元のオンスクリーンのGraphicsオブジェクトを使ってコンポーネントにコピーする.

    : 一つのコンポーネントやその親のどれかに対して複数のrepaint()呼び出しが行われると、これらの複数の呼び出しが、今repaint()が呼び出されている最上位(いちばん上の親)のコンポーネントのpaintImmediately()への一回のコールバックにまとめられることがあります。たとえばJTabbedPaneの中にJTableがあって、この収容階層のそのほかのリペイントの処理中に両者が共にrepaint()を呼び出すと、この2つのrepaint()呼び出しはJTabbedPaneの一回のpaintImmediately()呼び出しとして処理され、そこから、両コンポーネントのpaint()が実行されます。

したがってSwingのコンポーネントでは、update()は呼び出されません。

repaint()を呼び出すとpaintImmediately()が呼び出されますが、このメソッドはペイント"コールバック"ではないので、ペイントを行うコードをpaintImmediately()の中に書いてはいけません。というより、ふつう、paintImmediately()をオーバライドするような状況はほとんどありません。

同期ペイント(synchronous painting)

前の節で述べたように、paintImmediately()はSwingのコンポーネントに自分をペイントせよと命じるための入り口となるメソッドです。ここから始まって、すべての必要なペイントが最後まで確実に行われます。このメソッドを使って、その名前どおりの同期ペイントを行わせることもできます。同期ペイントは、コンポーネントの内部ステート(状態)の変化と、その変化に伴って必要な視覚的変化を、リアルタイムで同期させるペイントです。ステートが変わったら即座に表示も変わらなければならない、そんなときに同期ペイントを使います。たとえばスクロール操作をしているときのJScrollPaneは、そんなペイントを必要とします。

リアルタイムのペイントをしなければならない正当な理由がないかぎり、プログラムが直接、このメソッドを呼び出してはいけません。非同期のrepaint()呼び出しは複数の呼び出しを一つにまとめて効率化できますが、直接のpaintImmediately()呼び出しはそれができません。また、このメソッドを呼び出すときには必ず、イベントディスパッチスレッドから呼び出さないといけません。したがって、このメソッドを使ってペイントのコードをマルチスレッド化することは困難です。Swingのシングルスレッドモデルについては、この保存記事:"Threads and Swing."を読んでください。(Swingとスレッドに関する比較的新しい記事は:Concurrency in Swing。)


リペイントマネージャ(RepaintManagerクラス)

SwingのRepaintManagerクラスの仕事は、Swingのコンテナ〜コンポーネントの収容階層のリペイント処理の効率を上げることと、Swingの‘更新確認(revalidate)’の仕組みを実装することです(後者については別の記事で説明する予定です)。このクラスが実装しているリペイントの仕組みは、Swingのコンポーネントたちに対するすべてのリペイントリクエストを捕捉します(したがってもはやAWTは処理に介入しません)。また、更新を要するコンポーネントや領域を“汚れた区域(dirty region)”と呼ばれる、このクラス独自のプロパティで管理します。そしてこのクラス(のオブジェクト)は、実行を待っているリクエストをイベントディスパッチスレッドの上でinvokeLater()を使って処理します。それについては、上の“Swingのペイントの処理過程”の節の(B)の項で説明しました。

RepaintManagerはSwingの内部的なシステムの一環なので知る必要も使う必要もない、というプログラムが実際には多いでしょう。しかしプログラムがこのAPIを使うと、ペイント操作のいくつかの側面を細かく制御できます。

"現在の(current)"RepaintManager(カレントリペイントマネージャ)

RepaintManagerは動的に(ランタイムに)差し替えできますが、何もしなくても最初からデフォルトのインスタンスが提供されています。以下のstaticメソッドを使って、プログラムは"現在の(current)"RepaintManager(カレントリペイントマネージャ)をget/setできます:

    public static RepaintManager currentManager(Component c)
    public static RepaintManager currentManager(JComponent c)
    public static void 
         setCurrentManager(RepaintManager aRepaintManager)

"現在の(current)"RepaintManager(カレントリペイントマネージャ)を変える

RepaintManagerのサブクラスを作って、そのオブジェクトをデフォルトのリペイントマネージャと置き換えることができます:

    RepaintManager.setCurrentManager(new MyRepaintManager());

このサンプルプログラムRepaintManagerDemoRepaintManagerをインストールするやり方とが分かるでしょう。このプログラムは、リペイントの過程に関する情報をプリントします。

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()を使ってRunnableEventQueueに置くことも、あなたが書かなければなりません。その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のペイントのためのガイドライン

Swingのプログラムでペイントのコードを書くときには、次の点に注意してください:

  1. Swingのコンポーネントでは、システムが起動するペイントでもアプリケーションが起動するペイントでも、どちらのリクエストでもpaint()が呼び出されます。Swingのコンポーネントでは、update()は呼び出されません。

  2. プログラムはrepaint()を呼び出して、システムが近い将来paint()を呼び出してくれるようリクエストします。プログラムが直接、paint()を呼び出してはいけません。

  3. 複雑な描画をするコンポーネントでは、引数のあるrepaint()を呼び出して、更新を要する矩形領域を指定すべきです。引数のないrepaint()は、コンポーネントの全領域をリペイントさせます。

  4. Swingのpaint()の実装は、次の3つのコールバックに分かれています:

    1. paintComponent()
    2. paintBorder()
    3. paintChildren()

    Swingのコンポーネントのサブクラスを作って独自のペイントを行わせたいときには、paint())ではなくpaintComponent()をオーバライドしてください。

  5. Swingはペイントの効率を上げるために次の2つのプロパティを使います:
    • opaque: は、コンポーネントのすべてのビットをペイントするのか、それともペイントしない部分(透明な部分)があるのか、を指定します[参照]
    • optimizedDrawingEnabled: は、このコンポーネントの子が重なっているかいないかを指定します[参照]

  6. Swingコンポーネントでは、opaqueプロパティをtrueにセットすると、それは、コンポーネントの全領域の全ビットをペイントするという、システムに対する約束(契約)です。それにはpaintComponent())が毎回、自分の領域を背景色で塗りつぶすことも含まれます。約束を守らないと、画面にゴミが表示されることもあります。 〔訳注:UIデリゲイトがあり、opaque==trueを継承するSwingコンポーネント-Japanelなど-のサブクラスは、paintComponent()の冒頭でsuper.paintComponent(g)を呼び出してこの約束(背景塗りつぶし)を守るべきである。この項と、下の8.を見よ。〕

  7. opaqueoptimizedDrawingEnabledfalseにすると、ペイントの処理の負担が増えますから、透明部分のあるコンポーネントや、複数のコンポーネントが重なるような描画を、濫用しないでください。

  8. UIデリゲイトのあるSwingのコンポーネント(JPanelなど)のサブクラスは、オーバライドした paintComponent()の冒頭でsuper.paintComponent()を呼び出してください。UIデリゲイトが、不透明なコンポーネントの背景のクリア(塗りつぶし)を担当します。このことによって、上の6.の条件が満たされます。 〔上6の訳注の続き:opaqueをtrueにセットしただけでは、コンポーネントの不透明性は実現しない。この[重要なフォーラム・スレッド]を読んでみましょう。〕

  9. Swingでは、JComponentdoubleBufferedプロパティを使って、すべてのコンポーネントがダブルバッファリングを内部的に実装しています(このプロパティはデフォルトではtrueです)。Swingのコンテナに対してこのプロパティをtrueにすると、そのコンテナ内に表示されるすべての軽量コンポーネントが、同じくダブルバッファリングonになります(個々の設定は無視されます)。

  10. Swingのコンポーネントはすべて、ダブルバッファリングをonにすることを、強力におすすめします。

  11. 複雑な描画を行うコンポーネントは、クリップ矩形(clip rectangle)を上手に使って、リペイントの範囲を必要最小限に狭めてください(上の3.を見てください)。


まとめ

AWTとSwingはどちらも、デベロッパのみなさんがGUIのプログラムを比較的簡単に書けるように、グラフィクスのコンテンツを画面に正しく描くための使いやすいAPI提供しています。通常のGUIにはSwingをおすすめしますが、それでも、AWTのペイントの仕組みを理解していることは有意義です。Swingも、その土台はAWTですから。

これらのAPIから最高の実行性能を引き出すためには、本稿で示したガイドラインを正しく守っているアプリケーションを書いてください。



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並行処理プログラミング」