Sử dụng JTable của Swing trong java


Ngày trước ở blog cũ, mình có dịch một loạt 9 bài viết khá hay về JTable của Swing trong java. Hôm nay quyết định chuyển loạt bài này sang “nhà mới” để cùng chia sẻ với mọi người 

Nếu để ý chúng ta hẳn sẽ thấy rằng rất nhiều ứng dụng cần hiển thị dữ liệu dưới dạng bảng biểu. Trong java, Swing cung cấp cho chúng ta một lớp để thực hiện điều này đó là lớp JTable.

Bên cạnh khả năng hiển thị thông tin, JTable cũng cho phép chúng ta dễ dàng sửa đổi thông tin, đặt kích cỡ và đầu đề cho từng cột, và điều khiển cách dữ liệu hiển thị trong bảng biểu. Đầu tiên chúng ta hãy học cách để hiển thị thông tin lên bảng biểu vì đây là chức năng cơ bản và cũng cần thiết nhất. Bản chất của JTable là nó lấy dữ liệu từ một data model và hiển thị dữ liệu từ đó lên. Vì thế, trước tiên chúng ta sẽ nghiên cứu về cái data model.

Data model

Ngoài lớp JTable, Swing còn cung cấp một số các lớp, các interface khác nữa. Các lớp, interface này sẽ được sử dụng bởi lớp JTable và chúng được định nghĩa trong gói javax.swing.table. Một trong số đó là interface TableModel. Interface TableModel tạo nên một sự giao tiếp giữa một cái JTable và cái data model của nó. Giống như các thành phần khác của Swing, JTable được thiết kế theo mô hình model/view/controller mà ở đó các thành phần dùng để hiển thị (JTable )sẽ được tách riêng biệt so với các thành phần lưu trữ dữ liệu (TableModel). Điều này mang đến một sự linh động hơn trong thiết kế và tăng khả năng sử dụng lại các thành phần. Tuy nhiên, nó cũng làm cho việc sử dụng JTable trở nên phức tạp hơn. Nhưng cũng thật may mắn, Swing cũng cung cấp một số cài đặt mặc định làm cho việc sử JTable trở nên đỡ phức tạp hơn trước.

Như đã nói, TableModel có nhiệm vụ là cung cấp dữ liệu hiển thị cho JTable. Bên cạnh đó, nó cũng có nhiệm vụ cung cấp cho JTable một vài thông tin khác như:

  • Số lượng dòng và số lượng cột trong bảng

  • Kiểu dữ liệu trong từng cột

  • Tiêu đề cho từng cột

  • Có cho phép sửa giá trị trong một ô hay không


Interface TableModel bao gồm 9 phương thức cần được cài đặt giống như dưới đây.



Cần phải cài đặt cả 9 phương thức thực sự không thích hợp trong những trường hợp chúng ta muốn tạo nhanh một bảng. Nhưng java cũng đã cung cấp cho chúng ta các lớp khác như làAbstractTableModel và DefaultTableModel. Cả hai lớp này đều đã cài đặt phần nào các phương thức của interface TableModel. Vì thế, khi sử dụng thì chúng ta sẽ tốn ít công hơn. Chẳng hạn như nếu chúng ta kế thừa lớp AbstractTableModel thì chúng ta chi cần cài đặt 3 phương thức sau:

  • Phương thức trả về số dòng của bảng

  • Phương thức trả về số cột của bảng

  • Phương thức trả về giá trị của mỗi ô


Dưới đây, chúng ta tạo một lớp là TableValues trong file TableValues.java. Lớp này kế thừa lớp AbstractTableModel và sẽ là data model cho một cái JTable.
 
public class TableValues extends AbstractTableModel{

public final static boolean GENDER_MALE = true;
public final static boolean GENDER_FEMALE = false;

public Object[][] values = {
{
"Clay", "Ashworth",
new GregorianCalendar(1962, Calendar.FEBRUARY, 20).getTime(),
new Float(12345.67), new Boolean(GENDER_MALE)
}, {
"Jacob", "Ashworth",
new GregorianCalendar(1987, Calendar.JANUARY, 6).getTime(),
new Float(23456.78), new Boolean(GENDER_MALE)
}, {
"Jordan", "Ashworth",
new GregorianCalendar(1989, Calendar.AUGUST, 31).getTime(),
new Float(34567.89), new Boolean(GENDER_FEMALE)
}, {
"Evelyn", "Kirk",
new GregorianCalendar(1945, Calendar.JANUARY, 16).getTime(),
new Float(-456.70), new Boolean(GENDER_FEMALE)
}, {
"Belle", "Spyres",
new GregorianCalendar(1907, Calendar.AUGUST, 2).getTime(),
new Float(567.00), new Boolean(GENDER_FEMALE)
}
};

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

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

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

}


Ở đây, dữ liệu chính là mảng hai chiều values. Vì để cho ví dụ đơn giản nên chúng ta viết dữ liệu ở trong code (hard-code). Tuy nhiên, ở ngoài thực tế, dữ liệu của chúng ta thường sẽ lấy ra từ cơ sở dữ liệu. Như đã nói ở trên, vì kết thừa lớp AbstractTableModel, nên chúng ta phải cài đặt 3 phương thức đó là getRowCount()getCollumnCount() và getValueAt()

Bây giờ chúng ta tạo một lớp SimpleTableTest để tạo một JTable và gán cho nó cái model vừa tạo ở trên
Đoạn code trong file SimpleTableTest.java sẽ như sau:
 
public class SimpleTableTest extends JFrame{
protected JTable table;

public SimpleTableTest(){
Container pane = getContentPane();
pane.setLayout(new BorderLayout());
TableValues tv = new TableValues();
table = new JTable(tv);
pane.add(table, BorderLayout.CENTER);
}

public static void main(String [] args){
SimpleTableTest stt = new SimpleTableTest();
stt.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
stt.setSize(400, 200);
stt.setVisible(true);
}
}


Trong contructor của lớp SimpleTableTest ta có hai dòng lệnh sau:
 
TableValues tv = new TableValues();
table = new JTable(tv);

Dòng lệnh thứ nhất là tạo ra một cái data model tên là tv. Dòng lệnh thứ hai tạo ra một cái JTable có tên là table và gán cho nó cái model tv. Sau khi chạy chương trình, dữ liệu trong model tv sẽ được hiển thị lên cái bảng table như sau:



Như vậy, chúng ta cũng thấy được, việc sử dụng AbstractTableModel có vẻ nhẹ nhành hơn rất nhiều so với việc sử dụng TableModel. Bên cạnh đó, việc sử dụng DefaultTableModel còn dễ dàng hơn nhiều so với việc sử dụng AbstractTableModel tuy nhiên nó lại không được khuyến khích để sử dụng. Nguyên nhân là bởi vì bản thân DefaultTableModel tự động tạo ra các tham chiếu đến các ô dữ liệu thay vì chúng ta phải cài đặt một phương thức nào đó giống như getValueAt() khi sử dụng AbstracTableModel. Điều này dẫn tới một sự không mềm dẻo và không thể mở rộng trong quá trình sử dụng. Bên cạnh đó, một vấn đề khác gặp phải chính là vấn đề sửa dữ liệu ở các ô. Để hiểu tại sao DefaultTableModel không có khả năng mở rộng, chúng ta cần phải hiểu cái JTable nó làm việc như thế nào.

Như chúng ta đã biết, TableModel có nhiệm vụ chỉ ra trong bảng có bao nhiêu dòng và có bao nhiêu cột. Hai phương thức getRowCount() và getColumnCount() sẽ được gọi ngay khi một bảng được tạo và hiển thị. Tuy nhiên, một bảng sẽ không bao giờ tham chiếu đến toàn bộ dữ liệu trong TableModel mà chỉ tham chiếu đến một phần nào đó để đủ hiển thị. Cho ví dụ như sau, giả sử chúng ta tạo một data model để trả về giá trị là 100 từ phương thức getRowCount(), nhưng cái bảng của chúng ta thì nằm trong một cái cửa sổ cuộn mà mỗi lần chúng ta chỉ nhìn tối đa được 10 dòng. Khi bảng được hiển thị, đầu tiên nó sẽ truy cập đến 10 dòng đầu của dữ liệu trong TableModel, và sẽ truy cập đến dữ liệu cho các dòng khác chỉ khi chúng ta cuộn thanh cuộn xuống. Tại sao điều này lại quan trọng như vậy? Bởi vì như thế nó cho phép chúng ta hiển thị một lượng lớn dữ liệu vào trong JTable mà không cần tải toàn bộ dữ liệu vào bộ nhớ một cách đồng thời. Lúc này, TableModel chỉ cần tải dữ liệu theo nhu cầu, và giảm thiểu tối đa dung lượng bộ nhớ được sử dụng.

Với điều này, giờ chúng ta quay lại việc sử dụng DefaultTableModel và việc nó tự tạo ra các tham chiếu đến dữ liệu bên trong nó. Bởi vì nó yêu cầu tham chiếu tới toàn bộ dữ liệu, nên toàn bộ dữ liệu phải nằm ở bên trong bộ nhớ chừng nào chúng ta vẫn còn sử dụng cái model đó. Đây chính là nhược điểm của việc sử dụng DefaultTableModel và một lời khuyên là chúng ta nên dùng AbstractTableModel để thay thế. Tuy nhiên, với một lượng dữ liệu nhỏ, chúng ta vẫn có thể cân nhắc sử dụng DefaultTableModel bởi vì nó đơn giản. Hơn nữa dữ liệu được cache sẵn vào trong bộ nhớ sẽ giúp chúng ta truy cập nhanh hơn.

Trở lại với ví dụ ở trên, chúng ta thấy rằng có một vấn đề đó là khi của sổ của chúng ta thay đổi sao cho kích thước của nó nhỏ hơn kích thước của bảng thì có một phần dữ liệu sẽ bị mất không thể nhìn thấy như hình dưới đây:



Thêm vào nữa, các cột của chúng ta chưa có tiêu đề. Tất cả những vẫn đề này sẽ được giải quyết trong phần 2.

 

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


rong phần 1 chúng ta đã biết cách đưa dữ liệu từ một TableModel lên một JTable. Trong phần này chúng ta sẽ tiếp tục tìm hiểu về cách sử dụng JTable của Swing trong java như là tạo cửa sổ cuộn cho bảng, tạo tiêu đề cho các cột trong bảng và thao tác với hàng và cột.
Tạo cửa sổ cuộn cho bảng

Trong bất cứ trường hợp nào mà ở đó chúng ta có một số lượng lớn thông tin cần hiển thị, chúng ta sẽ cần đến lớp JScrollPane. Lớp này tạo ra một cửa sổ cuộn cho phép chúng ta nhìn một lượng lớn thông tin chỉ bằng thao tác kéo thanh cuộn trên cửa sổ. Trở lại ví dụ lần trước chúng ta chỉ cần thay đổi vài dòng trong lớp SimpleTableTest trong file SimpleTableTest.java. Trong đoạn code dưới đây, những dòng in đậm màu xanh là những dòng thêm mới vào đoạn code cũ.

public class SimpleTableTest extends JFrame{
protected JTable table;

public SimpleTableTest(){
[...]
table = new JTable(tv);
JScrollPane jsp = new JScrollPane(table);
pane.add(jsp, BorderLayout.CENTER);
}

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

Bây giờ chạy chương trình chúng ta sẽ thấy một thanh cuộn xuất hiện khi mà kích thước cửa sổ nhỏ hơn kích thước của bảng như hình dưới đây



Nếu để ý chúng ta cũng thấy việc tạo một cửa sổ cuộn cho bảng cũng sẽ làm xuất hiện tiêu đề cho các cột. Theo mặc định, tiêu đề các cột sẽ lần lượt được đánh theo thứ tự bảng chữ cái bắt đầu từ A rồi đến B, C… Vậy để làm sao cho tiêu đề của các cột đúng với những tên mà chúng ta mong muốn. Phần dưới đây sẽ hướng dẫn tạo tiêu đề cho các cột.
Tạo tiêu đề cho các cột trong bảng

Trước tiên phải nhớ tạo ra một cửa sổ cuộn cho bảng đã. Nếu như không tạo cửa sổ cuộn cho bảng thì tiêu đề các cột cũng không thể xuất hiện. Sau đó, chúng ta có thể đặt tên cho tiêu đề của các cột trong bảng bằng cách cài đặt phương thức getColumnName() trong TableModel của chúng ta. Trong ví dụ, chúng ta chỉ việc sửa lớp TableValues bằng cách thêm vào các đoạn mã màu xanh sau:

public class TableValues extends AbstractTableModel{

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 Balance","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];
}
}


Giờ chạy chương trình, các tiêu đề ở các cột đã có tên rất đàng hoàng



Chúng ta để ý rằng khi mà kích thước cửa sổ nhỏ hơn kích thước của bảng. Một thanh cuộn dọc sẽ xuất hiện nhưng lại không có một thanh cuộn ngang. Thay vào đó, các cột trong bảng sẽ tự động co lại giống như hình dưới đây.



Để hiểu tại sao điều này lại xảy ra, chúng ta cần phải tìm hiểu về thiết kế của JTable và các lớp hỗ trợ của nó làm việc như thế nào.
Thiết kế hướng cột (column-oriented design) của JTable

Thành phần JTable được thiết kế hướng cột, mỗi một JTable sẽ có một tham chiếu đến một cài đặt của interface TableColumnModel. Một cài đặt của TableColumnModel chẳng hạn nhưDefaultTableColumnModel nằm trong gói javax.swing.table và miêu tả một tập hợp các cột ở trong một cái JTable. Mỗi một cột được biểu diễn là một đối tượng của lớp TableColumn. Cho ví dụ như sau, giả sử chúng ta tạo một TableModel trong đó số cột của bảng được chỉ định là 5. Nếu sau đó chúng ta tạo một đối tượng của lớp JTable và đối tượng này sử dụng cái model vừa tạo thì lúc đó một đối tượng của lớp DefaultTableColumnModel sẽ được tạo ra. Đối tượng của lớp JTable sẽ lấy thông tin về số cột của bảng từ TableModel, sau đó tạo ra 5 đối tượng của lớp TableColumn rồi thêm 5 đối tượng này vào đối tượng của lớp DefaultTableColumnModel vừa tạo trước đó.

Mỗi đối tượng của lớp TableColumn mang thông tin cho một cột như tiêu đề của cột đó, giá trị về độ rộng hiện tại, độ rộng nhỏ nhất, độ rộng lớn nhất, độ rộng được ưa thích của cột đó, chỉ định xem cột đó có được phép thay đổi kích thước hay không. Ban đầu, khi mà được tạo, độ rộng hiện tại và độ rộng ưa thích của một cột được đặt giá trị là 75, độ rộng nhỏ nhất là 15 và độ rộng lớn nhất được đặt đến vô hạn (Integer.MAX_VALUE).

Sau khi chúng ta tạo một cột, chúng ta có thể thay đổi giá trị độ rộng của cột một cách trực tiếp bằng việc sử dụng các phương thức như setWithsetMaxWithsetMinWidthsetPreferedWidth.

Mỗi một đối tượng của lớp JTable đều được thiết lập một chế độ để chỉ ra cách nó xử lý khi mà kích thước của nó bị thay đổi. Chế độ này có thể rơi vào một trong 5 giá trị. Mỗi giá trị tương ứng với một hằng số được định nghĩa trong JTable như sau:

  • AUTO_RESIZE_ALL_COLUMNS

  • AUTO_RESIZE_LAST_COLUMN

  • AUTO_RESIZE_NEXT_COLUMN

  • AUTO_RESIZE_OFF

  • AUTO_RESIZE_SUBSEQUENT_COLUMNS


Những giá trị này quy định cách các cột của bảng thay đổi kích thước khi mà độ rộng của bảng hoặc một trong bảng thay đổi giá trị.
Sự thay đổi kích thước của bảng

Nếu chế độ tự động thay đổi kích thước của bảng được đặt giá trị là AUTO_RESIZE_OFF thì việc thay đổi kích thước của bảng không làm ảnh hưởng đến kích thước hiện tại của các cột trong bảng đó. Nhưng nếu nó nhận một trong bốn giá trị còn lại thì một sự thay đổi về kích thước của bảng sẽ được phân phối đều đến tất cả các cột trong bảng theo tỷ lệ dựa trên kích thước ưa thích (prefered width) của các cột. Cho ví dụ, giả sử rằng một bảng có hai cột trong đó một cột có độ rộng ưa thích là 200 và cột còn lại có độ rộng ưa thích là 100. Trong trường hợp này, cột thứ nhất sẽ chiếm hai phần ba chiều rộng của bảng, và cột thứ hai chiếm một phần ba độ rộng còn lại. Nếu bảng đó được kéo rộng ra thêm 30 pixel thì cột thứ nhất sẽ rộng ra thêm 20 pixels và cột thứ hai rộng ra thêm 10 pixel.

Nếu chế độ tự động thay đổi kích thước của bảng được đặt giá trị là AUTO_RESIZE_OFF, và tổng độ rộng của tất cả các cột lớn hơn độ rộng của bảng thì khi đó thanh cuộn ngang sẽ xuất hiện. Bây giờ quay trở lại ví dụ, ta thay đổi contructor của lớp SimpleTableTest như sau:

public class SimpleTableTest extends JFrame{
protected JTable table;

public SimpleTableTest(){
[...]
table = new JTable(tv);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
JScrollPane jsp = new JScrollPane(table);
pane.add(jsp, BorderLayout.CENTER);
}

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


Mỗi cột có một độ rộng mặc định là 75, khi độ rộng của bảng quá nhỏ để hiển thị tất cả các cột, một thanh trượt ngang xuất hiện như hình dưới đây:


Sự thay đổi kích thước của cột

Chúng ta đã được thấy việc thay đổi độ rộng của một bảng ảnh hưởng như thế nào đến độ rộng của các cột bên trong bảng đó. Chúng ta còn phải xem xét đến việc thay đổi kích thước của một cột sẽ ảnh hưởng đến kích thước của các cột khác như thế nào. Chúng ta có thể thay đổi kích thước của cột bằng việc gọi các phương thức trong code hoặc có thể thay đổi qua giao diện của JTable.

Khi kích thước của một cột thay đổi, nó sẽ ảnh hưởng đến kích thước của các cột khác tùy thuộc vào việc chế độ tự động thay đổi kích thước của bảng được đặt giá trị như thế nào. Trong phần này, chúng ta sẽ xem xét sự thay đổi trên mỗi giá trị cụ thể.

AUTO_RESIZE_OFF
Ở chế độ này khi một cột thay đổi kích thước sẽ không làm ảnh hưởng đến kích thước của các cột khác trong bảng. Nếu kích thước của bảng nhỏ hơn tổng kích thước của tất cả các cột, thanh trượt nằm ngang sẽ xuất hiện. Nếu kích thước của bảng lớn hơn tổng kích thước của tất cả các cột, sẽ có những khoảng trống trong bảng như hình dưới đây



AUTO_RESIZE_NEXT_COLUMN

Trong chế độ này, khi một cột thay đổi kích thước thì cột bên phải liền sát nó sẽ được nới rộng hoặc bị co lại. Trong hình dưới đây, cột Date of Birth tăng kích thước, cột phía bên phải liền sát nó là cột Account Balance sẽ bị co lại



AUTO_RESIZE_SUBSEQUENT_COLUMNS

Trường hợp này gần giống như trường hợp sử dụng AUTO_RESIZE_NEXT_COLUMN, ngoại trừ rằng khi một cột thay đổi kích thước thì tất cả các cột phía bên phải nó sẽ được mở rộng hay co lại. Trong hình dưới đây, cột Date of Birth mở rộng ra khiến hai cột phía bên phải nó là Account Balance và Gender bị co lại



Độ chênh lệch giữa kích thước ban đầu và kích thước về sau của cột có kích thước bị thay đổi (trong ví dụ là cột Date of Birth) gọi là giá trị delta. Giá trị này được phân phối theo tỷ lệ đến các cột phía bên phải cột đó.

AUTO_RESIZE_LAST_COLUMN

Trong chế độ này, giá trị delta được phân phối đến cột cuối cùng của bảng và có thể làm cho độ rộng của nó lớn hơn hoặc nhỏ đi. Trong hình dưới đây, khi kích thước của cột Date of Birth tăng lên là nguyên nhân cột Gender bị co lại



AUTO_RESIZE_ALL_COLUMNS

Đây chính là chế độ mặc định cho một JTable nếu chúng ta không chỉ định chế độ tự động thay đổi kích thước cho nó. Trong chế độ này, nếu ta thay đổi kích thước cho một cột, tất cả các cột khác trong bảng cũng sẽ thay đổi kích thước theo tỷ lệ. Trong hình dưới đây, cột Date of Birth tăng kích thước làm cho tất cả các cột khác nhỏ đi.



Đến đây, có một vấn đề là định dạng dữ liệu trong các cột chưa được theo ý muốn. Ví dụ, giá trị dữ liệu ở cột giới tính hiển thị là “true” và “false” trong khi nhẽ ra phải là “Male” và “Female” mới chuẩn. Hoặc như cột tài khoản hiển thị các giá trị kiểu số nhưng lại không theo chuẩn định dạng của tiền tệ.

Post a Comment

Mới hơn Cũ hơn