Добавил:
СПбГУТ * ИКСС * Программная инженерия Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

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

.pdf
Скачиваний:
143
Добавлен:
07.10.2020
Размер:
4.63 Mб
Скачать

Таблицы

525

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

}

}

В примере мы наследуем наш класс от окна с рамкой JFrame и размещаем в панели содержимого окна таблицу JTable, которая получает данные из созданной нами стандартной модели, а также две кнопки, позволяющие динамически, непосредственно во время работы программы, изменять данные модели (именно модели, код в теле слушателей не знает о существовании пользовательского интерфейса программы и о деталях таблицы JTable, он просто работает с данными, хранящимися в стандартной модели). Прежде всего создается сама стандартная модель DefaultTableModel. Мы использовали конструктор без параметров, а это означает, что после создания модель хранит нулевое количество строк и столбцов. Далее мы добавляем в модель значения строк методом addRow(), в который передаем массив со значениями, хранящимися в строке. Задействовать можно не только массив; перегруженная версия метода addRow() обеспечивает добавление строки, данные которой хранятся в векторе Vector. Заметьте, что перед добавлением в модель новых строк мы вызвали метод setColumnIdentifiers(), позволяющий задать названия (а значит и количество) столбцов таблицы. Если вызвать этот метод после добавления в модель DefaultTableModel новых строк, он посчитает, что в модели хранится нулевое количество столбцов (добавление строк не меняет хранимое в модели количество столбцов) и просто удалит все уже добавленные нами строки. Правда, можно задать количество столбцов с помощью метода setColumnCount(), не указывая их названий. Если вы вызовете этот метод перед добавлением в модель строк, последующий вызов метода setColumnIdentifiers() не удалит добавленные строки. Это общее правило работы со стандартной моделью таблицы DefaultTableModel: при вызове методов, изменяющих количество строк или столбцов, данные могут теряться, так что нужно следить за тем, чтобы новое количество строк или столбцов было достаточным для хранимых в модели данных, и заранее заботиться о правильности структуры таблицы.

После добавления в модель всех нужных данных мы передаем ее таблице JTable, которая затем разместится в панели прокрутки JScrollPane в центре панели содержимого (вы помните, что по умолчанию расположение в панели содержимого полярное). Далее создается пара кнопок add и remove, позволяющих динамически манипулировать данными модели. Мы присоединяем к ним слушателей и при щелчках на кнопках соответственно добавляем в модель новую строку и удаляем из нее последнюю строку. О таблице (или нескольких таблицах, или других видах) думать нам не придется — модель сама оповестит всех зарегистрированных в ней слушателей об изменениях в своих данных, а правильно отобразить эти изменения или провести какие-либо другие действия обязаны будут эти слушатели, заинтересованные в данных нашей модели.

526

ГЛАВА 17

Наконец, таблица и кнопки (помещенные в дополнительную панель на юге окна) размещаются в окне, и оно выводится на экран. Запустив программу с примером, вы убедитесь, что стандартная модель исправно снабжает таблицу данными и заботится о том, чтобы информация обо всех изменениях тут же поступала в таблицу. Щелкая на кнопках, нетрудно убедиться, что данными модели прекрасно можно манипулировать и динамически. Единственное «но» — никогда не следует добавлять, удалять или каким-либо другим образом манипулировать большими объемами данных, которые вы храните в стандартной модели, уже после присоединения ее к виду (таблице). Таблица будет отображать каждое, даже самое незначительное изменение в данных, и это может привести к значительному снижению производительности. В таких случаях лучше написать собственную модель, в которой оповещать об изменениях в данных только после завершения изменений. Основной же объем данных, так же как это делалось для моделей списков или деревьев, следует добавлять перед присоединением модели к виду.

Стандартная модель довольно удобна, когда вам не хочется думать о представлении своих данных в виде модели TableModel; она позволяет просто добавить строки и задать информацию о столбцах так, как будто вы работаете с очередным контейнером данных, подобным списку или очереди. Но чаще гораздо лучше создать собственную модель, в которой можно сполна учесть специфику источника данных, получить данные, при необходимости подготовить и отсортировать их, сообщить таблице дополнительную информацию об этих данных (в том числе и о том, можно ли их изменять, и каков их тип). Собственная модель позволяет также учесть, когда удобнее всего сообщать присоединенным к модели слушателям об изменениях в данных (вы помните, что слишком частые оповещения об изменениях приводят к снижению производительности). Благодаря модели все действия, связанные с данными, будут выполняться в одном месте, что упрощает обновление и расширение программы.

Есть два способа создать свою модель данных для таблицы. Во-первых, можно «с нуля» реализовать описывающий модель данных таблицы интерфейс TableModel, обеспечив всю функциональность модели самостоятельно (в том числе придется реализовать поддержку слушателей). Во-вторых, можно унаследовать класс своей модели от абстрактного класса AbstractTableModel, в котором уже реализована поддержка слушателей и есть удобные методы для инициирования событий нужного вам вида. Второй способ проще и быстрее, так что мы выбираем его. Для того чтобы наша модель заработала, нужно совсем немного — написать три метода: методы getRowCount() и getColumnCount() позволяют таблице определять количество строк и столбцов, а основной метод модели getValueAt() возвращает значение ячейки с указанными номерами строки и столбца. Рассмотрим пример, в котором создадим простую модель на основе класса AbstractTableModel:

//SimpleTableModel.java

//Создание простой модели для таблицы import javax.swing.*;

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

public class SimpleTableModel extends JFrame { public SimpleTableModel() {

super("SimpleTableModel"); setDefaultCloseOperation(EXIT_ON_CLOSE); // создаем таблицу на основе нашей модели

Таблицы

527

JTable table = new JTable(new SimpleModel()); table.setRowHeight(32);

add(new JScrollPane(table)); // выводим окно на экран setSize(400, 300); setVisible(true);

}

// наша модель

class SimpleModel extends AbstractTableModel {

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

return 100000;

}

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

return 3;

}

//тип данных, хранимых в столбце

public Class getColumnClass(int column) { switch (column) {

case 1: return Boolean.class; case 2: return Icon.class; default: return Object.class;

}

}

// данные в ячейке

public Object getValueAt(int row, int column) { boolean isEven = (row % 2 == 0);

// разные данные для разных стобцов switch (column) {

case 0: return "" + row; case 1: return isEven;

case 2: return new ImageIcon("Table.gif");

}

return "Пусто";

}

}

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

new Runnable() {

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

528

ГЛАВА 17

}

}

Мы создаем небольшое окно, в панель содержимого которого добавляем таблицу, включенную в панель прокрутки JScrollPane. Данные для таблицы будет поставлять наша собственная модель SimpleModel, которую мы унаследовали от абстрактного класса AbstractTableModel. Модель мы передаем прямо в конструктор класса JTable (хотя вы можете использовать для присоединения модели и метод setModel()), этого достаточно, чтобы таблица начала отображать данные, хранимые моделью. Дополнительно мы немного увеличиваем высоту строк таблицы методом setRowHeight(). Как вы сможете увидеть при запуске программы с примером, это позволяет таблице лучше справиться с отображением значков (в модели есть столбец, данные которого представляют собой значки Icon).

Самое интересное скрыто в модели. Для того чтобы определить структуру и тип данных, а также передать таблице эти данные, нам приходится переопределять несколько описанных в интерфейсе TableModel методов, в которых и выполняется вся работа. Давайте кратко опишем эти методы, а затем обсудим, как мы использовали их в нашем примере. При создании таблиц к этим методам приходится обращаться так часто, что табл. 17.2 пригодится нам в качестве удобного справочника (звездочкой отмечены те методы, которые обязательно необходимо описать при наследовании от класса AbstractTableModel, чтобы создать простейшую модель для таблицы).

Таблица 17.2. Основные методы модели данных таблицы TableModel

Название метода

Описание

getRowCount()*

Метод должен возвращать целое число — количество строк в та-

 

блице. Если вы изменяете количество строк, не забудьте опове-

 

стить об этом слушателей модели (чаще всего это таблицы или

 

их UI-представители), чтобы они смогли сразу же отобразить из-

 

менения. Это верно и для остальных данных модели — при их

 

изменении не забывайте оповещать слушателей

getColumnCount()*

Метод возвращает количество столбцов в вашей таблице в виде

 

целого (int) числа. Количество столбцов, как и количество строк,

 

также может изменяться по мере работы программы, и об этом

 

необходимо сообщать слушателям модели чтобы увидеть изме-

 

нения на экране

getValueAt(строка, столбец)*

Основной метод модели данных таблицы. Позволяет указать,

 

какие данные находятся в ячейке, определяемой по указанным

 

строке и столбцу. Данные могут иметь любой тип (возвращается

 

ссылка на базовый тип Object). Отсчет строк и столбцов, как не-

 

трудно догадаться, ведется с нуля

getColumnName(столбец)

Метод позволяет задать имя для столбца, которое отображается

 

в заголовке таблицы JTableHeader (как вы помните, заголовок та-

 

блицы появляется, только если последняя помещается в панель

 

прокрутки). Реализация в абстрактном классе AbstractTableModel

 

именует столбцы как в электронных таблицах: латинскими буква-

 

ми, сначала одиночными, затем сдвоенными, и т. д.

isCellEditable(строка, столбец)

Методпозволяетуказать, можнолиредактироватьячейкувтабли-

 

це с указанным местоположением. Если в вашей таблице есть ре-

 

дактируемые ячейки, не забудьте определить метод setValueAt(),

 

иначе как бы старательно пользователь не редактировал данные,

 

в модели (а значит, и на экране) изменения отражаться не будут

Таблицы

529

Таблица 17.2 (продолжение)

 

 

 

Название метода

Описание

 

 

setValueAt(значение, строка,

Метод используется для изменения значения некоторой ячейки

столбец)

таблицы. Реализуйте этот метод, если в вашей таблице есть ре-

 

дактируемые ячейки, иначе их значение невозможно будет поме-

 

нять

getColumnClass(столбец)

Один из самых интересных методов модели данных таблицы. По-

 

зволяет задать тип данных, хранимых в столбце (тип задается

 

в виде объекта Class). На основе типа данных столбца таблица

 

определяет, как следует отображать и редактировать эти данные.

 

Таблица JTable стандартно поддерживает несколько типов дан-

 

ных для столбцов, подробнее мы их вскоре обсудим

Как видно из кода примера, мы в нашей модели SimpleModel сразу же задаем количество строк и столбцов. Обратите внимание, какое гигантское количество строк в нашей модели, и как просто его задать. Используй мы построчное добавление в стандартную модель или двухмерные массивы, написание даже простой программы рисковало превратиться в сущий ад. В методе getValueAt() возвращается значение каждой ячейки таблицы: для первого столбца мы возвращаем номер строки, для второго — логическую переменную isEven, которая автоматически преобразуется языком в объект Boolean (она равна true для четных строк и false для нечетных), для третьего — значок ImageIcon, созданный на основе файла в формате GIF с небольшим изображением. Тут же возникает вопрос: «Как же таблица сможет это все отобразить?» Мы уже привыкли, что для правильного отображения нестандартных данных (отличных от обычных строк) приходится писать вспомогательные объекты, но как оказывается, таблица JTable способна справиться со многими объектами сама, нужно лишь правильно вызывать метод модели getColumnClass().

Как мы уже отметили в табл. 17.2, метод getColumnClass() позволяет указать тип (а это в Java эквивалентно классу, так что метод возвращает объект Class) данных столбца, так чтобы таблица соответствующим образом отображала и позволяла редактировать эти данные. Типы данных для столбцов, которые поддерживает таблица JTable, перечислены в табл. 17.3.

Таблица 17.3. Типы данных для столбцов таблиц

Тип данных

Описание

Number.class

Столбец с таким типом данных должен хранить данные в виде объ-

 

ектов Integer, Long, Byte и т. д., то есть в виде стандартных классов-

 

оболочек для целых чисел, унаследованных от базового класса

 

Number. Таблицапозаботитсяотом, чтобычислаотображалисьвпод-

 

ходящем виде (к примеру, числа в ячейке выравниваются по правому

 

краю, хотя это зависит от выбранного языка) и чтобы при редактиро-

 

вании пользователь мог вводить в качестве нового значения ячейки

 

только целые числа в подходящем диапазоне (в противном случае

 

пользователь не сможет закончить редактирование и вернуться к ра-

 

боте с таблицей)

Float.class или Double.

Если столбец имеет такой тип данных, то он должен хранить числа

class

с плавающей запятой, «обернутые» в объекты Float или Double. Та-

 

блица будет отображать эти объекты как числа с плавающей запятой

 

и соответствующим образом проводить редактирование, запрещая

 

пользователю ввод неподходящих значений

530

ГЛАВА 17

Таблица 17.2 (продолжение)

 

 

 

Тип данных

Описание

 

 

Boolean.class

В столбце такого типа хранятся данные о логических переменных

 

(boolean), «обернутых» в стандартный класс Boolean. Данные такого

 

столбца таблица отображает, помещая в ячейку экземпляр флажка

 

JCheckBox, который может быть и редактируемым (если ячейка с та-

 

ким типом данных редактируема, состояния флажка можно изменить,

 

используя мышь или клавиатуру)

Date.class

Удобный тип столбца, особенно полезный в таблицах, отображающих

 

результаты запросов к базам данных, где даты встречаются очень ча-

 

сто. Столбец с таким типом должен хранить данные в виде дат Date

 

из пакета java.util. Таблица будет отображать даты согласно локаль-

 

ным настройкам операционной системы (в разных странах стандарты

 

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

 

DateFormat. Правда, в Swing нет компонентов или стандартных диа-

 

логовых окон для ввода дат, так что их редактирование проводится

 

так же, как редактирование обычных строк, что неудобно и запуты-

 

вает пользователя. Поэтому, если в вашей программе нужно редак-

 

тировать даты, проще написать собственный редактор ячеек с таким

 

типом. Как это сделать, мы вскоре увидим

Icon.class

Очень полезный тип столбца, указывающий, что в нем хранятся знач-

 

ки Icon, к которым в том числе относятся и значки на основе изобра-

 

жений ImageIcon. Как правило, ячейки со значками не редактируются,

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

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

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

Используя метод getColumnClass(), мы с легкостью можем указать таблице, что в наших столбцах хранятся нестандартные данные, так что она сможет соответствующим образом вывести их на экран. Для второго столбца нашей таблицы мы будем использовать тип Boolean.class (так что логические переменные отобразятся флажками), а для третьего — Icon.class, после чего таблица будет выводить в соответствующих ячейках не текст, а значки. Для первого столбца нашей таблицы можно было бы указать тип Integer. class, но мы для простоты ограничились простыми строками String, автоматически преобразуя любые объекты и примитивные типы в строки.

Таблицы

531

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

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

Далее мы увидим, что внешний вид столбцов и способ редактирования хранящихся

вних ячеек может быть настроен гибко. Это можно сделать с помощью типа столбца, который вы сообщаете методом модели getColumnClass(). При этом вы не ограничены стан-

дартными типами данных для столбцов и можете регистрировать новые типы, указывая таблице JTable, как их нужно отображать и редактировать. Можно также действовать «в обход» модели, указав, как следует отображать и редактировать данные для некоторого столбца, представленного объектом TableColumn. Пока же нам важно то, как модель позволяет гибко описать все ячейки таблицы независимо от того, где и как эти ячейки будут отображаться.

Модель таблицы для работы с базами данных

Большая часть современных приложений так или иначе взаимодействует с базами данных, либо напрямую сотрудничая с системами управления базами данных (СУБД), либо посредством вспомогательных компонентов, выполняющих дополнительные действия, таких как EJB-компоненты (аббревиатура EJB означает Enterprise JavaBeans). Идея разделения данных и места, где эти данные (или с их часть) обрабатываются, давно признана удачной и активно эксплуатируется, позволяя эффективно разделять, обновлять и защищать данные (в меньшем масштабе эта идея прекрасно прижилась в библиотеке Swing, в которой разделены модели, представляющие собой данные, и виды, эти данные отображающие).

Не составляет большого труда заметить, что в Swing лучше всего для отображения результатов запросов к базам данных подходит именно таблица JTable. Она с легкостью отобразит любое количество столбцов и строк, позволит задать различные типы данных для столбцов, соответствующим образом редактировать и отображать их (а в современных базах данных типы хранимой информации задаются именно для столбцов), при необходимости внести в данные изменения и передать модифицированные результаты обратно в хранилище. Самое приятное, что благодаря интерфейсу TableModel мы можем один раз создать модель, получающую информацию из базы данных, а затем использовать ее для вывода в таблицах JTable результатов запросов к разнообразным базам данных.

Работать с базами данных мы будем с помощью интерфейса JDBC, в котором результат запроса к базе данных возвращается в виде объекта ResultSet. С помощью этого объекта несложно получить данные любого типа, считывая их по строкам и столбцам. Для каждого объекта ResultSet существует дополнительная информация в виде объекта ResultSetMetaData, позволяющая выяснить количество, типы и названия столбцов, полученных в результате запроса. Мы используем эту информацию для настройки нашей модели. Итак, приступим:

//com/porty/swing/DatabaseTableModel.java

//Модель данных таблицы, работающая

//с запросами к базам данных

package com.porty.swing;

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

532 ГЛАВА 17

import java.util.*;

public class DatabaseTableModel extends AbstractTableModel {

// здесь мы будем хранить названия столбцов

private ArrayList<String> columnNames = new ArrayList<String>();

// список типов столбцов

private ArrayList<Class> columnTypes = new ArrayList<Class>(); // хранилище для полученных данных из базы данных

private ArrayList<ArrayList<Object>> data

=new ArrayList<ArrayList<Object>>();

//признак редактирования таблицы private boolean editable;

//конструктор позволяет задать возможность редактирования public DatabaseTableModel(boolean editable) {

this.editable = editable;

}

//количество строк

public int getRowCount() { return data.size();

}

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

return columnNames.size();

}

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

public Class getColumnClass(int column) { return columnTypes.get(column);

}

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

public String getColumnName(int column) { return columnNames.get(column);

}

// данные в ячейке

public Object getValueAt(int row, int column) { return (data.get(row)).get(column);

}

// возможность редактирования

public boolean isCellEditable(int row, int column) { return editable;

}

Таблицы

533

//замена значения ячейки public void setValueAt(

Object value, int row, int column){ (data.get(row)).set(column, value);

}

//получение данных из объекта ResultSet public void setDataSource(

ResultSet rs) throws Exception {

//удаляем прежние данные data.clear(); columnNames.clear(); columnTypes.clear();

//получаем вспомогательную информацию о столбцах

ResultSetMetaData rsmd = rs.getMetaData(); int columnCount = rsmd.getColumnCount(); for ( int i=0; i<columnCount; i++) {

//название столбца columnNames.add(rsmd.getColumnName(i+1));

//тип столбца

Class type = Class.forName(rsmd.getColumnClassName(i+1));

columnTypes.add(type);

}

// получаем данные while ( rs.next() ) {

// здесь будем хранить ячейки одной строки

ArrayList<Object> row = new ArrayList<Object>(); for ( int i=0; i<columnCount; i++) {

row.add(rs.getObject(i+1));

}

data.add(row);

}

// сообщаем об изменениях в структуре данных

fireTableStructureChanged();

}

}

Мы назвали нашу модель DatabaseTableModel и разместили ее в пакете com.porty.swing, так что при необходимости вы сможете включать ее в программы и без особых трудностей наглядно выводить результаты запросов к базам данных. Основной метод нашей модели — setDataSource(). Он принимает результат запроса к базе данных ResultSet, удаляет хранящиеся в модели данные, полученные после обработки предыдущих запросов (если таковые были), и заново описывает структуру столбцов таблицы на основе типов

534

ГЛАВА 17

данных полученного запроса. Все данные, включая названия и типы данных (объекты Class) столбцов, хранятся в списках ArrayList. При получении в метод setDataSource() нового результата запроса к базе данных мы удаляем из списков всю прежнюю информацию и приступаем к обработке объекта ResultSet.

Как уже было упомянуто, с каждым объектом ResultSet ассоциирован другой объект ResultSetMetaData, содержащий вспомогательную информацию о результатах запроса. Он позволит нам гибко, не вдаваясь в подробности того, к какой базе данных мы подключены, узнавать все необходимое для построения подходящей модели. Прежде всего мы получаем количество столбцов в полученном результате запроса (с помощью метода getColumnCount()), и для каждого столбца выясняем его название (это название столбца в базе данных, чаще всего оно не слишком удобно для прочтения человеком, но в реальном приложении вы сможете легко сменить идентификаторы столбцов на что-либо более подходящее) и тип (тип, как всегда в Java, аналогичен классу, так что мы получаем название класса и создаем его экземпляр статическим методом forName()). Обратите внимание на то, что при получении информации из объекта ResultSetMetaData (а затем и из объекта ResultSet) отсчет столбцов ведется с единицы, а не с нуля, как в остальных случаях. Так уж устроен интерфейс JDBC, это правило стоит помнить, иначе при попытке получения нулевого столбца запроса вы вместо данных «словите» исключение.

Далее можно приступать непосредственно к работе с данными. Для каждой строки с данными создается свой отдельный список ArrayList, в него будут поочередно добавляться значения данных всех столбцов текущей строки запроса. Данные можно просто получать методом getObject(), пригодным на все случаи жизни, класс ResultSet обещает вернуть в нем подходящий типу столбца объект. После заполнения данными очередной строки она добавляется в список всех данных data.

Как только мы выясняем «всю подноготную» новых столбцов таблицы и наполняем списки строками таблицы, вызывается метод fireTableStructureChanged(), который оповестит слушателей о том, что структура столбцов таблицы поменялась. Такое оповещение приведет к отображению на экране (в том случае если модель присоединена к виду, то есть к таблице JTable), и пользователь увидит, какие столбцы присутствуют в новом наборе данных. При смене структуры столбцов все данные также будут считаны заново и отображены на экране.

Остальные методы нам прекрасно знакомы и служат для правильной работы модели. Большая часть из них получает информацию (название столбца, количество строк и столбцов, тип столбца) из списков ArrayList, которые заполняются данными в уже рассмотренном нами методе setDataSource(). Конструктор нашей модели позволяет указать, будет ли она разрешать редактирование. Новые данные записываются в списке в методе модели setValueAt(), который вызывает таблица при окончании редактирования.

Атеперь проверим работу нашей новой модели. Приведем простой пример:

//DatabaseTable.java

//Таблица, работающая с базой данных

//посредством специальной модели

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

import com.porty.swing.*;

public class DatabaseTable { // параметры подключения private static String