Таблицы

В графическом интерфейсе пользователя очень часто встречаются таблицы разного вида. Некоторые из них только показывают список табличных данных, другие позволяют заполнять их информацией и даже выполнять простейшие расчеты.

В подавляющем большинстве информационных систем данные хранятся в таблицах реляционной базы данных. Результат запроса к базе данных тоже представляется в виде таблицы. Очень часто этот результат необходимо отразить на экране или в отчете также в виде таблицы, снабженной заголовком, комментариями, оформленной цветом и различными шрифтами.

В графической библиотеке Swing для решения таких задач имеются классы JTabie, JTableHeader, TableColumn и сопутствующие им классы, составляющие пакет j avax. swing. table. Эти классы позволяют создать не только статичные таблицы, отражающие результаты запроса, но и редактируемые таблицы, изменяющие данные, и даже полномасштабные электронные таблицы. Рассмотрим последовательно эти классы.

Класс JTable

Класс JTable очень велик. Он определяет множество методов построения таблиц и работы с ними. С ним связано несколько классов-делегатов. В своей работе класс JTable использует класс DefaultTableModel для хранения данных, класс JTableHeader для построения заголовков столбцов, класс TableCellEditor для редактирования ячеек, класс TableCellRenderer для отображения ячеек на экране, класс TableColumn для сбора информации о свойствах каждого столбца. Для изменения свойств таблицы эти классы нужно заменить их расширениями или реализовать соответствующие интерфейсы.

Таблицы сконструированы по схеме "Model-View-Controller". Для хранения содержимого таблицы и ее характеристик имеются три модели данных: модель ячеек таблицы, описанная интерфейсом TableModel, модель столбцов таблицы, описанная интерфейсом TableColumnModel, и модель выделения ячеек таблицы, в качестве которой взята модель выделения списков ListSelectionModel (о ней мы уже говорили в главе 11).

Для создания таблицы предлагается несколько конструкторов.

? Конструктор по умолчанию JTable () создает пустую таблицу без строк и столбцов с пустыми моделями данных.

? Конструктор JTable(int rows, int columns) формирует пустую редактируемую таблицу с заданным числом rows строк и columns столбцов с моделью данных по умолчанию, которая предполагает хранить в таблице объекты типа Object в виде вектора типа Vector, состоящего из векторов. В нее можно вводить данные прямо с клавиатуры.

? Конструктор JTable (Object[] [] data, Object[] colNames) создает таблицу, заполненную объектами data. Параметр colNames содержит имена столбцов таблицы. Все строки массива data должны содержать столько же элементов, сколько существует в массиве colNames. Пример создания таблицы этим конструктором приведен в листинге 13.1.

? Конструктор JTable (Vector data, Vector colNames) делает то же самое, но параметры заданы векторами. Пример его использования представлен в листинге 13.3.

Надо заметить, что заголовки столбцов автоматически появляются на экране, только

если таблица заключена в панель JScrollPane. Если при этом у столбцов не заданы имена, то они помечаются буквами A, B, C и т. д., как принято в электронных таблицах.

Остальные конструкторы определяют таблицу с заранее заданными моделями данных.

? Конструктор JTable (TableModel) использует заданную параметром модель ячеек таблицы. Модель столбцов и модель выделения данных определяются по умолчанию.

? Конструктор JTable (TableModel, TableColumnModel) оставляет модель выделения данных по умолчанию.

? Конструктор JTable (TableModel, TableColumnModel, ListSelectionModel) задает все три модели данных.

Листинг 13.1 показывает простой пример определения таблицы с помощью массива

объектов, в данном случае — строк.

Листинг 13.1. Простое определение таблицы

import java.awt.*; import javax.swing.*;

public class SimpTable extends JFrame{

SimpTable(){

super(" My Table"); setLayout(new FlowLayout());

String[][] data = {{"-27", "32"}, {"-45", "55"}}; String[] colNames = {"Вчера", "Сегодня"};

JTable t1 = new JTable(data, colNames);

add(new JScrollPane(t1)); setSize(400, 400);

setDefaultCloseOperation(EXIT ON CLOSE); setVisible(true);

}

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

}

}

Таблицу можно заполнить методом

setValueAt(Object data, int row, int column);

Он заменяет старое содержимое ячейки (row, column) Вида объектом data. Следует учесть, что в этом методе параметр column указывает номер столбца в Виде, но не в Модели данных. При выводе содержимого таблицы из Модели данных в Вид порядок столбцов можно изменить, да и необязательно выводить все столбцы таблицы.

В таблице можно задать или заменить Модель ячеек таблицы методом setModel (TableModel) и модель столбцов таблицы с помощью метода setColumnModel(TableColumnModel). Рассмотрим их подробнее.

Модель данных таблицы

Таблица класса JTable пользуется тремя моделями данных для хранения своих элементов. Две модели, описанные интерфейсами TableModel и TableColumnModel, специфичны для таблиц, третья модель — ListSelectionModel — заимствована у списков JList. Она уже рассматривалась нами в главе 11.

Модель ячеек таблицы

Модель хранения содержимого ячеек таблицы описана интерфейсом TableModel, который частично реализован абстрактным классом AbstractTableModel и полностью реализован его подклассом DefaultTableModel.

Эта модель предполагает, что в ячейках таблицы могут храниться объекты любого типа и в разных ячейках даже одного столбца могут храниться объекты разных типов.

Строки и столбцы пронумерованы, начиная от нуля. Общий суперкласс всех ячеек столбца с индексом ind можно получить методом getColumnClass(int ind). Текущее число строк в таблице можно узнать методом getRowCount ( ), число столбцов- методом

getColumnCount (). У столбца может быть имя, получить которое можно методом getColumnName (int), возвращающим строку класса String.

Содержимое ячейки таблицы можно получить из модели данных в виде объекта класса Object методом getValueAt (int rowInd, int colInd), а установить в модель, если ячейка

редактируема, — методом setValueAt(Object data, int rowInd, int colInd).

Проверить, редактируема ячейка или нет, можно логическим методом

isCellEditable(int rowInd, int colInd).

Легко создать свою модель ячеек таблицы, расширив класс AbstractTableModel. При этом необходимо определить три метода:

int getRowCount(); int getColumnCount();

Object getValueAt(int, int);

Остальные методы переопределяются по усмотрению разработчика. Но если этого не сделать, то получится нередактируемая таблица только для чтения, поскольку метод

isEditable () в классе AbstractTableModel возвращает false, а метод setValueAt () пуст. Для того чтобы таблица стала редактируемой, надо переопределить и эти два метода.

В листинге 13.2 приведен пример расширения класса AbstractTableModel — модель ячеек таблицы, запрещающая редактировать первый столбец таблицы.

Листинг 13.2. Модель запрещает редактировать первый столбец

import java.awt.*; import javax.swing.*; import javax.swing.table.*;

public class SimpTable extends JFrame{

SimpTable(){

super(" Таблица с неизменяемым первым столбцом"); setLayout(new FlowLayout());

JTable t1 = new JTable(new FirstColumnTableModel());

add(new JScrollPane(t1));

setSize(400, 400);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE); setVisible(true);

}

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

}

}

class FirstColumnTableModel extends AbstractTableModel{ protected Obj ect[][] data = {

{"Текст", Color.black, new Boolean(true)},

{"Фон", new Color(130, 56, 187), new Boolean(true)},

{"Рамка", new Color(200, 45, 125), new Boolean(false)},

};

protected String[] colNames = {"Элемент", "Цвет", "Выбрать"};

public FirstColumnTableModel(){ super(); }

// Определяем обязательные методы

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

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

public Object getValueAt(int row, int col){ return data[row][col]; }

// Запрещаем заполнять ячейки первого столбца public void setValueAt(Object value, int row, int col){ if (col != 0) data[row][col] = value;

// Сообщаем, что первый столбец нельзя редактировать public boolean isCellEditable(int row, int col){ return col != 0;

}

// Для визуализации содержимого ячеек графическим // компонентом определяем класс ячеек столбца public Class getColumnClass(int col){ return data[0][col].getClass();

}

// Для показа имен в строке заголовков public String getColumnName(int col){ return colNames[col];

}

}

Как видно из рис. 13.1, объект класса Boolean показан В таблице компонентом JCheckBox. Это результат действия метода getColumnClass (). На рис. 13.2 тот же объект изображен в стандартном виде. Но объект класса Color по-прежнему показан строкой — результатом действия метода toString ( ).

Рис. 13.1. Таблица со сложными объектами

Класс DefaultTableModel кроме реализации методов интерфейса TableModel добавляет несколько конструкторов и полезных методов работы с моделью данных. Он создает редактируемую модель ячеек таблицы. Для хранения данных он создает вектор строк класса Vector, хранящий строки опять-таки в виде вектора. Получившийся вектор векторов хранится в защищенном (protected) поле dataVector.

Конструктор по умолчанию DefaultTableModel () создает объект с нулевым количеством строк и столбцов.

Конструктор DefaultTableModel (int rowCount, int colCount) создает модель данных с заданным числом строк и столбцов. В ячейках этой модели — пустые ссылки null.

Конструкторы

DefaultTableModel(Vector data, Vector colNames);

DefaultTableModel(Object[][] data, Object[] colNames);

сразу же заполняют модель данными.

Еще два конструктора

DefaultTableModel(Vector colNames, int rowCount);

DefaultTableModel(Obj ect[] colNames, int rowNames);

задают модель с поименованными столбцами и пустыми строками.

Методы класса DefaultTableModel, как и положено модели данных, добавляют в конец модели строки методами addRow() и столбцы методами addColumn (), вставляют строку в указанное место методами insertRow( ). Аргументы этих методов могут быть типа Object или Vector.

Модель позволяет заполнить ячейки объектами методом

setValueAt(Object value, int row, int column);

Многочисленные методы getXxx () предоставляют сведения о модели.

В модели можно переставить строки методом

moveRow(int start, int end, int to);

Первые два аргумента — start и end — задают диапазон переставляемых строк, последний аргумент to — новый индекс строки, имевшей до перестановки индекс start.

Наконец, методы setDataVector() позволяют вообще заменить все содержимое модели, т. е. содержимое поля dataVector.

При создании модели ячеек таблицы часто бывает удобнее расширить класс DefaultTableModel, а не абстрактный класс AbstractTableModel. В листинге 13.3 показан пример такого расширения — модель заполняется сведениями о файлах, полученными из каталога, имя которого указывается в командной строке.

Листинг 13.3. Заполнение модели ячеек таблицы данными из каталога

import javax.swing.*; import javax.swing.table.*; import java.io.*; import java.util.*;

public class FileTable extends JFrame{

public FileTable(File dir){ super(" Таблица файлов");

JTable table = new JTable(new FileTableModel(dir));

add(new JScrollPane(table)); setSize(600, 400); setVisible(true);

setDefaultCloseOperation(EXIT ON CLOSE);

}

public static void main(String[] args){

File dir = args.length > 0 ? new File(args[0]) : new File(System.getProperty("user.home"));

new FileTable(dir);

}

}

class FileTableModel extends DefaultTableModel{

protected File dir; protected String[] fName;

protected String[] colName = new String[]{ "Имя", "Размер", "Дата и время",

"Каталог", "Для чтения", "Для записи"

};

protected Class[] colClass = new Class[]{ String.class, Long.class, Date.class, Boolean.class, Boolean.class, Boolean.class

};

public FileTableModel(File dir){ super(dir.list().length, 6); this.dir = dir; fName = dir.list();

}

public String getColumnName(int col){ return colName[col]; } public Class getColumnClass(int col){ return colClass[col]; }

public Object getValueAt(int row, int col){

File f = new File(dir, fName[row]); switch(col){

case 0: return fName[row];

case 1: return new Long(f.length());

case 2: return new Date(f.lastModified());

case 3: return f.isDirectory() ?

Boolean.TRUE : Boolean.FALSE; case 4: return f.canRead() ?

Boolean.TRUE : Boolean.FALSE; case 5: return f.canWrite() ?

Boolean.TRUE : Boolean.FALSE; default: return null;

}

}

}

При всяком изменении модели или ее содержимого в ней происходит событие класса TableModelEvent. Этот класс различает три типа событий: insert, delete и update и возвращает тип события методом getType (). Кроме того, класс отслеживает диапазон строк, в которых произошло событие, и столбец. Эти сведения можно получить методами getFirstRow( ), getLastRow() и getColumn(). Если последний из этих методов вернул константу all_columns, то это означает, что событие затронуло все столбцы. Слуша-

тель данного события может быть присоединен к модели методом

addTableModelListener(TableModelEvent).

Есть еще две комбинации типов события TableModelEvent. Сочетание update, all_columns и константы MAX_VALUE в качестве значения метода getLastRow () говорит о том, что была изменена вся Модель и Вид должен переписать на экране всю таблицу. Сочетание

update, all_columns и header_row — результат применения метода getFirstRow() — сообщает о том, что структура таблицы изменена — добавлен или удален столбец, Виду надо изменить структуру столбцов и перечертить таблицу на экране. Впрочем, это изменение отслеживается событием TableColumnModelEvent.

После определения модели ячеек ее надо установить в таблицу методом setModel (TableModel) или воспользоваться конструктором таблицы, как показано в листинге 13.2.

Свойства столбца таблицы TableColumn

Класс TableColumn хранит информацию о столбце таблицы: минимальную, максимальную и текущую ширину столбца, возможность изменения ширины, заголовок, индекс столбца в модели данных, ссылки на объекты классов TableCellEditor и TableCellRenderer. Для доступа к этой информации в классе есть методы

getXxx()/setXxx().

Особенно часто приходится использовать методы setMinWidth(int), setMaxWidth (int) и setPreferredWidth (int), поскольку ширина столбцов и всей таблицы, задаваемая по умолчанию, редко удовлетворяет разработчика.

Модель столбцов таблицы

Модель хранения столбцов таблицы описана интерфейсом TableColumnModel, который реализован классом DefaultTableColumnModel. Эта модель собирает сведения обо всех столбцах таблицы в виде вектора экземпляров класса TableColumn. Кроме того, запоминается ширина промежутков между колонками и общая ширина таблицы. Модель хранит объект класса ListSelectionModel, определяющий правила выделения столбцов и содержащий сведения о выделенных столбцах таблицы.

Порядок следования столбцов в модели может не совпадать с их порядком в модели ячеек таблицы. Модель может переставлять столбцы методом moveColumn(int oldind, int newind), эти перестановки не меняют модель ячеек таблицы.

При каждом изменении модели — добавлении или удалении столбцов, их перестановках, выделении столбцов — происходит событие класса TableColumnModelEvent, которое модель может отследить методом

addColumnModelListener(ColumnModelListener);

Заголовки столбцов таблицы JTableHeader

В начале таблицы можно вывести строку с именами столбцов. Она позволяет изменять ширину столбцов с помощью мыши в заданных для столбцов пределах.

Строка заголовков появляется автоматически, если таблица помещена в панель класса JScrollPane, даже если имена столбцов не были заданы. В таком случае в строку заголовков выводятся заглавные латинские буквы A, B, C и т. д., как определено в модели ячеек таблицы класса DefaultTableModel.

Строка заголовков — это объект класса JTableHeader из пакета j avax. swing. table. Данный объект хранит заголовок в модели столбцов типа TableColumnModel.

Экземпляр класса с моделью столбцов таблицы по умолчанию создается конструктором JTableHeader ( ). Второй конструктор, JTableHeader(TableColumnModel), создает объект с заданной моделью столбцов.

После создания строки заголовков ее надо связать с таблицей методом setTable (JTable).

Второй способ создания строки заголовков — получить ее экземпляр методом

getTableHeader( ) класса JTable.

Полученная строка заголовков — самостоятельный объект, который надо отдельно выводить на экран. Например, в листинге 13.4 заголовок таблицы выводится на "север", а

сама таблица- в "центр" размещения BorderLayout. Таким способом строку заголовков

можно вывести последней строкой таблицы, поместив ее на "юг" и превратив в итоговую строку таблицы. О классе BorderLayout и вообще о менеджерах размещения компонентов речь пойдет в следующей главе.

Листинг 13.4. Отдельный вывод заголовка таблицы

import java.awt.*; import javax.swing.*; import javax.swing.table.*;

public class HeadTable extends JFrame{

HeadTable(){

super(" Сотрудники");

Vector data = new Vector();

Vector row = new Vector(); row.addElement("Иванов"); row.addElement(new Integer(1970)); row.addElement(new Boolean(false)); data.addElement(r); row = new Vector(); row.addElement("Петров"); row.addElement(new Integer(1980)); row.addElement(new Boolean(true)); data.addElement(r);

Vector col = new Vector(); col.addElement("Фамилия"); col.addElement("Год рождения"); col.addElement("Семейное положение");

JTable t2 = new JTable(data, col);

JTableHeader th = t2.getTableHeader(); add(th, BorderLayout.NORTH); add(t2, BorderLayout.CENTER);

setSize(400, 400);

setDefaultCloseOperation(EXIT ON CLOSE); setVisible(true);

}

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

}

}

На рис. 13.2 показан вывод программы, записанной в листинге 13.4. Как видно из рисунка, объект класса Boolean отображен строкой — результатом действия метода

toString().

Рис. 13.2. Отдельный вывод заголовка таблицы

Границы между заголовками столбцов служат для изменения ширины столбца с помощью мыши. Изменение может быть сделано в пределах от минимальной до максимальной ширины. Поведение остальных столбцов при изменении ширины одного из них регулируется методом setAutoResizeMode(int) класса JTable. Аргумент этого метода — одна из следующих констант:

? auto_resize_off — не изменять ширину остальных столбцов;

? auto_resize_next_column — изменить ширину следующего столбца;

? auto_resize_subsequent_columns — изменить пропорционально ширину следующих столбцов (по умолчанию);

? auto_resize_last_column — изменить ширину последнего столбца;

? auto_resize_all_columns — изменить пропорционально ширину всех столбцов.

Модель выделения ячеек

Правила выделения ячеек таблицы регулируются интерфейсом ListSelectionModel. В классе JTable по умолчанию используется реализация DefaultListSelectionModel этого интерфейса. Она была описана в главе 11 при рассмотрении класса JList. Эта модель задает три режима выделения: выделение отдельного элемента single_selection, выделение смежных элементов single_interval_selection, выделение нескольких участков смежных элементов multiple_interval_selection.

Режим выделения можно установить непосредственно методами модели выделения или методом setSelectionMode (int) класса JTable, в котором надо задать одну из трех указанных ранее констант. Первый способ позволяет установить разные режимы выделения для строк и столбцов таблицы. Для строк режим выделения можно задать просто методом setSelectionModel (ListSelectionModel) класса JTable. Чтобы задать режим выделения столбцов, надо предварительно получить ссылку на объект типа TableColumnModel, а потом установить новую модель. Все это можно сделать так:

table.getColumnModel().setSelectionModel(new SomeSelectionModel());

Цвет текста и фона выделенных ячеек можно задать методами

setSelectionForeground(Color) и setSelectionBackground(Color).

Остальные свойства выделения можно задать и узнать методами модели выделения, получив ссылку на нее методом getSelectionModel ( ) класса JTable.

Визуализация ячеек таблицы

Непосредственным выводом содержимого ячеек на экран — его визуализацией — занимается еще один делегат класса JTable, описанный интерфейсом TableCellRenderer.

Интерфейс TableCellRenderer описывает всего один метод:

Component getTableCellRendererComponent(

JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col);

Как видно из описания, для каждой ячейки с индексами (row, col) можно задать свой способ визуализации. Этот способ может также меняться в зависимости от содержимого value ячейки, от того, выделена ли ячейка isSelected, имеет ли она фокус ввода hasFocus, и даже от того, какая таблица table использует этот метод. По этим данным метод должен сформировать компонент, содержащий значение value, и вернуть его. Обычно возвращается объект класса, определяющего этот метод, т. е. this. Затем полученный объект рисует себя на экране своим методом paint (). Поэтому удобно реализовать интерфейс каким-нибудь графическим компонентом, имеющим метод paint ().

Все это похоже на визуализацию элементов списка JList, описанную в главе 11.

В библиотеке Swing интерфейс TableCellRenderer реализован классом

DefaultTableCellRenderer, расширяющим класс JLabel. Но класс DefaultTableCellRenderer не реализует даже все возможности класса JLabel, например не отображаются изображения типа Icon. Используется только метод setText(String) класса JLabel в таком виде:

protected void setValue(Object value){

setText((value == null) ? "" : value.toString());

}

Это означает, что, хотя ячейка таблицы может содержать любой объект, на экране появляется только строка, полученная методом toString() этого объекта.

Для того чтобы показать графические объекты, хранящиеся в ячейках таблицы, в графическом виде, чаще всего достаточно переопределить метод setValue(Object) класса DefaultTableCellRenderer. Например, в программе листинга 13.2 объекты класса Color выводятся текстовой строкой, как видно на рис. 13.1. Чтобы в ячейке показать цвет, надо расширить класс DefaultTableCellRenderer, переопределив его метод setValue ( ):

class ColorRenderer extends DefaultTableCellRenderer{ public void setValue(Object value){

setBackground((Color)value);

}

}

Для того чтобы этот способ визуализации применялся только к объектам класса Color, следует обратиться к методу setDefaultRenderer(Class, TableCellRenderer) класса JTable. В примере листинга 13.2 в конструктор класса SimpTable надо вставить строку

t1.setDefaultRenderer(Color.class, new ColorRenderer());

Этот метод можно применить несколько раз для разных классов, задав таким путем свой класс-рисовальщик для объекта каждого класса. Например, можно выводить на экран изображения, хранящиеся в ячейках таблицы, определив класс:

class IconRenderer extends DefaultTableCellRenderer{

public void setValue(Object value){ setIcon((Icon)value); }

}

и добавив строку

t1.setDefaultRenderer(Icon.class, new IconRenderer());

Возможности класса JLabel, который фактически рисует на экране содержимое ячеек таблицы в классе DefaultTableCellRenderer, ограничены. Для более сложного вывода на экран следует непосредственно реализовать интерфейс TableCellRenderer. Листинг 13.5 показывает, как можно реализовать его для вывода многострочных ячеек, воспользовавшись текстовой областью класса JTextArea, описанного в главе 12.

Листинг 13.5. Несколько строк в одной ячейке таблицы

import java.awt.*; import javax.swing.*; import javax.swing.table.*; import javax.swing.border.*; import java.util.*;

public class MultiLineTable extends JFrame{ MultiLineTable(int lineCount){

super(" Таблица с многострочными ячейками");

// Расширяем модель ячеек, переопределяя метод,

// возвращающий класс содержимого столбца DefaultTableModel tm = new DefaultTableModel(){ public Class getColumnClass(int col){ return getValueAt(0, col).getClass();

}

};

// Заносим в модель ячеек данные tm.setDataVector( new Obj ect [][] {

{"ИмяиФамилия","ИваниПетров",,,ПетрпИванов"}, {"ОтделпДолжность", "СбыгтпВодитель", "Сбы1тпЭкспедитор"}

},

new Object[] {,,Данные,,,,,1,,,,,2"}

// Создаем таблицу с новой моделью ячеек

JTable t = new JTable(tm);

// Изменяем выюоту ячеек на экране, чтобы1 поместились // все строки содержимого ячейки t.setRowHeight( t.getRowHeight() * lineCount);

// Устанавливаем новым класс-рисовальщик t.setDefaultRenderer(String.class, new MultiLineCellRenderer());

add(new JScrollPane(t));

setSize(400, 400);

setDefaultCloseOperation(JFrame.EXIT ON CLOSE); setVisible(true);

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

new MultiLineTable(2);

}

}

// Класс-рисовальщик

class MultiLineCellRenderer extends JTextArea implements TableCellRenderer{

public MultiLineCellRenderer(){ setLineWrap(true); setWrapStyleWord(true); setOpaque(true);

}

public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col){ if (isSelected){

setForeground(table.getSelectionForeground()); setBackground(table.getSelectionBackground()); }else{

setForeground(table.getForeground()); setBackground(table.getBackground());

}

if (hasFocus){

setBorder(UIManager.getBorder("Table.focusCellHighlightBorder")); if (table.isCellEditable(row, col)){

setForeground(UIManager.getColor("Table.focusCellForeground"));

setBackground( UIManager.getColor("Table.focusCellBackground"));

}

}else setBorder(new EmptyBorder(1, 2, 1, 2)); setFont(table.getFont());

setText((value == null) ? "" : value.toString()); return this;

}

}

Часто требуется поместить несколько строк в заголовки столбцов таблицы. Для этого можно воспользоваться программой листинга 13.5, написав новый метод

getTableCellRendererComponent() :

public Component getTableCellRendererComponent(

JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col){

if (table != null){

JTableHeader header = table.getTableHeader(); if (header != null){

setForeground(header.getForeground()); setBackground(header.getBackground()); setFont(header.getFont());

}

}

setText((value == null) ? "" : value.toString()); setBorder(UIManager.getBorder("TableHeader.cellBorder")); return this;

}

Затем надо установить экземпляр измененного класса (назовем его MultiLineHeader) в заголовок каждого столбца, взяв заголовки из модели заголовков таблицы:

MultiLineHeader mlh = new MultiLineHeader();

Enumeration e = t.getColumnModel().getColumns(); while (e.hasMoreElements())

((TableColumn)e.nextElement()).setHeaderRenderer(mlh);

Редактор ячеек таблицы

По умолчанию таблица создается редактируемой. Это означает, что содержимое ее ячеек можно изменять вводом новых значений с клавиатуры. Для редактирования ячеек используется еще один класс-делегат. Им может стать любой класс, реализующий интерфейс TableCellEditor.

Интерфейс TableCellEditor расширяет интерфейс cellEditor и добавляет к его методам только один метод

public Component getTableCellEditorComponent(JTable table,

Object value, boolean isSelected, int row, int column);

который должен выполнять любой редактор ячеек таблицы. Этот метод формирует компонент, пригодный для редактирования, например объект класса JTextField, для заданной ячейки с индексами (row, column) и содержимым value. Дополнительно указывается таблица table, в которой происходит редактирование. Метод может учесть параметр isSelected, показывающий, выделена ячейка или нет.

Сам же интерфейс CellEditor описывает несколько методов, из которых наиболее важны два. Один из них- логический метод stopCellEditing() - возвращает true, если

редактирование ячейки завершено и следует сохранить сделанные изменения. Он возвращает false, если редактирование еще не завершено. В это время удобно сделать проверку измененной ячейки. Второй метод cancelCellEditing () отменяет редактирование.

При изменении содержимого ячейки происходит событие класса ChangeEvent, которое можно отследить, присоединив к редактору слушателя события методом

addCellEditorListener(CellEditorListener).

Интерфейс TableCellEditor реализован классом DefaultCellEditor. Конструкторы этого класса применяют в качестве конкретных редакторов компоненты JCheckBox, JComboBox и JTextField.

По умолчанию таблицы используют редактор с полем ввода JTextField. Хотя в ячейках таблицы могут располагаться любые объекты, этот редактор обрабатывает только текст, получающийся применением метода toString() такого объекта.

Компонент JCheckBox используется для изображения в виде флажка логического содержимого ячейки типа boolean, как показано на рис. 13.1. Этот редактор позволяет изменять истинность содержимого ячейки.

Компонент JComboBox применяется для ввода в ячейку одного из нескольких значений, содержащихся в раскрывающемся списке.

В листинге 13.6 приведен пример программы, позволяющей устанавливать разные редакторы для изменения содержимого различных ячеек таблицы. В примере ячейка в третьей строке и во втором столбце редактируется компонентом JComboBox. Результат показан на рис. 13.3.

Листинг 13.6. Разные редакторы ячеек таблицы

import java.util.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.table.*; import javax.swing.event.*;

public class RowEd extends JFrame{ public RowEd(){

super(, Редактор строки,);

DefaultTableModel dm = new DefaultTableModel(); dm.setDataVector(

new Object[] [Н^Имя11, ''Иван'1},

{''Фамилия'', ''Петров'1},

{''Пол'', ''Мужской''}}, new Object[]{"Cотрудник", ''Сведения''});

JTable table = new JTable(dm);

JComboBox cb = new JComboBox(); cb. addItem (''Мужской'') ; cb.addItem('Женский');

RowEditor rowEd = new RowEditor(table); rowEd.setEditorAt(2, new DefaultCellEditor(cb)); table.getColumn('Сведения').setCellEditor(rowEd);

add(new JScrollPane(table)); setSize(400, 100); setVisible(true);

}

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

}

}

class RowEditor implements TableCellEditor{

protected Hashtable editors;

protected TableCellEditor editor, defEditor;

JTable table;

public RowEditor(JTable table){ this.table = table; editors = new Hashtable();

defEditor = new DefaultCellEditor(new JTextField());

}

public void setEditorAt(int row, TableCellEditor editor){ editors.put(new Integer(row), editor);

}

public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column){

return editor.getTableCellEditorComponent(table, value, isSelected, row, column);

}

public Object getCellEditorValue(){ return editor.getCellEditorValue();

}

public boolean stopCellEditing(){ return editor.stopCellEditing();

public void cancelCellEditing(){ editor.cancelCellEditing();

}

public boolean isCellEditable(EventObject anEvent){ selectEditor((MouseEvent)anEvent); return editor.isCellEditable(anEvent);

}

public void addCellEditorListener(CellEditorListener l){ editor.addCellEditorListener(l);

}

public void removeCellEditorListener(CellEditorListener l){ editor.removeCellEditorListener(l);

}

public boolean shouldSelectCell(EventObject anEvent){ selectEditor((MouseEvent)anEvent); return editor.shouldSelectCell(anEvent);

}

protected void selectEditor(MouseEvent e){ int row = (e == null) ?

table.getSelectionModel().getAnchorSelectionIndex() : table.rowAtPoint(e.getPoint());

editor = (TableCellEditor)editors.get(new Integer(row)); if (editor == null) editor = defEditor;

}

}

Г-Редактор строки В®®’ Сотрудник Сведения Имя Иван - Фамилия Петров Пол Мч/жпклй т 4 МужскойЖенский

Рис. 13.3. Компонент JComboBox в ячейке таблицы

Сортировка строк таблицы

Вы знаете, что, работая с электронными таблицами, такими как MS Excel, можно отсортировать строки таблицы по какому-нибудь столбцу, просто щелкнув мышью по заголовку столбца. Повторный щелчок вызовет сортировку в обратном порядке.

Такая возможность есть и в классе JTable. Самый простой способ реализовать ее — это включить сортировку методом setAutoCreateRowSorter(true):

JTable table = new JTable(); table.setAutoCreateRowSorter(true);

Все! После этого щелчок мышью по заголовку столбца таблицы вызовет сортировку строк таблицы по этому столбцу, а повторный щелчок — сортировку в обратном порядке.

Сортировку обеспечивает абстрактный настраиваемый класс RowSorter<M>. Он рассчитан не только на таблицы, а вообще на компоненты, сконструированные по схеме MVC (см. главу 3), и осуществляет связь между Видом и Моделью в этой схеме. Порядок строк меняется только во View, строки в Model остаются без изменения.

Класс RowSorter<M> расширен абстрактным классом DefaultRowSorter<M, I>, рассчитанным на модель, в которой данные хранятся в виде таблицы.

Для сортировки строк таблицы сделано его расширение-класс TableRowSorter<M extends

TableModel>. Объект этого класса осуществляет связь View и Model таблицы, которую можно записать следующим образом:

FileTableModel model = new FileTableModel();

JTable table = new JTable(model);

table.setRowSorter(new TableRowSorter(model));

Разумеется, тип элементов столбца, по которому ведется сортировка, должен допускать сравнение элементов по величине, реализовав интерфейс Comparator (см. главу 6). Это можно проверить логическим методом isSortable(int column) класса DefaultRowSorter. Если это не сделано, то можно поправить дело методом

setComparator(int column, Comparator comparator);

того же класса. Если же и это не сделано, то сортируются строки, полученные из элементов столбца их методами toString ( ).

В тех случаях, когда надо отсортировать строки сразу по нескольким столбцам, следует задать очередность сортировки столбцов. Для этого создается список типа List, содержащий объекты вложенного класса RowSorter.SortKey. При создании этих объектов указывается номер столбца и порядок его сортировки. Порядок сортировки, прямой или обратный, определяется константой ASCENDING или DESCENDING перечисления SortOrder. Очередность столбцов при сортировке соответствует порядку объектов в списке. Все это вместе выглядит так:

List<RowSorter.SortKey> keys = new ArrayList<RowSorter.SortKey>(); keys.add(new RowSorter.SortKey(1, SortOrder.ASCENDING)); keys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));

TableRowSorter sorter = new TableRowSorter(model)); sorter.setSortKeys(keys);

Как уже говорилось ранее, перестановка строк при сортировке происходит только при выводе их из Model на экран, точнее говоря, во View. Методами

int modelRowIndex = table.convertRowIndexToModel(viewRowIndex); int viewRowIndex = table.convertRowIndexToView(modelRowIndex);

можно отследить соответствие порядкового номера строки viewRowIndex во View и ее порядкового номера modelRowIndex в Model.

Фильтрация строк таблицы

Кроме сортировки, класс RowSorter позволяет выбрать строки таблицы из Model по какому-нибудь критерию для вывода их во View. Для этого надо создать фильтр, отбирающий строки — объект абстрактного настраиваемого класса RowFilter<M, I >, и передать ссылку на него методом setRowFilter() класса DefaultRowSorter. Это можно сделать по такой схеме:

TableRowSorter sorter = new TableRowSorter(model));

RowFilter filter = RowFilter.regexFilter(MjavaM, 0); sorter.setRowFilter(filter);

В этом примере отбираются строки таблицы, содержащие в нулевом столбце строку символов, в которой встречается подстрока "java".

Для создания фильтра в классе RowFilter есть несколько статических методов. Кроме использованного ранее метода

public static <M, I > RowFilter<M, I > regexFilter(String regex, int... indices);

фильтрующего строки регулярным выражением regex, полезны еще два метода:

public static <M, I > RowFilter<M, I >

numberFilter(RowFilter.ComparisionType type, Number number, int... indices);

public static <M, I > RowFilter<M, I >

dateFilter(RowFilter.ComparisionType type, Date date, int... indices);

Параметр type этих методов — одна из констант after, before, equal или not_equal вложенного перечисления RowFilter.ComparisionType. Эти константы показывают, что надо передать во View только строки со значением большим, меньшим, равным или не равным значению number или date, служащему вторым параметром методов. Последние параметры indices всех трех методов перечисляют индексы столбцов, значения которых сравниваются со вторым аргументом. Отсутствие параметров indices означает просмотр всех столбцов таблицы.

В более сложных случаях, например, когда задан один или несколько диапазонов значений, можно связать несколько фильтров методами

public static <M, I > RowFilter<M, I > notFilter(RowFilter<M, I > filter);

public static <M, I > RowFilter<M, I >

andFilter(Iterable<? extends RowFilter<? super M, ? super I >> filters);

public static <M, I > RowFilter<M, I >

orFilter(Iterable<? extends RowFilter<? super M, ? super I >> filters);

Первый из этих трех методов, notFilter(), создает фильтр, пропускающий те и только те строки таблицы, которые были бы отвергнуты его аргументом. Второй метод, andFilter (), дает фильтр, пропускающий те и только те строки, которые проходят через все фильтры коллекции filters. Третий метод, orFilter(), пропускает строки, удовлетворяющие хотя бы одному фильтру из коллекции filters.

Для более сложной фильтрации строк таблицы нужно расширить класс RowFilter. При этом достаточно переопределить только один метод

public boolean include(RowFilter.Entry<? extends M, ? extends I > row);

Объект класса DefaultRowSorter обращается к этому методу при просмотре Model, передавая ему каждую строку таблицы. Метод возвращает true, если строка отвечает фильтру и ее надо передать во View.

Параметр этого метода — ссылка на строку таблицы, представленную в виде объекта вложенного настраиваемого класса RowFilter.Entry. Класс RowFilter.Entry предоставляет информацию о строке следующими методами:

? I getIdentifier () — возвращает идентификатор строки в модели;

? m getModel () — возвращает ссылку на модель;

? String getStringValue (int index) — возвращает значение столбца с индексом index в виде строки;

? Object getValue (int index) - возвращает значение столбца с индексом index в виде

ссылки;

? int getValueCount () — возвращает количество столбцов в данной строке.

Эту информацию можно использовать в методе include (), чтобы принять решение об отборе строки. В следующем примере создан фильтр для отбора тех строк таблицы, у которых в первом столбце записан заданный текст, а сумма числовых значений остальных столбцов больше заданного целого числа

class SumRowFilter extends RowFilter{ private String text; private int limit;

public SumRowFilter(String text, int limit){ this.text = text; this.limit = limit;

}

public boolean include(Entry entry){ int sum = 0;

if (entry.getStringValue(0).equals(text)){

for (int i = 1; i < entry.getValueCount() — 1; i++){ sum += ((Number)entry.getValue(i)).intValue();

}

if ( sum > limit) return true;

}

return false;

}

}

Печать таблицы

Несколько логических методов print () вызывают на экран стандартное диалоговое окно печати, позволяющее выбрать принтер и распечатать содержимое таблицы. Методы возвращают false, если пользователь отменил печать, щелкнув мышью по кнопке Cancel в диалоговом окне, и true в противном случае.

? print (JTable. PrintMode mode) - печать без колонтитулов с выбором сжатия столбцов.

Столбцы таблицы при печати будут сжиматься под ширину листа бумаги, если за-

дана константа FIT_WIDTH вложенного в класс JTable перечисления JTable.PrintMode, или не будут — константа normal.

? print () — печать содержимого таблицы без колонтитулов со сжатием столбцов в режиме JTable. PrintMode. FIT_WIDTH.

? print(JTable.PrintMode mode, MessageFormat header, MessageFormat footer) — добавляется печать верхнего header и нижнего footer колонтитулов, оформленных как объекты класса MessageFormat из пакета java.text.

Остальные, более сложные методы печати используют сервер печати, что выходит за

рамки нашей книги.

Примеры создания таблиц и связи их с базами данных имеются в документации Java SE

в каталоге $JAVA_HOME/demo/jfc/TableExample/.

Вопросы для самопроверки

1. Как конструктивная схема MVC использована для создания классов таблиц?

2. Можно ли хранить в таблицах класса JTable образцы цвета?

3. Можно ли хранить в ячейках таблицы раскрывающиеся списки?

4. Можно ли сделать таблицу с ячейками-кнопками?

5. Можно ли сделать отдельные ячейки таблицы не редактируемыми?

6. Можно ли делать вычисления в таблице класса JTable, как это делается в электронных таблицах?

7. Можно ли сортировать строки таблиц класса JTable?

8. Можно ли сделать электронную таблицу средствами класса JTable?

ГЛАВА 14