Author Archives: Phở Code

Python – Lập trình mạng với Socket

Python cung cấp module Socket hỗ trợ thực thi các giao thức mạng, dùng tạo các ứng dụng server hoặc client.

Chúng ta sẽ viết một ứng dụng Echo Server đơn giản.

Server

import socket

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
print ("Starting server on port 10000")
server.bind((socket.gethostname(), 10000))
server.listen(1)

while True:
    conn, client = server.accept()
    try:
       print ("Connection from", client)
 
    while True:
        data = conn.recv(1024)
        print ("Receive from client:", data)
        if data:
            print ("Response to client")
            conn.sendall(data)
        else:
            print ("No data received")
            break
    finally:
        conn.close()

Server sẽ lắng nghe nghe các kết nối từ client, nhận tin nhắn từ client nếu có và gửi trả lời về lại client.

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

Đầu tiên chúng ta tạo một đối tượng socket, tham số AF_INET cho biết chúng ta sử dụng IP v4, SOCK_TREAM là dùng giao thức TCP. Ngoài ra còn một số giá trị khác như AF_INET6 là dùng IP v6, AF_UNIX là chỉ kết nối các ứng dụng trong một máy (không dùng mạng), SOCK_DGRAM là dùng giao thức UDP.

print("Starting server on port 10000")
server.bind((socket.gethostname(), 10000))

Phương thức bind() chỉ định socket sẽ lắng nghe với địa chỉ IP của máy lấy từ phương thức gethostname() trên cổng 10000.

server.listen(1)

while True:
    conn, client = server.accept()

Phương thức listen() cho python biết socket này là một server, tham số của phương thức này là số lượng các kết nối có thể có trong hàng đợi, ít nhất là 0 và cao nhất là do hệ điều hành chỉ định (thường là 5). Phương thức accept() sẽ đưa server vào trạng thái chờ đợi cho đến khi có kết nối thì sẽ trả về một tuple gồm có một socket khác dùng để truyền dữ liệu qua lại với client và một tuple nữa bao gồm địa chỉ ip và port của ứng dụng client.

while True:
    data = conn.recv(1024)
    print ("Receive from client:", data)
    if data:
        print ("Response to client")
        conn.sendall(data)
    else:
        print ("No data received")
        break

Phương thức recv() sẽ đọc các byte dữ liệu có trong socket conn, tham số 1024 tức là mỗi lần chỉ đọc tối ta 1024 byte dữ liệu, nên chúng ta đặt trong vòng lặp while để có thể đọc hết những gì client gửi sang, nếu có dữ liệu gửi sang thì chúng ta gửi trả lời về client thông qua phương thức sendall()ở đây chúng ta chỉ đơn giản là gửi lại những gì client đã gửi lên server thôi.

finally:
    conn.close()

Khi đã đọc hết dữ liệu từ client, chúng ta break vòng lặp và ngắt kết nối bằng phương thức close().

Client

import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect((socket.gethostname(), 10000))
try:
    message = "Hello from client"
    client.sendall(message.encode('ascii'))

    amount_received = 0
    amount_expected = len(message)

    while amount_received < amount_expected:
        data = client.recv(1024)
        amount_received += len(data)
        print ("Response from server:", data)
finally:
    print ("Closing socket")
    client.close()

Chúng ta cũng tạo một đối tượng socket kết nối và gửi tin nhắn lên server.

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 10000))

Đối tượng socket ở client cũng sử dụng IP v4 và giao thức TCP. Phương thức connect() sẽ chỉ định cho python biết đây là một ứng dụng client và tạo kết nối đến server.

message = "Hello from client"
client.sendall(message.encode('ascii'))

Sau khi đã kết nối đến server, chúng ta gửi tin nhắn lên bằng phương thức sendall(), dữ liệu truyền đi chỉ là các bit mà trong Python 3 thì mặc định chuỗi luôn luôn là Unicode nên chúng ta phải mã hóa chuỗi này sang ASCII bằng phương thức encode() trước (để giải mã thì chúng ta dùng phương thức decode('ascii')).

finally:
    print ("Closing socket")
    client.close()

Cũng tương tự như server, khi đã kết thúc công việc chúng ta phải ngắt kết nối socket nếu không sẽ lãng phí tài nguyên. Mặc định thì khi kết thúc một chương trình python thì bộ thu gom rác của python đã tự động ngắt kết nối socket rồi nhưng chúng ta nên tự ngắt thì tốt hơn.

C:\User\Python>python server.py
Starting server on port 10000
Connecting from ('192.168.0.103', 56419)
Receive from client: b'Hello from client'
Response to client
Receive from client: b''
No data received
C:\User\Python\python client.py
Response from server: b'Hello from client'
Closing socket

Java Swing – Xử lý sự kiện

Event (sự kiện) là một phần quan trọng của bất kỳ ứng dụng GUI nào. Tất cả các ứng dụng dạng GUI xử lý event trong suốt thời gian mà nó chạy. Event phần lớn được tạo ra bởi người dùng, nhưng cũng có lúc được tạo ra bởi chính ứng dụng. Có ba thành phần tham gia vào hệ thống event:

  • Event nguồn
  • Đối tượng event
  • Đối tượng lắng nghe event

Event nguồn là đối tượng tạo ra sự thay đổi. Cứ có gì đó trong ứng dụng tạo ra sự thay đổi nào đó thì nó chính là event nguồn. Đối tượng event là chính bản thân cái event đó đã được mã hóa. Đối tượng lắng nghe event làm công việc xử lý event đó.

Đối tượng Event

Mỗi khi có thứ gì đó xảy ra thì một đối tượng event sẽ được tạo. Chẳng hạn như khi bạn click vào một button hay chọn một item trong một danh sách. Đối tượng event lưu trữ thông tin về loại sự kiện đã xảy ra. Chúng ta sẽ xem xét thông tin đó trong ví dụ dưới đây.

import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.DefaultListModel;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JList;


public class EventObjectEx extends JFrame {

    private JList list;
    private DefaultListModel model;
    

    public EventObjectEx() {
        
        initUI();
    }
    
    private void initUI() {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);   

        model = new DefaultListModel();
        list = new JList(model);
        list.setMinimumSize(new Dimension(250, 150));
        list.setBorder(BorderFactory.createEtchedBorder());

        JButton okButton = new JButton("OK");
        okButton.addActionListener(new ClickAction());

        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(okButton)
                .addGap(20)
                .addComponent(list)
        );

        gl.setVerticalGroup(gl.createParallelGroup()
                .addComponent(okButton)
                .addComponent(list)
        );
        
        pack();

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }
    
    private class ClickAction extends AbstractAction {
        
        @Override
        public void actionPerformed(ActionEvent e) {
            
            Locale locale = Locale.getDefault();
            Date date = new Date(e.getWhen());
            String tm = DateFormat.getTimeInstance(DateFormat.SHORT,
                    locale).format(date);

            if (!model.isEmpty()) {
                model.clear();
            }

            if (e.getID() == ActionEvent.ACTION_PERFORMED) {
                model.addElement("Event Id: ACTION_PERFORMED");
            }

            model.addElement("Time: " + tm);

            String source = e.getSource().getClass().getName();
            model.addElement("Source: " + source);           
        }
    }        

    public static void main(String[] args) {

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

Trong ví dụ này chúng ta tạo một button và một list, list sẽ hiển thị các thông tin về đối tượng event được gây ra bởi button.

okButton.addActionListener(new ClickAction());

Lớp ClickAction lắng nghe sự kiện từ button OK.

private class ClickAction extends AbstractAction {
    
    @Override
    public void actionPerformed(ActionEvent e) {
        ...
    }
}        

Phương thức actionPerformed() được gọi mỗi lần sự kiện xảy ra, phương thức này nhận tham số là một đối tượng ActionEvent.

Locale locale = Locale.getDefault();
Date date = new Date(e.getWhen());
String s = DateFormat.getTimeInstance(DateFormat.SHORT,
        locale).format(date);

Chúng ta có thể lấy thời điểm event xảy ra từ phương thức getWhen(), thời gian trong phương thức này tính theo mili giây nên chúng ta phải định dạng lại cho phù hợp.

String source = e.getSource().getClass().getName();
model.addElement("Source: " + source);

Chúng ta có thể lấy được thông tin về đối tượng gây ra event từ phương thức getSource() và một số thông tin bên trong lớp đó.

Capturaae

Tạo đối tượng lắng nghe sự kiện

Có 3 cách để tạo một đối tượng lắng nghe sự kiện trong Java Swing.

  • Tạo lớp ẩn nội (Anonymous Inner class)
  • Tạo lớp nội (Inner class)
  • Tạo lớp kế thừa (Derived class)

Tạo lớp ẩn nội

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;


public class AnonymousInnerClassEx extends JFrame {


    public AnonymousInnerClassEx() {

        initUI();
    }

    private void initUI() {
        
        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);                

        JButton closeButton = new JButton("Close");
        closeButton.setBounds(40, 50, 80, 25);

        closeButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                System.exit(0);
            }
        });
        
        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(220)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(180)
        );
        
        pack();        

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Trong ví dụ này chúng ta tạo một đối tượng lắng nghe sự kiện từ một button.

JButton closeButton = new JButton("Close");

Khi click button thì thoát chương trình.

closeButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
        System.exit(0);
    }
});

Để gắn một đối tượng lắng nghe vào một component thì chúng ta dùng phương thức addActionListener(). Ở đây chúng ta dùng lớp “ẩn nội”, nếu bạn chưa biết thì tức là tạo một đối tượng và code các phương thức trừu tượng ngay bên trong một phương thức khác.

Tạo lớp nội

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;

public class InnerClassExample extends JFrame {

    public InnerClassExample() {

        initUI();
    }

    private void initUI() {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);    

        JButton closeButton = new JButton("Close");

        ButtonCloseListener listener = new ButtonCloseListener();
        closeButton.addActionListener(listener);

        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(220)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(180)
        );
        
        pack();        

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private class ButtonCloseListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Ví dụ này cũng giống ví dụ trên, chỉ khác là chúng ta dùng lớp “nội”: định nghĩa một lớp bên trong một lớp khác, tuy nhiên các lớp này phải được implements từ lớp ActionListener.

private class ButtonCloseListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        System.exit(0);
    }
}

Tạo lớp kế thừa

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;

public class DerivedClassExample extends JFrame {

    public DerivedClassExample() {

        initUI();
    }

    private void initUI() {
        
        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);            
        
        MyButton closeButton = new MyButton("Close");

        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(220)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addGap(180)
        );
        
        pack();   
        
        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private class MyButton extends JButton 
        implements ActionListener {

        public MyButton(String text) {
            super.setText(text);
            addActionListener(this);
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            System.exit(0);
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Trong ví dụ này chúng ta cũng tạo một button có chức năng thoát chương trình, nhưng ở đây chúng ta tạo một lớp kế thừa từ lớp JButton là implements sẵn giao diện ActionListenerTức là bản thân lớp này đã tự động lắng nghe các sự kiện click rồi, không cần phải dùng đến phương thức addActionListener() như với lớp ẩn nội nữa.

private class MyButton extends JButton 
    implements ActionListener {

    public MyButton(String text) {
        super.setText(text);
        addActionListener(this);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        System.exit(0);
    }
}

Gắn một listener vào nhiều component

Một listener có thể lắng nghe từ nhiều component khác nhau.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JLabel;
import javax.swing.JPanel;

public class MultipleSources extends JFrame {

    private JLabel statusbar;

    public MultipleSources() {

        initUI();
    }

    private void initUI() {
        
        JPanel panel = new JPanel();
        GroupLayout gl = new GroupLayout(panel);
        panel.setLayout(gl);
        
        statusbar = new JLabel("Ready");

        statusbar.setBorder(BorderFactory.createEtchedBorder());
        
        ButtonListener butlist = new ButtonListener();

        JButton closeButton = new JButton("Close");
        closeButton.addActionListener(butlist);

        JButton openButton = new JButton("Open");
        openButton.addActionListener(butlist);

        JButton findButton = new JButton("Find");
        findButton.addActionListener(butlist);

        JButton saveButton = new JButton("Save");
        saveButton.addActionListener(butlist);

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);
        
        gl.setHorizontalGroup(gl.createParallelGroup()
                .addComponent(closeButton)
                .addComponent(openButton)
                .addComponent(findButton)
                .addComponent(saveButton)
                .addGap(250)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(closeButton)
                .addComponent(openButton)
                .addComponent(findButton)
                .addComponent(saveButton)
                .addGap(20)
        );
        
        gl.linkSize(closeButton, openButton, 
                findButton, saveButton);
        
        add(panel, BorderLayout.CENTER);
        add(statusbar, BorderLayout.SOUTH);
        
        pack();
                
        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private class ButtonListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {

            JButton o = (JButton) e.getSource();
            String label = o.getText();
            statusbar.setText(" " + label + " button clicked");
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                MultipleSources ms = new MultipleSources();
                ms.setVisible(true);
            }
        });
    }
}

Chúng ta tạo 4 button và một statusbar. Statusbar sẽ hiển thị thông tin tùy vào button nào được click.

JButton closeButton = new JButton("Close");
closeButton.addActionListener(butlist);

JButton openButton = new JButton("Open");
openButton.addActionListener(butlist);

...

Ở đây chúng ta tạo lớp nội ButtonListener và gắn vào từng button.

private class ButtonListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {

        JButton o = (JButton) e.getSource();
        String label = o.getText();
        statusbar.setText(" " + label + " button clicked");
    }
}

Đoạn text trên statusbar được hiển thị tùy theo từng button.

Capture

Gắn nhiều listener vào một component

Nếu có thể dùng một listener cho nhiều component thì cũng có thể gắn nhiều listener vào một component.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Calendar;
import javax.swing.BorderFactory;
import javax.swing.GroupLayout;
import static javax.swing.GroupLayout.Alignment.CENTER;
import static javax.swing.GroupLayout.DEFAULT_SIZE;
import static javax.swing.GroupLayout.PREFERRED_SIZE;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;


public class MultipleListeners extends JFrame {

    private JLabel statusbar;
    private JSpinner spinner;
    private int count = 0;

    public MultipleListeners() {

        initUI();
    }

    private void initUI() {
        
        JPanel panel = new JPanel();
        GroupLayout gl = new GroupLayout(panel);
        panel.setLayout(gl);   
        add(panel, BorderLayout.CENTER);

        statusbar = new JLabel("0");
        statusbar.setBorder(BorderFactory.createEtchedBorder());
        add(statusbar, BorderLayout.SOUTH);

        JButton addButton = new JButton("+");
        addButton.addActionListener(new ButtonListener1());
        addButton.addActionListener(new ButtonListener2());

        Calendar calendar = Calendar.getInstance();
        int currentYear = calendar.get(Calendar.YEAR);

        SpinnerModel yearModel = new SpinnerNumberModel(currentYear,
                currentYear - 100,
                currentYear + 100,
                1);

        spinner = new JSpinner(yearModel);
        spinner.setEditor(new JSpinner.NumberEditor(spinner, "#"));

        
        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(addButton)
                .addGap(20)
                .addComponent(spinner, DEFAULT_SIZE,
                        DEFAULT_SIZE, PREFERRED_SIZE)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup(CENTER)
                        .addComponent(addButton)
                        .addComponent(spinner, DEFAULT_SIZE,
                                DEFAULT_SIZE, PREFERRED_SIZE))
        );
        
        pack();         
        
        setTitle("Event Example");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private class ButtonListener1 implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            Integer val = (Integer) spinner.getValue();
            spinner.setValue(++val);
        }
    }

    private class ButtonListener2 implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            statusbar.setText(Integer.toString(++count));
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                MultipleListeners ml = new MultipleListeners();
                ml.setVisible(true);
            }
        });
    }
}

Trong ví dụ này chúng ta tạo một button, một spinner và một statusbar. Chúng ta định nghĩa 2 listener và gắn cả hai listener này vào button, một listener sẽ thay đổi giá trị trên spinner, một listener sẽ thay đổi đoạn text trong statusbar.

SpinnerModel yearModel = new SpinnerNumberModel(currentYear,
        currentYear - 100,
        currentYear + 100,
        1);

spinner = new JSpinner(yearModel);

Chúng ta khởi tạo một đối tượng SpinnerModel, bạn có thể hiểu đối tượng này làm công việc khởi tạo giá trị cho JSpinner, tham số đầu tiên là giá trị khởi tạo, tham số thứ 2 và thứ 3 là giá trị min và max trong listener, tham số thứ 4 là giá trị tăng thêm lên bao nhiêu sau mỗi lần click vào spinner.

spinner.setEditor(new JSpinner.NumberEditor(spinner, "#"));

Dòng code trên có tác dụng loại bỏ dấu phẩy trong định dạng số (2,016 → 2016).

private class ButtonListener1 implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        Integer val = (Integer) spinner.getValue();
        spinner.setValue(++val);
    }
}

Listener thứ nhất thay đổi giá trị của spinner.

private class ButtonListener2 implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
        statusbar.setText(Integer.toString(++count));
    }
}

Listener thứ hai làm công việc thay đổi đoạn text trong statusbar.

Capture

Gỡ bỏ listener ra khỏi component

Để làm việc này thì chỉ đơn giản là dùng phương thức removeActionListener().

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class RemoveListenerEx extends JFrame {

    private JLabel lbl;
    private JButton addButton;
    private JCheckBox activeBox;
    private ButtonListener buttonlistener;
    private int count = 0;

    public RemoveListenerEx() {
        
        initUI();
    }
    
    private void initUI() {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);  

        addButton = new JButton("+");
        buttonlistener = new ButtonListener();

        activeBox = new JCheckBox("Active listener");
        activeBox.addItemListener(new ItemListener() {

            @Override
            public void itemStateChanged(ItemEvent event) {
                
                if (activeBox.isSelected()) {
                    addButton.addActionListener(buttonlistener);
                } else {
                    addButton.removeActionListener(buttonlistener);
                }
            }
        });

        lbl = new JLabel("0");
        
        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup()
                        .addComponent(addButton)
                        .addComponent(lbl))
                .addGap(30)
                .addComponent(activeBox)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addGroup(gl.createParallelGroup()
                        .addComponent(addButton)
                        .addComponent(activeBox))
                .addGap(30)
                .addComponent(lbl)
        );
        
        pack(); 

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private class ButtonListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            
            lbl.setText(Integer.toString(++count));
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Trong ví dụ này chúng ta tạo một button, một checkbox và một label. Button được gắn một listener và chỉ khi checkbox được check thì listener này mới lắng nghe sự kiện từ button.

buttonlistener = new ButtonListener();

Lớp listener được tạo ra phải là lớp không ẩn thì mới có thể gỡ ra khỏi component được,

if (activeBox.isSelected()) {
    addButton.addActionListener(buttonlistener);
} else {
    addButton.removeActionListener(buttonlistener);
}

Chúng ta kiểm tra nếu checkbox được check thì gắn listener vào còn không nếu thì gỡ ra.

Capture

Xử lý sự kiện di chuyển

Trong ví dụ này chúng ta tìm hiểu về cách bắt sự kiện có liên quan đến sự hiện diện của component.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import javax.swing.GroupLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;


public class MovingWindowEx extends JFrame
        implements ComponentListener {

    private JLabel labelx;
    private JLabel labely;

    public MovingWindowEx() {

        initUI();
    }

    private void initUI() {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);  
        
        addComponentListener(this);

        labelx = new JLabel("x: ");
        labelx.setFont(new Font("Serif", Font.BOLD, 14));
        labelx.setBounds(20, 20, 60, 25);

        labely = new JLabel("y: ");
        labely.setFont(new Font("Serif", Font.BOLD, 14));
        labely.setBounds(20, 45, 60, 25);

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);
        
        gl.setHorizontalGroup(gl.createParallelGroup()
                .addComponent(labelx)
                .addComponent(labely)
                .addGap(250)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(labelx)
                .addComponent(labely)
                .addGap(130)
        );
        
        pack();   

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    @Override
    public void componentResized(ComponentEvent e) {
    }

    @Override
    public void componentMoved(ComponentEvent e) {
        
        int x = e.getComponent().getX();
        int y = e.getComponent().getY();
        
        labelx.setText("x: " + x);
        labely.setText("y: " + y);
    }

    @Override
    public void componentShown(ComponentEvent e) {
    }

    @Override
    public void componentHidden(ComponentEvent e) {
    }


    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Chúng ta sẽ gắn một listener vào cửa sổ chính để lấy tọa độ của cửa sổ trên màn hình.

public class MovingWindowExample extends JFrame
        implements ComponentListener {

Giao diện ComponentListener là giao diện dùng để lắng nghe các sự kiện liên quan đến sự xuất hiện của component.

@Override
public void componentResized(ComponentEvent e) {
}

@Override
public void componentMoved(ComponentEvent e) {
    
    int x = e.getComponent().getX();
    int y = e.getComponent().getY();
    
    labelx.setText("x: " + x);
    labely.setText("y: " + y);
}

@Override
public void componentShown(ComponentEvent e) {
}

@Override
public void componentHidden(ComponentEvent e) {
}

Theo luật của Java thì chúng ta phải override toàn bộ phương thức ảo nhưng ở đây chúng ta chỉ quan tâm tới phương thức componentMoved() thôi.

int x = e.getComponent().getX();
int y = e.getComponent().getY();

Để lấy tọa độ của component thì chúng ta dùng phương thức getX()getY().

Capture

Sử dụng lớp Adapter thay thế cho Listener

Trong ví dụ trên chúng ta implement giao diện ComponentListener và phải override toàn bộ các phương thức ảo trong giao diện này, như thế rất khó chịu, chính vì thế và Java đã tạo ra các lớp adapter. Adapter đơn giản là các lớp đã implement sẵn các lớp listener tương ứng và override toàn bộ phương thức ảo trước rồi. Vì thế nên khi dùng adapter chúng ta chỉ cần dùng các phương thức mà chúng ta muốn thôi.

Ví dụ:

  • Giao diện ComponentListener bắt buộc bạn phải override các phương thức ảo componentResized(), componentMoved(), componentShown(), và componentHidden() mặc dù bạn không muốn.
  • Lớp ComponentAdapter đã implement sẵn giao diện ComponentListener và override toàn bộ các phương thức ảo trên rồi, bạn chỉ cần override lại phương thức mà bạn muốn.

Ví dụ dưới đây giống như ví dụ ở trên, chỉ khác là chúng ta dùng lớp ComponentAdapter.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.GroupLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;

public class AdapterExample extends JFrame {

    private JLabel labelx;
    private JLabel labely;

    public AdapterExample() {

        initUI();
    }

    private void initUI() {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);  
        
        addComponentListener(new MoveAdapter());

        labelx = new JLabel("x: ");
        labelx.setFont(new Font("Serif", Font.BOLD, 14));
        
        labely = new JLabel("y: ");
        labely.setFont(new Font("Serif", Font.BOLD, 14));

        gl.setAutoCreateContainerGaps(true);
        gl.setAutoCreateGaps(true);
        
        gl.setHorizontalGroup(gl.createParallelGroup()
                .addComponent(labelx)
                .addComponent(labely)
                .addGap(250)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(labelx)
                .addComponent(labely)
                .addGap(130)
        );
        
        pack();   

        setTitle("Event Example");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);        
    }

    private class MoveAdapter extends ComponentAdapter {

        @Override
        public void componentMoved(ComponentEvent e) {
            
            int x = e.getComponent().getX();
            int y = e.getComponent().getY();
            
            labelx.setText("x: " + x);
            labely.setText("y: " + y);
        }
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Java Swing – Quản lý layout

Các component trong Java Swing được chia làm hai loại là container và children, trong đó container là loại component dùng để chứa các component khác, ví dụ như JFrame, JPanel… còn children là các component cụ thể như JLabel, JButton… khi chúng ta đặt các component con lên các container thì việc đặt vị trí và sắp xếp chúng là một công việc rất mất thời gian, từ đó khái niệm layout ra đời.

Layout có tác dụng tự động bố trí các component trên container, bạn tưởng tượng layout giống như một căn nhà có nhiều phòng và các phòng này có thể tự thay đổi kích thước cũng như vị trí theo một quy luật nào đó và theo đó mà cách vật dụng trong phòng cũng tự thay đổi theo. Đối với người mới bắt đầu lập trình GUI thì layout là một khái niệm khá khó hiểu vì nó không có hình ảnh trực quan như text hay button.

Cũng có những trường hợp chúng ta không cần dùng đến layout. Tuy nhiên nên dùng thì vẫn tốt hơn.

FlowLayout

Đây là loại layout đơn giản nhất, layout này thường được dùng kết hợp với các layout khác. Layout này quy định kích thước của các component con là vừa đủ với nội dung hiển thị của component.

Mặc định các component sẽ được sắp xếp trên một hàng từ trái sang phải, nếu không vừa đủ một hàng thì chúng sẽ xuống hàng. Các component sẽ cách nhau 5 pixel và cách viền cửa sổ 5 pixel. Tuy nhiên chúng ta có thể thay đổi các thuộc tính này.

FlowLayout()
FlowLayout(int align)
FlowLayout(int align, int hgap, int vgap) 

Ở trên là 3 kiểu khởi tạo một FlowLayout, align quy định sắp xếp từ trái sang phải hay ngược lại, hgap là khoảng cách giữa các component theo chiều ngang, vgap là khảng cách giữa các component theo chiều dọc.

import java.awt.Dimension;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTree;
import javax.swing.SwingUtilities;


public class FlowLayoutExample extends JFrame {


    public FlowLayoutExample() {

        initUI();
    }

    public final void initUI() {

        JPanel panel = new JPanel();

        JTextArea area = new JTextArea("text area");
        area.setPreferredSize(new Dimension(100, 100));

        JButton button = new JButton("button");
        panel.add(button);

        JTree tree = new JTree();
        panel.add(tree);

        panel.add(area);

        add(panel);

        pack();

        setTitle("Layout Example");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {

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

Trong ví dụ trên chúng ta tạo một đối tượng JButton, JTreeJTextArea. Lớp JTextArea tạo một ô gõ văn bản. Lớp JTree hiển thị dữ liệu theo dạng cây, khi chúng ta tạo một đối tượng JTree rỗng thì mặc nhiên trong này có sẵn một số giá trị rồi.

JPanel panel = new JPanel();

Mặc định khi tạo một JPanel thì layout được dùng sẽ là flowLayout luôn nên ở đây chúng ta không cần phải thiết lập lại.

JTextArea area = new JTextArea("text area");
area.setPreferredSize(new Dimension(100, 100));

Mặc định flowLayout sẽ thiết lập kích thước cho các component con vừa đủ để bao bọc nội dung bên trong component đó, nên nếu muốn có kích thước riêng thì chúng ta phải tự thiết lập lại bằng phương thức setPreferredSize().

panel.add(area);

Và để thêm một component con vào một container thì chỉ đơn giản là dùng phương thức add() là được.

Capture

GridLayout

Layout này sắp xếp các component theo dạng bảng. Các component sẽ có kích thước bằng nhau.

import java.awt.GridLayout;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class GridLayoutExample extends JFrame {

    public GridLayoutExample() {

        initUI();
    }

    public final void initUI() {

        JPanel panel = new JPanel();

        panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        panel.setLayout(new GridLayout(5, 4, 5, 5));

        String[] buttons = {
            "Cls", "Bck", "", "Close", 
            "7", "8", "9", "/", 
            "4", "5", "6", "*", 
            "1", "2", "3", "-", 
            "0", ".", "=", "+"
        };

        for (int i = 0; i < buttons.length; i++) {

            if (i == 2)
                panel.add(new JLabel(buttons[i]));
            else
                panel.add(new JButton(buttons[i]));
        }

        add(panel);

        setTitle("Layout Example");
        setSize(350, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {

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

Chúng ta sẽ tạo giao diện máy tính bỏ túi.

panel.setLayout(new GridLayout(5, 4, 5, 5));

Trong phương thức khởi tạo GridLayout, 2 tham số đầu là số hàng và số cột, hai tham số cuối là khoảng cách giữa các component theo chiều ngang và chiều dọc.

Capture

BorderLayout

BorderLayout sắp xếp các component theo vùng, ở đây có 5 vùng là Đông (EAST), Tây (WEST), Nam (SOUTH), Bắc (NORTH), và Chính giữa (CENTER). Nhưng mỗi vùng chỉ được chứa một component, do đó khi muốn đưa nhiều component vào một vùng thì chúng ta đặt một layout khác vào vùng đó rồi đặt các component vào layout mới đó. Khác với các layout khác, kích thước của các component trong mỗi vùng là do chúng ta thiết lập chứ không phải do layout tự co dãn, ngoại trừ vùng CENTER sẽ có kích thước thay đổi tùy thuộc vào 4 vùng còn lại.

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Insets;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JTextArea;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;


public class BorderLayoutExample extends JFrame {


    public BorderLayoutExample() {

        initUI();
    }

    public final void initUI() {

        // Create menu bar
        JMenuBar menubar = new JMenuBar();
        JMenu file = new JMenu("File");
 
        menubar.add(file);
        setJMenuBar(menubar);
 
        // Create horizontal toolbar (NORTH)
        JToolBar horizontalToolbar = new JToolBar();
        horizontalToolbar.setFloatable(false);
 
        ImageIcon exitIcon = new ImageIcon("F:/exit.png");
        JButton exitButton = new JButton(exitIcon);
        exitButton.setBorder(new EmptyBorder(0, 0, 0, 0));
        horizontalToolbar.add(exitButton);
 
        add(horizontalToolbar, BorderLayout.NORTH);
 
        // Create vertical toolbar (WEST)
        JToolBar verticalToolbar = new JToolBar(JToolBar.VERTICAL);
        verticalToolbar.setFloatable(false);
        verticalToolbar.setMargin(new Insets(10, 5, 5, 5));
 
        ImageIcon driveIcon = new ImageIcon("F:/drive.png");
        ImageIcon computerIcon = new ImageIcon("F:/computer.png");
        ImageIcon printerIcon = new ImageIcon("F:/printer.png");
 
        JButton driveButton = new JButton(driveIcon);
        driveButton.setBorder(new EmptyBorder(3, 0, 3, 0));
 
        JButton computerButton = new JButton(computerIcon);
        computerButton.setBorder(new EmptyBorder(3, 0, 3, 0));
 
        JButton printerButton = new JButton(printerIcon);
        printerButton.setBorder(new EmptyBorder(3, 0, 3, 0));
 
        verticalToolbar.add(driveButton);
        verticalToolbar.add(computerButton);
        verticalToolbar.add(printerButton);
 
        add(verticalToolbar, BorderLayout.WEST);
 
        // Craete Text area (CENTER)
        add(new JTextArea(), BorderLayout.CENTER);
 
        // Create Status bar (SOUTH)
        JLabel statusbar = new JLabel("Statusbar");
        statusbar.setPreferredSize(new Dimension(-1, 22));
        statusbar.setBorder(LineBorder.createGrayLineBorder());
        add(statusbar, BorderLayout.SOUTH);
  
        setSize(350, 300);
        setTitle("Layout Example");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                BorderLayoutExample ex = new BorderLayoutExample();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ này chúng ta sẽ tạo một giao diện giống như các ứng dụng thường thấy bao gồm 1 thanh menu, 1 toolbar nằm ngang (NORTH), 1 toolbar nằm dọc (WEST), 1 text area (CENTER), 1 status bar (SOUTH).

Mặc định JFrame dùng sẵn BorderLayout nên chúng ta không cần phải thiết lập lại.

Capture

BoxLayout

BoxLayout cho phép chúng ta tạo các giao diện phức tạp. Layout này sắp xếp các component theo chiều ngang hoặc chiều dọc, tức là giống với FlowLayout, chỉ khác ở chỗ là BoxLayout cho phép các component có thể tự thay đổi kích thước dựa vào kích thước cửa sổ chính. Ngoài ra bạn còn có thể lồng một BoxLayout vào một BoxLayout khác.

BoxLayout thường đi cùng với một lớp khác nữa là lớp Box, lớp này có tác dụng tạo các khoảng trống để “đẩy” các component về các vị trí khác nhau. Các loại khoảng trống mà Box có thể tạo ra là Glue, StrutRigidArea. Mình sẽ giải thích ngay bên dưới.

Trong ví dụ này chúng ta sẽ tạo 2 button nằm ở góc dưới bên phải cửa sổ.

import java.awt.Dimension;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class TwoButtonsExample extends JFrame {

    public TwoButtonsExample() {

        initUI();
    }

    public final void initUI() {

        JPanel basic = new JPanel();
        basic.setLayout(new BoxLayout(basic, BoxLayout.Y_AXIS));
        add(basic);

        basic.add(Box.createVerticalGlue());

        JPanel bottom = new JPanel();
        bottom.setAlignmentX(Component.RIGHT_ALIGNMENT);
        bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));

        JButton ok = new JButton("OK");
        JButton close = new JButton("Close");

        bottom.add(ok);
        bottom.add(Box.createRigidArea(new Dimension(5, 0)));
        bottom.add(close);
        bottom.add(Box.createRigidArea(new Dimension(15, 0)));

        basic.add(bottom);
        basic.add(Box.createRigidArea(new Dimension(0, 15)));

        setTitle("Layout Example");
        setSize(300, 150);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                TwoButtonsExample ex = new TwoButtonsExample();
                ex.setVisible(true);
            }
        });
    }
}

Bạn có thể hình dung sơ đồ bố trí các component như hình dưới.

Untitled11

Chúng ta tạo 2 panel, một panel chính sắp xếp các component theo chiều dọc, một panel thứ 2 sắp xếp các component theo chiều ngang, panel thứ 2 sẽ chứa 2 button căn lề phải. Để panel thứ 2 có thể nằm phía dưới cùng của cửa sổ (hay phía dưới cùng của panel chính) thì chúng ta dùng đến Glue. 

basic.setLayout(new BoxLayout(basic, BoxLayout.Y_AXIS));

Đầu tiên chúng ta tạo panel chính với BoxLayout theo chiều dọc.

basic.add(Box.createVerticalGlue());

Chúng ta tạo khoảng trống glue bằng phương thức createVerticalGlue(). Cơ chế hoạt động ở đây rất đơn giản. Ví dụ bạn đặt một button, sau đó gọi phương thức tạo glue, thì một vùng trống sẽ được thêm vào phía sau button (ở dưới button hoặc bên phải button tùy vào layout ngang hay dọc).

2fill5fill

JPanel bottom = new JPanel();
bottom.setAlignmentX(Component.RIGHT_ALIGNMENT);
bottom.setLayout(new BoxLayout(bottom, BoxLayout.X_AXIS));

Tiếp theo chúng ta tạo panel thứ hai và thiết lập cho các component con trượt qua lề bên phải bằng phương thức setAlignmentX(), panel này dùng BoxLayout theo chiều ngang.

bottom.add(Box.createRigidArea(new Dimension(5, 0)));

Chúng ta thêm một khoảng trống nhỏ giữa 2 button. Nhưng ở đây chúng ta không dùng glue và dùng rigid. Sự khác nhau của glue với rigid là kích thước của glue tự động thay đổi tùy theo các component, còn kích thước của rigid là do chúng ta quy định.

3fill 6fill

basic.add(bottom);

Sau khi đã thiết kế giao diện cho panel thứ hai thì chúng ta đặt panel đó vào panel chính.

basic.add(Box.createRigidArea(new Dimension(0, 15)));

Đồng thời chúng ta cũng tạo một khoảng trống rigid giữa panel thứ 2 với cạnh đáy của cửa sổ.

Capture

Java Swing – Menu và toolbar

Trong bài này chúng ta sẽ học cách làm menu và toolbar.

Java Swing có sẵn 3 lớp hỗ trợ tạo menu là a JMenuBarJMenu và JMenuItem.

Tạo menu

Chúng ta tạo một thanh menu có 1 item là Exit, click vào item này thì thoát chương trình.

JMenuBar menubar = new JMenuBar();

Đầu tiên chúng ta tạo một đối tượng menubar từ lớp JMenuBar.

ImageIcon icon = new ImageIcon("E:/exit.png");

Tiếp theo là tạo một đối tượng ImageIcon để làm ảnh icon của item.

JMenu file = new JMenu("File");
file.setMnemonic(KeyEvent.VK_F);

Chúng ta dùng lớp JMenu để tạo một menu, sau đó thiết lập phím tắt cho menu này là tổ hợp Alt+F.

JMenuItem eMenuItem = new JMenuItem("Exit", icon);
eMenuItem.setMnemonic(KeyEvent.VK_E);

Chúng ta tạo các item trong một menu từ lớp JMenuItem với tên item và ảnh icon, sau đó chúng ta gán phím tắt cho menu con này là tổ hợp Alt+E.

eMenuItem.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
        System.exit(0);
    }
});

Bản thân JMenuItem là một lớp kế thừa từ lớp AbstractButton, tức là cũng thuộc một loại button đặc biệt nên chúng ta có thể gắn listener cho đối tượng thuộc lớp này, ở đây chúng ta gọi System.exit() để thoát chương trình.

file.add(eMenuItem);
menubar.add(file);

Chúng ta thêm item vào menu rồi thêm menu đó vào menubar.

setJMenuBar(menubar);

Cuối cùng thiết lập menubar từ phương thức setJMenuBar().

Untitled

Tạo menu con

Trong ví dụ này chúng ta tạo một menu con.

JMenu saveMenu = new JMenu("Save");
...
menuFile.add(saveMenu);

Một menu con cũng chỉ đơn giản là một menu item thôi, bạn chỉ cần tạo nó ra rồi dùng phương thức add() của một menu khác để thêm vào làm menu con.

menuFile.addSeparator();

Ngoài ra ở đây chúng ta còn tạo một separator, đây đơn giản chỉ là một đường kẻ ngang để phân nhóm các item có chung mục đích với nhau lại.

Untitled

Tạo phím tắt cho menu item

Trong Java có 2 kiểu phím tắt là MnemonicAccelerator, trong đó mnemonic là tổ hợp của phím Alt với phím do chúng ta chỉ định, khi bấm vào tổ hợp phím này thì menu hoặc menu item sẽ xổ xuống, trong ví dụ đầu tiên chúng ta đã làm quen với mnemonic, còn Accelerator là kiểu phím tắt mà khi bấm tổ hợp phím này thì menu item sẽ được thực thi, tổ hợp phím trong accelerator là do chúng ta chỉ định.

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.AbstractAction;
import static javax.swing.Action.MNEMONIC_KEY;
import static javax.swing.Action.SMALL_ICON;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import static javax.swing.JFrame.EXIT_ON_CLOSE;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;


public class ShortCutsEx extends JFrame {

    public ShortCutsEx() {
        
        initUI();
    }

    private void initUI() {

        JMenuBar menuBar = new JMenuBar();
 
        ImageIcon newIcon = new ImageIcon("F:/new.png");
        ImageIcon openIcon = new ImageIcon("F:/open.png");
        ImageIcon saveIcon = new ImageIcon("F:/save.png");
        ImageIcon exitIcon = new ImageIcon("F:/exit.png");
 
        JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic(KeyEvent.VK_F);
 
        JMenuItem newItem = new JMenuItem("New", newIcon);
        JMenuItem openItem = new JMenuItem("Open", openIcon);
        JMenuItem saveItem = new JMenuItem("Save", saveIcon);
        JMenuItem exitItem = new JMenuItem("Exit", exitIcon);
 
        exitItem.setMnemonic(KeyEvent.VK_E);
        exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, 
                                ActionEvent.CTRL_MASK));
        exitItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                System.exit(0);
            }
        });
 
        fileMenu.add(newItem);
        fileMenu.add(openItem);
        fileMenu.add(saveItem);
        fileMenu.addSeparator();
        fileMenu.add(exitItem);
 
       menuBar.add(fileMenu);
       setJMenuBar(menuBar);
 
        setTitle("Menu Example");
        setSize(300, 300);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);        
    }        

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                
                ShortCutsEx ex = new ShortCutsEx();
                ex.setVisible(true);
            }
        });
    }
}

Trong ví dụ này chúng ta tạo phím tắt theo cả 2 kiểu.

JMenu fileMenu = new JMenu("File");
fileMenu.setMnemonic(KeyEvent.VK_F);

Đầu tiên chúng ta thiết lập mnemonic cho menu File với tổ hợp phím Alt+F.

exitItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W,
    ActionEvent.CTRL_MASK));

Để thiết lập phím tắt accelerator thì chúng ta dùng phương thức setAccelerator().

Untitled

Tạo check box item

Chúng ta có thể tạo menu item là check box. Java Swing cung cấp lớp JCheckBoxMenuItem để hỗ trợ làm việc này.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyEvent;
import javax.swing.BorderFactory;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuBar;

public class CheckBoxMenuItemEx extends JFrame {

    private JLabel statusbar;

    public CheckBoxMenuItemEx() {

        initUI();
    }

    private void initUI() {

        JMenuBar menubar = new JMenuBar();
        JMenu fileMenu = new JMenu("File");
        fileMenu.setMnemonic(KeyEvent.VK_F);
 
        JMenu viewMenu = new JMenu("View");
        viewMenu.setMnemonic(KeyEvent.VK_V);
 
        JCheckBoxMenuItem showStatusBar = new JCheckBoxMenuItem("Show statusbar");
        showStatusBar.setMnemonic(KeyEvent.VK_S);
        showStatusBar.setDisplayedMnemonicIndex(5);
        showStatusBar.setSelected(true);
 
        showStatusBar.addItemListener(new ItemListener() {

            @Override
            public void itemStateChanged(ItemEvent e) { 
                statusbar.setVisible(e.getStateChange() == ItemEvent.SELECTED);
            }
        });
 
        viewMenu.add(showStatusBar);
        menubar.add(fileMenu);
        menubar.add(viewMenu);
 
        setJMenuBar(menubar);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Chúng ta sẽ tạo một check box có chức năng ẩn/hiện thanh trạng thái.

statusbar = new JLabel("Ready");
statusbar.setBorder(BorderFactory.createEtchedBorder());
add(statusbar, BorderLayout.SOUTH);   

Để tạo status bar thì chúng ta tạo một đối tượng JLabel rồi dùng phương thức JFrame.add() để thêm label này vào phía dưới cửa sổ chỉ định bằng lớp BorderLayout, ngoài ra ở đây chúng ta dùng thêm phương thức setBorder() để tạo viền xung quanh label.

JCheckBoxMenuItem showStatusBar = new JCheckBoxMenuItem("Show statubar");
showStatusBar.setMnemonic(KeyEvent.VK_S);
showStatusBar.setDisplayedMnemonicIndex(5);

Chúng ta thiết lập mnemonic cho check box, ngoài ra chúng ta chỉ định cho kí tự được gạch chân thông qua phương thức setDisplayedMnemonicIndex() vì trong đoạn text của check box có nhiều kí tự S.

showStatusBar.setSelected(true);

Chúng ta thiết lập cho check box mặc định là đã được check.

showStatusBar.addItemListener(new ItemListener() {

    @Override
    public void itemStateChanged(ItemEvent e) {        
        statusbar.setVisible(e.getStateChange() == ItemEvent.SELECTED)        
    }

});

Chúng ta tạo một listener cho check box để lắng nghe sự kiện thay đổi trạng thái check và thiết lập ẩn/hiện cho status bar.

Untitled

Tạo popup menu

Popup menu hay còn được gọi là menu ngữ cảnh là loại menu chỉ hiển thị khi chúng ta click chuột lên vùng trống của cửa sổ. Java Swing cung cấp lớp JPopupMenu để hỗ trợ tạo popupmenu.

import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;

public class PopupMenuEx extends JFrame {

    private JPopupMenu popupMenu;

    public PopupMenuEx() {

        initUI();
    }

    private void initUI() {

        popupMenu = new JPopupMenu();
 
        JMenuItem maxItem = new JMenuItem("Maximize");
        maxItem.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                if(getExtendedState() != JFrame.MAXIMIZED_BOTH)
                    setExtendedState(JFrame.MAXIMIZED_BOTH);
            } 
        });
    
        popupMenu.add(maxItem);
 
        JMenuItem quitItem = new JMenuItem("Quit");
        quitItem.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
            System.exit(0);
            }
        });
 
        popupMenu.add(quitItem);
 
        addMouseListener(new MouseAdapter()
        {
            @Override
            public void mouseReleased(MouseEvent e)
            {
                if(e.getButton() == MouseEvent.BUTTON3)
                popupMenu.show(e.getComponent(), e.getX(), e.getY());
            }
        });

        setTitle("Menu 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() {
                PopupMenuEx pm = new PopupMenuEx();
                pm.setVisible(true);
            }
        });
    }
}

Trong ví dụ này chúng ta tạo popup menu có 2 item là maximizequit. Chức năng maximize sẽ phóng to cửa sổ.

popupMenu = new JPopupMenu();

Đầu tiên chúng ta tạo một đối tượng JPopupMenu.

JMenuItem maxItem = new JMenuItem("Maximize");
maxItem.addActionListener(new ActionListener() {
    
    @Override
    public void actionPerformed(ActionEvent e) {

        if (getExtendedState() != JFrame.MAXIMIZED_BOTH) {
            setExtendedState(JFrame.MAXIMIZED_BOTH);
        }

    }
});

Chúng ta tạo item maximize mà tạo listener cho item này. Phương thức JFrame.setExtendedState(JFrame.MAXIMIZED_BOTH) sẽ phóng to cửa sổ lên mức tối đa, ngoài ra còn có các kiểu co dãn cửa sổ khác là NORMAL, ICONIFIEDMAXIMIZED_HORIZ, và MAXIMIZED_VERT.

popupMenu.add(maxItem);

Sau đó chúng ta thêm item vào menu như thường.

addMouseListener(new MouseAdapter() {

    @Override
    public void mouseReleased(MouseEvent e) {

        if (e.getButton() == MouseEvent.BUTTON3) {
            popupMenu.show(e.getComponent(), e.getX(), e.getY());
        }
    }
});

Chúng ta dùng phương thức show() để hiển thị popup menu tại vị trí mà chúng ta muốn, ở đây mình tạo một listener cho cửa sổ chính và xử lý sự kiện thả chuột (sau khi nhấn chuột rồi thả ra thì phương thức mouseReleased() sẽ được gọi, chúng ta có thể lấy tọa độ của chuột cũng như các thông tin khác trong tham số MouseEvent).

Untitled

Tạo toolbar

Nếu như menu gộp nhóm các nút chức năng lại với nhau thì toolbar hiển thị các nút chức năng một cách độc lập để người dùng có thể thao tác một cách nhanh nhất. Java Swing cung cấp lớp JToolbar hỗ trợ tạo toolbar.

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JToolBar;


public class ToolbarEx extends JFrame {

    public ToolbarEx() {
        
        initUI();
    }

    private void initUI() {
        
        JMenuBar menubar = new JMenuBar();
        JMenu fileMenu = new JMenu("File");
        menubar.add(fileMenu);
        setJMenuBar(menubar);
 
        JToolBar toolbar = new JToolBar();
 
        ImageIcon icon = new ImageIcon("F:/exit.png");
 
        JButton exitButton = new JButton(icon);
        toolbar.add(exitButton);
 
        exitButton.addActionListener(new ActionListener()
        {
            @Override
            public void actionPerformed(ActionEvent e)
            {
                System.exit(0);
            }
        });
 
        add(toolbar, BorderLayout.NORTH);
 
        setTitle("Menu Example");
        setSize(300, 300);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);        
    }

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                ToolbarEx ex = new ToolbarEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta sẽ tạo một toolbar có một button.

JToolBar toolbar = new JToolBar();

Đầu tiên là tạo một đối tượng JToolbar.

JButton exitButton = new JButton(icon);
toolbar.add(exitButton);

Tiếp theo chúng ta tạo một đối tượng JButton rồi thêm vào toolbar.

add(toolbar, BorderLayout.NORTH);

Chúng ta hiển thị toolbar lên cửa sổ với phương thức add(), tham số BorderLayout.NORTH hiển thị toolbar ở phía trên cửa sổ.

Untitled

Java Swing – Ví dụ mở đầu

Trong bài này chúng ta sẽ viết một số chương trình nhỏ để làm quen với Swing.

Ví dụ 1

Trong ví dụ dưới đây chúng ta sẽ hiển thị một cửa sổ lên màn hình.

import java.awt.EventQueue;
import javax.swing.JFrame;

public class SimpleEx extends JFrame {

    public SimpleEx() {

        initUI();
    }

    private void initUI() {
        
        setTitle("Simple example");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

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

Bạn có thể thay đổi kích thước, phóng to, thu nhỏ cửa sổ bằng chuột… mặc định thì những thứ như thế phải code rất nhiều nếu bạn dùng thư viện của hệ thống (như Windows API viết bằng C), tuy nhiên ở đây Swing tự động làm các công việc đó cho bạn rồi nên đoạn code ở trên khá đơn giản.

public class SimpleEx extends JFrame {

Lớp SimpleEx được kế thừa từ component JFrame, đây là một component đặc biệt dùng để chứa các component khác.

Lưu ý: các lớp tạo nên toàn bộ mọi thứ trong swing có tên chung là Component, tiếng việt có nghĩa là thành phần, nhưng trong series này mình sẽ không dùng tiếng việt mà dùng từ component luôn.

public SimpleEx() {

    initUI();
}

Theo kinh nghiệm lập trình của mình thì tốt nhất là chúng ta không nên để các đoạn code trong phương thức khởi tạo mà nên di chuyển chúng ta một phương thức khác dành riêng cho từng loại công việc.

setTitle("Simple example");

Phương thức setTitle() sẽ thay đổi tiêu đề của cửa sổ.

setSize(300, 200);

Phương thức setSize() thay đổi kích thước cửa sổ.

setLocationRelativeTo(null);

Dòng code trên có tác dụng hiển thị cửa sổ lên vị trí giữa màn hình.

setDefaultCloseOperation(EXIT_ON_CLOSE);

Dòng code trên thiết lập việc tắt chương trình khi click nào nút X trên thanh tiêu đề. Nếu bạn không thêm dòng đó vào thì khi bạn bấm nút X, cửa sổ chương trình vẫn sẽ biến mất nhưng bản thân chương trình thì vẫn chạy ngầm bên dưới chứ không tắt hẳn.

EventQueue.invokeLater(new Runnable() {
    @Override
    public void run() {
        SimpleExample ex = new SimpleExample();
        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

Ví dụ 2

Trong ví dụ này, chúng ta sẽ làm việc với Button.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;

public class QuitButtonEx extends JFrame {

    public QuitButtonEx() {

        initUI();
    }

    private void initUI() {

        JButton quitButton = new JButton("Quit");

        quitButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent event) {
                System.exit(0);
            }
        });

        createLayout(quitButton);

        setTitle("Quit button");
        setSize(300, 200);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    private void createLayout(JComponent... arg) {

        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);

        gl.setAutoCreateContainerGaps(true);

        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
        );
    }

    public static void main(String[] args) {

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

Chúng ta đặt một JButton lên cửa sổ và gắn một ActionListener vào button này.

JButton quitButton = new JButton("Quit");

Dòng code trên tạo một đối tượng JButton với tham số là đoạn text hiển thị trên button đó.

quitButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent event) {
        System.exit(0);
    }
});

Chúng ta gắn một ActionListener vào button, phương thức actionPerformed() sẽ được gọi mỗi lần chúng ta click vào button, bên trong phương thức chúng ta gọi System.exit() để thoát chương trình.

createLayout(quitButton);

Khi các component con được tạo xong thì chúng ta tiến hành đưa chúng lên cửa sổ chính, tại đây mình chuyển đoạn code làm công việc đó qua phương thức createLayout().

Container pane = getContentPane();
GroupLayout gl = new GroupLayout(pane);
pane.setLayout(gl);

Khu vực bên trong khung cửa sổ JFrame được gọi là pane hoặc panel, đây là vùng để chúng ta đặt các component vào đó. Vùng này được quản lý bởi các đối tượng layout (chúng ta sẽ tìm hiểu về quản lý Layout trong các bài viết sau), mặc định thì lớp layout chính quản lý cửa sổ khi mới tạo ra là BorderLayout, các chức năng của lớp này rất hạn chế nên ở đây mình dùng đến lớp GroupLayout.

gl.setAutoCreateContainerGaps(true);

Phương thức setAutoCreateContainerGaps() sẽ tự động tạo các khoảng trống phù hợp giữa các component và giữa các component với khung cửa sổ.

private void createLayout(JComponent... arg)
{
...
}

Lưu ý ở đây mình dùng cú pháp varargs để truyền tham số vào phương thức, cú pháp này cho phép chúng ta truyền vào số lượng tham số bất kỳ. Trong cú pháp này, tên của kiểu dữ liệu sẽ có 3 dấu chấm đằng sau nó, biến truyền vào sẽ trở thành một mảng. Bạn có thể tìm hiểu thêm về cú pháp varargs tại đây.

gl.setHorizontalGroup(gl.createSequentialGroup()
        .addComponent(quitButton)
);

gl.setVerticalGroup(gl.createSequentialGroup()
        .addComponent(quitButton)
);

GroupLayout sắp xếp các component theo chiều ngang và chiều dọc của cửa sổ. Nhưng ở đây chúng ta chỉ có một component là JButton nên cũng chưa thấy nhiều tiện ích cho lắm.

Capture

Ví dụ 3

Trong ví dụ này chúng ta sẽ cho hiển thị tooltip.

import java.awt.EventQueue;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;


public class TooltipEx extends JFrame {

    public TooltipEx() {
        
        initUI();
    }

    private void initUI() {

        JButton btn = new JButton("Button");
        btn.setToolTipText("A button component");

        createLayout(btn);
        
        setTitle("Tooltip");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }
    
    private void createLayout(JComponent... arg) {
        
        JPanel pane = (JPanel) getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);
        
        pane.setToolTipText("Content pane");        
        
        gl.setAutoCreateContainerGaps(true);
        
        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
                .addGap(200)
        );

        gl.setVerticalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
                .addGap(120)
        );
        
        pack();        
    }

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
        
            @Override
            public void run() {
                TooltipEx ex = new TooltipEx();
                ex.setVisible(true);
            }
        });
    }
}

Tooltip là các đoạn text mô tả được hiện lên bên cạnh các component. Trong ví dụ này, mỗi khi di chuột vào button hoặc lên vùng trống của sổ, một tooltip sẽ hiện ra.

btn.setToolTipText("A button component");

Để cài đặt tooltip cho bất cứ component nào thì chúng ta dùng phương thức setTooltipText().

JPanel pane = (JPanel) getContentPane();
GroupLayout gl = new GroupLayout(pane);
pane.setLayout(gl);

Như đã nói ở trên, vùng trống trên cửa sổ được gọi là panel, để lấy đối tượng panel thì chúng ta dùng phương thức getContentPane(), tuy nhiên phương thức này trả về lớp Container, lớp này không có sẵn phương thức nào để làm việc với tooltip nên chúng ta chuyển đổi kiểu dữ liệu sang lớp JPanel.

Capture

pane.setToolTipText("Content pane");

Để tạo tooltip thì chỉ đơn giản là gọi phương thức setToolTipText() là được.

gl.setHorizontalGroup(gl.createSequentialGroup()
        .addComponent(arg[0])
        .addGap(200)
);

gl.setVerticalGroup(gl.createSequentialGroup()
        .addComponent(arg[0])
        .addGap(120)
);

Phương thức addGap() tạo khoảng trống xung quanh một đối tượng theo cả chiều ngang và chiều dọc.

pack();

Phương thức pack() sẽ tự động thay đổi kích thước của JFrame dựa trên kích thước của các component mà nó chứa kể cả các vùng được định nghĩa thêm như khoảng trống từ phương thức addGap().

Untitled

Ví dụ 4

Trong ví dụ này chúng ta sẽ tạo phím tắt cho các đối tượng, trong Java có hai cách thiết lập phím tắt cho đối tượng là dùng Mnemonics hoặc dùng KeyBinding, ở đây chúng ta sẽ dùng mnemonics.

import java.awt.Container;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import javax.swing.GroupLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;


public class MnemonicEx extends JFrame {
    
    public MnemonicEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        JButton btn = new JButton("Button");
        btn.addActionListener(new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                System.out.println("Button pressed");
            }
            
        });
        
        btn.setMnemonic(KeyEvent.VK_B);
        
        createLayout(btn);
        
        setTitle("Mnemonics");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(EXIT_ON_CLOSE);        
    }
    
    private void createLayout(JComponent... arg) {
        
        Container pane = getContentPane();
        GroupLayout gl = new GroupLayout(pane);
        pane.setLayout(gl);        

        gl.setAutoCreateContainerGaps(true);

        gl.setHorizontalGroup(gl.createSequentialGroup()
                .addComponent(arg[0])
                .addGap(200)
        );

        gl.setVerticalGroup(gl.createParallelGroup()
                .addComponent(arg[0])
                .addGap(200)
        );       

        pack();
    }        

    public static void main(String[] args) {

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

Chúng ta tạo một button và một listener gắn vào button đó, sau đó chúng ta thiết lập phím tắt cho button này là tổ hợp Alt+B.

btn.setMnemonic(KeyEvent.VK_B);

Phương thức setMnemonic() thiết lập phím tắt cho button, phương thức này mặc nhiên thiết lập phím tắt là phím Alt với phím do chúng ta chọn trong lớp KeyEvent.

Java Swing – Giới thiệu

Thư viện Swing là một bộ toolkit được phát hành bởi Sun Microsystems với mục đích hỗ trợ việc tạo giao diện đồ họa người dùng với Java.

Một số đặc điểm chính của Swing:

  • Độc lập với thiết bị
  • Có thể tùy chỉnh
  • Có thể mở rộng
  • Có thể cấu hình
  • Khá nhẹ

Toàn bộ thư viện Swing có tổng cộng 18 package sau:

  • javax.accessibility
  • javax.swing
  • javax.swing.border
  • javax.swing.colorchooser
  • javax.swing.event
  • javax.swing.filechooser
  • javax.swing.plaf
  • javax.swing.plaf.basic
  • javax.swing.plaf.metal
  • javax.swing.plaf.multi
  • javax.swing.plaf.synth
  • javax.swing.table
  • javax.swing.text
  • javax.swing.text.html
  • javax.swing.text.html.parser
  • javax.swing.text.rtf
  • javax.swing.tree
  • javax.swing.undo

Swing là một bộ cung cụ cao cấp, cung cấp rất nhiều widget từ cơ bản như Button, Label cho đến nâng cao như Tree, Table…

Swing là một phần của Java Foundation Classes (JFC), đây là một tập các gói thư viện hỗ trợ đầy đủ cho veiecj tạo các ứng dụng desktop. Các thư viện khác ngoài Swing trong JFC là AWT, Accessibility, Java 2D, và Drag and Drop.  Swing được phát hành vào năm 1997 đi kèm với JDK 1.2, do đó đây là một toolkit khá lớn tuổi.

Thư viện SWT

Java còn có một bộ thư viện khác hỗ trợ lập trình GUI là Standard Widget Toolkit (SWT). Thư viện SWT ban đầu được phát triển bởi tập đoàn IBM nhưng bây giờ thì nó đã trở thành một dự án mã nguồn mở được duy trì bởi cộng đồng Eclipse.

Java 2D – Trò chơi xếp hình – Tetris

Trong phần này chúng ta sẽ viết một game rất nổi tiếng đó là game Xếp hình 😀 (Tetris)

Tetris

Game được thiết kế và phát triển lần đầu tiên vào năm 1985 bởi một lập trình viên người Nga tên là Alexey Pajitnov. Kể từ đó, game này được phát triển thành nhiều phiên bản trên nhiều hệ máy khác nhau.

Trong game có 7 khối gạch có dạng hình chữ S, hình chữ Z, hình chữ T, hình chữ L, hình đường thẳng, hình chữ L ngược và hình vuông. Mỗi khối gạch được tạo nên bởi 4 hình vuông. Các khối gạch sẽ rơi từ trên xuống. Người chơi phải di chuyển và xoay các khối gạch sao cho chúng vừa khít với nhau, nếu các khối gạch lấp đầy một đường ngang thì người chơi ghi điểm. Trò chơi kết thúc khi các khối gạch chồng lên nhau quá cao.

tetrominoes

 

Mô tả game

Kích thước của các đối tượng trong game này được tính theo đơn vị 1 hình vuông chứ không theo pixel nữa. Kích thước của cửa sổ giờ đây là 10×22 hình vuông. Kích thước thật của các hình vuông được tính động theo kích thước cửa sổ. Tức là nếu kích thước cửa sổ là 200×400 pixel thì kích thước thật của môi hình vuông là 200×400 / 10×22 là khoảng 20×18 pixel (nhưng chúng ta cũng không quan tâm đến con số này).

Mỗi lần chạy chương trình là game sẽ tự động chạy ngay, tuy nhiên chúng ta có thể cho tạm dừng game bằng cách bấm phím P. Nếu muốn khối gạch rơi nhanh xuống đáy ngay lập tức thì bấm phím Space. Bấm phím D sẽ làm cho khối gạch rơi xuống thêm 1 hàng, có thể dùng phím này để tăng tốc độ rơi thêm một chút. Tốc độ game không thay đổi. Điểm sẽ là số hàng đã được loại bỏ.

import java.awt.BorderLayout;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.SwingUtilities;


public class Tetris extends JFrame {

    private JLabel statusbar;

    public Tetris() {
        
        initUI();
   }
    
   private void initUI() {

        statusbar = new JLabel(" 0");
        add(statusbar, BorderLayout.SOUTH);
        Board board = new Board(this);
        add(board);
        board.start();

        setSize(200, 400);
        setTitle("Tetris");
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);       
   }

   public JLabel getStatusBar() {
       
       return statusbar;
   }

    public static void main(String[] args) {

        SwingUtilities.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                
                Tetris game = new Tetris();
                game.setVisible(true);
            }
        });                
    } 
}

Toàn bộ game này chúng ta viết trong 3 file, đầu tiên là file Tetris.java, file này là file main, làm các công việc khởi tạo cửa sổ, khởi tạo kích thước, chạy luồng main…

board.start();

Chúng ta gọi phương thức start() để khi chương trình chạy thì game cũng chạy luôn.

import java.util.Random;

public class Shape {

    protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
               TShape, SquareShape, LShape, MirroredLShape };

    private Tetrominoes pieceShape;
    private int coords[][];
    private int[][][] coordsTable;


    public Shape() {

        coords = new int[4][2];
        setShape(Tetrominoes.NoShape);
    }

    public void setShape(Tetrominoes shape) {

         coordsTable = new int[][][] {
            { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
            { { 0,-1 },   { 0, 0 },   {-1, 0 },   {-1, 1 } },
            { { 0,-1 },   { 0, 0 },   { 1, 0 },   { 1, 1 } },
            { { 0,-1 },   { 0, 0 },   { 0, 1 },   { 0, 2 } },
            { {-1, 0 },   { 0, 0 },   { 1, 0 },   { 0, 1 } },
            { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
            { {-1,-1 },   { 0,-1 },   { 0, 0 },   { 0, 1 } },
            { { 1,-1 },   { 0,-1 },   { 0, 0 },   { 0, 1 } }
        };

        for (int i = 0; i < 4 ; i++) {
            
            for (int j = 0; j < 2; ++j) {
                
                coords[i][j] = coordsTable[shape.ordinal()][i][j];
            }
        }
        
        pieceShape = shape;
    }

    private void setX(int index, int x) { coords[index][0] = x; }
    private void setY(int index, int y) { coords[index][1] = y; }
    public int x(int index) { return coords[index][0]; }
    public int y(int index) { return coords[index][1]; }
    public Tetrominoes getShape()  { return pieceShape; }

    public void setRandomShape() {
        
        Random r = new Random();
        int x = Math.abs(r.nextInt()) % 7 + 1;
        Tetrominoes[] values = Tetrominoes.values(); 
        setShape(values[x]);
    }

    public int minX() {
        
      int m = coords[0][0];
      
      for (int i=0; i < 4; i++) {
          
          m = Math.min(m, coords[i][0]);
      }
      
      return m;
    }


    public int minY() {
        
      int m = coords[0][1];
      
      for (int i=0; i < 4; i++) {
          
          m = Math.min(m, coords[i][1]);
      }
      
      return m;
    }

    public Shape rotateLeft() {
        
        if (pieceShape == Tetrominoes.SquareShape)
            return this;

        Shape result = new Shape();
        result.pieceShape = pieceShape;

        for (int i = 0; i < 4; ++i) {
            
            result.setX(i, y(i));
            result.setY(i, -x(i));
        }
        
        return result;
    }

    public Shape rotateRight() {
        
        if (pieceShape == Tetrominoes.SquareShape)
            return this;

        Shape result = new Shape();
        result.pieceShape = pieceShape;

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

            result.setX(i, -y(i));
            result.setY(i, x(i));
        }
        
        return result;
    }
}

Tiếp theo là file Shape.java, file này lưu thông tin về các khối gạch.

protected enum Tetrominoes { NoShape, ZShape, SShape, LineShape, 
            TShape, SquareShape, LShape, MirroredLShape };

Chúng ta định nghĩa tên các khối gạch trong enum Tetrominoes. Ngoài 7 khối cơ bản chúng ta tạo thêm 1 mối đặc biệt nữa là NoShape, khối này không có hình thù.

public Shape() {

    coords = new int[4][2];
    setShape(Tetrominoes.NoShape);
}

Trong phương thức khởi tạo, chúng ta tạo mảng coords để lưu giữ tọa độ thật của khối gạch.

coordsTable = new int[][][] {
   { { 0, 0 },   { 0, 0 },   { 0, 0 },   { 0, 0 } },
   { { 0, -1 },  { 0, 0 },   { -1, 0 },  { -1, 1 } },
   { { 0, -1 },  { 0, 0 },   { 1, 0 },   { 1, 1 } },
   { { 0, -1 },  { 0, 0 },   { 0, 1 },   { 0, 2 } },
   { { -1, 0 },  { 0, 0 },   { 1, 0 },   { 0, 1 } },
   { { 0, 0 },   { 1, 0 },   { 0, 1 },   { 1, 1 } },
   { { -1, -1 }, { 0, -1 },  { 0, 0 },   { 0, 1 } },
   { { 1, -1 },  { 0, -1 },  { 0, 0 },   { 0, 1 } }
};

Mảng coordsTable lưu tọa độ của các hình vuông tạo nên từng loại khối gạch.

for (int i = 0; i < 4 ; i++) {
    
    for (int j = 0; j < 2; ++j) {
        
        coords[i][j] = coordsTable[shape.ordinal()][i][j];
    }
}

Phương thức setShape() sẽ quy định cho đối tượng khối gạch đó là loại nào, tham số của phương thức này là một giá trị của enum Tetrominoes, từ tham số đó chúng ta lấy số thứ tự của thuộc tính đó bằng phương thức ordinal(), rồi dùng số thứ tự đó để lấy tọa độ trong mảng coordsTable và chép vào mảng coords.

Lý do tại sao tọa độ ở đây chỉ là 0, 1 và -1 thì bạn xem hình dưới đây để hình dung. Ví dụ { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, -1 } , sẽ đại diện cho khối gạch hình chữ S.

Coordinates
public Shape rotateLeft() {
    
    if (pieceShape == Tetrominoes.SquareShape)
        return this;

    Shape result = new Shape();
    result.pieceShape = pieceShape;

    for (int i = 0; i < 4; ++i) {
        
        result.setX(i, y(i));
        result.setY(i, -x(i));
    }
    
    return result;
}

Phương thức rotateLeft() làm nhiệm vụ xoay khối gạch qua bên trái. Ở đây mình sẽ không giải thích vì sao chúng ta lại có đoạn code xoay như trên vì rất khó giải thích, bạn cứ nhìn hình ở trên rồi tự suy ra sẽ thấy, nếu thắc mắc chỗ nào thì hãy comment ở cuối bài này và mình sẽ giải thích. Phương thức rotateRight() cũng tương tự là xoay khối gạch sang bên phải.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;

import Shape.Tetrominoes;

public class Board extends JPanel 
        implements ActionListener {

    private final int BoardWidth = 10;
    private final int BoardHeight = 22;

    private Timer timer;
    private boolean isFallingFinished = false;
    private boolean isStarted = false;
    private boolean isPaused = false;
    private int numLinesRemoved = 0;
    private int curX = 0;
    private int curY = 0;
    private JLabel statusbar;
    private Shape curPiece;
    private Tetrominoes[] board;

    public Board(Tetris parent) {

        initBoard(parent);
    }
    
    private void initBoard(Tetris parent) {
        
       setFocusable(true);
       curPiece = new Shape();
       timer = new Timer(400, this);
       timer.start(); 

       statusbar =  parent.getStatusBar();
       board = new Tetrominoes[BoardWidth * BoardHeight];
       addKeyListener(new TAdapter());
       clearBoard();          
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        
        if (isFallingFinished) {
            
            isFallingFinished = false;
            newPiece();
        } else {
            
            oneLineDown();
        }
    }

    private int squareWidth() { return (int) getSize().getWidth() / BoardWidth; }
    private int squareHeight() { return (int) getSize().getHeight() / BoardHeight; }
    private Tetrominoes shapeAt(int x, int y) { return board[(y * BoardWidth) + x]; }


    public void start()  {
        
        if (isPaused)
            return;

        isStarted = true;
        isFallingFinished = false;
        numLinesRemoved = 0;
        clearBoard();

        newPiece();
        timer.start();
    }

    private void pause()  {
        
        if (!isStarted)
            return;

        isPaused = !isPaused;
        
        if (isPaused) {
            
            timer.stop();
            statusbar.setText("paused");
        } else {
            
            timer.start();
            statusbar.setText(String.valueOf(numLinesRemoved));
        }
        
        repaint();
    }
    
    private void doDrawing(Graphics g) {
        
        Dimension size = getSize();
        int boardTop = (int) size.getHeight() - BoardHeight * squareHeight();

        for (int i = 0; i < BoardHeight; ++i) {
            
            for (int j = 0; j < BoardWidth; ++j) {
                
                Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
                
                if (shape != Tetrominoes.NoShape)
                    drawSquare(g, 0 + j * squareWidth(),
                               boardTop + i * squareHeight(), shape);
            }
        }

        if (curPiece.getShape() != Tetrominoes.NoShape) {
            
            for (int i = 0; i < 4; ++i) { 
                int x = curX + curPiece.x(i); 
                int y = curY - curPiece.y(i); 
                drawSquare(g, 0 + x * squareWidth(), boardTop + (BoardHeight - y - 1) * squareHeight(), curPiece.getShape()); 
            } 
        } 
    } 
    
    @Override 
    public void paintComponent(Graphics g) { 
        super.paintComponent(g); 
        doDrawing(g); 
    } 

    private void dropDown() { 
        int newY = curY; 
        while (newY > 0) {
            
            if (!tryMove(curPiece, curX, newY - 1))
                break;
            --newY;
        }
        
        pieceDropped();
    }

    private void oneLineDown()  {
        
        if (!tryMove(curPiece, curX, curY - 1))
            pieceDropped();
    }


    private void clearBoard() {
        
        for (int i = 0; i < BoardHeight * BoardWidth; ++i)
            board[i] = Tetrominoes.NoShape;
    }

    private void pieceDropped() {
        
        for (int i = 0; i < 4; ++i) {
            
            int x = curX + curPiece.x(i);
            int y = curY - curPiece.y(i);
            board[(y * BoardWidth) + x] = curPiece.getShape();
        }

        removeFullLines();

        if (!isFallingFinished)
            newPiece();
    }

    private void newPiece()  {
        
        curPiece.setRandomShape();
        curX = BoardWidth / 2 + 1;
        curY = BoardHeight - 1 + curPiece.minY();

        if (!tryMove(curPiece, curX, curY)) {
            
            curPiece.setShape(Tetrominoes.NoShape);
            timer.stop();
            isStarted = false;
            statusbar.setText("game over");
        }
    }

    private boolean tryMove(Shape newPiece, int newX, int newY) {
        
        for (int i = 0; i < 4; ++i) {
            
            int x = newX + newPiece.x(i);
            int y = newY - newPiece.y(i);
            
            if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
                return false;
            
            if (shapeAt(x, y) != Tetrominoes.NoShape)
                return false;
        }

        curPiece = newPiece;
        curX = newX;
        curY = newY;

        repaint();

        return true;
    }

    private void removeFullLines() {
        
        int numFullLines = 0;

        for (int i = BoardHeight - 1; i >= 0; --i) {
            boolean lineIsFull = true;

            for (int j = 0; j < BoardWidth; ++j) {
                if (shapeAt(j, i) == Tetrominoes.NoShape) {
                    lineIsFull = false;
                    break;
                }
            }

            if (lineIsFull) {
                ++numFullLines;
                for (int k = i; k < BoardHeight - 1; ++k) {
                    for (int j = 0; j < BoardWidth; ++j) 
                        board[(k * BoardWidth) + j] = shapeAt(j, k + 1); 
                } 
            } 
        } 
        if (numFullLines > 0) {
            
            numLinesRemoved += numFullLines;
            statusbar.setText(String.valueOf(numLinesRemoved));
            isFallingFinished = true;
            curPiece.setShape(Tetrominoes.NoShape);
            repaint();
        }
     }

    private void drawSquare(Graphics g, int x, int y, Tetrominoes shape)  {
        
        Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102), 
            new Color(102, 204, 102), new Color(102, 102, 204), 
            new Color(204, 204, 102), new Color(204, 102, 204), 
            new Color(102, 204, 204), new Color(218, 170, 0)
        };

        Color color = colors[shape.ordinal()];

        g.setColor(color);
        g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);

        g.setColor(color.brighter());
        g.drawLine(x, y + squareHeight() - 1, x, y);
        g.drawLine(x, y, x + squareWidth() - 1, y);

        g.setColor(color.darker());
        g.drawLine(x + 1, y + squareHeight() - 1,
                         x + squareWidth() - 1, y + squareHeight() - 1);
        g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1,
                         x + squareWidth() - 1, y + 1);

    }

    class TAdapter extends KeyAdapter {
        
         @Override
         public void keyPressed(KeyEvent e) {

             if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape) {  
                 return;
             }

             int keycode = e.getKeyCode();

             if (keycode == 'p' || keycode == 'P') {
                 pause();
                 return;
             }

             if (isPaused)
                 return;

             switch (keycode) {
                 
             case KeyEvent.VK_LEFT:
                 tryMove(curPiece, curX - 1, curY);
                 break;
                 
             case KeyEvent.VK_RIGHT:
                 tryMove(curPiece, curX + 1, curY);
                 break;
                 
             case KeyEvent.VK_DOWN:
                 tryMove(curPiece.rotateRight(), curX, curY);
                 break;
                 
             case KeyEvent.VK_UP:
                 tryMove(curPiece.rotateLeft(), curX, curY);
                 break;
                 
             case KeyEvent.VK_SPACE:
                 dropDown();
                 break;
                 
             case 'd':
                 oneLineDown();
                 break;
                 
             case 'D':
                 oneLineDown();
                 break;
             }
         }
     }
}

Cuối cùng chúng ta có file Board.java. File này chịu trách nhiệm điều khiển các khối gạch cũng như xử lý các hoạt động diễn ra trong game.

...
private boolean isFallingFinished = false;
private boolean isStarted = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
...

Ý nghĩa của các thuộc tính trên như sau:

  • isFallingFinished: cho biết khối gạch hiện tại đã rơi xuống xong chưa, nếu rồi thì tạo một khối gạch mới
  • isStartedisPaused: dùng để kiểm tra trạng thái hiện tại của game là đang chơi hay tạm dừng hay đã kết thúc
  • numLinesRemoved: lưu số lượng các đường ngang đã bị xóa
  • curXcurY: lưu tọa độ hiện tại của khối gạch đang rơi
setFocusable(true);

Chúng ta phải gọi phương thức setFocusable() một cách tường minh để bàn phím có thể nhận sự kiện bấm phím.

timer = new Timer(400, this);
timer.start(); 

Đối tượng Timer sẽ giải phóng sự kiện sau mỗi 400 mili-giây và gọi đến phương thức actionPerformed(), có thể hiểu đây là vòng lặp của game.

@Override
public void actionPerformed(ActionEvent e) {
    
    if (isFallingFinished) {
        
        isFallingFinished = false;
        newPiece();
    } else {
        
        oneLineDown();
    }
}

Trong phương thức actionPerformed(), chúng ta kiểm tra xem khối gạch đang rơi đã rơi xong chưa, nếu chưa thì chúng ta gọi phương thức oneLineDown() để cho khối gạch này rơi thêm 1 hàng, nếu đã rơi xong rồi thì chúng ta gọi phương thức newPiece() để tạo một khối gạch mới.

Trong phương thức doDrawing() chúng ta vẽ các đối tượng lên màn hình.

for (int i = 0; i < BoardHeight; ++i) {
    
    for (int j = 0; j < BoardWidth; ++j) {
        
        Tetrominoes shape = shapeAt(j, BoardHeight - i - 1);
        
        if (shape != Tetrominoes.NoShape)
            drawSquare(g, 0 + j * squareWidth(),
                        boardTop + i * squareHeight(), shape);
    }
}

Đầu tiên chúng ta vẽ các khối gạch đã rơi xuống đáy màn hình. Các khối gạch đã rơi xong sẽ được lưu lại vị trí trong mảng board, chúng ta lấy lại thông tin đó thông qua phương thức shapeAt().

if (curPiece.getShape() != Tetrominoes.NoShape) {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        drawSquare(g, 0 + x * squareWidth(),
                    boardTop + (BoardHeight - y - 1) * squareHeight(),
                    curPiece.getShape());
    }
}

Tiếp theo chúng ta vẽ khối gạch đang rơi.

private void dropDown() {
    
    int newY = curY;
    
    while (newY > 0) {
        
        if (!tryMove(curPiece, curX, newY - 1))
            break;
        --newY;
    }
    
    pieceDropped();
}

Nếu bấm phím space, khối gạch sẽ được rơi xuống đáy ngay lập tức bằng cách cho chạy một vòng lặp riêng.

private void clearBoard() {
    
    for (int i = 0; i < BoardHeight * BoardWidth; ++i)
        board[i] = Tetrominoes.NoShape;
}

Phương thức clearBoard() có nhiệm vụ lấp đầy cửa sổ bằng khối gạch NoShape, chúng ta làm thế để phục vụ việc kiểm tra va chạm sau này.

private void pieceDropped() {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        board[(y * BoardWidth) + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished)
        newPiece();
}

Phương thức pieceDropped() được gọi mỗi lần có một khối gạch đã rơi xong (chạm đáy hoặc chạm một khối gạch khác đã nằm sẵn), phương thức này sẽ thêm tọa độ mới của các khối gạch trong mảng board sau đó tiến hành kiểm tra xem có hàng nào full hay không thông qua phương thức removeFullLines(), cuối cùng khởi tạo một khối gạch mới (chúng ta thêm câu lệnh if để kiểm tra xem khối gạch chắc chắn đã rơi xong chưa).

private void newPiece()  {
    
    curPiece.setRandomShape();
    curX = BoardWidth / 2 + 1;
    curY = BoardHeight - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY)) {
        
        curPiece.setShape(Tetrominoes.NoShape);
        timer.stop();
        isStarted = false;
        statusbar.setText("game over");
    }
}

Phương thức newPiece() khởi tạo một khối gạch mới và tính toán vị trí cho khối gạch. Nếu vị trí khởi tạo của khối gạch không đủ vì phần gạch bên dưới đã quá cao thì chúng ta tiến hành cho game kết thúc.

private boolean tryMove(Shape newPiece, int newX, int newY) {
    
    for (int i = 0; i < 4; ++i) {
        
        int x = newX + newPiece.x(i);
        int y = newY - newPiece.y(i);
        
        if (x < 0 || x >= BoardWidth || y < 0 || y >= BoardHeight)
            return false;
        
        if (shapeAt(x, y) != Tetrominoes.NoShape)
            return false;
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;

    repaint();

    return true;
}

Phương thức tryMove() sẽ di chuyển khối gạch, phương thức này trả về false nếu khối gạch không thể di chuyển được do đã chạm biên cửa sổ hoặc chạm các khối gạch khác.

int numFullLines = 0;

for (int i = BoardHeight - 1; i >= 0; --i) {
    boolean lineIsFull = true;

    for (int j = 0; j < BoardWidth; ++j) {
        if (shapeAt(j, i) == Tetrominoes.NoShape) {
            lineIsFull = false;
            break;
        }
    }

    if (lineIsFull) {
        ++numFullLines;
        for (int k = i; k < BoardHeight - 1; ++k) {
            for (int j = 0; j < BoardWidth; ++j)
                    board[(k * BoardWidth) + j] = shapeAt(j, k + 1);
        }
    }
}

Phương thức removeFullLines() kiểm tra xem có hàng nào đã full hay chưa, nếu có thì chúng ta tăng một biến đếm lên để dùng vào việc tính điểm, đồng thời chúng ta kéo các hàng phía trên xuống một hàng, để đảm bảo tất cả các hàng full đều bị xóa.

Các khối gạch của game có màu khác nhau. Các khối gạch được vẽ trong phương thức drawSquare().

g.setColor(color.brighter());
g.drawLine(x, y + squareHeight() - 1, x, y);
g.drawLine(x, y, x + squareWidth() - 1, y);

Chúng ta vẽ các hình vuông có cạnh bên trái và cạnh trên màu sáng hơn với 2 cạnh còn lại để tạo hiệu ứng khối 3D.

Sự kiện bàn phím sẽ được nhận thông qua phương thức keyPressed() của lớp KeyAdapter.

case KeyEvent.VK_LEFT:
    tryMove(curPiece, curX - 1, curY);
    break;

Chẳng hạn như nếu bấm phím mũi tên trái, chúng ta thử di chuyển khối gạch sang bên trái.

Untitled

Java 2D – Xử lý sự kiện chuột

Trong phần này chúng ta sẽ làm một số thao tác với chuột.

Ví dụ 1

Bất cứ hình nào trong Java cũng đều kế thừa từ lớp Shape, lớp này có phương thức contains() dùng để kiểm tra xem một điểm có nằm trong phạm vi của hình đó hay không. Trong ví dụ này chúng ta sẽ sử dụng đến phương thức này,

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Rectangle2D rect;
    private Ellipse2D ellipse;
    private float alpha_rectangle;
    private float alpha_ellipse;

    public Surface() {
        
        initSurface();
    }
    
    private void initSurface() {
        
        addMouseListener(new HitTestAdapter());

        rect = new Rectangle2D.Float(20f, 20f, 80f, 50f);
        ellipse = new Ellipse2D.Float(120f, 30f, 60f, 60f);

        alpha_rectangle = 1f;
        alpha_ellipse = 1f;        
    }

    private void doDrawing(Graphics g) {

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

        g2d.setPaint(new Color(50, 50, 50));

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

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

        g2d.setRenderingHints(rh);

        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                alpha_rectangle));
        g2d.fill(rect);

        g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
                alpha_ellipse));
        g2d.fill(ellipse);
        
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {

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

    class RectRunnable implements Runnable {

        private Thread runner;

        public RectRunnable() {
            
            initThread();
        }
        
        private void initThread() {
            
            runner = new Thread(this);
            runner.start();
        }

        @Override
        public void run() {

            while (alpha_rectangle >= 0) {
                
                repaint();
                alpha_rectangle += -0.01f;

                if (alpha_rectangle < 0) { 
                    alpha_rectangle = 0; 
                } 

                try { 
                    Thread.sleep(50); 
                } catch (InterruptedException ex) { 
                    Logger.getLogger(Surface.class.getName()).log(Level.SEVERE, 
                                                                  null, ex); 
                } 
            } 
         } 
    } 
    
    class HitTestAdapter extends MouseAdapter implements Runnable { 
        private RectRunnable rectAnimator; 
        private Thread ellipseAnimator; 

        @Override 
        public void mousePressed(MouseEvent e) { 
            int x = e.getX(); 
            int y = e.getY(); 
            if (rect.contains(x, y)) { 
                rectAnimator = new RectRunnable(); 
            } 
            if (ellipse.contains(x, y)) { 
                ellipseAnimator = new Thread(this); 
                ellipseAnimator.start(); 
            } 
        } 

        @Override 
        public void run() { 
            while (alpha_ellipse >= 0) { 
                repaint(); 
                alpha_ellipse += -0.01f; 
                if (alpha_ellipse < 0) { 
                    alpha_ellipse = 0; 
                } 
                try { 
                    Thread.sleep(50); 
                } catch (InterruptedException ex) { 
                    Logger.getLogger(Surface.class.getName()).log(Level.SEVERE, 
                                     null, ex); 
                } 
           } 
        } 
    } 
} 

public class HitTestingEx extends JFrame { 
    public HitTestingEx() { 
        add(new Surface()); 
        setTitle("Hit testing"); 
        setSize(250, 150); 
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
        setLocationRelativeTo(null); 
    } 

    public static void main(String[] args) { 
        EventQueue.invokeLater(new Runnable() { 
            @Override 
            public void run() { 
                HitTestingEx ex = new HitTestingEx(); 
                ex.setVisible(true); 
            } 
         }); 
    } 
} 

Chúng ta vẽ một hình chữ nhật và một hình tròn, nếu click chuột lên hình nào thì hình đó sẽ từ từ mờ dần rồi biến mất.

private float alpha_rectangle;
private float alpha_ellipse;

Hai biến alpha_rectanglealpha_allipse lưu trữ độ trong suốt của 2 hình. Hai biến này được kiểm soát thông qua các luồng độc lập (chi tiết về lập trình đa luồng sẽ được bàn tới trong một bài viết khác).

Chúng ta tạo lớp HitTestAdapter, lớp này xử lý sự kiện click chuột. Lớp này implements giao diện Runnable.

if (ellipse.contains(x, y)) {

    ellipseAnimator = new Thread(this);
    ellipseAnimator.start();
}

Khi click vào bên trong hình tròn thì chúng ta tạo một Thread mới rồi gọi phương thức start() để chạy luồng, ở đây là phương thức run() của chính lớp HitTestAdapter.

if (rect.contains(x, y)) {

    rectAnimator = new RectRunnable();
}

Đối với hình chữ nhật thì chúng ta chạy trong một luồng khác (từ lớp RectRunnable), chúng ta cho lớp này tự tạo luồng và tự chạy luôn trong phương thức khởi tạo.

public void run() {

    while (alpha_ellipse >= 0) {

        repaint();
        alpha_ellipse += -0.01f;
        ...
    }

Bên trong phương thức run() chúng ta code các vòng lặp để giảm dần độ trong suốt của các hình.

Untitled

Ví dụ 2

Trong ví dụ này chúng ta sẽ di chuyển và phóng to/thu nhỏ hình bằng chuột.

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;


class Surface extends JPanel {

    private ZRectangle zrect;
    private ZEllipse zell;

    public Surface() {

        initUI();
    }
    
    private void initUI() {
        
        MovingAdapter ma = new MovingAdapter();

        addMouseMotionListener(ma);
        addMouseListener(ma);
        addMouseWheelListener(new ScaleHandler());

        zrect = new ZRectangle(50, 50, 50, 50);
        zell = new ZEllipse(150, 70, 80, 80);
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g;
        
        Font font = new Font("Serif", Font.BOLD, 40);
        g2d.setFont(font);
        
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                        RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                        RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        g2d.setPaint(new Color(0, 0, 200));
        g2d.fill(zrect);
        g2d.setPaint(new Color(0, 200, 0));
        g2d.fill(zell);        
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        doDrawing(g);        
    }

    class ZEllipse extends Ellipse2D.Float {
        
        public ZEllipse(float x, float y, float width, float height) {
            
            setFrame(x, y, width, height);
        }

        public boolean isHit(float x, float y) {
            
            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {
            
            this.x += x;
        }

        public void addY(float y) {
            
            this.y += y;
        }

        public void addWidth(float w) {
            
            this.width += w;
        }

        public void addHeight(float h) {
            
            this.height += h;
        }
    }

    class ZRectangle extends Rectangle2D.Float {

        public ZRectangle(float x, float y, float width, float height) {
            
            setRect(x, y, width, height);
        }

        public boolean isHit(float x, float y) {
            
            return getBounds2D().contains(x, y);
        }

        public void addX(float x) {
            
            this.x += x;
        }

        public void addY(float y) {
            
            this.y += y;
        }

        public void addWidth(float w) {
            
            this.width += w;
        }

        public void addHeight(float h) {
            
            this.height += h;
        }
    }

    class MovingAdapter extends MouseAdapter {

        private int x;
        private int y;

        @Override
        public void mousePressed(MouseEvent e) {
            
            x = e.getX();
            y = e.getY();
        }

        @Override
        public void mouseDragged(MouseEvent e) {

            doMove(e);
        }   
        
        private void doMove(MouseEvent e) {
            
            int dx = e.getX() - x;
            int dy = e.getY() - y;

            if (zrect.isHit(x, y)) {
                
                zrect.addX(dx);
                zrect.addY(dy);
                repaint();
            }

            if (zell.isHit(x, y)) {
                
                zell.addX(dx);
                zell.addY(dy);
                repaint();
            }

            x += dx;
            y += dy;            
        }
    }

    class ScaleHandler implements MouseWheelListener {
        
        @Override
        public void mouseWheelMoved(MouseWheelEvent e) {

            doScale(e);
        }
        
        private void doScale(MouseWheelEvent e) {
            
            int x = e.getX();
            int y = e.getY();

            if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

                if (zrect.isHit(x, y)) {
                    
                    float amount =  e.getWheelRotation() * 5f;
                    zrect.addWidth(amount);
                    zrect.addHeight(amount);
                    repaint();
                }

                if (zell.isHit(x, y)) {
                    
                    float amount =  e.getWheelRotation() * 5f;
                    zell.addWidth(amount);
                    zell.addHeight(amount);
                    repaint();
                }
            }            
        }
    }
}

public class MovingScalingEx extends JFrame {
    
    public MovingScalingEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());

        setTitle("Moving and scaling");
        setSize(300, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);        
    }

    public static void main(String[] args) {        
        
        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                MovingScalingEx ex = new MovingScalingEx();
                ex.setVisible(true);
            }
        });
    }
}

Chúng ta vẽ một hình vuông và một hình tròn, khi chuột đang nằm trong phạm vi của một trong hai hình, chúng ta có thể kéo hình đó đi bằng cách click giữ và kéo, hoặc phóng to/thu nhỏ hình bằng con lăn trên chuột.

private ZRectangle zrect;
private ZEllipse zell;

Ở đây chúng ta không dùng các lớp hình học có sẵn mà tự viết một lớp kế thừa.

addMouseMotionListener(ma);
addMouseListener(ma);
addMouseWheelListener(new ScaleHandler());

Ba dòng code trên thêm các đối tượng lắng nghe sự kiện click chuột, kéo chuột và lăn con lăn.

class ZEllipse extends Ellipse2D.Float {
    
    public ZEllipse(float x, float y, float width, float height) {
        
        setFrame(x, y, width, height);
    }

    public boolean isHit(float x, float y) {
        
        return getBounds2D().contains(x, y);
    }
...
}

Chúng ta kế thừa các lớp hình học và thêm vào một số phương thức để chủ động hơn trong việc thay đổi kích thước, vị trí… Ngoài ra chúng ta còn có phương thức isHit() để kiểm tra xem vị trí của chuột có nằm trong hình hay không.

Lớp MovingAdapter chịu trách nhiệm xử lý sự kiện click chuột và kéo thả chuột.

@Override
public void mousePressed(MouseEvent e) {
    
    x = e.getX();
    y = e.getY();
}

Trong phương thức mousePressed(), chúng ta lưu lại vị trí của chuột.

int dx = e.getX() - x;
int dy = e.getY() - y;

if (zrect.isHit(x, y)) { 
    zrect.addX(dx); 
    zrect.addY(dy); 
    repaint(); 
}

if(zell.isHit(x, y)) {
    zell.addX(dx);
    zell.addY(dy);
    repaint();
}

x += dx; 
y += dy;

Trong phương thức doMove(), nếu vị trí của chuột nằm trong vùng của hình thì chúng ta tính khoảng cách x, y hiện tại của chuột với vị trí khi click rồi di chuyển hình theo đúng vị trí đó.

Lớp ScaleHandler phụ trách việc phóng to/thu nhỏ hình.

if (e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL) {

    if (zrect.isHit(x, y)) {
        
        float amount =  e.getWheelRotation() * 5f;
        zrect.addWidth(amount);
        zrect.addHeight(amount);
        repaint();
    }
...
}

Nếu chuột đang nằm trong hình và chúng ta lăn con lăn trên chuột thì kích thước của hình sẽ được phóng to hoặc thu nhỏ. Phương thức getWheelRotation() trả về số góc mà con lăn đã quay.

 Capture

Ví dụ 3

Trong ví dụ này, chúng ta vẽ một hình chữ nhật lớn, ở hai góc của hình chữ nhật này có 2 hình vuông nhỏ, chúng ta có thể kéo các hình vuông nhỏ này để thay đổi kích thước của hình chữ nhật lớn.

import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private Point2D[] points;
    private final int SIZE = 8;
    private int pos;

    public Surface() {

        initUI();
    }

    private void initUI() {

        addMouseListener(new ShapeTestAdapter());
        addMouseMotionListener(new ShapeTestAdapter());
        pos = -1;

        points = new Point2D[2];
        points[0] = new Point2D.Double(50, 50);
        points[1] = new Point2D.Double(150, 100);
    }
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2 = (Graphics2D) g;

        for (Point2D point : points) {
            double x = point.getX() - SIZE / 2;
            double y = point.getY() - SIZE / 2;
            g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
        }

        Rectangle2D r = new Rectangle2D.Double();
        r.setFrameFromDiagonal(points[0], points[1]);

        g2.draw(r);        
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        
        doDrawing(g);
    }

    private class ShapeTestAdapter extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent event) {

            Point p = event.getPoint();

            for (int i = 0; i < points.length; i++) {

                double x = points[i].getX() - SIZE / 2;
                double y = points[i].getY() - SIZE / 2;

                Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);

                if (r.contains(p)) {

                    pos = i;
                    return;
                }
            }
        }

        @Override
        public void mouseReleased(MouseEvent event) {

            pos = -1;
        }

        @Override
        public void mouseDragged(MouseEvent event) {

            if (pos == -1) {
                return;
            }

            points[pos] = event.getPoint();
            repaint();
        }
    }
}

public class ResizingRectangleEx extends JFrame {

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

        setTitle("Resize rectangle");
        setSize(300, 300);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);                  
    }
    
    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Có hai cách để vẽ một hình chữ nhật, cách thứ nhất là cho tọa độ x, y của điểm trái-trên và số đo dài-rộng rồi vẽ, cách thứ hai là cho tọa độ x, y của 2 điểm trái-trên và phải-dưới rồi vẽ. Trong ví dụ này chúng ta dùng cả hai cách đó.

private Point2D[] points;

Đầu tiên là hai chình vuông nhỏ, chúng ta lưu trong 2 đối tượng Point2D.

private final int SIZE = 8;

Kích thước của hai hình vuông lưu trong hằng số SIZE.

points = new Point2D[2];
points[0] = new Point2D.Double(50, 50);
points[1] = new Point2D.Double(150, 100);

Khởi tạo vị trí ban đầu cho hai hình vuông.

for (int i = 0; i < points.length; i++) {

    double x = points[i].getX() - SIZE / 2;
    double y = points[i].getY() - SIZE / 2;
    g2.fill(new Rectangle2D.Double(x, y, SIZE, SIZE));
}

Trong phương thức doDrawing(), đầu tiên chúng ta vẽ hai hình vuông nhỏ trước.

Rectangle2D s = new Rectangle2D.Double();
s.setFrameFromDiagonal(points[0], points[1]);

g2.draw(s);

Tiếp theo chúng ta vẽ một hình chữ nhật dựa theo hai hình vuông nhỏ.

@Override
public void mousePressed(MouseEvent event) {

    Point p = event.getPoint();

    for (int i = 0; i < points.length; i++) {

        double x = points[i].getX() - SIZE / 2;
        double y = points[i].getY() - SIZE / 2;

        Rectangle2D r = new Rectangle2D.Double(x, y, SIZE, SIZE);

        if (r.contains(p)) {

            pos = i;
            return;
        }
    }
}

Trong phương thức mousePressed(), chúng ta kiểm tra xem chuột có nằm trong một trong hai hình vuông nhỏ hay không, nếu có thì lưu thứ tự của hình vuông đó lại trong biến pos.

@Override
public void mouseDragged(MouseEvent event) {

    if (pos == -1) {
        return;
    }

    points[pos] = event.getPoint();
    repaint();
}

Trong phương thức mouseDragged(), nếu chuột đang nằm trong một hình vuông thì chúng ta cập nhật lại tọa độ của hình vuông đó theo tọa độ của chuột.

Capture

Java 2D – Font chữ

Trong phần này chúng ta sẽ tìm hiểu về hệ thống font chữ.

Vẽ kí tự lên màn hình là một chủ đề phức tạp, có khi viết nguyên một quyển sách cũng chưa hết, nên trong bài này mình chỉ bàn về một số thao tác cơ bản.

Có hai loại font là font vật lý (Physical Font) và font logic (Logical Font). Font vật lý là font thật, được lưu trong hệ điều hành, font logic không phải font thật mà thực ra chỉ là 5 hệ font được định nghĩa bởi Java: Serif, SansSerif, Monospaced, Dialog, DialogInput. Khi được gọi thì font logic sẽ được Java map vào font vật lý.

Một Font là một tập hợp các ký tự, một kí tự được gọi là TypeFace, tập hợp các TypeFace giống nhau nhưng khác kiểu font (hoặc các tính chất khác như độ lớn, độ nghiêng…) được gọi là Font-Family.

Font trong hệ điều hành

Đoạn code dưới đây sẽ in ra toàn bộ font chữ hiện được hỗ trợ bởi hệ điều hành của bạn.
AllFontsEx.java
import java.awt.Font;
import java.awt.GraphicsEnvironment;

public class AllFontsEx {

    public static void main(String[] args) {

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        Font[] fonts = ge.getAllFonts();

        for (Font font : fonts) {
            System.out.print(font.getFontName() + " : ");
            System.out.println(font.getFamily());
        }
    }
}

Bản thân Java không chứa thông tin về các kiểu font trong hệ điều hành, Java chỉ lấy thông tin về font của hệ điều hành để sử dụng. Mỗi hệ điều hành có bộ font khác nhau. Để lấy thông tin về font chữ trong hệ điều hành thì chúng ta sử dụng các lớp GraphicsEnvironment, Font.

Font[] fonts = ge.getAllFonts();

Phương thức getAllFonts() trả về danh sách font hiện có trong GraphicsEnvironment.

System.out.print(fonts[i].getFontName() + " : ");
System.out.println(fonts[i].getFamily());

Chúng ta dùng phương thức getFontName()getFamimy() để lấy về tên font và tên family.

...
.VnArial : .VnArial
.VnArial Bold : .VnArial
.VnArial Bold Italic : .VnArial
.VnArial Italic : .VnArial
.VnArial Narrow : .VnArial Narrow
.VnArial Narrow Bold : .VnArial Narrow
.VnArial Narrow Italic : .VnArial Narrow
.VnArial NarrowH : .VnArial NarrowH
.VnArialH : .VnArialH
.VnArialH Bold Italic : .VnArialH
...

In text lên màn hình

Trong ví dụ dưới đây, chúng ta sẽ in một đoạn lyric lên panel.

import java.awt.EventQueue;
import java.awt.Font;
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;

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

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

        g2d.setRenderingHints(rh);

        g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18));

        g2d.drawString("Most relationships seem so transitory", 20, 30);
        g2d.drawString("They're all good but not the permanent one", 20, 60);
        g2d.drawString("Who doesn't long for someone to hold", 20, 90);
        g2d.drawString("Who knows how to love you without being told", 20, 120);
        g2d.drawString("Somebody tell me why I'm on my own", 20, 150);
        g2d.drawString("If there's a soulmate for everyone", 20, 180);        
    }

    @Override
    public void paintComponent(Graphics g) {
        
        super.paintComponent(g);
        doDrawing(g);
    }
}

public class SoulmateEx extends JFrame {
    
    public SoulmateEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        setTitle("Soulmate");

        add(new Surface());

        setSize(420, 250);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);      
    }
 
    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {
            
            @Override
            public void run() {
                SoulmateEx ex = new SoulmateEx();
                ex.setVisible(true);
            }
        });
    }
}

Để thiết lập kiểu font mong muốn thì chúng ta dùng phương thức setFont().

g2d.setFont(new Font("NewellsHand", Font.PLAIN, 18));

Để in một đoạn text thì chúng ta dùng phương thức Graphics2D.drawString().

g2d.drawString("Most relationships seem so transitory", 20, 30);
Capture

Tạo hiệu dứng đổ bóng cho chữ

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextLayout;
import java.awt.image.BufferedImage;
import java.awt.image.ConvolveOp;
import java.awt.image.Kernel;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;


public class ShadowedTextEx extends JFrame {

    private final int width = 300;
    private final int height = 130;

    private final String text = "War is hell";
    private TextLayout textLayout;

    public ShadowedTextEx() {

        initUI();
    }

    private void initUI() {
        
        setTitle("Shadowed Text");
        
        BufferedImage image = createImage();
        add(new JLabel(new ImageIcon(image)));
        
        setSize(300, 130);
        setLocationRelativeTo(null);       
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void setRenderingHints(Graphics2D g) {
        
        g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                           RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        g.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
                           RenderingHints.VALUE_FRACTIONALMETRICS_ON);
    }

    private BufferedImage createImage()  {

        int x = 10;
        int y = 100;

        Font font = new Font("Georgia", Font.ITALIC, 50);
        
        BufferedImage image = new BufferedImage(width, height, 
                BufferedImage.TYPE_INT_RGB);
        Graphics2D g1d = image.createGraphics();
        setRenderingHints(g1d);
        textLayout = new TextLayout(text, font, g1d.getFontRenderContext());
        g1d.setPaint(Color.WHITE);
        g1d.fillRect(0, 0, width, height);

        g1d.setPaint(new Color(150, 150, 150));
        textLayout.draw(g1d, x+3, y+3);
        g1d.dispose();

        float[] kernel = {
          1f / 9f, 1f / 9f, 1f / 9f, 
          1f / 9f, 1f / 9f, 1f / 9f, 
          1f / 9f, 1f / 9f, 1f / 9f 
        };

        ConvolveOp op =  new ConvolveOp(new Kernel(3, 3, kernel), 
                ConvolveOp.EDGE_NO_OP, null);
        BufferedImage image2 = op.filter(image, null);

        Graphics2D g2d = image2.createGraphics();
        setRenderingHints(g2d);
        g2d.setPaint(Color.BLACK);
        textLayout.draw(g2d, x, y);
        
        g2d.dispose();

        return image2;
    }        

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {

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

Để tạo bóng cho một đoạn text thì chúng ta vẽ hai đoạn text, một đoạn text chính và một đoạn text dùng làm bóng, trong đó đoạn text đổ bóng được làm mờ, có vị trí lệch một ít với đoạn text gốc.

textLayout = new TextLayout(text, font, g1d.getFontRenderContext());

Chúng ta dùng lớp TextLayout để quy định các tính chất cho đoạn text, lớp này cho phép chúng ta thao tác sâu hơn với font.

textLayout.draw(g1d, x+3, y+3);

Chúng ta vẽ đoạn text lên màn hình có vị trí lệch 3 pixel so với đoạn text gốc.

float[] kernel = {
    1f / 9f, 1f / 9f, 1f / 9f, 
    1f / 9f, 1f / 9f, 1f / 9f, 
    1f / 9f, 1f / 9f, 1f / 9f 
};

ConvolveOp op =  new ConvolveOp(new Kernel(3, 3, kernel), 
        ConvolveOp.EDGE_NO_OP, null);

Để tăng thêm hiệu ứng thì chúng ta áp dụng hiệu ứng mờ lên đoạn text.

BufferedImage image2 = op.filter(image, null);

Chúng ta áp dụng hiệu ứng mờ lên đoạn text gốc rồi gán vào đoạn text thứ hai dùng làm đổ bóng.

textLayout.draw(g2d, x, y);

Sau đó chúng ta vẽ đoạn text gốc để đảm bảo đoạn text gốc nằm đè lên đoạn text đổ bóng.

Capture

Một số tính chất khác

import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.font.TextAttribute;
import java.text.AttributedString;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {

    private final String words = "Valour fate kinship darkness";
    private final String java = "Java TM";

    private void doDrawing(Graphics g) {

        Graphics2D g2d = (Graphics2D) g;

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        Font font = new Font("Serif", Font.PLAIN, 40);

        AttributedString as1 = new AttributedString(words);
        as1.addAttribute(TextAttribute.FONT, font);

        as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6);
        as1.addAttribute(TextAttribute.UNDERLINE, 
                TextAttribute.UNDERLINE_ON, 7, 11);
        as1.addAttribute(TextAttribute.BACKGROUND, Color.LIGHT_GRAY, 12, 19);
        as1.addAttribute(TextAttribute.STRIKETHROUGH,
                TextAttribute.STRIKETHROUGH_ON, 20, 28);

        g2d.drawString(as1.getIterator(), 15, 60);

        AttributedString as2 = new AttributedString(java);

        as2.addAttribute(TextAttribute.SIZE, 40);
        as2.addAttribute(TextAttribute.SUPERSCRIPT,
                TextAttribute.SUPERSCRIPT_SUPER, 5, 7);

        g2d.drawString(as2.getIterator(), 130, 125);
    }

    @Override
    public void paintComponent(Graphics g) {

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

public class TextAttributesEx extends JFrame {

    public TextAttributesEx() {

        initUI();
    }

    private void initUI() {
        
        add(new Surface());
        
        setSize(620, 190);
        setTitle("Text attributes");
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(new Runnable() {

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

Để thay đổi các tính chất của text thì chúng ta dùng các lớp Font, TextAttribute AttributedString. Lớp Font quy định kiểu font, lớp TextAttribute quy định một số tính chất như màu chữ, màu nền… còn lớp AttributedString lưu thông tin về đoạn text và các tính chất của nó.

AttributedString as1 = new AttributedString(words);

Đầu tiên chúng ta tạo một đối tượng AttributeString.

as1.addAttribute(TextAttribute.FOREGROUND, Color.red, 0, 6);

Chúng ta có thể thêm một số tính chất khác vào đoạn text thông qua phương thức addAttribute(), dòng trên có ý nghĩa là thiết lập màu chữ của các kí tự từ vị trí 0 đến vị trí 6 có màu đỏ.

g2d.drawString(as1.getIterator(), 15, 60);

Sau khi đã định nghĩa các tính chất mong muốn, chúng ta vẽ đoạn text lên nhưng chúng ta sẽ lấy đoạn text đó từ phương thức AttributedString.getIterator() chứ không dùng đoạn string gốc.

Capture

Xoay chữ

Trong bài xử lý ảnh chúng ta đã học cách xoay các đối tượng hình học. Bản thân các ký tự được vẽ trong Java cũng là các đối tượng hình học, chúng ta sẽ lấy các đối tượng đó ra mà xoay rồi vẽ như vẽ một đối tượng hình học bình thường.

import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.Shape;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.AffineTransform;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;

class Surface extends JPanel {
    
    private void doDrawing(Graphics g) {
        
        Graphics2D g2d = (Graphics2D) g.create();

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        String s = "Welcome to PhoCode";

        Font font = new Font("Courier", Font.PLAIN, 13);

        g2d.translate(20, 20);

        FontRenderContext frc = g2d.getFontRenderContext();

        GlyphVector gv = font.createGlyphVector(frc, s);
        int length = gv.getNumGlyphs();

        for (int i = 0; i < length; i++) {
            
            Point2D p = gv.getGlyphPosition(i);
            double theta = (double) i / (double) (length - 1) * Math.PI / 3;
            AffineTransform at = AffineTransform.getTranslateInstance(p.getX(),
                    p.getY());
            at.rotate(theta);

            Shape glyph = gv.getGlyphOutline(i);
            Shape transformedGlyph = at.createTransformedShape(glyph);
            g2d.fill(transformedGlyph);
        }        
        
        g2d.dispose();
    }    
    
    @Override
    public void paintComponent(Graphics g) {

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

public class RotatedTextEx extends JFrame {
    
    public RotatedTextEx() {
        
        initUI();
    }
    
    private void initUI() {
        
        add(new Surface());
        
        setTitle("Rotated text");
        setSize(280, 210);
        setLocationRelativeTo(null);        
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        
        EventQueue.invokeLater(new Runnable() {

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

Chúng ta sẽ dùng lớp FontRenderContext và lớp GlyphVector, lớp FontRenderContext chứa các thông tin cần thiết để lấy kích thước của kí tự, lớp GlyphVector lưu thông tin về các đặc điểm hình học của kí tự.

GlyphVector gv = font.createGlyphVector(frc, s);

Đầu tiên chúng ta tạo một đối tượng GlyphVector, trong đó lưu những thông tin về hình ảnh, vị trí…

int length = gv.getNumGlyphs();

Mỗi kí tự trong đoạn text sẽ được lưu trong một đối tượng hình học riêng, nên chúng ta lấy về số lượng các kí tự (cũng như các đối tượng hình học).

Point2D p = gv.getGlyphPosition(i);

Sau đó chúng ta lặp qua từng kí tự, tại mỗi lần lặp chúng ta lấy ra tọa độ của đối tượng vẽ ra kí tự đó bằng phương thức getGlyphPosition().

double theta = (double) i / (double) (length - 1) * Math.PI / 3;

Tiếp theo chúng ta tính góc xoay cho đối tượng đó dựa vào thứ tự của nó trong chuỗi.

AffineTransform at = AffineTransform.getTranslateInstance(p.getX(),
    p.getY());
at.rotate(theta);

Tiếp theo chúng ta xoay bằng cách sử dụng lớp AffineTransform.

Shape glyph = gv.getGlyphOutline(i);
Shape transformedGlyph = at.createTransformedShape(glyph);

Cuối cùng chúng ta dùng phương thức getGlyphOutline() để lấy về đối tượng hình học của kí tự hiện tại rồi dùng phương thức createTransformedShape() để tạo ra đối tượng đó ở trạng thái đã xoay.

g2d.fill(transformedGlyph);

Cuối cùng chúng ta vẽ đối tượng đó lên Panel.

Capture