Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Портянкин И. Swing

.pdf
Скачиваний:
210
Добавлен:
07.10.2020
Размер:
5 Мб
Скачать
☆

Таблицы

555

Мы привыкли, что обычно для сложного компонента задается единственный отображающий объект, который и отвечает за то, как выглядят составные элементы этого компонента. Однако таблица — самый мощный и самый сложный компонент библиотеки Swing, позволяющий выводить гигантские объемы разнородной информации, предоставляет нам более гибкие решения. На самом деле, представьте себе таблицу с десяткомдругим столбцов и сотней строк — написание для нее специализированного отображающего объекта рискует превратиться в адский труд, особенно если каждая ячейка должна выглядеть по-своему. Поэтому таблица обладает более гибким поведением.

Таблица позволяет задавать два типа отображающих объектов для ячеек. Отображающие объекты по умолчанию (default renderers) задаются посредством специальных методов таблицы JTable и отвечают за прорисовку данных определенного типа, который, как и всегда в Java, задается объектом Class. Вы помните, что разбирая модель данных таблицы TableModel, мы обратили особое внимание на метод getColumnClass(), позволяющий указать, данные какого типа будет содержать тот или иной столбец. Тип столбца и определяет, какой отображающий объект будет прорисовывать ячейки этого столбца. Если вы помните, таблица JTable изначально поддерживает несколько типов данных, для которых предоставляет стандартные отображающие объекты — это простые строки, значки, булевы значения, а также числа и даты. Все поддерживаемые таблицей типы данных прорисовываются с помощью стандартного объекта DefaultTreeCellRenderer или одного из его подклассов. Вы с помощью предназначенных для этого методов класса JTable можете регистрировать свои отображающие объекты для новых типов данных или же заменять стандартные отображающие объекты собственными. Отображающие объекты ячеек могут также задаваться и для отдельных столбцов, какой бы тип не имела хранящаяся в них информация, иногда такой подход удобнее.

Все стандартные отображающие объекты унаследованы от надписи JLabel, так что вы без зазрения совести можете применять для записи ячеек таблицы все возможности HTML и даже загружать изображения. Впрочем, загрузка изображений посредством встроенного языка HTML не позволяет затем успешно распространять приложения в виде единого JAR-файла, поэтому давайте применим, как мы не раз это уже делали для других компонентов Swing, отображающий элемент, способный прорисовывать одновременно и заранее имеющийся значок Icon, и текст. Прежде всего нам понадобится специальный объект для хранения текста и значка. Мы не станем изобретать велосипед, а возьмем уже имеющийся у нас со времен работы со списками класс ImageListElement:

//com/porty/swing/ImageListElement.java

//Класс, хранящий значок и текст элемента package com.porty.swing;

import javax.swing.*;

public class ImageListElement { // значок и текст

private Icon icon; private String text; // удобный конструктор

public ImageListElement(Icon icon, String text) { this.icon = icon;

this.text = text;

}

556

ГЛАВА 17

// методы для доступа к значку и тексту public Icon getIcon() { return icon; } public String getText() { return text; }

public void setIcon(Icon icon) { this.icon = icon; } public void setText(String text) { this.text = text; }

}

В объекте ImageListElement будет храниться текст и значок ячейки. Для удобства имеется конструктор, позволяющий сразу же задать то и другое, и методы для смены данных. На очереди отображающий объект, который займется с прорисовкой текста и значка:

//com/porty/swing/ImageTableCellRenderer.java

//Объект для прорисовки значка и текста в таблице package com.porty.swing;

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

public class ImageTableCellRenderer extends DefaultTableCellRenderer {

// метод возвращает компонент для прорисовки public Component getTableCellRendererComponent(

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

// получаем объект нужного типа

if ( value instanceof ImageListElement ) { ImageListElement imageCell = (ImageListElement)value;

//получаем настроенную надпись от базового класса

JLabel label = (JLabel)super. getTableCellRendererComponent(table,

imageCell.getText(), isSelected, hasFocus, row, column);

//устанавливаем значок и подсказку label.setIcon(imageCell.getIcon()); label.setToolTipText(imageCell.getText()); return label;

}else {

return super.getTableCellRendererComponent(

table, value, isSelected, hasFocus, row, column);

}

}

}

Наш новый отображающий объект унаследован от стандартного объекта DefaultTableCellRenderer (а тот, в свою очередь, — от надписи JLabel). Мы переопределяем

Таблицы

557

метод getTableCellRendererComponent(), возвращающий компонент, который и будет прорисовывать данные, хранящиеся в ячейках таблицы. Здесь, кстати, применимо правило, которое мы не раз уже обсуждали при изучении отображающих объектов: компоненты, возвращаемые этими объектами, не появляются на экране, они служат лишь «кистями», которые таблица использует при прорисовке определенных ячеек, поэтому компонент задействуется в единственном числе, а для каждого значения он заново настраивается. Это позволяет сэкономить уйму памяти и ресурсов системы графического интерфейса. Сначала данные преобразуются в тип ImageTableCell; во избежание ошибок преобразования типов мы сначала проверяем, что это подходящий нам тип. Смотрите, что происходит дальше: мы получаем от базового класса настроенную надпись (с подходящими цветами и шрифтами) с текстом, устанавливаем для нее значок, который также хранится в объекте ImageListElement, добавляем подсказку, и возвращаем таблице полученный результат. Именно отображающие объекты запрашиваются таблицей с целью получить подсказку для конкретной ячейки. Класс ImageListElement и новый отображающий объект разместились в пакете com.porty.swing, откуда их удобнее будет загружать в собственные программы.

Теперь остается указать таблице, что в некотором столбце будут храниться данные нового типа и зарегистрировать для нового типа подходящий отображающий объект. Тип данных столбца позволяет указать модель данных TableModel, а регистрацию новых отображающих объектов проводит метод setDefaultRenderer() класса JTable. У нас получается следующая программа:

//RegisteringTableRenderer.java

//Регистрация в таблице собственного

//объекта для отображения

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

public class RegisteringTableRenderer extends JFrame { public RegisteringTableRenderer() {

super("RegisteringTableRenderer"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//создаем таблицы на основе нашей модели

JTable table = new JTable(new SpecialModel());

//регистрируем объект для прорисовки новых данных table.setDefaultRenderer(ImageListElement.class,

new ImageTableCellRenderer());

//выводим окно на экран

add(new JScrollPane(table)); pack();

setVisible(true);

}

// модель таблицы

class SpecialModel extends AbstractTableModel { // значки

558 ГЛАВА 17

private Icon

image1 = new ImageIcon("clip1.gif"), image2 = new ImageIcon("clip2.gif");

// названия столбцов

private String[] columnNames = { "Компания", "Адрес"

}; // данные таблицы

private Object[][] data = {

{new ImageListElement(image1, "MegaWorks"), "<html><font color=\"red\">Париж" },

{new ImageListElement(image2, "TerraMedia"), "<html><b>С.-Петербург" }

};

//количество столбцов public int getColumnCount() {

return columnNames.length;

}

//названия столбцов

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

}

//количество строк public int getRowCount() {

return data.length;

}

//тип данных столбца

public Class getColumnClass(int column) { return data[0][column].getClass();

}

// значение в ячейке

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

}

}

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

new Runnable() {

public void run() { new RegisteringTableRenderer(); } });

}

}

Таблицы

559

В примере мы создаем окно с таблицей в его центре. Данные для таблицы будет поставлять специальная модель, унаследованная от класса AbstractTableModel, благодаря отдельной модели мы сможем указать, какие типы данных (представленные объектами Class) хранятся в каждом столбце. Собственно данные хранятся в двух массивах: в массиве columnNames мы храним названия столбцов, а в массиве data — данные ячеек. Заметьте, что в первом столбце данные хранятся в виде только что созданных нами объектов ImageListElement, позволяющих совместить изображение и текст. Методы модели TableModel нам хорошо знакомы, пристального внимания заслуживает лишь один из этих методов, а именно getColumnClass() — для первого столбца модели он возвращает тип (класс) ImageListElement, и данный тип нужно правильным образом отобразить.

В конструкторе окна создается таблица JTable, которой передается описанная во внутреннем классе модель. Чтобы объекты ImageListElement, хранимые первым столбцом таблицы, правильно отображались, необходимо зарегистрировать для них подходящий отображающий объект. Это и делает метод setDefaultRenderer(), сопоставляющий тип данных и объект, который обязуется эти данные отобразить. Запустив программу с примером, вы увидите наш новый отображающий объект в действии, а также еще раз убедитесь во всемогуществе встроенного в Swing языка HTML, и появлении у каждой ячейки своей подсказки. Стандартный отображающий объект унаследован от надписи JLabel, и все возможности последней остаются в силе.

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

Однако таблица JTable предоставляет еще одну возможность управления отображением данных, хранимых в ее ячейках. Иногда единое отображение всех данных одного типа (а тип сразу же задается для всего столбца посредством модели) оказывается недостаточно гибким. Вы можете захотеть отображать ячейки в одном и том же столбце поразному. В таких ситуациях таблица позволяет регистрировать разные отображающие объекты для одного столбца независимо от того, данные какого типа там хранятся. Если говорить точнее, отображающий объект регистрируется в объекте с информацией о столбце TableColumn, который нам уже известен.

Для отображения ячеек, как и прежде, используется объект, реализующий интерфейс TreeCellRenderer, только регистрируется он по-иному. Вы получаете нужный столбец из модели TableColumnModel (используя, например метод модели getColumn()) и задаете для его ячеек отображающий объект с помощью метода setCellRenderer(). Стоит отметить, что отображающий объект для вывода ячеек определенного столбца, имеет приоритет перед объектом по умолчанию, работающим в зависимости от типа данных, которые хранятся в столбце. Вы всегда сможете вернуться к объекту по умолчанию, передав в метод setCellRenderer() столбца TableColumn пустую (null) ссылку.

560

ГЛАВА 17

Задание отображающих объектов для столбцов позволяет проводить интересные эксперименты с процессом прорисовки таблицей содержимого своих столбцов. После изучения модели столбцов TableColumnModel нам известно, что одни и те же столбцы, описанные данными модели TableModel, могут быть несколько раз выведены на экран различными столбцами TableColumn. Задавая для таких столбцов разные отображающие объекты, вы сможете придать столбцам с одинаковыми данными уникальный внешний вид.

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

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

Таблица JTable разрешает пользователю изменять значения, хранящиеся в ее ячейках. Ячейки, значения которых можно менять, определяются с помощью модели данных TableModel: в ней есть метод isCellEditable(), говорящий, можно ли редактировать ячейку, расположенную в определенной строке и определенном столбце таблицы. Заведует редактированием ячеек таблицы не сама таблица и не ее UI-представитель, а отдельный объект, реализующий интерфейс TableCellEditor. Вы можете настроить стандартный объект для редактирования, поставляемый вместе с библиотекой Swing, или написать собственный объект и таким образом управлять процессом редактирования ячейки от начала до конца. Только не забывайте определять метод setValueAt() в модели данных TableModel: без него ни один редактор не сможет изменить значения в ячейках.

Объекты для редактирования в таблице JTable очень схожи с объектами для отображения — это также отдельные объекты, позволяющие легко наделить таблицу любыми возможностями редактирования. Так же как и отображающие объекты, объекты для редактирования разделяются на два типа: объекты по умолчанию и объекты, используемые для конкретного столбца таблицы (вторые имеют приоритет перед первыми). Объекты по умолчанию регистрируются посредством специальных методов класса JTable и предназначены для редактирования данных определенного типа (тип данных, хранимых в столбце, возвращает метод getColumnClass() модели TableModel). Изначально таблица поставляется с редакторами для строковых данных (строки редактируются так же, как все данные неизвестного типа, если для них не зарегистрировано специальных редакторов), булевых переменных (в качестве редактора используется флажок JCheckBox) и чисел.

Все редакторы, изначально встроенные в таблицу JTable, реализованы в стандартном объекте для редактирования DefaultCellEditor. Стандартный объект реализует интерфейс TableCellEditor и позволяет использовать для редактирования данных текстовое поле JTextField, флажок JCheckBox или раскрывающийся список JComboBox. Таким образом, стандартный объект довольно гибок и может помочь вам в редактировании ячейки без написания своего объекта для редактирования «с нуля»: текстовое поле позволит ввести любые строковые данные, а раскрывающийся список будет к месту, если вам понадобится организовать редактирование ячейки как выбор одного варианта из нескольких.

Интересно, что нам уже доводилось иметь дело со стандартным редактором ячеек DefaultCellEditor. В главе 15 мы использовали его для редактирования узлов дерева: класс DefaultCellEditor одновременно поддерживает интерфейсы TreeCellEditor (для деревьев) и TableCellEditor (для таблиц). Оба этих интерфейса унаследованы от базового интерфейса CellEditor, который описывает основные обязанности редактора ячеек таблицы или узлов дерева. Интерфейс TableCellEditor прибавляет к базовым методам CellEditor лишь один новый метод getTableCellEditorComponent(), который должен возвращать компонент,

Таблицы

561

применяемый для редактирования ячеек. Так что все, что мы узнали о стандартном редакторе DefaultCellEditor в главе 15, применимо и для таблиц.

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

//TableDefaultEditing.java

//Применение стандартного редактора для таблиц import javax.swing.*;

import java.awt.*;

public class TableDefaultEditing extends JFrame {

//название столбцов private String[] columns = {

"Имя", "Любимый Цвет" };

//данные для таблицы private String[][] data = {

{"Иван", "Зеленый" },

{"Александр", "Красный"},

{"Петр", "Синий"}

};

public TableDefaultEditing() { super("TableDefaultEditing"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создаем таблицу

JTable table = new JTable(data, columns);

//настраиваем стандартный редактор

JComboBox combo = new JComboBox(

new String[] { "Зеленый", "Желтый", "Белый"}); DefaultCellEditor editor =

new DefaultCellEditor(combo); table.getColumnModel().getColumn(1).

setCellEditor(editor);

//выводим окно на экран

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

}

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

new Runnable() {

public void run() { new TableDefaultEditing(); } });

}

}

562

ГЛАВА 17

Здесь в небольшое окно добавляется таблица JTable, созданная на основе двух массивов: одномерного с названиями столбцов и двухмерного с данными таблицы. Мы передаем эти массивы в конструктор таблицы, на их основе будет создана стандартная модель таблицы. Как мы знаем, в стандартной модели редактирование ячеек по умолчанию включено, так что беспокоиться об этом не нужно. Далее идет код настройки стандартного редактора ячеек таблицы. Как мы уже упомянули, стандартный редактор может выполнять редактирование на основе трех компонентов: текстового поля JTextField, флажка JCheckBox и раскрывающегося списка JComboBox. Для нашего примера мы выбираем раскрывающийся список JComboBox. Прежде чем передать его в конструктор стандартного редактора, необходимо наполнить список элементами, среди которых пользователь и будет делать выбор. Для простоты мы создаем список на основе массива из трех строк. Настроенный список передается стандартному редактору, после чего остается указать таблице, для каких ячеек этот редактор потребуется. В нашем примере мы указываем, что полученный редактор необходимо задействовать для второго столбца таблицы (редактор присоединяется к объекту TableColumn, который позволяет получить модель столбцов таблицы TableColumnModel).

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

Редактор дат

Когда мы обсуждали встроенные в таблицу JTable типы данных, то упомянули о том, что она способна отображать даты (представленные объектом Date), но вот встроенного редактора для этих дат нет, точнее, редактировать даты с помощью обычного текстового поля неудобно. Теперь, когда мы знаем обязанности редактора таблицы JTable, мы можем написать собственный редактор для дат, позволяющий комфортно редактировать их. В качестве компонента для редактирования мы применим счетчик JSpinner со специальной моделью — в наборе стандартных компонентов Swing это самый удобный способ менять даты.

Таблицы

563

Реализация «с нуля» интерфейса TableCellEditor — задача довольно трудоемкая, поэтому мы наследуем класс своего нового редактора от абстрактного класса AbstractCellEditor, который реализует большую часть методов базового интерфейса CellEditor. Точно также мы поступили и в главе 15, когда писали собственный редактор для узлов дерева JTree. Вообще, написание редакторов для таблиц и деревьев практически не отличается, и вы сможете легко перенести редакторы, созданные для одного компонента, в другой компонент. После наследования от класса AbstractCellEditor и реализации интерфейса TableCellEditor нам фактически останется определить два метода: метод getTableCellEditorComponent() должен будет возвращать компонент, который применяется при редактировании, а метод getCellEditorValue() сообщать, какое значение находится в данный момент в редакторе. Новый редактор мы поместим в пакет com.porty.swing длятого, чтобы было легко применять его в ваших программах.

//com/porty/swing/DateCellEditor.java

//Редактор для ячеек таблицы, отображающих даты package com.porty.swing;

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

public class DateCellEditor extends AbstractCellEditor implements TableCellEditor {

//редактор — прокручивающийся список private JSpinner editor;

//конструктор

public DateCellEditor() {

// настраиваем прокручивающийся список

SpinnerDateModel model = new SpinnerDateModel(

new Date(), null, null, Calendar.DAY_OF_MONTH); editor = new JSpinner(model);

}

// возвращает компонент, применяемый для редактирования public java.awt.Component getTableCellEditorComponent( JTable table, Object value, boolean isSelected,

int row, int column) {

// меняем значение и возвращаем список editor.setValue(value);

return editor;

}

// возвращает текущее значение в редакторе public Object getCellEditorValue() {

return editor.getValue();

}

}

564

ГЛАВА 17

Новый редактор получился совсем простым. В конструкторе мы создаем и настраиваем счетчик JSpinner, который и будет выступать в роли редактора дат. Как вы помните из главы 12, для того чтобы счетчик JSpinner можно было применять в качестве редактора дат, ему необходимо передать специальную модель SpinnerDateModel. Конструктор этой модели требует четыре параметра. Первый — это текущее значение (это дата, которая будет изначально отображаться в поле счетчика), в качестве которого мы просто передаем новый объект Date. Ограничители дат определяют диапазон изменения даты — для простоты в качестве ограничителей мы передали пустые (null) ссылки, а это означает, что выбор дат будет неограничен. Последним параметром является поле класса Calendar, определяющее, по какой части даты будет осуществляться «прокрутка» — в редакторе это будет день месяца.

Далее следует пара методов, определяющих работу редактора ячеек таблицы. Метод getTableCellEditorComponent() должен возвращать компонент, который и будет редактором таблицы. Здесь все просто: мы, не мудрствуя лукаво, передаем в счетчик значение текущей ячейки (и подразумеваем, что наш редактор будет применяться лишь для дат, представленных объектами Date, в противном случае возникнет исключение) и возвращаем счетчик. Метод getCellEditorValue(), предназначенный для получения текущего значения в редакторе, еще проще: он просто возвращает текущее значение счетчика. Кстати, текущим значением также будет объект Date, но здесь не должно возникать разногласий: счетчик знает, как такие объекты редактировать, а таблица знает, как их отображать (ей нужно лишь сообщить, что некоторый столбец имеет именно такой тип данных). Все остальные методы, определенные в интерфейсе CellEditor, любезно реализованы для нас базовым классом AbstractCellEditor. Вы всегда сможете переопределить их, если вашему редактору понадобится дополнительная функциональность.

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

//CustomTableEditor.java

//Применение специализированного редактора

//для ячеек таблицы

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

import java.awt.*; import com.porty.swing.*;

public class CustomTableEditor extends JFrame {

//заголовок столбцов таблицы private String[] columns = {

"Операция", "Дата" };

//данные таблицы

private Object[][] data = {

{"Покупка", new Date() },

{"Продажа", new Date() }

};

public CustomTableEditor() {