Category Archives: Java 2D

Java 2D – Các đối tượng hình học – phần 1

Trong phần này chúng ta sẽ vẽ các đối tượng hình học cơ bản.

Điểm

Đối tượng hình học cơ bản nhất là điểm, là một dấu chấm trên màn hình. Trong Java có sẵn lớp java.awt.Point để biểu diễn điểm nhưng không có phương thức nào để vẽ. Chúng ta sẽ vẽ điểm bằng phương thức drawLine(), phương thức này vẽ một đoạn thẳng từ điểm đầu tới điểm cuối, chúng ta dùng nó để vẽ điểm với hai điểm đầu và cuối trùng nhau.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

class Surface extends JPanel implements ActionListener {

    private final int DELAY = 150;
    private Timer timer;

    public Surface() {

        initTimer();
    }

    private void initTimer() {

        timer = new Timer(DELAY, this);
        timer.start();
    }        

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setPaint(Color.blue);

        int w = getWidth();
        int h = getHeight();

        Random r = new Random();

        for (int i = 0; i < 2000; i++) {

            int x = Math.abs(r.nextInt()) % w;
            int y = Math.abs(r.nextInt()) % h;
            g2d.drawLine(x, y, x, y);
        }
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        repaint();
    }
}

public class PointsEx extends JFrame {

    public PointsEx() {

        initUI();
    }

    private void initUI() {

        final Surface surface = new Surface();
        add(surface);      

        setTitle("Points");
        setSize(350, 250);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                PointsEx ex = new PointsEx();
                ex.setVisible(true);
            }
        });
    }
}

Đoạn code trên vẽ 2000 điểm ở vị trí ngẫu nhiên trên màn hình và thay đổi theo thời gian.

private void initTimer() {

    timer = new Timer(DELAY, this);
    timer.start();
}

Lớp javax.swing.Timer được dùng để tạo hiệu ứng chuyển động. Lớp này sẽ giải phóng sự kiện ActionEvents cứ sau một khoảng thời gian nhất định.

g2d.setPaint(Color.blue);

Điểm được vẽ sẽ có màu xanh lam.

int w = getWidth();
int h = getHeight();

Ở 2 dòng code trên, chúng ta lấy kích thước cửa sổ.

Random r = new Random();
int x = Math.abs(r.nextInt()) % w;
int y = Math.abs(r.nextInt()) % h;

Sau đó tính vị trí ngẫu nhiên dựa vào kích thước đó.

g2d.drawLine(x, y, x, y);

Chúng ta dùng phương thức drawLine() để vẽ điểm.

@Override
public void actionPerformed(ActionEvent e) {
    repaint();
}

Trong phương thức xử lý sự kiện, chúng ta gọi phương thức repaint() để chương trình cập nhật lại vị trí mới của mỗi điểm.

Capture

Đoạn thẳng

Đoạn thẳng là một đối tượng hình học cơ bản. Trong lập trình thì đoạn thẳng là một đối tượng kết nối giữa hai điểm. Chúng ta dùng phương thức drawLine() để vẽ đoạn thẳng.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.drawLine(30, 30, 200, 30);
        g2d.drawLine(200, 30, 30, 200);
        g2d.drawLine(30, 200, 200, 200);
        g2d.drawLine(200, 200, 30, 30);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class LinesEx extends JFrame {

    public LinesEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Lines");
        setSize(350, 250);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                
                LinesEx ex = new LinesEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong đoạn code trên chúng ta vẽ bốn đoạn thẳng.

g2d.drawLine(30, 30, 200, 30);

Phương thức drawLine() nhận vào 4 tham số là tọa độ (x, y) của điểm đầu và điểm cuối.

Capture

Định dạng đoạn thẳng

Định dạng đoạn thẳng là cách mà đoạn thẳng đó được vẽ. Lớp BasicStroke là lớp định nghĩa một tập các cách vẽ khác nhau.

import java.awt.BasicStroke;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        float[] dash1 = {2f, 0f, 2f};
        float[] dash2 = {1f, 1f, 1f};
        float[] dash3 = {4f, 0f, 2f};
        float[] dash4 = {4f, 4f, 1f};

        g2d.drawLine(20, 40, 250, 40);

        BasicStroke bs1 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash1, 2f);

        BasicStroke bs2 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash2, 2f);

        BasicStroke bs3 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash3, 2f);

        BasicStroke bs4 = new BasicStroke(1, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_ROUND, 1.0f, dash4, 2f);

        g2d.setStroke(bs1);
        g2d.drawLine(20, 80, 250, 80);

        g2d.setStroke(bs2);
        g2d.drawLine(20, 120, 250, 120);

        g2d.setStroke(bs3);
        g2d.drawLine(20, 160, 250, 160);

        g2d.setStroke(bs4);
        g2d.drawLine(20, 200, 250, 200);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class BasicStrokesEx extends JFrame {

    public BasicStrokesEx() {

        initUI();
    }
    
    private void initUI() {

        add(new Surface());

        setTitle("Basic strokes");
        setSize(280, 270);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                BasicStrokesEx ex = new BasicStrokesEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong đoạn code trên chúng ta vẽ một số kiểu đường thẳng khác nhau.

Graphics2D g2d = (Graphics2D) g.create();

Ở đây chúng ta sẽ thay đổi thuộc tính stroke của đối tượng Graphics, do đó chúng ta nên tạo một bản sao của đối tượng này rồi làm việc với bản sao đó.

float[] dash1 = { 2f, 0f, 2f };
float[] dash2 = { 1f, 1f, 1f };
float[] dash3 = { 4f, 0f, 2f };
float[] dash4 = { 4f, 4f, 1f };

Thuộc tính dash định nghĩa kiểu vẽ giữa các phần hiện và các phần ẩn. Trong đoạn code trên thì dash1 có ý nghĩa là vẽ một đoạn dài 2f pixel, sau đó là khoảng trắng dài 0f pixel rồi lại vẽ đoạn thẳng dài 2f, cứ như thế.

BasicStroke bs1 = new BasicStroke(1, BasicStroke.CAP_BUTT, 
    BasicStroke.JOIN_ROUND, 1.0f, dash1, 2f );

Chúng ta tạo một đối tượng BasicStroke.

g2d.setStroke(bs1);

Phương thức setStroke() quy định kiểu vẽ cho đối tượng Graphics.

g2d.dispose();

Cuối cùng chúng ta gọi phương thức dispose() để hủy đối tượng graphics đã được copy.

Capture

Tham số CAP

Tham số CAP quy định phần viền ở góc đoạn thẳng. Tham số này có 3 giá trị là CAP_BUTT, CAP_ROUND, và CAP_SQUARE.

  • CAP_BUTT: capbutt
  • CAP_ROUND: capround
  • CAP_SQUARE: capsquare
import java.awt.BasicStroke;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        RenderingHints rh = new RenderingHints(
                RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        rh.put(RenderingHints.KEY_RENDERING,
                RenderingHints.VALUE_RENDER_QUALITY);

        g2d.setRenderingHints(rh);

        BasicStroke bs1 = new BasicStroke(8, BasicStroke.CAP_BUTT,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs1);
        g2d.drawLine(20, 30, 250, 30);

        BasicStroke bs2 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs2);
        g2d.drawLine(20, 80, 250, 80);

        BasicStroke bs3 = new BasicStroke(8, BasicStroke.CAP_SQUARE,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs3);
        g2d.drawLine(20, 130, 250, 130);

        BasicStroke bs4 = new BasicStroke();
        g2d.setStroke(bs4);

        g2d.drawLine(20, 20, 20, 140);
        g2d.drawLine(250, 20, 250, 140);
        g2d.drawLine(254, 20, 254, 140);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class CapsEx extends JFrame {

    public CapsEx() {

        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Caps");
        setSize(280, 270);
        setLocationRelativeTo(null); 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                CapsEx ex = new CapsEx();
                ex.setVisible(true);
            }
        });
    }
}

Đoạn code trên ví dụ về 3 tham số CAP khác nhau.

Capture

Tham số JOIN

Tham số JOIN quy định kiểu đường cong nối giữa hai đoạn thẳng cắt nhau. Tham số này có 3 giá trị là JOIN_BEVEL, JOIN_MITER, và JOIN_ROUND.

  • JOIN_BEVEL: joinbevel
  • JOIN_MITER: joinmiter
  • JOIN_ROUND: joinround
import java.awt.BasicStroke;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g.create();

        BasicStroke bs1 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_BEVEL);
        g2d.setStroke(bs1);
        g2d.drawRect(15, 15, 80, 50);

        BasicStroke bs2 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_MITER);
        g2d.setStroke(bs2);
        g2d.drawRect(125, 15, 80, 50);

        BasicStroke bs3 = new BasicStroke(8, BasicStroke.CAP_ROUND,
                BasicStroke.JOIN_ROUND);
        g2d.setStroke(bs3);
        g2d.drawRect(235, 15, 80, 50);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class JoinsEx extends JFrame {

    public JoinsEx() {

        initUI();
    }
    
    private void initUI() {

        add(new Surface());

        setTitle("Joins");
        setSize(340, 110);
        setLocationRelativeTo(null);  
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {

                JoinsEx ex = new JoinsEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong đoạn code trên chúng ta vẽ 3 hình chữ nhật với 3 kiểu JOIN khác nhau.

g2d.drawRect(15, 15, 80, 50);

Phương thức drawRect() vẽ hình chữ nhật với 4 tham số là tọa độ góc trái trên và góc phải dưới của hình chữ nhật.

Capture

Java 2D – Giới thiệu

Chào mừng bạn đến với series lập trình đồ họa 2D với Java. Series này chủ yếu nhắm đến các bạn mới bắt đầu bước vào lập trình đồ họa cơ bản.

Đồ họa Vector

Đồ họa trong máy tính có 2 loại là đồ họa vector và đồ họa raster. Trong đó đồ họa raster được dựng nên từ tập hợp các điểm ảnh (pixel), còn vector được dựng từ các đối tượng đồ họa cơ sở như điểm, đường thẳng, đường cong… các đối tượng đồ họa này được tính toán bằng các phương trình toán học. Cả hai loại đồ họa này đều có ưu và nhược điểm riêng. Đồ họa vector có các ưu điểm sau:

  • Kích thước nhỏ
  • Có thể zoom không giới hạn
  • Di chuyển, phóng to, xoay hình… không làm giảm chất lượng hình

Thư viện đồ họa 2D của Java hỗ trợ cả đồ họa vector và raster.

 

Cơ chế vẽ của Java

Bât cứ đoạn code nào bạn dùng để vẽ ra thứ gì cũng được đặt trong phương thức paintComponent(), phương thức này được override từ lớp JPanel. Phương thức này tự động được gọi bởi phương thức paint() (trong lớp java.awt.Component), ngoài phương thức paintComponent() còn có 2 phương thức khác được gọi cùng nữa là paintBorder() và paintChildren(). Nếu muốn bạn có thể override cả 2 phương thức đó cũng được nhưng thường thì cũng không cần thiết lắm. Trong series này thì chúng ta chỉ dùng đến paintComponent().

Đối tượng Graphics

Tham số của phương thức paintComponent() là một đối tượng lớp Graphics. Đối tượng này cho phép chúng ta vẽ các vật thể lên JPanel. Ngoài ra trong Java còn có lớp Graphics2D kế thừa từ lớp Graphics cung cấp các phương thức cao cấp hơn để hỗ trợ cho người lập trình vẽ một cách dễ dàng hơn.

Đối tượng này được khởi tạo bởi hệ thống và tự động được truyền vào phương thức paintComponent() khi phương thức này được gọi. Ngoài phương thức paintComponent() đối tượng Graphics còn được truyền vào hai phương thức đã nói trên là paintBorder()paintChildren(). Nếu một trong các phương thức này làm thay đổi trạng thái của đối tượng Graphics thì có thể dẫn đến một số vấn đề nên thường thì chúng ta sẽ tạo một đối tượng khác từ đối tượng Graphics gốc bằng phương thức create() và hủy đối tượng mới này với phương thức dispose(). Nếu chúng ta chỉ thay đổi các thuộc tính bình thường như màu, font chữ… thì không cần tạo mới, nhưng nếu chúng ta thay đổi các thuộc tính cao cấp bằng các phép biến đổi hình như cắt hình, xoay hình, zoom to nhỏ… thì chúng ta phải tạo một đối tượng Graphics khác.

Code ví dụ mẫu

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;
        g2d.drawString("Java 2D", 50, 50);
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }
}

public class BasicEx extends JFrame {

    public BasicEx() {

        initUI();
    }

    private void initUI() {

        add(new Surface());

        setTitle("Simple Java 2D example");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                BasicEx ex = new BasicEx();
                ex.setVisible(true);
            }
        });
    }
}

Đoạn code trên vẽ một đoạn text lên JPanel.

class Surface extends JPanel {
...
}

Chúng ta  tạo lớp Surface kế thừa từ lớp JPanel. Lớp này là lớp chính dùng để vẽ.

Graphics2D g2d = (Graphics2D) g;

Chúng ta tạo đối tượng Graphics2D để tận dụng một số tính năng cao cấp mà lớp Graphics không có.

g2d.drawString("Java 2D", 50, 50);

Phương thức drawString() “vẽ” một đoạn text tại vị trí (50, 50).

@Override
public void paintComponent(Graphics g) {

    super.paintComponent(g);
    doDrawing(g);
}

Tất cả mọi thứ đều được vẽ bên trong phương thức paintComponent(). Trong đó super.paintComponent() gọi lại phương thức paintComponent() của lớp cha là lớp JPanel, mà trong phương thức này ở lớp cha chỉ đơn giản là vẽ một cửa sổ trống không, tức là khi gọi phương thức này chúng ta làm công việc là xóa trắng toàn bộ cửa sổ, việc này rất có ích khi tạo hiệu ứng chuyển động. Ngoài ra ở đây mình không vẽ trực tiếp trong phương thức paintComponent() mà tạo phương thức doDrawing() rồi vẽ trong đó, mình làm thế là vì… thích 🙂

private void initUI() {
...
}

Phương thức initUI() làm công việc khởi tạo giao diện cho ứng dụng.

add(new Surface());

Chúng ta thêm Surface vào JFrame.

EventQueue.invokeLater(new Runnable() {

    @Override
    public void run() {
        BasicEx ex = new BasicEx();
        ex.setVisible(true);
    }
});

Phương thức invokeLater() sẽ chạy ứng dụng của chúng ta trong một luồng do EventQueue quản lý. Thực ra bạn cũng không cần đến EventQueue làm gì, chỉ cần tạo một đối tượng BasicEx rồi setVisible(true) là có thể chạy được nhưng trên tài liệu của Oracle thì lại khuyên chúng ta đặt bên trong EventQueue vì lý do là làm như vậy sẽ đảm bảo an toàn cho ứng dụng (mình cũng không hiểu tại sao) nên thôi thì mình cứ làm vậy 🙂

Capture