[Java Swing] Tùy biến JList – JList custom renderer

Trong bài JList chúng ta đã biết cách tạo ra một JList đơn giản với một số thao tác bắt sự kiện. Trong bài này chúng ta tiếp tục tìm hiểu cách tùy biến JList để tạo được một list các phần tử theo ý muốn của chúng ta. Ví dụ như danh bạ (ảnh, tên liên lạc, số điện thoại) hay danh sách các cuốn sách yêu thích (gồm có bìa sách, tên sách, tên tác giả) như hình dưới đây:
custom jlist
Trước tiên chúng ta hãy tạo ra class Book để lưu thông tin sách đã. Trong class Book này gồm ba thuộc tính là tên sách, tác giả và tên bìa sách với các phương thức khởi tạo, getter, setter như dưới đây:

package nguyenvanquan7826.JList;

public class Book {
	private String name;
	private String author;
	private String iconName;

	public Book(String name, String author, String iconName) {
		super();
		this.name = name;
		this.author = author;
		this.iconName = iconName;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public String getAuthor() {
		return author;
	}

	public void setAuthor(String author) {
		this.author = author;
	}

	public String getIconName() {
		return iconName;
	}

	public void setIconName(String iconName) {
		this.iconName = iconName;
	}
}

Tiếp theo chúng ta tạo JFrame hiển thị danh sách book của chúng ta. Trong JFrame chứa một JPanel là main panel, JPanel này chứa 1 JList là listBook, listBook được đặt trong 1 JScrollPane để có thể cuộn lên xuống trong trường hợp danh sách dài, JList không hiển thị được hết.

package nguyenvanquan7826.JList;

import java.awt.BorderLayout;

import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.border.EmptyBorder;

public class JListCustomRenderer extends JFrame {
	private int width = 350;
	private int height = 200;
	private JList<Book> listBook;

	public JListCustomRenderer() {
		// add main panel
		add(createMainPanel());
		// set display
		setTitle("JLIstCustomRenderer");
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setSize(width, height);
		setLocationRelativeTo(null);
		setVisible(true);
	}

	private JPanel createMainPanel() {
		JPanel panel = new JPanel(new BorderLayout());
		panel.setBorder(new EmptyBorder(10, 10, 10, 10));
		// create list book and set to scrollpane and add to panel
		panel.add(new JScrollPane(listBook = createListBooks()),
				BorderLayout.CENTER);
		return panel;
	}

	private JList<Book> createListBooks() {
		// create List model
		DefaultListModel<Book> model = new DefaultListModel<>();
		// add item to model
		model.addElement(new Book("C/C++ Programming", "A", "cpp"));
		model.addElement(new Book("Java Programming", "B", "java"));
		model.addElement(new Book("C# Programming", "C", "cs"));
		model.addElement(new Book("IOS Programming", "D", "ios"));
		model.addElement(new Book("Windows Phone Programming", "E", "wp"));
		model.addElement(new Book("Android Programming", "F", "android"));
		// create JList with model
		JList<Book> list = new JList<Book>(model);
		return list;
	}

	public static void main(String[] args) {
		new JListCustomRenderer();
	}
}

Xong rồi, vậy là chúng ta đã có danh sách các cuốn sách rồi, chạy chương trình lên xem nó ra cái gì? Nó ra cái này =)) rất bất ngờ, không theo ý muốn chút nào cả @@

custom jlist

Tại sao lại như vậy, chúng ta đã add vào list những cuốn sách gồm có tên sách và tên tác giả, Vậy tại sao chương trình lại hiển thị như vậy? Thực ra thì JList được xử lý thông qua ListModel và hơn nữa việc hiển thị đối tượng của JList thực hiện bởi một đối tượng gọi là Renderer và với mỗi JList nó có một Renderer mặc định. Vậy Renderer mặc định này hoạt động như thế nào? Renderer chịu trách nhiệm hiển thị các phần tử trong JList. Nó thực hiện bằng cách gọi phương thức toString() của đối tượng trong JList và đưa chuỗi lấy được để hiện thị lên. Và do trong class Book chúng ta đã không viết đề phương thức toString() nên Renderer đã lấy chuỗi mà phương thức toString() mặc định trong class Book. Và các chuỗi mà chúng ta nhận được trong chương trình trên cũng là chuỗi mà hệ thống cung cấp cho mỗi đối tượng Book trong JList.
Bây giờ chúng ta hãy thử viết đè phương thức toString() trong class Book như sau:

@Override
public String toString() {
	return name + " - " + author;
}

Chúng ta viết đè phương thức toString() để trả về tên và tác giả của cuốn sách. Vì vậy khi chạy chương trình chúng ta được một List như sau:

custom jlist

Dường như chúng ta đã thấy ổn rồi? Chưa thì phải! Chúng ta cần hiển thị cả bìa sách như hình ảnh mở đầu thì phải làm sao? Khi mà chúng ta đã biết việc hiển thị của JList là do Renderer rồi thì khi muốn thay đổi nó là hoàn toàn dễ dàng bằng cách viết lại Renderer của JList theo ý muốn của chúng ta.
Bây giờ chúng ta hãy xác định xem mỗi một phần tử trong JList là gì? Ở đây chúng ta muốn nó là một JPanel chứa ba JLabel, một JLabel là ảnh bìa cuốn sách, một JLabel là tên cuốn sách và JLabel cuối cùng là tên tác giả có màu xanh.
Để tùy biến được Renderer chúng ta sẽ viết thêm một class nữa tên là BookRenderer kế thừa từ một JPanel. Ngoài ra class này cần implements một Interface tên là ListCellRenderer. Interface này có một phương thức getListCellRendererComponent() trả lại đối chính là JPanel của chúng ta và nó được xử lý để hiển thị lên JList.

package nguyenvanquan7826.JList;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridLayout;

import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.ListCellRenderer;

public class BookRenderer extends JPanel implements ListCellRenderer<Book> {

	private JLabel lbIcon = new JLabel();
	private JLabel lbName = new JLabel();
	private JLabel lbAuthor = new JLabel();

	public BookRenderer() {
		setLayout(new BorderLayout(5, 5));

		JPanel panelText = new JPanel(new GridLayout(0, 1));
		panelText.add(lbName);
		panelText.add(lbAuthor);
		add(lbIcon, BorderLayout.WEST);
		add(panelText, BorderLayout.CENTER);
	}

	@Override
	public Component getListCellRendererComponent(JList<? extends Book> list,
			Book book, int index, boolean isSelected, boolean cellHasFocus) {

		lbIcon.setIcon(new ImageIcon(getClass().getResource(
				"/nguyenvanquan7826/JList/" + book.getIconName() + ".jpg")));
		lbName.setText(book.getName());
		lbAuthor.setText(book.getAuthor());
		lbAuthor.setForeground(Color.blue);

		return this;
	}
}

Mỗi phần tử trong JList là một JPanel. JPanel này có layout là BorderLayout, nó chứa Icon ở phía tây (WEST) và panelText. panelText là một JPanel chứa hai JLabel là tên sách và tên tác giả.
Chúng ta đặt text cũng như icon cho các JLabel trong phương thức getListCellRendererComponent(). Phương thức này có một đối tượng thuộc lớp Book truyền vào là book. chính đối tượng book này là phần tử trong JList mà nó được hiển thị. Do đó ta đặt các icon, text của các JLabel thông qua việc lấy iconName, name, author của đối tượng book này.
Bây giờ chúng ta quay lại class JListCustomRenderer và sửa lại phương thức createListBook như sau:

private JList<Book> createListBooks() {
	// create List model
	DefaultListModel<Book> model = new DefaultListModel<>();
	// add item to model
	model.addElement(new Book("C/C++ Programming", "A", "cpp"));
	model.addElement(new Book("Java Programming", "B", "java"));
	model.addElement(new Book("C# Programming", "C", "cs"));
	model.addElement(new Book("IOS Programming", "D", "ios"));
	model.addElement(new Book("Windows Phone Programming", "E", "wp"));
	model.addElement(new Book("Android Programming", "F", "android"));
	// create JList with model
	JList<Book> list = new JList<Book>(model);
	// set cell renderer 
	list.setCellRenderer(new BookRenderer());
	return list;
}

Bây giờ thì chạy chương trình và ta sẽ được JList như mong muốn.
custom jlist

Tuy nhiên chưa xong đâu. Các bạn để ý là khi chúng ta click chọn một item thì không thấy có hiện tượng gì cả @@. Đó là vì chúng ta đã viết lại Renderer nên các hiệu ứng mà Renderer default tạo ra không có nữa (màu nền khi chọn 1 item). Chúng ta sẽ đặt màu nền cho JPanel, cho các JLabel khi nó được chọn hoặc không được chọn như sau (viết trong phương thức getListCellRendererComponent() của class BookRenderer)

@Override
public Component getListCellRendererComponent(JList<? extends Book> list,
		Book book, int index, boolean isSelected, boolean cellHasFocus) {

	lbIcon.setIcon(new ImageIcon(getClass().getResource(
			"/nguyenvanquan7826/JList/" + book.getIconName() + ".jpg")));
	lbName.setText(book.getName());
	lbAuthor.setText(book.getAuthor());
	lbAuthor.setForeground(Color.blue);

	// set Opaque to change background color of JLabel
	lbName.setOpaque(true);
	lbAuthor.setOpaque(true);
	lbIcon.setOpaque(true);

	// when select item
	if (isSelected) {
		lbName.setBackground(list.getSelectionBackground());
		lbAuthor.setBackground(list.getSelectionBackground());
		lbIcon.setBackground(list.getSelectionBackground());
		setBackground(list.getSelectionBackground());
	} else { // when don't select
		lbName.setBackground(list.getBackground());
		lbAuthor.setBackground(list.getBackground());
		lbIcon.setBackground(list.getBackground());
		setBackground(list.getBackground());
	}
	return this;
}

Vậy là xong, giờ chúng ta có thể thực hiện chọn các item rồi.

custom jlist

Còn một phần rất nhỏ nữa là các ảnh bìa của sách hình như tương đối gần nhau? làm chúng cách nhau ra một khoảng cho đẹp giống như hình ở đầu bài viết nhé, đó là việc các bạn có thể tự làm (gợi ý đặt lbIcon vào 1 JPanel và set Border Empty cho JPanel đó).
Các bạn có thể download package tại đây Custom Jlist
custom jlist

Bài viết có sự tham khảo tại: codejava.net