Vietnamese Developers' Blog

Gọi repaint() nhiều lần trong JFrame và JApplet

Posted in Java by kiennguyen on the November 22nd, 2008

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 :D

Tài liệu tham khảo: The repaint() method and the GUI thread

Tagged with: ,

2 Responses to 'Gọi repaint() nhiều lần trong JFrame và JApplet'

Subscribe to comments with RSS or TrackBack to 'Gọi repaint() nhiều lần trong JFrame và JApplet'.

  1. Katatunix said, on March 14th, 2010 at 8:21 pm

    “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.”

    Tôi cho rằng điều này ko đúng! Bản chất vấn đề ở đây là: bình thường chương trình của ta chỉ có duy nhất 1 thread (AWT thread) được chạy mà thôi. Khi sự kiện button click được gọi, nó chạy hàm được đăng ký ngay trên thread duy nhất này. Khi đó, nó (AWT Thread) đang mải mê chạy vòng lặp trong hàm, thì làm sao có thể vẽ lại giao diện?

    Hàm repaint() chỉ đơn giản gửi vào 1 queue gì đó của AWT, nói rằng tôi cần repaint(). Sau khi thoát ra hàm sự kiện, AWT mới ngó lại trong queue và vẫn gọi đủ số lần repaint() đã yêu cầu (chứ ko trộn làm 1). Tuy nhiên, hàm repaint() vẽ ra hình ảnh phụ thuộc x, y; mà lúc này x, y đã là 1 giá trị cố định cuối cùng. Nên ta thấy hình ảnh cuối cùng chứ ko thấy hoạt cảnh.

    Vì lý do này, trong các game Java, tôi luôn luôn thấy game loop phải đặt trong 1 thread khác, như bạn đã làm.

    Thân ái.

  2. kiennguyen said, on March 20th, 2010 at 12:10 am

    @Katatunix: Cảm ơn bạn đã góp ý. Không biết bạn có tài liệu gì nói về vấn đề này không. Mình có tìm hiểu nhưng thấy các tài liệu thường giải thích một cách khá mơ hồ.

Leave a Reply