Jatable_Swing part 3


Như đã nói ở phần 2, ở ví dụ, dữ liệu trong các cột của bảng không hiển thị đúng như những gì mà chúng ta mong muốn. Cụ thể là ba cột sau

  • Cột Date of Birth hiển thị cả ngày và giờ trong khi đó chúng ta muốn nó chỉ hiển thị ngày thôi và định dạng của ngày sinh sẽ không có thứ.
  • Cột Account Balance chỉ hiện thị kiểu số đơn giản trong khi chúng ta muốn hiển thị dữ liệu ở cột này dưới định dạng của tiền tệ.
  • Cột Gender hiển thị giới tính thì phải hiển thị giá trị là “Male” hoặc “Female” thay vì “true” or “false”
Định dạng biểu diễn dữ liệu cho một ô trong bảng

Mỗi một ô trong bảng JTable được vẽ nên bởi một đối tượng gọi là cell renderer. Đối tượng này sinh ra từ các lớp có cài đặt interface là TableCellRenderer. Trong interface này có định nghĩa một phương thức làgetTableCellRendererComponent. Phương thức này trả về một tham chiếu đến một thành phần và thành phần này sẽ thực hiện chức năng vẽ nên hình hài cho cái ô của bảng. Một cách tiện lợi nhất cho chúng ta là định nghĩa ra một lớp vừa cài đặt interface TableCellRenderer và cũng vừa kế thừa từ các lớp có khả năng thực hiện chức năng vẽ (JComboBox, JTextBox…). Chính vì vậy nên thường thường một đối tượng của lớp cài đặt interface TableCellRenderer, trong phương thức getTableCellRendererComponent của nó sẽ trả về một tham chiếu đến chính nó. Phương thức getTableCellRendererComponent gồm có các tham số đầu vào sau:

  • Một tham chiếu đến cái JTable có các ô dữ liệu đang được vẽ
  • Một tham chiếu đến giá trị của ô dữ liệu
  • Một biến cờ để xác định xem ô này có đang được chọn (kích chuột vào) hay không
  • Một biến cờ để xác định xem ô này có là một ô để nhập dữ liệu đầu vào và đang được trỏ đến hay không
  • Một chỉ số hàng của ô đang được vẽ
  • Một chỉ số cột của ô đang được vẽ

Ngoài việc trả về một tham chiếu đến một thành phần có khả năng hiển thị, phương thức getTableCellRendererComponent còn có nhiệm vụ khởi tạo trang thái ban đầu cho thành phần đó. Chú ý rằng một trong những tham số đầu vào của phương thức là một tham chiếu đến giá trị của một ô. Và giá trị này sẽ được lưu trong cái thành phần trước khi phương thức trả về một tham chiếu đến nó.

JTable cung cấp sẵn một số các renderers đã được định nghĩa trước, tuy nhiên trước hết chúng ta hãy tìm hiểu cách để tạo một renderer tùy biến.

Tạo renderer tùy biến
Trở lại ví dụ, chúng ta sẽ tạo một renderer tùy biến cho cột Gender, sao cho mỗi ô của nó hiển thị dưới dạng một JComboBox và giá trị “true” và “false” sẽ lần lượt được thay thế bằng “Male” và “Female”.

Trước hết ta sửa cái model TableValues trong file TableValues.java bằng cách thêm vào các hằng số, các hằng số này lần lượt lưu các chỉ số của các cột trong bảng. Cột đầu tiên sẽ có chỉ số là 0 và cứ thế tăng lên một đơn vị cho các cột tiếp 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 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];
}
}

Sau đó tạo một renderer có tên là GenderRenderer, lớp này nằm file GenderRenderer.java như sau:


public class GenderRenderer extends JComboBox implements TableCellRenderer{

public GenderRenderer(){
super();
addItem(“Male”);
addItem(“Female”);
}

public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
if(isSelected){
setForeground(table.getSelectionForeground());
super.setBackground(table.getSelectionBackground());
}else{
setForeground(table.getForeground());
setBackground(table.getBackground());
}

boolean isMale = ((Boolean) value).booleanValue();
setSelectedIndex(isMale?0:1);

return this;
}
}

Như đã nói ở trên, cái renderer này sẽ cài đặt interface TableCellRenderer và để cho thuật tiện nó kế thừa luôn một thành phần có khả năng hiển thị chính là JComboBox. Vì vậy, trong phương thức getTableCellRendererComponent có trả về tham chiếu đến chính cái renderer bằng dòng lệnh return this.

Tiếp theo, trong lớp SimpleTableTest ở file SimpleTableTest.java, ta thay đổi như sau:


public class SimpleTableTest extends JFrame{
protected JTable table;

public SimpleTableTest(){
[…]
table = new JTable(tv);
TableColumnModel tcm = table.getColumnModel();
TableColumn tc = tcm.getColumn(TableValues.GENDER);
tc.setCellRenderer(new GenderRenderer());
JScrollPane jsp = new JScrollPane(table);
pane.add(jsp, BorderLayout.CENTER);
}

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

Sau khi chạy chương trình chúng ta sẽ thấy mỗi ô trong cột Gender sẽ được biểu diễn bởi một JComboBox với giá trị là “Male” hoặc Female như hình dưới đây:

Một điều quan trọng mà chúng ta phải biết đó là cách mà một renderer được thêm vào một đối tượng của JTable sẽ không giống với cách một thành phần có thể hiển thị được thêm vào một Container. Điều này có nghĩa rằng trong ví dụ của chúng ta cái JTable không thực sự chứa bất cứ đối tượng nào của JComboBox cả. Thay vào đó, khi cái bảng được vẽ lên, mỗi một ô sẽ có nhiệm vụ để vẽ ra những gì bên trong nó. Điều này được thực hiện bằng cách truyền một đối tượng của lớp Graphics vào phương thức paint của renderer, và khi đó vùng không gian trong một ô sẽ được vẽ lên. Với cách trên, một thành phần hiển thị có thể vẽ lên hầu hết thậm chí tất cả các ô của bảng thay vì cấp phát hẳn một thành phần hiển thị cho chỉ một ô, 100 ô sẽ cần 100 thành phần hiển thị và điều này sẽ tốn nhiều bộ nhớ của chúng ta hơn.

Trong rất nhiều trường hợp, phương pháp dễ nhất để định nghĩa một renderer là kế thừa lớpDefaultTableCellRenderer là renderer mặc định cho mọt ô trong JTable. DefaultTableCellRenderer kế thừa JLabel, và biểu diễn các giá trị ở các ô đưới dạng String. Dạng biểu diễn String của một đối tượng được lấy ra bằng cách gọi phương thức toString của đối tượng đó, và DefaultTableCellRenderer sẽ truyền biểu diễn này vào phương thứcsetText mà nó đã kế thừa từ lớp JLabel. Các hành động trên được thực hiện trong phương thức setValue. Phương thức này nhận một tham số đầu vào là một tham chiếu đến giá trị của ô đang được vẽ.

 public void setValue(Object value){
 setText((value == null)? "" : value.toString()); 
} 

Nói tóm lại DefaultTableCellRenderer đơn giản là một JLabel với nội dung là giá trị của ô đang được vẽ.

Trong rất nhiều trường hợp, việc gọi phương thức toString() không phải là một phương pháp thích hợp để lấy về dạng biểu diễn String cho giá trị của một ô. Ví dụ như cột Account Balance trong ví dụ của chúng ta. Giá trị được hiển thị trong cột này thì đúng nhưng chúng không được định dạng đúng khi mà bản thân chúng đang biểu diễn các giá trị tiền tệ. Tuy nhiên, chúng ta có thể dễ dàng khắc phục vấn đề này bằng cách tạo ra một TableCellRenderer tùy biến và gán cho nó nhiệm vụ vẽ các giá trị dưới dạng tiền tệ. Ta đặt tên cho renderer đó là CurrencyRenderervà nó nằm ở trong file CurrencyRenderer.java

public class CurrencyRenderer extends DefaultTableCellRenderer{
public CurrencyRenderer(){
super();
setHorizontalAlignment(javax.swing.SwingConstants.RIGHT);
}

@Override
public void setValue(Object value){
if((value != null) && (value instanceof Number)){
Number numberValue = (Number)value;
NumberFormat formater = NumberFormat.getCurrencyInstance();
value = formater.format(numberValue.doubleValue());
}
super.setValue(value);
}
}

Lớp CurrencyRenderer thực hiện hai việc, thứ nhất nó căn lại lề ngang cho cái JLabel trong suốt quá trình khởi tạo. Thứ hai là nó cài đặt lại phương thức setValue của DefaultTableCellRenderer. Bởi vì chúng ta biết rằng lớp renderer này sẽ được sử dụng để vẽ cho các ô có kiểu dữ liệu là số, cho nên chúng ta ép kiểu cho giá trị của ô về kiểuNumber và sau đó định dạng lại giá trị này theo kiểu tiền tệ bằng việc sử dụng lớp NumberFormat.

Như vậy là chúng ta đã tạo một renderer tùy biến cho cột Account Balance, và chúng ta cần phải sử dụng renderer này để vẽ các ô cho cột đó. Chúng ta có thể thực hiện điều này bằng cách trực tiếp gán cái renderer đến TableColumn giống như chúng ta đã làm trước đây đối với cột Gender. Tuy nhiên, vẫn còn một cách khác để làm điều này. Bên cạnh việc kết hợp một renderer với một cột, chúng ta còn có thể kết hợp renderer đó với một kiểu dữ liệu riêng biệt nào đó. Khi đó, trong bảng, các ô ở các cột có chung kiểu dữ liệu sẽ được vẽ chỉ bởi một renderer.

Khi JTable được khởi tạo, nó tạo một ánh xạ giữa các kiểu và các renderer. Nó sử dụng ánh xạ này để chọn ra renderer sẽ vẽ cho các ô trong các cột trong trường hợp mà các renderer cho các cột không được chỉ ra một cách tường minh. Hay nói cách khác nếu chúng ta không chỉ ra một cách trực tiếp một renderer cho một cột nào đó giống như đã làm với cột Gender thì JTable sẽ tự chọn ra renderer dựa trên kiểu dữ liệu được lưu trong cột đó. Nó xác định kiểu dữ liệu cho các cột bằng việc gọi phương thức getColumnClass trong TableModel. Tuy nhiên, phương thức getColumnClass trong AbstractTableModel mặc định coi tất cả các cột của bảng sẽ có kiểu là Object.

public Class getColumnClass(int columnIndex){
return Object.class;
}

Bởi vì AbstractTableModel mặc định không thể biết được cụ thể kiểu của các cột, cho nên chỉ có một giả thuyết an toàn đó là nó coi tất cả các ô đều có kiểu là Object. Tuy nhiên, trong thực tế, các ô sẽ hầu hết sẽ chứa các giá trị có kiểu là lớp con của lớp Object chẳng hạn như Float, Date, … Vì vậy, nếu chúng ta muốn bảng có khả năng xác định được một kiểu riêng biệt cho một cột của nó, chúng ta phải cài đặt lại phương thức getColumnClass trong TableModel của chúng ta. Cho ví dụ, bởi vì tất cả các giá trị trong cột Account Balance có kiểu là Float, nên chúng ta có thể cài đặt lại phương thức getColumClass trong lớp TableValues như sau:


public class TableValues extends AbstractTableModel{

public final static int FIRST_NAME = 0;
[…]

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;
}
return dataType;
}
}

Bây giờ JTable đã có thể xác định được rằng cột Account Balance có kiểu là Float. Bây giờ chúng ta cần kết hợp CurrencyRenderer với kiểu dữ liệu này bằng việc gọi phương thức setDefaultRenderer trong contructor của lớp SimpleTableTest


public class SimpleTableTest extends JFrame{
protected JTable table;

public SimpleTableTest(){
[…]
tc.setCellRenderer(new GenderRenderer());
table.setDefaultRenderer(Float.class, new CurrencyRenderer());
JScrollPane jsp = new JScrollPane(table);
pane.add(jsp, BorderLayout.CENTER);
}

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

Với dòng lệnh màu vàng trên, CurrencyRenderer trở thành renderer mặc định cho tất cả các cột có kiểu dữ liệu là Float trong bảng. Vì vậy, CurrencyRenderer trở thành renderer cho cột Account Balance. Sau khi chạy chương trình ta thấy giá trị trong cột Account Balance có định dạng là tiền tệ giống như hình dưới đây:

Bây giờ, điều gì sẽ xảy ra nếu như chúng ta không trực tiếp gán một renderer cho một cột và cũng không có một ánh xạ nào từ một renderer đến kiểu dữ liệu của cột đó. Khi đó, JTable sẽ thực hiện việc đi dò trên dựa trên sự kế thừa của kiểu dữ liệu trong cột bằng việc tìm kiếm một ánh xạ từ renderer đến kiểu cha của kiểu đó. Cho ví dụ, nếu phương thức getColumnClass chỉ ra rằng cột đó có kiểu là Float nhưng không có renderer nào được chỉ ra cho kiểu Float cả, khi đó JTable sẽ tìm kiếm xem có renderer nào được chỉ ra cho kiểu cha của kiểu Float là kiểu Number hay không. Nếu vẫn không có một renderer nào được chỉ ra cho kiểu Number, JTable sẽ tìm kiếm một renderer cho kiểu Object là kiểu cha của kiểu Number. Đến đây thì nó đã tìm ra một renderer cho cột đó, chính là DefaultTableCellRenderer theo mặc định.

Vậy tổng kết lại, một JTable sẽ tìm kiếm một renderer cho các cột của nó theo các bước như sau:

  1. Nếu một renderer được đặt trực tiếp cho một cột (cột Gender), sẽ sử dụng luôn renderer đó
  2. Lấy về kiểu của các cột bằng việc gọi phương thức getColumnClass của TableModel
  3. Nếu có một renderer nào được chỉ định đi với một kiểu, sẽ sử dụng renderer đó cho tất cả các cột có kiểu dữ liệu đó (ngoại trừ những cột đã được chỉ định trực tiếp ở trên)
  4. Nếu không có renderer nào được chỉ định cho một kiểu, sẽ thực hiện tìm kiếm renderer cho kiểu cha của kiểu đó, cứ lặp lại như thế cho đến khi tìm ra một renderer thì dừng lại
Các renderer mặc định của JTable

Theo như trên, chúng ta đã biết tạo ra một renderer tùy biến của chúng ta như thế nào và làm sao để kết hợp một renderer đến một kiểu dữ liệu nào đó. Tuy nhiên, thường thì chúng ta cũng không cần để làm như thế bởi vì JTable đã cung cấp sẵn cho chúng ta một tập các renderer đã được định nghĩa trước cho một số kiểu dữ liệu thông dụng và sẽ tự động được thêm vào ánh xạ từ renderer đến kiểu dữ liệu. Một ví dụ chúng ta đã từng nói đó là có một sự kết hợp giữa kiểu Object và DefaultTableCellRenderer. Ngoài ra, còn có các renderer cho các kiểu dữ liệu khác nữa. Điều này có nghĩa rằng, nếu một trong các renderer có sẵn này có thể dùng được trong bảng của chúng ta, thì tất cả những gì chúng ta cần làm chỉ là chỉ ra kiểu dữ liệu cho các cột của bảng trong phương thức getColumnClass. Nhờ vào đó, JTable sẽ biết cách để sử dụng các renderer có sẵn theo tương ứng với kiểu dữ liệu ở các cột. Trong ví dụ của chúng ta, chúng ta có thể sử dụng renderer có sẵn cho kiểu java.util.Date để làm renderer cho cột Date of Birth. Tất cả những gì chúng ta cần làm là thêm những dòng màu vàng sau vào phương thức getColumnClass trong lớp TableValues như sau.


public class TableValues extends AbstractTableModel{

public final static int FIRST_NAME = 0;
[…]

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;
}
return dataType;
}
}

 

Bây giờ chạy chương trình chúng ta sẽ thấy giá trị trong cột Date of Birth sẽ hiển thị đúng theo định dạng ngày sinh như hình dưới đây:

Ngoài ra còn các renderer cho các kiểu dữ liệu khác như sau:

java.lang.Number
Đây là kiểu cha cho các kiểu số như Integer, Float, Long … Cái renderer được định nghĩa đi với kiểu Number là một lớp con của lớp DefaultTableCellRenderer và nó căn lề cho giá trị lệch sang bên phải giống như chúng ta đã làm trong CurrencyRenderer. Nói cách khác, renderer cho kiểu Number biểu diễn giá trị dưới dạng String, nhưng khác với mặc định là nó căn lề sang bên phải thay vì bên trái như mặc định. Chẳng hạn như ta áp dụng nó với cột Account Balance thì sẽ giống như hình dưới đây

javax.swing.ImageIcon
Renderer kết hợp với lớp này cho phép chúng ta hiển thị một Icon lên trên bảng của chúng ta. Renderer này chính là DefaultTableCellRenderer và nó khai thác tính năng của một JLabel là có thể hiển thị cả chữ và ảnh. Thay vì hiển thị chữ, nó sẽ hiển thị hình ảnh cho các ô trong cột có kiểu dữ liệu là ImageIcon.

java.lang.Boolean
Khi renderer này được sử dụng, nó sẽ hiển thị nội dung cho các ô trong các cột có kiểu là Boolean dưới dạng các JCheckBox. Các JCheckBox này sẽ ở trạng thái được đánh dấu nếu giá trị ở các ô đó là true và không được đánh dấu nếu giá trị ở các ô đó là false. Hình dưới đây minh họa điều này khi ta áp dụng nó cho cột Gender trong ví dụ.

Đến đây, có vẻ như chúng ta đã biết cách để hiển thị mọi thứ lên cái bảng của chúng ta. Tuy nhiên, việc sử dụng bảng không chỉ dừng lại ở mức độ hiển thị thông tin. Chúng ta còn muốn chúng ta có thể sửa thông tin các ô trong bảng. Điều này sẽ được nói một cách cụ thể trong phần 4.

One Response to “Jatable_Swing part 3”

  1. Swing in Java « Thái Hoàng Hải Says:

    […] 1 Part 2 Part 3 Share this:TwitterFacebookLike this:LikeBe the first to like this post. Comments RSS […]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: