- PR -

paintImmediately で描画が汚い

投稿者投稿内容
maru
ぬし
会議室デビュー日: 2003/01/27
投稿数: 412
投稿日時: 2003-07-15 15:23
こんにちは。お世話になってます。

現在、Win2000 + JDK1.4.1でJavaアプリケーションを作成しています。
自動処理みたいな機能があって、その機能(ボタン)を実行すると、自動処理中の
ダイアログが出るようになっています。で、そのダイアログは、メインスレッドとは
別のスレッドで、自動処理中とわかるようにアニメーションしています。
アニメーションはダイアログに貼り付けているJLabelに定期的にアイコンを差し替えて
処理しています。
で、ある時をトリガーにメインスレッドで少し重い処理を実行しています。その処理中も
アニメーションを続けています。

はじめ、マルチスレッドで処理しているにもかかわらず、メインスレッドで少し重い処理
が走るとダイアログのアニメーションの描画がとまってしまっていたので、アニメーション
スレッド内に、

lblAnime.paintImmediately( lblAnime.getVisibleRect() );

というように、アニメ用にダイアログに貼り付けてあるJLabelの必要な部分のみを再
描画するように変えました。

すると、アニメーションはとまらずにスムーズに動くようになったのですが、今度は
たまに、そのダイアログの lblAnime 以外の部分や lblAnime とは関係の無い自動
処理ダイアログの呼び出し元のフォームの一部の描画がおかしくなったり(アニメの
残像が残ったり)します。

このような場合、どうすればきれいなアニメーションができるのでしょうか?
ご存知の方はおられませんか?

[ メッセージ編集済み 編集者: maru 編集日時 2003-07-15 15:28 ]
ocean
ベテラン
会議室デビュー日: 2003/07/06
投稿数: 65
投稿日時: 2003-07-15 16:19
私も、アニメーションをしようとしてpaintImmediatelyを使ったことがありますが、この関数は子コンポーネントも描画するためか、GCが頻発して重かったのを覚えています。

アニメーションには、直接Graphicsオブジェクトを取得し、アニメーション中それに書き込むのが正しい使い方のようです。

コード:

private void sleep(long msec)
{
    try
    {
        Thread.Sleep(msec);
    }
    catch (InterruptedException e)
    {
    }
}

private void animate()
{
    final Graphics g = getGraphics();

    if (g == null)
    {
        return;
    }

    try
    {
        for (int i = 0; i <= 16; ++i)
        {
            sleep(15);

            g.Draw????();
        }
    }
    finally
    {
        g.dispose();
    }
}


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

引用:

maruさんの書き込み (2003-07-15 15:23) より:
はじめ、マルチスレッドで処理しているにもかかわらず、メインスレッドで少し重い処理
が走るとダイアログのアニメーションの描画がとまってしまっていたので、アニメーション
スレッド内に、

lblAnime.paintImmediately( lblAnime.getVisibleRect() );

というように、アニメ用にダイアログに貼り付けてあるJLabelの必要な部分のみを再
描画するように変えました。


JComponent クラスの getVisibleRect メソッドを使われているので、
Swing を使われているのだと推測します。

せっかくスレッドを分けているのだから paintImmediately を使わずに、
普通に paint/paintComponent メソッドを用意して、
それを repaint で呼び出すほうが良いと思います。
(私は paintImmediately はスレッドを分けないときに簡便に使えるものだけど、
あくまでも簡易的な用途のために使うべきだと思っています。)
そのためには、重い処理は GUI スレッドでおこなうのではなく、
新たに生成した別スレッドでやるようにする、
すなわちスレッドの役割を逆転させることになります。

こうするとその別スレッドの優先度を下げることもできます
(相対的に GUI スレッドの優先度が上がる)。
Kissinger
ぬし
会議室デビュー日: 2002/04/30
投稿数: 428
お住まい・勤務地: 愛知県
投稿日時: 2003-07-15 23:36
表示が壊れるのは、定期的に走るスレッドと、Swingのスレッドが
競合しているからでしょう。

paintImmidiately()の使用をやめ、重たい処理を行っているスレッド
の優先順位を Swingスレッドのそれ以下にして見てください。
Win2000+J2SDK1.4ならこれで改善するのではないでしょうか?

(ここから先は興味があったら読んで。)
ただし、この解決方法は不完全です。ご存知の通り、Javaのスレッド
のスケジューリングでは、複数のスレッドを同時に走らせようとし
たとき、時分割のように走るのか、どちらか片方が終了または waitに
なるまでもう一方がブロックするのかは、実装により異なります。
(優先順の高いスレッドが readyになっても優先順の低いスレッドが
走りつづける可能性もあります。かつての Solarisの Green Threadが
そうだったと思います。*1)
上記の方法で解決したように見えても、Win2000以外のOS、Sun以外のVM、
将来のVMでは問題が再燃するかも知れません。*2

優先順を上げるだけでなく、重い処理のスレッドに、時々 yield()や
sleep() を入れてやると実行環境の影響を受けにくくなります。
例えば数百万回の演算をするなら、数千回の演算を行なうごとに
yieldするとか。(回数は適当に調整してみて。)

*1 変な仕様と思わないで下さい。
  多くのリアルタイムOSでも、イベントやメッセージがディスパッチ
  されてコンテキストスイッチが発生するまでは、それまで動いていた
  スレッド(タスク)が動きつづけます。
  Win32 Threadや Unix Processのように時分割になるものばかりとは
  限りません。

*2 マルチプロセッサ環境でも結果が異なることが多いです。
  『マルチなんてサーバだけやろ』って思うかもしれませんが、
  Hyper threadingが一般化すれば、デスクトップでも珍しくなくなる
  かもしれません。

[ メッセージ編集済み 編集者: Kissinger 編集日時 2003-07-15 23:39 ]
ocean
ベテラン
会議室デビュー日: 2003/07/06
投稿数: 65
投稿日時: 2003-07-15 23:58
定期的にアイコンを切り替えるだけなら、スレッドを起こす必要はありません。javax.swing.Timerなどいかがでしょうか。ここにも解説があります。
Kissinger
ぬし
会議室デビュー日: 2002/04/30
投稿数: 428
お住まい・勤務地: 愛知県
投稿日時: 2003-07-16 00:27
javax.swing.Timerは便利です。

ただし、その中でスレッドがどう振舞うかある程度は知っていたほう
が良いかも知れません。

アニメーションするだけとか、時間待ちするだけとか、ファイル読み
込みながらのアイコン更新なら良いですが、『重い処理』というのが
演算主体の場合には SwingTimerは良い結果をもたらさない場合があり
ます。
ocean
ベテラン
会議室デビュー日: 2003/07/06
投稿数: 65
投稿日時: 2003-07-16 01:10
引用:

Kissingerさんの書き込み (2003-07-16 00:27) より:
『重い処理』というのが
演算主体の場合には SwingTimerは良い結果をもたらさない場合があり
ます。



実際にやってみたら、重い処理の最中にはタイマーのイベントが実行されませんね・・・。いいかげんなことを書いてすみません。
maru
ぬし
会議室デビュー日: 2003/01/27
投稿数: 412
投稿日時: 2003-07-16 10:19
多くのアドバイスありがとうございます。

もう少し、アプリの具体的な仕様と今の作りを説明しますと・・・

自動転送機能を持ったFTPクライアントを作っています。自動転送モードを実行すると
例のアニメーションをするダイアログが表示され別スレッドでアニメします。

そのアニメーションはスレッドのrun()の中で JLabelのアイコンを切り替えて、
paintImmediatelyで再描画し、sleepするのをwhile文で延々ループします。
停止を押すと、whileを抜けてスレッドが停止し、ダイアログが消えます。

で、メインのスレッドでは、JTimerで定期的にタイマーイベントを発生させて、指定の
フォルダ内をスキャンして、更新のあるファイルを自家製FTPのAPIで転送します。
転送はファイルを一定バイト読みながらソケットに書く処理をwhileでループしてます。
そのwhile内で一定の転送バイト数毎にイベントを発生させるようにしており、
そのイベント内で、進捗バーをカウントアップし、その進捗バーでもpaintImmediately
で再描画してます。

アニメーションダイアログが出ないメインスレッド単独の手動転送モードや、小さな
ファイル転送なら描画に問題ないですが、自動転送アニメーション中に数10MB単位
の10秒以上メインスレッドの処理(FTP転送)が動く場合に、まれにアニメダイア
ログだけでなく、メインのフォームも描画がおかしくなります。

unibonさんの言われるように、アニメ用JLabelのpaintをオーバーライドしてアニメ
表示するようにして、paintImmediatelyの変わりにrepaintを呼び出そうともしたの
ですが、単純にrepaintの部分だけ作り変えてみても再描画してなさそうだったので
実際にpaintをオーバーライトするまでの実験はしていませんでした。

unibonさんのpaintオーバーライト案と、oceanさんのGraphics直接描画で一度やって
みます。

ちなみに、今は、FTP転送後もメインのフォームそのものもrepaintして逃げています。
転送中はまれに画面が若干崩れても、転送後にきれいに再描画されます。
不細工ですけど、解決に時間がかかりそうであれば、これでもいいかなぁ?と、勝手に
甘えています。

[ メッセージ編集済み 編集者: maru 編集日時 2003-07-16 10:22 ]

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