●が動いて軌跡が残るプログラムを書いてみます。●●●と右に伸びていき最後には次の様になることを想定します。クリックする度に高さを変化させて右へ動いていくようにする予定です。
ボタンを3つにしたEventRandomRGB.javaを元に作りました。今のところ一番左のものしか使いませんが、あとの2つも生かしておきます。
import java.awt.*; import javax.swing.*; import java.awt.event.*; import java.awt.image.BufferedImage; public class AnimeDisk1 extends JFrame implements ActionListener{ JButton rbtn; JButton gbtn; JButton bbtn; MyPanel mypnl; //コンストラクタ public AnimeDisk1() { setDefaultCloseOperation(EXIT_ON_CLOSE); setTitle("Animation Disk"); mypnl = new MyPanel(400,400); rbtn = new JButton("start"); //今回はこれだけ使用 gbtn = new JButton("green"); bbtn = new JButton("blue"); JPanel btnpnl = new JPanel(); btnpnl.setLayout(new GridLayout(1,3,0,0)); btnpnl.add(rbtn); btnpnl.add(gbtn); btnpnl.add(bbtn); setLayout(new BorderLayout()); add(mypnl, BorderLayout.CENTER); add(btnpnl,BorderLayout.SOUTH); rbtn.addActionListener(this); gbtn.addActionListener(this); bbtn.addActionListener(this); pack(); setVisible(true); } //イベント処理 @Override public void actionPerformed(ActionEvent e) { if (e.getSource() == rbtn) { mypnl.moveDisk(); //●を描くメソッド } if (e.getSource() == gbtn) { mypnl.drawToBuff('g'); } if (e.getSource() == bbtn) { mypnl.drawToBuff('b'); } } public static void main(String[] args){ AnimeDisk1 myframe = new AnimeDisk1(); } //内部クラス public class MyPanel extends JPanel{ BufferedImage buffimg; Graphics2D bfg; Color bgcolor= new Color(255,255,191); //背景の色 public MyPanel(int width, int height){ setPreferredSize(new Dimension(width,height)); int r=2; buffimg = new BufferedImage( width*r,height*r,BufferedImage.TYPE_INT_RGB); bfg = buffimg.createGraphics(); bfg.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); bfg.setColor(bgcolor); bfg.fillRect(0, 0, buffimg.getWidth(), buffimg.getHeight()); } @Override public void paintComponent(Graphics myg){ //super.paintComponent(myg); int pnlw = getSize().width; int imgh = buffimg.getHeight() * pnlw / buffimg.getWidth(); myg.drawImage(buffimg, 0, 0, pnlw, imgh, this); //getSize().widthはMyPanelのインスタンスの幅 } //円を動かすメソッド public void moveDisk() { int x = buffimg.getWidth()/4; //100 int y = (int)(buffimg.getHeight()*Math.random()); int d = buffimg.getWidth()/40; //円の大きさ 10 int dx = buffimg.getWidth()/40; //動きの大きさ 10 bfg.setColor(Color.red); while ( buffimg.getWidth() > x ){ bfg.fillOval(x-d/2,y-d/2,d,d); repaint(); x+=dx; //100ms(0.1秒)停止 try { Thread.sleep(100); } catch(InterruptedException ex) { System.err.println(ex); } } } public void drawToBuff(char rgb){ for(int i=0; 10>i; i++){ Color rcolor = randomColor(rgb); bfg.setColor(rcolor); int x = (int)(buffimg.getWidth()*Math.random()); int y = (int)(buffimg.getHeight()*Math.random()); int h = (int)(buffimg.getWidth()*Math.random()/8+buffimg.getWidth()/80); bfg.fillOval(x-h/2,y-h/2,h,h); } repaint(); } public Color randomColor(char rgb){ int r=0; int g=0; int b=0; int dc=256; int x = (int)(dc*Math.random()); if (x > 255){ x = 0; } if (rgb=='r') r=x; if (rgb=='g') g=x; if (rgb=='b') b=x; Color c = new Color(r,g,b); return c; } } }
強調部分が主要な変更です。redボタンがstartになっていて、クリックでmypnl.moveDisk()が呼ばれます。
mypnl.moveDisk()は円を一つ描いて、xを少し増やしてまた円を描くという動作を右端にくるまで繰り返します。
速すぎると動いていくところが見えませんので、描く度に100ms(0.1秒)停止させます。Thread.sleep(100)が停止の部分です。
ところが、このプログラムはうまく動きません。
startボタンが押したままになり、円がでません。
しばらくすると、startボタンが戻ると同時に円が一度に出てきます。
まず、一度に出てきてしまう問題。これはrepaint()が再描画が必要なことを知らせる仕組みなのですぐには描画せず、複数回の書き替えを一度にまとめられてしまったのです。
これはrepaint()の代わりにpaintImmediately()を使うことで解消できます。
//円を動かすメソッド public void moveDisk() { int x = buffimg.getWidth()/4; //100 int y = (int)(buffimg.getHeight()*Math.random()); int d = buffimg.getWidth()/40; //円の大きさ 10 int dx = buffimg.getWidth()/40; //動きの大きさ 10 bfg.setColor(Color.red); while ( buffimg.getWidth() > x ){ //buffimg.getWidth()はbuffimgの幅 bfg.fillOval(x-d/2,y-d/2,d,d); //repaint(); paintImmediately(0,0,getSize().width,getSize().height); x+=dx; //100ms(0.1秒)停止 try { Thread.sleep(100); } catch(InterruptedException ex) { System.err.println(ex); } } }
paintImmediately()はすぐにpaintComponentを実行することを要求します。()内の引数は mypnl の全部の領域を表します。
さらにstartボタンが押したままになる問題もあります。実はこちらの方が大きな問題で、paintImmediatelyでも解決しません。
startボタンが押したままになるのはstartボタンで要求した作業が終わっていないからでプログラムの動作は正当です。
でもこのままでは作業が終わるまで他のボタンは反応しないし「閉じる」ボタンも効きません。(実際には受け付けられて保留になります。作業が終わってから他のボタンの処理をし、「閉じる」の処理もします)
この問題は次のページで解決します。
次に示す解決策では paintImmediately() を使わないので、どうでもよいことですが付け加えておきます。
paintImmediately()は何度もやらせるとコンピュータの動作が重たくなる可能性があるので、上記の様に画面全体を書き直すのではなく、画像の変更のあったところだけを矩形(四角)で指定する方がよいのです。無駄な作業をさせない配慮です。
しかし、mypnl の拡大・縮小を許しているので buffimg と mypnl では変更のあった場所がずれます。
bfg.fillOval(x-d/2,y-d/2,d,d); paintImmediately(x-d/2,y-d/2,d,d);
としたのでは、mypnl の拡大・縮小をしないときだけうまくいきますが、そうでないときは書き換えられなくなります。
(x-d/2,y-d/2,d,d)は buffimg に対するものです。mypnl 用に計算しなければなりません。
intで計算すると誤差がでるので消し残しが出るので汚くなります。そこでdoubleで計算し、さらにMath.ceilで切り上げ処理をして少し大きめにしています。そのたるかなり面倒になりました。
//paintImmediately(x-d/2,y-d/2,d,d);ではだめなので、次のようにします。 double rx = 1.0*getSize().width/buffimg.getWidth(); double ry = 1.0*getSize().height/buffimg.getHeight(); paintImmediately( (int)Math.ceil((x-d/2)*rx), (int)Math.ceil((y-d/2)*ry), (int)Math.ceil(d*rx), (int)Math.ceil(d*ry) );
repaint() を使って円がまとめて表示されることを確認しなさい。
repaint() をpaintImmediately(0,0,getSize().width,getSize().height)に替えて円が次々と表示されることを確認しなさい。
動く円が描き終わらないうちにgreenやblueボタンを押し、保留になっていることを確認しなさい。