- PR -

AWT で再描画すべき領域が L 字型だと paint メソッドが2回呼ばれてしまう

投稿者投稿内容
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-26 13:13
unibon です。こんにちわ。

AWT の Component クラスの paint メソッドが、
1 回呼ばれれば済みそうな状況にもかかわらず、
2 回呼ばれてしまうことがあることに気づきました。
#以下、図で書いたほうが分かりやすいのですが。
たとえば Java の Component の隅っこ(たとえば右上隅)が隠れるように
別のウィンドウ(Windows のメモ帳など)を重ねた後、
そのウィンドウを斜め方向(たとえば右上方向)にほんの少しだけズラす(まだ重なりは残る状態にする)と、
いままで Component の見えなかった部分の内 L 字型の領域の部分は見えるようになるため、
この領域は再描画すべき領域として、paint の対象になるはずです。
このような再描画の領域が L 字型であったとしても、
一度だけ paint すれば良いと思うのですが、
なぜか 2 回に分けて paint されます
(L 字型の内、縦棒に相当する部分と、横棒に相当する部分に分けて描画されるようです)。

この現象は Windows 2000 上の JDK 1.3 や 1.4 で起こることを確認しましたが、
JDK 1.1.8 や Microsoft の jview.exe では起きませんでした。
都合によりまだ 1.2 では試していません。
なお、Windows の「画面のプロパティ」の「効果」で
「ドラッグ中にウィンドウの内容を表示する」はオフにしています。

疑問としては、この現象は、JDK のバージョンアップにより AWT の仕様が変わっただけなのでしょうか。
たとえば、なにか事前の API 呼び出しなどによって、描画を1回で済ませられるように
描画ロジックを選択できるものでしょうか。

以下は、再現するサンプルです。

コード:

import java.awt.*;

public class Painter extends Component {

public void paint(Graphics g) {
super.paint(g);
System.out.println("paint");
}

public static void main(String[] args) {
Frame frame = new Frame();
Component component = new Painter();
frame.add(component);
frame.setSize(new Dimension(400, 300));
frame.setVisible(true);
}
}


なお 1.3 や 1.4 の場合は AWT だけでなく Swing でも同様の挙動でした。

ちなみに Bug Parade を見ると、
http://developer.java.sun.com/developer/bugParade/bugs/4109065.html

http://developer.java.sun.com/developer/bugParade/bugs/4267393.html
あたりが現象的に近いのですが、
いずれも State が "Closed, fixed" になっていました。



#あとで、説明文の細かい言い回しを編集して修正しました。

[ メッセージ編集済み 編集者: unibon 編集日時 2003-06-26 13:16 ]

#編集したらタグが化けたので再度修正しました。


[ メッセージ編集済み 編集者: unibon 編集日時 2003-06-26 13:19 ]
H2
ぬし
会議室デビュー日: 2001/09/06
投稿数: 586
お住まい・勤務地: 港
投稿日時: 2003-06-26 14:14
同じ所がpaintされていないのであればバグではないと思います。バグパレードに出ているのは同じ所が2度重なってpaintされる現象です。

2回に分けてペイントされるのはスムースな動きを表すために、細かく再描画しているかだと思います。特に右斜め上に動いた場合、右に動いてからpaintがトリガーされ、上に動いてから2回目のpaintがトリガーされるはずです。

Windowsが別の処理で忙しい場合やユーザーの動きが非常に早い場合、イベントのトリガーが遅くなり一回のpaintになることもありえます。

仕様がかわったのかどうかは分かりませんが、一回の描画よりも、細かく分けた描画のほうがスムースに見えるはずです。(多分JVM、OS、ハードの処理が早くなったためかと思うのですが)

paintに以下のコードを使うとどこがpaintされるのかが分かります。これを見ると、同じ所がpaintされているわけではないことが分かります。
コード:
public void paint(Graphics g) {
    super.paint(g);
    System.out.println(g.getClipBounds());
}

unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-26 23:39
unibon です。こんにちわ。

引用:

H2さんの書き込み (2003-06-26 14:14) より:
paintに以下のコードを使うとどこがpaintされるのかが分かります。これを見ると、同じ所がpaintされているわけではないことが分かります。


ありがとうございます。確かに、重複しない領域に分かれて paint が呼ばれていました。

しかし、疑問としては、一気にウィンドウをずらしているにもかかわらず、
2回に分けて paint が呼ばれる理由がやはり分かりません。
推測になりますが、Windows からは1回のイベント(ウィンドウメッセージ)として来ているのを、
Java の側で無理やり2回に分割しているような気もします。

また、これは私の都合になるのですが、
paint メソッド内の描画処理は、特にクリッピング領域を意識せず、
paint が呼ばれるごとにすべて描画し直しをしているため、
なまじクリッピング領域を分けて呼ばれても、描画が二度手間になるだけなので、
全体としての処理は逆に遅くなってしまいます。
こういう描画のやりかたのために、
昔の描画ロジックを選択できてもよさそうだと思うのですが、
API リファレンスを見てもちょっと見当たりません。


#あとで、引用を追加しました。

[ メッセージ編集済み 編集者: unibon 編集日時 2003-06-26 23:40 ]
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-27 11:58
unibon です。こんにちわ。

引用:

unibonさんの書き込み (2003-06-26 23:39) より:
しかし、疑問としては、一気にウィンドウをずらしているにもかかわらず、
2回に分けて paint が呼ばれる理由がやはり分かりません。
推測になりますが、Windows からは1回のイベント(ウィンドウメッセージ)として来ているのを、
Java の側で無理やり2回に分割しているような気もします。


その後、デバッガで調べたところ、sun.awt パッケージの RepaintArea クラスで、
意図的にそうしていることが分かりました。
L 字型の L の線が細いような場合(ウィンドウのずらしが少ない場合)は、
縦線の領域と横線の領域に分けて描画し、
L の線が太い場合(ずらしが多い場合)は、分けずに描画する、
という細工をやっているようです。

引用:

H2さんの書き込み (2003-06-26 14:14) より:
仕様がかわったのかどうかは分かりませんが、一回の描画よりも、細かく分けた描画のほうがスムースに見えるはずです。(多分JVM、OS、ハードの処理が早くなったためかと思うのですが)


すなわち、H2さんのこのご指摘のとおりでした。

なお、実際に、前回のサンプルでも、コンポーネントのほぼ9割ほどをウィンドウで隠してから、
ウインドウをドラッグして、コンポーネントのほとんどが見えるが隅っこがわずかに隠れる程度にすると、
paint メソッドは2回呼ばれず1回だけ呼ばれました。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-27 12:15
Bug Parade で調べると、
http://developer.java.sun.com/developer/bugParade/bugs/4292488.html
が該当するようです。

しかし、なんだか、ひょんな思いつきだけで変えているような感じがしました。
Kissinger
ぬし
会議室デビュー日: 2002/04/30
投稿数: 428
お住まい・勤務地: 愛知県
投稿日時: 2003-06-28 00:16
このように、再描画が必要な領域を幾つかの単純な矩形に分けて処
理する方法は、Javaよりもっと以前(1988年ころ)から X-Window
Systemでは行われていますね。

多くの場合、このほうが処理が速いか、気にしなくても良いそうです。
特に、昔は CPU上での演算に比べてフレームバッファへの書きこみの
ほうが不利でしたから。
Swingの軽量コンポーネントで使用される Componentもそれに倣った
のでしょう。(最近は直接フレームバッファにアクセスするものもあ
るようです。)

しかし、気になる場合もあります。
そういうとき、X-Windowでは各々の矩形の exposeイベントに付けら
れている、分割領域の残り数を見て最後の一回だけ描画するようにで
きました。
AWTにはこれはできないようですね。

何回も呼ばれると遅くなるのは、描画するときに演算を必要とするも
のと思われます。
これは演算結果を一度オフスクリーンバッファに描画しておいて、再
描画ではこれをコピーするようにすれば処理は軽くなるのではないで
しょうか。(その分メモリ喰くいますが。)
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-30 09:58
unibon です。こんにちわ。

引用:

Kissingerさんの書き込み (2003-06-28 00:16) より:
しかし、気になる場合もあります。
そういうとき、X-Windowでは各々の矩形の exposeイベントに付けら
れている、分割領域の残り数を見て最後の一回だけ描画するようにで
きました。
AWTにはこれはできないようですね。


その後、いろいろ探してみているのですが、
切り替えスイッチのようなものは見つけられていません。

引用:

Kissingerさんの書き込み (2003-06-28 00:16) より:
何回も呼ばれると遅くなるのは、描画するときに演算を必要とするも
のと思われます。
これは演算結果を一度オフスクリーンバッファに描画しておいて、再
描画ではこれをコピーするようにすれば処理は軽くなるのではないで
しょうか。(その分メモリ喰くいますが。)


そうです。描画の計算に処理時間がかかるものを paint しようとしています。
そして、L 字型だと見るからに遅く、
画面上の表示も2回に分けて描いているのが目で見ても分かったので、
このような描画ロジックに気づきました。
たしかにご指摘のようにオフスクリーンバッファにすれば解決できるのですが、
現在、スクロールペインに入れてスクロールして見るようなコンポーネントを考えており、
描画範囲が広い(おおまかに10000×10000ピクセルほど)のでメモリ消費が激しく、
躊躇しています。
unibon
ぬし
会議室デビュー日: 2002/08/22
投稿数: 1532
お住まい・勤務地: 美人谷        良回答(20pt)
投稿日時: 2003-06-30 10:07
unibon です。追加です。

引用:

unibonさんの書き込み (2003-06-30 09:58) より:
たしかにご指摘のようにオフスクリーンバッファにすれば解決できるのですが、
現在、スクロールペインに入れてスクロールして見るようなコンポーネントを考えており、
描画範囲が広い(おおまかに10000×10000ピクセルほど)のでメモリ消費が激しく、
躊躇しています。


自己フォローになりますが、
今回のような AWT の再描画ロジックの仕組み(paint の2回呼び出し)による遅さを回避するだけならば、
10000×10000ピクセルのバッファを持たなくても良く、
画面上に見えているコンポーネントの領域だけ、
すなわち最大でも物理的なスクリーンのサイズ(たとえば1024×768ピクセルなどの領域だけ)を
バッファリング(キャッシュ)すればできそうだと気づきました。
これならそんなにメモリも必要としないので、実現できそうな見込みです。
ありがとうございました。

スキルアップ/キャリアアップ(JOB@IT)