Album ảnh

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


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

Trong phần này, chúng ta sẽ tìm hiểu cách để sắp xếp các hàng trong một bảng theo chiều tăng dần hoặc giảm dần của các giá trị trong một cột nào đó của bảng.

Sắp xếp các hàng trong một bảng
Khi hiển thị thông tin trong một JTable, sẽ có lúc chúng ta muốn sắp xếp lại các hàng trong bảng đó theo chiều tăng dần hoặc giảm dần của các giá trị trong một hoặc nhiều cột của bảng. Bởi vì việc sắp xếp thường là chậm và sẽ trở thành phức tạp khi mà tập dữ liệu của chúng ta lớn, vì vậy chúng ta nên có các dữ liệu đã được sắp xếp sẵn từ một ứng dụng bên ngoài. Ví dụ như nếu chúng ta hiển thị dữ liệu lấy từ một cơ sở dữ liệu quan hệ, chúng ta có thể lấy được tập dữ liệu đã được sắp xếp sẵn từ câu truy vấn. Tuy nhiên, sẽ có đôi khi vì một lý do nào đó chúng ta cần phải sắp xếp dữ liệu trên chính cái bảng của chúng ta, và bởi vì JTable không trực tiếp hỗ trợ việc sắp xếp này, cho nên chúng ta phải tự tay viết những dòng lệnh để thực hiện việc này.

Để sắp xếp dữ liệu hiển thị trong một bảng, chúng ta có thể sử dụng một trong hai cách tiếp cận: Cách thứ nhất là sắp xếp dữ liệu “ngay tại chỗ”, cách thứ hai là việc sắp xếp được thực hiện thông qua một lớp nằm giữa JTable và TableModel. Sắp xếp dữ liệu ngay tại chỗ có nghĩa là chúng ta thay đổi vị trí của dữ liệu trong một mảng hoặc trong một đối tượng collection. Ví dụ như trong trường hợp của chúng ta, dữ liệu được định nghĩa trong lớp TableValues, chúng ta sẽ sắp xếp lại các giá trị này trong mảng sao cho chúng tăng dần hoặc giảm dần. Tức là trong cách tiếp cận này, ta sắp xếp lại dữ liệu trong cái model của bảng.

Một cách tiếp cận có vẻ linh động hơn đó là thêm một lớp làm việc sắp xếp giữa bảng và cái model của nó. Cụ thể hơn, nó liên quan đến việc tạo ra một TableModel thứ hai gọi là model sắp xếp (sort model). Model sắp xếp này sẽ có một tham chiếu đến cái model gốc (source model – trong ví dụ chính là TableValues) của chúng ta. Trong trường hợp này, dữ liệu trong model gốc không cần phải chuyển đổi hay thay đổi gì. Thay vào đó, model sắp xếp có thể tạo một danh sách các chỉ số. Các chỉ số này tham chiếu đến các dữ liệu trong model gốc và được sắp xếp theo thứ tự tăng dần hoặc giảm dần. Cho ví dụ, giả sử có ba giá trị kiểu String được lưu trong model gốc như sau:

  • Kirk
  • Ashworth
  • Spyres

Model sắp xếp có thể sắp xếp các giá trị này và tạo một tập các chỉ số tham chiếu đến chúng. Trong trường hợp này, các chỉ số sẽ được đánh như sau:

  • 1
  • 0
  • 2

Bằng việc sử dụng một danh sách các chỉ số để tham chiếu đến các hàng trong model gốc, dữ liệu có thể xuất hiện theo thứ tự đã sắp xếp, kể cả dù trong thực tế, dữ liệu vẫn được lưu theo thứ tự ban đầu của nó. Chúng ta tạo một lớp là SortedTableModel trong file SortedTableModel.java như sau:


public class SortedTableModel extends AbstractTableModel{

    protected TableModel sourceModel;
    protected int[] indexValues;

    public SortedTableModel(TableModel model){
        super();
        sourceModel = model;
    }

    public int getRowCount() {
        return sourceModel.getRowCount();
    }

    public int getColumnCount() {
        return sourceModel.getColumnCount();
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
        if(indexValues != null){
            rowIndex = getSourceIndex(rowIndex);
        }
        return sourceModel.getValueAt(rowIndex, columnIndex);
    }

    @Override
    public void setValueAt(Object value, int row, int column){
        if(indexValues != null){
            row = getSourceIndex(row);
        }
    }
    
    @Override
    public boolean isCellEditable(int row, int column){
        return sourceModel.isCellEditable(row, column);
    }
    
    @Override
    public String getColumnName(int column){
        return sourceModel.getColumnName(column);
    }
    
    @Override
    public Class getColumnClass(int column){
        return sourceModel.getColumnClass(column);
    }
    
    public int getSourceIndex(int index){
        if(indexValues != null){
            return indexValues[index];
        }
        return -1;
    }
    
    public void sortRows(int column, boolean ascending){
        SortedItemHolder holder;
        TreeSet sortedList = new TreeSet();
        int count = getRowCount();
        for(int i =0; i< count; i++){
            holder = new SortedItemHolder(sourceModel.getValueAt(i, column),i);
            sortedList.add(holder);
        }
        indexValues = new int[count];
        Iterator iterator = sortedList.iterator();
        int index = (ascending ?0: count-1);
        while(iterator.hasNext()){
            holder = (SortedItemHolder)(iterator.next());
            indexValues[index] = holder.position;
            index += (ascending ? 1: -1);
        }
        refreshViews();
    }
    
    public void clearSort(){
        indexValues = null;
        refreshViews();
    }
    
    public void refreshViews(){
        fireTableDataChanged();
    }
    
    class SortedItemHolder implements Comparable{

        public final Object value;
        public final int position;
        
        public SortedItemHolder(Object value, int position){
            this.value = value;
            this.position = position;
        }
        
        public int compareTo(Object parm) {
            SortedItemHolder holder = (SortedItemHolder)parm;
            Comparable comp = (Comparable)value;
            int result = comp.compareTo(holder.value);
            if(result == 0){
                result = (position < holder.position) ? -1: 1;
            }
            return result;
        }
        
        @Override
        public int hashCode(){
            return position;
        }
        
        @Override
        public boolean equals(Object comp){
            if(comp instanceof SortedItemHolder){
                SortedItemHolder other = (SortedItemHolder)comp;
                if((position == other.position) && (value == other.value)){
                    return true;
                }
            }
            return false;
        }
    }
}

Phương thức sortRows được sử dụng để chỉ ra dữ liệu sẽ được sắp xếp theo cột nào và theo thứ tự là tăng dần hay giảm dần. Lớp SortedItemHolder nằm ở bên trong lớp SortedTableModel làm nhiệm vụ sắp xếp các giá trị và tất nhiên, bên trong lớp SortedTableModel phải có một tham chiếu đến model gốc. Thêm vào đó, một phần của hai phương thức getValueAt() và setValueAt() là thực hiện việc chuyển đổi chỉ số hàng giữa hai model.

Bằng việc sử dụng lớp này, chúng ta có thể hiển thị dữ liệu trong bảng theo một thứ tự được sắp xếp. Chẳng hạn sửa đoạn mã trong lớp SimpleTableTest sau sẽ sắp xếp dữ liệu trong bảng theo giá trị tăng dần (từ trên xuống dưới) của cột Account Balance:


public class SimpleTableTest extends JFrame{
    protected JTable table;

    public SimpleTableTest(){
        Container pane = getContentPane();
        pane.setLayout(new BorderLayout());
        TableValues tv = new TableValues();
        SortedTableModel stm = new SortedTableModel(tv);
        stm.sortRows(TableValues.ACCOUNT_BALANCE, true);
        table = new JTable(stm);
        table.setRowSelectionAllowed(false);
        [...]
        JScrollPane jsp = new JScrollPane(table);
        pane.add(jsp, BorderLayout.CENTER);
        addHeaderListener();
        [...]
    }

    public void addHeaderListener(){
        [...]
    }

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

Kết quả khi chạy chương trình, các hàng trong bảng sẽ được sắp xếp lại sao cho giá trị trong cột Account Balance tăng dần như hình dưới đây:

Trong ví dụ trên, một cột được chọn để sắp xếp sẽ được cố định ở trong đoạn mã và chúng ta không thể thay đổi khi mà chương trình đã chạy. Tuy nhiên, việc tạo một giao diện để cho phép người sử dụng chọn một cột để sắp xếp theo thì cũng khá dễ dàng. Chúng ta có thể tạo một renderer cho các tiêu đề của các cột trong bảng. Renderer này sẽ phát hiện hiện tượng nhấp chuột, xác định cột nào đang được con trỏ chuột chỉ tởi và sắp xếp các dữ liệu trong bảng dựa trên các giá trị của cột đó. Chúng ta tạo lớp SortedColumnHeaderRenderer trong file SortedColumnHeaderRenderer như sau:


public class SortedColumnHeaderRenderer implements TableCellRenderer{
    protected TableCellRenderer textRenderer;
    protected SortedTableModel sortedModel;
    protected int sortColumn = -1;
    protected boolean sortAscending = true;

    public SortedColumnHeaderRenderer(SortedTableModel model, TableCellRenderer renderer){
        sortedModel = model;
        textRenderer = renderer;
    }

    public SortedColumnHeaderRenderer(SortedTableModel model){
        this(model, null);
    }

    public Component getTableCellRendererComponent(JTable table, Object value, 
                      boolean isSelected, boolean hasFocus, int row, int column) {
        Component text;
        JPanel panel = new JPanel();
        panel.setLayout(new BorderLayout());

        if(textRenderer != null){
            text = textRenderer.getTableCellRendererComponent(table, value, isSelected, 
                                                               hasFocus, row, column);
        }else{
            text = new JLabel((String)value, JLabel.CENTER);
            LookAndFeel.installColorsAndFont((JComponent)text, "TableHeader.background", 
                                              "TableHeader.foreground", "TableHeader.font");
        }
        panel.add(text,BorderLayout.CENTER);

        if(column == sortColumn){
            BasicArrowButton bab = new BasicArrowButton((sortAscending?SwingConstants.NORTH:SwingConstants.SOUTH));
            panel.add(bab,BorderLayout.WEST);
        }
        LookAndFeel.installBorder(panel, "TableHeader.cellBorder");
        return panel;
    }

    public void columnSelected(int column){
        if(column!=sortColumn){
            sortColumn = column;
            sortAscending = true;
        }else{
            sortAscending = !sortAscending;
            if(sortAscending) sortColumn = -1;
        }
        if(sortColumn != -1){
            sortedModel.sortRows(sortColumn, sortAscending);
        }else{
            sortedModel.clearSort();
        }
    }
}

Chúng ta cần chú ý đến hai điểm quan trọng trong renderer này. Thứ nhất, nó có thể được truyền vào một tham chiếu đến renderer khác. Renderer khác này có nhiệm vụ là vẽ đoạn chữ cho tiêu đề. Điều này cho phép chúng ta kết hợp được các chức năng của nhiều renderer lại với nhau. Nói cách khác, chúng ta vừa có thể tạo một bảng với các tiêu đề có thể hiển thị đoạn chữ trên nhiều dòng, vừa cho phép chúng ta chọn cột sắp xếp một cách động bằng cách nhấp chuột vào tiêu đề của cột đó.

Thứ hai, lớp này duy trì một biến để nhận diện ra cột nào sẽ được sắp xếp. Bởi vì thế, chúng ta chỉ có thể sử dụng một đối tượng duy nhất của lớp renderer này cho tất cả các cột mà chúng ta muốn chọn để sắp xếp trong bảng.

Khi chúng ta gán renderer này đến các ô tiêu đề, nó cho phép chúng ta có thể sắp xếp dữ liệu theo giá trị trong một cột bằng cách nhấp chuột vào tiêu đề của cột đó. Lần đầu tiên chúng ta kích vào tiêu đề của một cột, các hàng trong bảng sẽ được sắp xếp theo thứ tự tăng dần của giá trị trong cột đó. Nếu chúng ta kích chuột một lần nữa vào tiêu đề đó, các hàng sẽ được sắp xếp lại nhưng lần này là theo thứ tự giảm dần và lần kích chuột thứ ba sẽ là nguyên nhân để các hàng được trở về vị trí ban đầu khi chưa được sắp xếp. Khi mà giá trị trong bảng được sắp xếp, sẽ có một hiển thị xuất hiện để chỉ thị giá trị này được sắp xếp như thế nào. Đó là hình một mũi tên hướng lên trong trường hợp sắp xếp các giá trị theo thứ thự tăng dần và hình một mũi tên hướng xuống trong trường hợp sắp xếp các giá trị theo thứ tự giảm dần (từ trên xuống dưới). Chúng ta thay đổi lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;
    protected SortedColumnHeaderRenderer renderer;

    public SimpleTableTest(){
        Container pane = getContentPane();
        pane.setLayout(new BorderLayout());
        TableValues tv = new TableValues();
        SortedTableModel stm = new SortedTableModel(tv);
        table = new JTable(stm);
        table.setRowSelectionAllowed(false);
        table.setColumnSelectionAllowed(true);
        TableColumnModel tcm = table.getColumnModel();
        TableColumn tc = tcm.getColumn(TableValues.GENDER);
        tc.setCellRenderer(new GenderRenderer());
        tc.setCellEditor(new GenderEditor());
        MultiLineHeaderRenderer mlhr = new MultiLineHeaderRenderer();
        renderer = new SortedColumnHeaderRenderer(stm, mlhr);
        int count = tcm.getColumnCount();
        for(int i=0;i<count; i++){
            tc = tcm.getColumn(i);
            tc.setHeaderRenderer(renderer);
        }
        JTableHeaderToolTips jthtt = new JTableHeaderToolTips(table.getColumnModel());
        [...]
        addHeaderListener();

        [...]
    }

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

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

Sau khi chạy chương trình và nhấp chuột lên cột Date of Birth, ta sẽ thấy các giá trị trong cột này được sắp xếp lại giống như hình dưới đây:

Sử dụng interface Comparable
Một trong những giới hạn của cách sắp xếp mà chúng ta đã trình bày ở trên là việc sử dụng interface Comparable để xác định mối tương quan giữa giá trị của hai đối tượng (lớn hơn, nhỏ hơn, bằng nhau). Thông thường thì trong Java 2, hầu hết các lớp đều cài đặt interface Comparable và điều đó cho phép chúng ta có thể sắp xếp các đối tượng của chúng theo một cách thức nào đó. Cho ví dụ như các lớp Integer, Float, Long, String, Date… đều cài đặt interface Comparable. Tuy nhiên, lớp Boolean thì không cài đặt interface Comparable(1), bởi vì mặc dù tất nhiên là giá trị true không bằng với giá trị false, nhưng sẽ rất mập mờ trong việc quy định giữa true và false thì giá trị nào lớn hơn. Trong ví dụ của chúng ta, nếu chúng ta nhấp chuột vào tiêu đề của cột Gender, chương trình sẽ sinh ra một ClassCastException. Đấy là bởi vì chúng ta đang cố ép các giá trị thuộc kiểu Boolean về các giá trị kiểu Comparable.

Một trong những cách để giải quyết vấn đề trên đó là chúng ta sẽ kiểm tra kiểu dữ liệu trong cột được chọn trước khi các dữ liệu trong cột đó được sắp xếp. Việc lấy kiểu dữ liệu của một cột sẽ được thực hiện bởi phương thức getColumnClass() của TableModel và chúng ta có thể sử dụng đối tượng trả về thuộc kiểu Class của phương thức đó để xác định xem lớp của nó có cài đặt interface Comparable hay không. Trở lại ví dụ, chúng ta thực hiện việc này bằng việc thay đổi lớp SimpleTableTest như sau:


public class SimpleTableTest extends JFrame{
    protected JTable table;
    protected SortedColumnHeaderRenderer renderer;

    public SimpleTableTest(){
        [...]
    }

    public void addHeaderListener(){
        table.getTableHeader().addMouseListener(new MouseAdapter(){
            public void mousePressed(MouseEvent event){
                JTableHeader header = (JTableHeader)(event.getSource());
                int index = header.columnAtPoint(event.getPoint());
                Class dataType = table.getModel().getColumnClass(index);
                Class[] interfaces = dataType.getInterfaces();
                for(int i = 0; i<interfaces.length; i++){
                    if(interfaces[i].equals(java.lang.Comparable.class)){
                        renderer.columnSelected(index);
                        break;
                    }
                }
                table.setColumnSelectionInterval(index, index);
            }
        });

    }

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

Khi chạy chương trình, với việc sửa đổi như trên giờ đây chúng ta không thể chọn sắp xếp theo giá trị trong cột Gender như hình dưới đây:

Tuy nhiên, vẫn còn một vấn đề nữa sinh ra từ việc làm này đó là khi ta chọn các cột như First Name hoặc Last Name, các giá trị cũng không được sắp xếp. Đấy là bởi vì trong TableModel chúng ta chỉ chỉ định kiểu dữ liệu cho hai cột đó là Date of Birth và Account Balance. Các cột khác sẽ được ngầm hiểu rằng chúng có kiểu dữ liệu là Object. Hay nói cách khác, chương trình sẽ không thực hiện sắp xếp trên các cột First Name, Last Name, Gender bởi vì chương trình đang hiểu các cột này có kiểu dữ liệu là Object, mà kiểu Object thì lại không cài đặt interface Comparable. Tuy nhiên, vấn đề này có thể giải quyết một cách dễ dàng bằng cách chúng ta sửa lại phương thức getColumnClass() trong lớp TableValues như sau:


public class TableValues extends AbstractTableModel{

    public final static int FIRST_NAME = 0;
    public final static int LAST_NAME = 1;
    public final static int DATE_OF_BIRTH = 2;
    public final static int ACCOUNT_BALANCE = 3;
    public final static int GENDER = 4;

    public final static boolean GENDER_MALE = true;
    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 = {
        [...]
    };

    public int getRowCount() {
        return values.length;
    }

    public int getColumnCount() {
        return values[0].length;
    }

    public Object getValueAt(int rowIndex, int columnIndex) {
        return values[rowIndex][columnIndex];
    }

    @Override
    public String getColumnName(int column){
        return columnNames[column];
    }

    @Override
    public Class getColumnClass(int column){
        Class dataType = super.getColumnClass(column);
        if(column == ACCOUNT_BALANCE){
            dataType = Float.class;
        }else if(column == DATE_OF_BIRTH){
            dataType = java.util.Date.class;
        }else if((column == FIRST_NAME) || (column == LAST_NAME)){
            dataType = String. class;
        }else if(column == GENDER){
            dataType = Boolean.class;
        }
        return dataType;
    }

    @Override
    public boolean isCellEditable(int row, int column){
        if(column == GENDER){
            return true;
        }
        return false;
    }
    
    @Override
    public void setValueAt(Object value, int row, int column){
        values[row][column] = value;
    }
}

Bây giờ chạy chương trình, chúng ta có thể sắp xếp dữ liệu trong bảng dựa vào các giá trị của cột First Name như hình dưới đây:

Chú ý
(1): Điều này có lẽ chỉ đúng trong phiên bản java cũ mà tác giả dùng để viết ví dụ minh họa. Theo mình được biết bắt đầu từ JDK 1.5, lớp Boolean của chúng ta đã cài đặt interface Comparable và bằng chứng là khi chạy chương trình của chúng ta, cột Gender vẫn được sắp xếp bình thường như hình dưới đây:

Các bạn cũng có thể tham khảo thêm tại đây.

Như vậy sau bài viết này chúng ta đã nắm được cách để sắp xếp các hàng trong một bảng theo chiều tăng dần hoặc giảm dần của các giá trị trong một cột nào đó. Phần 9 sẽ trình bày cách thêm và xóa một dòng trong bảng.

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

2 comments on “Sử dụng JTable của Swing trong java (phần 8)

  1. cám ơn bài viết của bạn ,thật hữu ích .Mình xin hoi mot vấn đề về colum trong table tuc la gia sử mình có cột địa chỉ 30 ky tự, neu minh co 10 colum vay thi hien thị của cột địa chỉ sẽ bị thu hẹp .Mình muốn có mot scrollbar nằm dưới côt địa chỉ để kéo qua lại không biết nên viết thế nào?

    Thích

Bình luận về bài viết này