Gọi repaint() nhiều lần trong JFrame và JApplet
Khi học Java, chúng ta thường bắt gặp những chương trình đơn giản về animation trong các sách dạy AWT và Swing, chẳng hạn như chương trình sau đây:
import java.awt.*;
import javax.swing.*;
public class Animation1 {
public static void main( String[] args ) {
Animation1 gui = new Animation1();
gui.go();
}
private void go() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
panel_ = new MyPanel();
frame.getContentPane().add( panel_, BorderLayout.CENTER );
frame.setSize( 500, 500 );
frame.setVisible( true );
for( int i = 0; i < 400; i++ ) {
x_++;
y_++;
panel_.repaint();
try {
Thread.sleep( 10 );
} catch( Exception ex ) { }
}
}
class MyPanel extends JPanel {
public void paintComponent( Graphics g ) {
g.setColor( Color.white );
g.fillRect( 0, 0, this.getWidth(), this.getHeight() );
g.setColor( Color.green );
g.fillOval( x_, y_, 40, 40 );
}
}
private JPanel panel_;
private int x_ = 0;
private int y_ = 0;
}
Chương trình này vẽ ra một vòng tròn trên một panel, tính toán lại tọa độ của nó rồi gọi repaint() để vẽ lại vòng tròn. Phương thức repaint() yêu cầu các component trên frame tự vẽ lại.Thao tác vẽ lại liên tục với các vị trí khác nhau sẽ tạo ra cảm giác vòng tròn chạy trên panel. Câu lệnh Thread.sleep(10) làm giảm tốc độ di chuyển của vòng tròn giúp người dùng dễ theo dõi.
Chúng ta thử sáng tạo thêm một chút bằng cách thêm vào frame một button dùng kể kích hoạt animation (học event handler và inner class luôn). Animation sẽ được kích hoạt khi người dùng ấn nút “Start animation”. Chương trình được cải tiến như sau:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Animation2 {
public static void main( String[] args ) {
Animation2 gui = new Animation2();
gui.go();
}
private void go() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
panel_ = new MyPanel();
frame.getContentPane().add( panel_, BorderLayout.CENTER );
button_ = new JButton( "start animation" );
button_.addActionListener( new StartAnimationAction() );
frame.getContentPane().add( button_, BorderLayout.SOUTH );
frame.setSize( 500, 500 );
frame.setVisible( true );
}
class MyPanel extends JPanel {
public void paintComponent( Graphics g ) {
g.setColor( Color.white );
g.fillRect( 0, 0, this.getWidth(), this.getHeight() );
g.setColor( Color.green );
g.fillOval( x_, y_, 40, 40 );
}
}
class StartAnimationAction implements ActionListener {
public void actionPerformed( ActionEvent e ) {
for( int i = 0; i < 400; i++ ) {
x_++;
y_++;
panel_.repaint();
try {
Thread.sleep( 10 );
} catch( Exception ex ) { }
}
}
}
private JPanel panel_;
private JButton button_;
private int x_ = 0;
private int y_ = 0;
}
Phiên bản mới nhìn qua thì rất “đẹp”, nhưng thực ra nó không chạy đúng như chúng ta mong đợi! Chúng ta không nhìn thấy vòng tròn di chuyển trên panel mà chỉ thấy nó nhảy từ vị trí ban đầu đến vị trí cuối cùng, các trạng thái trung gian đã bị mất. Vậy đâu là nguyên nhân của hành vi kì lạ này?
Thực ra, khi chúng ta đặt phương thức repaint() vào trong một vòng lặp, AWT sẽ trộn các lời gọi repaint() lại với nhau và chỉ có lời gọi repaint() cuối cùng được thực hiện. Bởi vậy chúng ta không thể nhìn thấy các trạng thái trung gian của vòng tròn trên panel.
Vậy làm thế nào giải quyết vấn đề này? Một giải pháp là đưa các lời gọi repaint() sang một thread khác như sau:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Animation3 {
public static void main( String[] args ) {
Animation3 gui = new Animation3();
gui.go();
}
private void go() {
JFrame frame = new JFrame();
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
panel_ = new MyPanel();
frame.getContentPane().add( panel_, BorderLayout.CENTER );
button_ = new JButton( "start animation" );
button_.addActionListener( new StartAnimationAction() );
frame.getContentPane().add( button_, BorderLayout.SOUTH );
frame.setSize( 500, 500 );
frame.setVisible( true );
}
class MyPanel extends JPanel {
public void paintComponent( Graphics g ) {
g.setColor( Color.white );
g.fillRect( 0, 0, this.getWidth(), this.getHeight() );
g.setColor( Color.green );
g.fillOval( x_, y_, 40, 40 );
}
}
class StartAnimationAction implements ActionListener, Runnable {
public void actionPerformed( ActionEvent e ) {
Thread thread = new Thread( this );
thread.start();
}
public void run() {
for( int i = 0; i < 400; i++ ) {
x_++;
y_++;
panel_.repaint();
try {
Thread.sleep( 10 );
} catch( Exception ex ) { }
}
}
}
private JPanel panel_;
private JButton button_;
private int x_ = 0;
private int y_ = 0;
}
Bây giờ thì chương trình của chúng ta đã chạy ngon lành. Để giải thích cặn kẽ về vấn đề gọi repaint() nhiều lần có lẽ cần đến những hiểu biết nhất định về thread trong Java. Bởi vậy “tác giả”, với trình độ còn rất hạn chế, đành tạm thời hài lòng với giải pháp nói trên
Tài liệu tham khảo: The repaint() method and the GUI thread