Sử dụng JTable của Swing trong java (phần 6)

https://codersontrang.com/2012/09/04/su-dung-jtable-cua-swing-trong-java-phan-6/

Như đã giới thiệu ở phần 5, trong phần này, chúng ta sẽ học cách sử dụng và thao tác với các tiêu đề trong một bảng. Mỗi cột trong một bảng đều cần phải có một tiêu đề để giúp người sử dụng hình dung ra các giá trị trong cột đó là để biểu hiện cái gì. Trong phần 2 của loạt bài viết về JTable này, chúng ta đã được biết làm sao để tạo tiêu đề cho các cột trong bảng và phần này, sẽ bổ sung thêm những điều thú vị khi thao tác với các tiêu đề trong một bảng.

Việc tạo ra tiêu đề cho các cột trong một bảng không còn xa lạ với chúng ta. Tuy nhiên việc tạo ra các tiêu đề như thế chỉ giúp về mặt hiển thị và không hề có một chức năng gì khác nữa. Chẳng hạn như chúng chẳng có phản ứng gì khi chúng ta nhấp chuột vào chúng, hoặc đoạn chữ cho tiêu đề chỉ có thể nằm trên một dòng duy nhất.

Khi thiết kế giao diện, sẽ có lúc chúng ta cần đòi hỏi các chức năng khác nữa từ các tiêu đề này. Cho ví dụ, chúng ta có thể làm một tooltip để miêu tả rõ hơn về mỗi tiêu đề, hoặc cho phép một cột có thể được chọn và tự động sắp xếp theo thứ tự giảm dần hoặc tăng dần của các giá trị trong một cột bằng việc nhấp chuột vào tiều đề đó, hoặc cho phép đoạn chữ trong tiêu đề cho thể được ngắt ra làm nhiều dòng. Các chức năng này hoàn toàn có thể làm được nếu chúng ta đã nắm vững về cách làm việc của các tiêu đề trong JTable.

Vẽ tiêu đề

Ô tiêu đề cho một cột trong bảng cũng giống như các ô dữ liệu khác, được vẽ nên bởi các renderer. Renderer mặc định cho một ô trong bảng sẽ kế thừa từ một JLabel và ô tiêu đề cũng không ngoại lệ. Một renderer cho một ô tiêu đề của một cột có thể được lấy ra từ phương thức getHeaderRenderer() của đối tượng TableColumn tương ứng với cột đó. Cho ví dụ, đoạn mã sau sẽ lấy ra một tham chiếu đến renderer của tiêu đề cho cột thứ hai trong một bảng:


JTable table;
[...]
TableColumnModel tcm = table.getColumnModel();
TableColumn tc = tcm.getColumn(1);
TableCellRenderer tcr = tc.getHeaderRenderer();

Ngoài khả năng có thể lấy về một renderer cho tiêu đề của một cột nhất định, chúng ta còn có thể chỉ định một renderer cụ thể cho một tiêu đề của cột đó. Trong ví dụ của chúng ta, nếu chúng ta muốn tạo một renderer để giúp đoạn chữ trong tiêu đề của một cột có thể hiển thị trên nhiều dòng khác nhau, thì khi đó renderer phải kế thừa một JPanel chứa nhiều JLabel thành các dòng thay vì chỉ một JLabel như mặc định. Bởi vì JLabel không hỗ trợ viết một đoạn chữ trên nhiều dòng giống như JPanel. Như vậy, đầu tiên ta phải tạo một renderer có tên là MultiLineHeaderRenderer nằm trong file MultiLineHeaderRenderer.java như sau:


public class MultiLineHeaderRenderer extends JPanel implements TableCellRenderer{

    public Component getTableCellRendererComponent(JTable table, Object value, 
                         boolean isSelected, boolean hasFocus, int row, int column) {
        JLabel label;
        removeAll();

        StringTokenizer strtok = new StringTokenizer((String)value, "\r\n");
        setLayout(new GridLayout(strtok.countTokens(), 1));
        while(strtok.hasMoreElements()){
            label = new JLabel((String)strtok.nextElement(), JLabel.CENTER);
            LookAndFeel.installColorsAndFont(label,"TableHeader.background", 
                                             "TableHeader.foreground", "TableHeader.font");
            add(label);
        }
        LookAndFeel.installBorder(this, "TableHeader.cellBorder");

        return this;
    }
}


Renderer trên yêu cầu tên tiêu đề của cột phải chứa các kí tự như ‘\r’ (carriage return) hoặc ‘\n’ (linefeed) ở những chỗ muốn xuống dòng. Cho ví dụ, ta thay đổi tiêu đề các cột trong lớp TableValues như sau:


public class TableValues extends AbstractTableModel{

    [...]
    public final static boolean GENDER_FEMALE = false;
    

public final static String[] columnNames = { "First Name", "Last Name", "Date of Birth", "Account\nBalance","Gender" };

public Object[][] values = { [...] }; [...] }

Bây giờ ta đặt cái MultiLineHeaderRenderer vừa tạo ở trên làm renderer cho tiêu đề của các cột trong bảng. Việc này thực hiện trong lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        [...]
        TableColumnModel tcm = table.getColumnModel();
        TableColumn tc = tcm.getColumn(TableValues.GENDER);
        tc.setCellRenderer(new GenderRenderer());
        tc.setCellEditor(new GenderEditor());
        

MultiLineHeaderRenderer mlhr = new MultiLineHeaderRenderer(); tc = tcm.getColumn(TableValues.ACCOUNT_BALANCE); tc.setHeaderRenderer(mlhr);

table.setDefaultRenderer(Float.class, new CurrencyRenderer()); JScrollPane jsp = new JScrollPane(table); pane.add(jsp, BorderLayout.CENTER); [...] } public static void main(String [] args){ [...] } }

Giờ chạy chương trình chúng ta có thể thấy tiêu đề của cột Account Balance sẽ được ngắt thành hai dòng như hình đưới đây:

Đặt Tooltip cho tiêu đề
Trong phần này chúng ta sẽ học cách thêm tooltip vào trong tiêu đề của các cột trong bảng. Bởi vì chúng ta thường cho renderer kế thừa từ các thành phần hiển thị, các thành phần hiển thị thì được kế thừa từ lớp JComponent, vì thế chúng ta có thể hoàn toàn gọi phương thức setToolTipText() từ renderer của một tiêu đề để đặt tooltip cho tiêu đề đó. Trở lại ví dụ, giờ chúng ta muốn đặt tooltip cho tiêu đề của cột Account Balance, ta chỉ cần sửa lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        [...]
        MultiLineHeaderRenderer mlhr = new MultiLineHeaderRenderer();
        

mlhr.setToolTipText("This is the person's current account balance");

tc = tcm.getColumn(TableValues.ACCOUNT_BALANCE); [...] } public static void main(String [] args){ [...] }

Giờ chúng ta chạy chương trình, khi di chuyển chuột qua tiêu tề của cột Account Balance, tooltip cho cột này sẽ hiện lên như hình dưới đây:

Ở đây, chúng ta sẽ nhận ra một vấn đề có liên quan đến sự chỉ định tooltip cho một cột. Ở ví dụ của chúng ta, chúng ta chỉ gán một renderer cho một cột duy nhất. Tuy nhiên trong thực tế thì một renderer có thể được gán cho nhiều cột trong một bảng. Hay nói cách khác, một renderer có thể được sử dụng để vẽ cho rất nhiều ô trong bảng và điều này cũng đúng cho các renderer của tiêu đề các cột. Trong ví dụ, một đối tượng của lớp MultiLineHeaderRenderer có thể được sử dụng để vẽ cho các tiêu đề của cả cột Account Balance lẫn cột Date of Birth. Tuy nhiên, việc đặt tooltip thì chỉ có thể là duy nhất cho một cột mà thôi. Nếu như ta cũng sử dụng lại renderer của cột Account Balance cho cột Date of Birth thì tooltip của cột Account Balance cũng trở thành tooltip của cột Date of Birth và như vậy thì hoàn toàn không thích hợp như hình dưới đây:

Có rất nhiều cách để khắc phục vấn đề này. Một trong những cách dễ nhất là gán cho các cột các đối tượng renderer khác nhau. Chẳng hạn, trong ví dụ chúng ta sẽ gán renderer là hai đối tượng khác nhau của lớp MultiLineHeaderRenderer cho hai cột Account Balance và Date of Birth. Ngoài ra, chúng ta còn có cách khác sẽ được miêu tả sau.

JTableHeader

Trở lại với phần 1 của loạt bài viết này, khi đó ta chạy chương trình ví dụ, thì bảng hiện lên không có tiêu đề cho các cột. Vấn đề này được khắc phục trong phần 2 khi mà JTable của chúng ta được thêm vào một JScrollPane và phương thức getColumnName() trong model được cài đặt để trả về tiêu đề cho các cột trong bảng. Tiêu đề của các cột sẽ tự động hiển thị khi mà bảng của chúng ta được hiển thị trong một JScrollPane và cụ thể hơn, chúng được hiển thị trong vùng gọi là column header viewport của cửa sổ cuộn. Column header viewport là phần không gian ở bên trên phần không gian chính của một JScrollPane, và tất nhiên, chúng ta hoàn toàn có thể truy cập, chỉnh sửa các thành phần được hiển thị trong vùng này sử dụng hai phương thức là getColumHeader()setColumnHeader() của JScrollPane. Cho ví dụ, giả sử chúng ta muốn hiển thị một nút bấm JButton trong phần column header viewport của cửa sổ cuộn trong ví dụ, chúng ta có thể tạm thời sửa lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        [...]
        table.setDefaultRenderer(Float.class, new CurrencyRenderer());
        

JScrollPane jsp = new JScrollPane(table){ public void setColumnHeaderView(Component comp){ super.setColumnHeaderView(new JButton("This is a JButton")); } };

pane.add(jsp, BorderLayout.CENTER); [...] } public static void main(String [] args){ [...] } }

Đoạn mã trên đã cài đặt lại phương thức setColumnHeaderView() JScrollPane và điều này dẫn tới việc một JButton được hiển thị trong phần column header viewport của JScrollPane như hình dưới đây:

Mặc dù chúng ta thường không có nhu cầu để sử dụng phương thức này, nhưng nó chỉ ra rằng, chúng ta có thể sử dụng bất cứ thành phần nào để làm thành một tiêu đề. Một câu hỏi đặt ra đó là, nếu chúng ta không chỉ ra rõ ràng như ở ví dụ trên thì mặc định thành phần nào sẽ được sử dụng? Câu trả lời đó là một đối tượng của lớp JTableHeader.

JTableHeader là một thành phần hiển thị để cung cấp cho chúng ta các hành vi tương tác liên quan đến việc di chuyển và thay đổi kích thước các cột của một JTable. Cho ví dụ, trong phần 2 của loạt bài viết, chúng ta có đề cập về việc thay đổi kích thước của một cột trong bảng, và đây chính là chúng ta đang tương tác với một đối tượng của JTableHeader. Ngoài ra, chúng ta còn có thể xắp xếp lại các cột vào các vị trí mà chúng ta muốn bằng các nhấp chuột lên từng cột đó, giữ chuột trái rồi kéo thả đến vị trí chúng ta muốn. Và đó cũng là một trong những chức năng mà JTableHeader cung cấp cho chúng ta.

Một trong những chức năng khác của JTableHeader là trả về đoạn chữ tooltip khi mà chúng ta di chuột lên trên tiêu đề của một cột nào đó trong bảng. Bên trên chúng ta đã nói về việc sinh ra tooltip cho một cột là nhờ các renderer, và chúng ta cũng có thể ngạc nhiên là tại sao JTableHeader ở đây lại sinh ra tooltip nữa? Nhưng thực vậy, chính là JTableHeader sinh ra tooltip cho một cột trong bảng nhưng để lấy tooltip đó, nó phải chuyển yêu cầu đến renderer của cột tương ứng.

Khi phương thức getToolTipText() của JTableHeader được gọi, nó được truyền vào một tham số đầu vào là một MoustEvent để cho phép JTableHeader xác định được tiêu đề của cột nào đang được con trỏ chuột di chuyển qua. Sau đó, JTableHeader chọn ra renderer tương ứng với cột đó để lấy đoạn chữ tooltip trả về từ renderer đó. Cách này có vẻ tốt khi mà mỗi tiêu đề cho mỗi cột trong bảng có một renderer của riêng nó, nhưng sẽ không thích hợp khi mà một renderer được gán cho nhiều cột. Để khác phục vấn đề này, ta tạo một lớp kế thừa từ JTableHeader và trong nó duy trì một mảng gồm các tooltip, sau đó trả về tooltip lấy ra từ mảng đó thay vì phải gọi đến các renderer. Chúng ta tạo lớp JTableHeaderToolTips trong file JTableHeaderToolTip.java như sau:


public class JTableHeaderToolTips extends JTableHeader{
    protected String[] toolTips;
    public JTableHeaderToolTips(TableColumnModel tcm){
        super(tcm);
    }

    public void setToolTips(String [] tips){
        toolTips = tips;
    }

    @Override
    public String getToolTipText(MouseEvent event){
        String tip = super.getToolTipText(event);
        int column = columnAtPoint(event.getPoint());
        if((toolTips != null) && (column < toolTips.length) && (toolTips[column] != null)){
            tip = toolTips[column];
        }
        return tip;
    }
}

Bây giờ chúng ta sử dụng lớp JTableHeaderToolTips trong lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        [...]
        MultiLineHeaderRenderer mlhr = new MultiLineHeaderRenderer();
        tc = tcm.getColumn(TableValues.ACCOUNT_BALANCE);
        tc.setHeaderRenderer(mlhr);
        

JTableHeaderToolTips jthtt = new JTableHeaderToolTips(table.getColumnModel()); jthtt.setToolTips(new String[]{"Customer's First Name", "Customer's Last Name", "Customer's Date of Birth", "Customer's Account Balance", "Customer's Gender"}); table.setTableHeader(jthtt);

table.setDefaultRenderer(Float.class, new CurrencyRenderer()); JScrollPane jsp = new JScrollPane(table); pane.add(jsp, BorderLayout.CENTER); [...] } public static void main(String [] args){ [...] } }

Giờ chạy chương trình chúng ta sẽ có được các tooltip trên các tiêu đề của các cột:

Chúng ta có thể làm như trên đó là nhờ khả năng xác định được cột nào đang được con trỏ chuột di chuyển qua. Để làm như vậy chúng ta phải sử dụng phương thức columAtPoint() được định nghĩa trong JTableHeader. Chúng ta cũng có thể sử dụng kĩ thuật trên để hiển thị tooltip cho các ô khác trong bảng. Các bước cũng tương tự, để làm tooltip cho các ô khác thì chúng ta phải cài đặt lại phương thức getToolTipText() của JTableHeader, sử dụng hai phương thức là rowAtPoint()columAtPoint() để nhận diện ra vị trí của ô đang được con trỏ chuột di chuyển qua.

Một trường hợp khác mà chúng ta vẫn phải sử dụng đến JTableHeader đó là để phát hiện và sử lý các sự kiện chuột xảy ra trên các tiêu đề. Cho ví dụ, giả sử ứng dụng của chúng ta cho phép người sử dụng chọn một cột của bảng bằng việc kích vào tiêu đề của cột đó. Mặc định thì JTable không cho phép chúng ta làm điều này, vì vậy, chúng ta sẽ phải tự tay bắt và xử lý sự kiện nhấp chuột. Trong trường hợp yêu cầu cho tooltip, yêu cầu này sẽ được chuyển đến renderer của cột tương ứng. Tuy nhiên, trong trường hợp này của chúng ta thì không giống như vậy, để có thể bắt được sự kiện chuột, chúng ta phải đăng kí một listener cho JTableHeader. Chúng ta chỉnh sửa lớp SimpleTableTest trong file SimpleTableTest.java như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        [...]
        table = new JTable(tv);
        

table.setRowSelectionAllowed(false); table.setColumnSelectionAllowed(true);

TableColumnModel tcm = table.getColumnModel(); [...] JScrollPane jsp = new JScrollPane(table); pane.add(jsp, BorderLayout.CENTER);

addHeaderListener();

JPanel outerPanel = new JPanel(); [...] }

public void addHeaderListener(){ table.getTableHeader().addMouseListener(new MouseAdapter(){ public void mousePressed(MouseEvent event){ JTableHeader header = (JTableHeader)(event.getSource()); int index = header.columnAtPoint(event.getPoint()); table.setColumnSelectionInterval(index, index); } }); }

public static void main(String [] args){ [...] } }

Bây giờ chạy chương trình, khi chúng ta nhấp chuột vào tiêu đề của cột Date of Birth, cả cột đó sẽ được chọn như hình dưới đây:

Như vậy, trong phần này chúng ta đã tìm hiểu về các thao tác khi làm việc với tiêu đề của các cột trong bảng. Chúng ta có thể tùy biến thiết kế tiêu đề và làm cho nó thêm phong phú với các tính năng tương tác với người dùng qua các sự kiện chuột. Trong phần 7, chúng ta sẽ tiếp tục học cách để tạo một tiêu đề cho các hàng trong bảng và tạo một cột đóng băng.

Nguồn: Brett Spell – Pro Java Programming, Second Edition

Chia sẻ bài viết

Thích bài này:

Thích

Đang tải…