●が動いて軌跡が残るプログラムを書いてみます。●●●と右に伸びていき最後には次の様になることを想定します。クリックする度に高さを変化させて右へ動いていくようにする予定です。
ボタンを3つにしたEventRandom5.javaを元に作りました。今のところ一番左のものしか使いませんが、あとの2つも生かしておきます。
import java.awt.*; import javax.swing.*; import java.awt.event.*; import java.awt.image.*; public class MoveDisk1 extends JFrame implements ActionListener{ JButton rbtn; JButton gbtn; JButton bbtn; MyPanel mypnl; JPanel btnpnl; //constractor public MoveDisk1() { setDefaultCloseOperation(EXIT_ON_CLOSE); setTitle("MoveDisk"); mypnl = new MyPanel(); rbtn = new JButton("start"); //今回はこれだけ gbtn = new JButton("green"); bbtn = new JButton("blue"); 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); //BufferedImageを作るのはpaintComponentに移した } //イベント処理 public void actionPerformed(ActionEvent e) { if (e.getSource() == rbtn) { mypnl.moveDisk(); //●を描くメソッド //mypnl.repaint(); } if (e.getSource() == gbtn) { mypnl.drawRdm('g'); mypnl.repaint(); } if (e.getSource() == bbtn) { mypnl.drawRdm('b'); mypnl.repaint(); } } public static void main(String[] args){ MoveDisk1 myframe = new MoveDisk1(); } //内部クラス public class MyPanel extends JPanel{ BufferedImage buffimg; Graphics bfg; Color bc= new Color(255,255,191); //背景の色 Color c = new Color(0,0,0); //楕円の色 boolean firsttime = true; public MyPanel(){ setBackground(new Color(255,255,191)); setPreferredSize(new Dimension(400,400)); //panel側で大きさを指定する } public void paintComponent(Graphics myg){ super.paintComponent(myg); if (firsttime){ //1回目の描画でBufferedImageをつくる buffimg = new BufferedImage( getSize().width, getSize().height, BufferedImage.TYPE_INT_RGB); bfg = buffimg.createGraphics(); bfg.setColor(bc); bfg.fillRect(0, 0, getSize().width, getSize().height); firsttime = false; //次回はやらない } myg.drawImage(buffimg, 0, 0 ,getSize().width, getSize().height,this); //getSize().widthはMyPanelのインスタンスの幅 } //円を動かすメソッド public void moveDisk() { int x = 100; int y = (int)(buffimg.getHeight()*Math.random()); int d = 10; //円の大きさ int dx = 10; //動きの大きさ bfg.setColor(Color.red); while ( buffimg.getWidth() > x ){ //buffimg.getWidth()はbuffimgの幅 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 drawRdm(char rgb) { for(int i=0; 10>i; i++){ nextColor(rgb); bfg.setColor(c); int x = (int)(400*Math.random()); int y = (int)(400*Math.random()); int h = (int)(50*Math.random()+5); bfg.fillOval(x-h/2,y-h/2,h,h); } } //色を変化させるメソッド public void nextColor(char rgb){ int r=0; int g=0; int b=0; if (rgb=='r'){ r = (int)(r + 256*Math.random()); } if (rgb=='g'){ g = (int)(g + 256*Math.random()); } if (rgb=='b'){ b = (int)(b + 256*Math.random()); } c = new Color(r,g,b); } } }
強調部分が主要な変更です。redボタンがstartになっていて、クリックでmypnl.moveDisk()が呼ばれます。
mypnl.moveDisk()は円を一つ描いて、xを少し増やしてまた円を描くという動作を右端にくるまで繰り返します。
これでは動いていくところが見えませんので、描く度に100ms(0.1秒)停止させます。Thread.sleep(100)が停止の部分です。
ところが、予想に判してこのプログラムはうまく動きません。
startボタンが押したままになり、円がでません。
しばらくすると、startボタンが戻ると同時に円が一度に出てきます。
まず、一度に出てきてしまう問題。これはrepaint()が再描画が必要なことを知らせる仕組みなので複数回の書き替えを一度にまとめられてしまったのです。
これはrepaint()の代わりにpaintImmediately()を使うことで解消できます。
//円を動かすメソッド public void moveDisk() { int x = 100; int y = (int)(getSize().height*Math.random()); int d = 10; //円の大きさ int dx = 10; //動きの大きさ bfg.setColor(Color.red); while ( buffimg.getWidth() > x ){ //buffimg.getWidth()はbuffimgの幅 bfg.fillOval(x-d/2,y-d/2,d,d); //repaint(); paintImmediately(x-d/2,y-d/2,d,d); x+=dx; //100ms(0.1秒)停止 try { Thread.sleep(100); } catch(InterruptedException ex) { System.err.println(ex); } } }
paintImmediately()はすぐにpaintComponentを実行することを要求します。()内の記述は画像の変更のあったところを矩形(四角)で指定するものです。paintImmediately()は何度もやらせるとコンピュータの動作が重たくなる可能性があるので、せめて書き換えなくてもいい部分の無駄な作業をさせない配慮です。
次にstartボタンが押したままになる問題。実はこちらの方が大きな問題で、paintImmediatelyでも解決しません。
startボタンが押したままになるのはstartボタンで要求した作業が終わっていないからでプログラムの動作は正当です。
でもこのままでは作業が終わるまで他のボタンは反応しないし「閉じる」ボタンも効きません。(実際には受け付けられて保留になります。作業が終わってから他のボタンの処理をし、「閉じる」の処理もします)
今回の注目点はrepaint()とpaintImmediately()、そしてイベント処理の中で時間のかかる処理を置くとユーザーが操作できない時間ができてしまうということです。
しかし、その他にも本質と関係のないところでの変更があります。
いままで400x300のフレームを作っていたが、その中に入るMyPanelはフレームの枠のために小さくなっていた。MyPanelの大きさを指定してフレームをそれに合わせるためプログラムを変更した。
setPreferredSize(new Dimension(400,400))を指定するとpack()の時にこの大きさを確保してくれる
BufferedImageを作る場所をpaintComponent()にし、最初にpaintComponent()が呼ばれるときにその大きさに合わせてBufferedImageを作ることにした。JPanelはインスタンスを作っても表示されないうちは大きさが定まらないのでBufferedImageの大きさを決めることができない。表示をするときには必ずpaintComponent()が呼ばれるはずなのでここでBufferedImageを作ることにする。
ただし最初の1回だけ作るために、firsttimeという変数を使っている。firsttimeは最初trueにしておく。firsttime=trueの時だけBufferedImageを作りfirsttime=falseとすると次回からは実行されない。
ウインドウをリサイズしてもBufferedImageを拡大・縮小して張り付けることにしていたが、paintImmediatelyでそのことに対する考慮を忘れている。
上記paintImmediatelyの引数はBufferedImageに対する大きさ。mypanlに対するものにしないと拡大・縮小で見えなくなる。
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()に替えて円が次々と表示されることを確認しなさい。
動く円が描き終わらないうちにgreenやblueボタンを押し、保留になっていることを確認しなさい。
startボタンを押し、戻らないうちに blue - start と押すと、見た目の順序はblueが最後になるが、実はblueが先に行われているのに表示されないだけだとわかる現象がある。これを探しなさい。