●が動いて軌跡が残るプログラムを書いてみます。●●●と右に伸びていき最後には次の様になることを想定します。クリックする度に高さを変化させて右へ動いていくようにする予定です。
ボタンを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(400,400);
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);
}
//イベント処理
@Override
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{
Color c = new Color(0,0,0); //楕円の色(drawRdmで使用)
Color bc= new Color(255,255,191); //背景の色
BufferedImage buffimg;
Graphics bfg;
public MyPanel(int width, int height){
buffimg = new BufferedImage(
width,
height,
BufferedImage.TYPE_INT_RGB);
bfg = buffimg.createGraphics();
bfg.setColor(bc);
bfg.fillRect(0, 0, width, height);
setPreferredSize(new Dimension(width,height));
}
@Override
public void paintComponent(Graphics myg){
super.paintComponent(myg);
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 ){
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);
}
}
}
setPreferredSize(new Dimension(400,400))を指定するとpack()の時にこの大きさを確保してくれます。
いままで400x300のフレームを作っていましたが、その中に入るMyPanelはフレームの枠のために小さくなっていました。このようにMyPanelの大きさを指定してpack()すると、フレームをそれに合わせてくれます。
強調部分が主要な変更です。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(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ボタンを押し、保留になっていることを確認しなさい。