Портянкин И. Swing
.pdf
Списки |
325 |
Прежде всего стоит отметить, что модель ComboBoxModel, как и модель обычного списка ListModel, очень проста. Раскрывающийся список имеет много общего со списком обычным (он также хранит некоторую последовательность элементов, правда, в отличие от обычного списка, выбирать можно только один элемент), так что во избежание лишней работы создатели раскрывающегося списка решили унаследовать модель ComboBoxModel от модели обычного списка ListModel. Если вспомнить модель ListModel, то у нас в распоряжении оказывается четыре унаследованных метода: два для добавления и удаления заинтересованных в изменениях модели слушателей, один для получения элемента на некоторой позиции и еще один для получения количества хранящихся в списке элементов. Кроме того, в интерфейсе ComboBoxModel появилось пара новых методов: getSelectedItem() — для получения выбранного в данный момент элемента иsetSelectedItem() — для смены выбранного элемента. У раскрывающегося списка JComboBox нет отдельной модели выделения (было бы слишком расточительно и сложно использовать отдельную модель для единственного выбранного элемента), так что информацию о выделенном элементе раскрывающийся список с помощью двух новых методов сохраняет в модели ComboBoxModel вместе с информацией обо всех своих элементах.
Создавать модель вы можете не только с помощью интерфейса ComboBoxModel, помимо него раскрывающийся список может использовать модель MutableComboBoxModel. Она унаследована от обычной модели ComboBoxModel и дополнительно описывает четыре метода, позволяющие динамически изменять содержимое раскрывающегося списка. С помощью этих методов вы сможете добавлять новые элементы (к концу списка или на произвольные позиции) или удалять уже имеющиеся элементы7.
Прежде чем начать обдумывать, как реализовать собственную модель для раскрывающегося списка, вы можете обратить внимание на стандартную модель DefaultComboBoxModel. Она унаследована от вспомогательного класса AbstractListModel, позволяющего упростить создание моделей для обычных списков JList, и реализует интерфейс MutableComboBoxModel. В качестве хранилища для элементов списка в ней используется динамический массив (вектор) Vector8. Стандартная модель предоставляет вам соответствующий набор методов, так что вы практически не заметите разницы между работой с ней и с динамическим массивом, а все достоинства модели останутся между тем с вами. Давайте рассмотрим небольшой пример, в котором применим стандартную модель раскрывающегося списка:
//UsingComboBoxModel.java
//Использование стандартной модели раскрывающихся списков import javax.swing.*;
import java.awt.event.*; import java.awt.*;
public class UsingComboBoxModel extends JFrame {
7Появление для раскрывающегося списка отдельной «изменяемой» модели довольно странно
иконтрастирует с обычным списком JList, который обладает лишь одной «неизменяемой» моделью. Логичнее было бы предположить, что свойство «изменяемости» находится во власти конкретной модели, как это и было со списком JList. Похоже, что классы JComboBox и JList писали разные
люди, и это впечатление только усиливается при взгляде на методы этих классов. Нам же от этого только сложнее знакомиться с очень похожими классами, работа с которыми «по идее» должна быть почти одинаковой.
8 Да-да, библиотека Swing так до сих пор и не удосужилась перейти на новую библиотеку коллекций, появившуюся в Java 2, и повсюду все еще используют часто становившийся объектом для нападок и споров довольно медленный динамический массив Vector. К счастью, это один из немногих очевидных просчетов создателей Swing.
326 |
ГЛАВА 11 |
// наша стандартная модель private DefaultComboBoxModel cbm; public UsingComboBoxModel() {
super("UsingComboBoxModel"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем стандартную модель и наполняем ее данными cbm = new DefaultComboBoxModel();
for (int i=0; i<10; i++) { cbm.addElement("Элемент списка №: " + i);
}
//сменяем выбранный элемент cbm.setSelectedItem(cbm.getElementAt(4));
//список на основе нашей модели
JComboBox combo = new JComboBox(cbm); combo.setMaximumRowCount(5);
//стандартная модель позволяет
//динамически манипулировать данными
JButton add = new JButton("Добавить"); add.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) { // случайно выбираем позицию
int pos = (int) (Math.random()*cbm.getSize()); cbm.insertElementAt("Новинка!", pos);
}
});
//добавляем список и кнопку в панель setLayout(new FlowLayout()); add(combo);
add(add);
//выводим окно на экран
setSize(400, 200); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingComboBoxModel(); } });
}
}
Вприведенном примере в окно добавляется раскрывающийся список JComboBox
ипростая кнопка JButton. Раскрывающийся список создается на основе стандартной модели DefaultComboBoxModel, которую мы создаем и наполняем данными еще до при-
Списки |
327 |
соединения к раскрывающемуся списку. Как вы помните, это общее правило для всех стандартных моделей (чаще всего использующих для хранения данных динамические массивы Vector): основную часть данных следует добавлять в модель до присоединения к виду (или нескольким видам), потому что после присоединения модель начинает оповещать вид о каждом изменении в своих данных, что может значительно замедлить работу приложения, особенно если вид уже выведен на экран. Итак, мы наполняем модель элементами в цикле, используя для этого метод addElement() — полный аналог одноименного метода класса Vector. Заметьте, как модель позволяет сменить выбранный элемент с помощью метода setSelectedItem(). Правда, использовать его нужно немного необычно: метод требует передавать ему не позицию, а сам элемент (это необходимо для поддержки редактируемых списков, выбранным элементом в которых может быть любое значение). Поэтому нам приходится сначала получить элемент на нужной позиции (с помощью все той же модели), а затем только выбрать его. После этого мы присоединяем модель к списку JComboBox, передавая ее в конструктор (то же самое можно было бы сделать методом setModel()). Для удобства работы со списком (длинные выпадающие меню довольно неудобны, особенно если им не хватает места на экране) мы ограничили количество видимых на экране элементов меню пятью, изменив уже известное нам свойство maximumRowCount.
Для демонстрации динамической работы с нашей стандартной моделью мы добавили в окно кнопку add, слушатель ActionListener которой при щелчке на кнопке вводит
вмодель новый элемент, причем на случайную позицию, так что вы точно будете знать, что изменения происходят динамически. Запустив приложение, вы сможете увидеть, как стандартная модель поставляет данные раскрывающемуся списку и при этом позволяет без особых трудностей динамически изменять его содержимое. Созданную модель вы сможете разделять между несколькими видами и отдельно обрабатывать хранящиеся
вней данные.
Раскрывающийся список хранит довольно простые данные, и к тому же ему не нужна отдельная модель выделения (выделенный элемент хранится вместе с остальными элементами списка), так что талантов стандартной модели вполне хватает для большинства ситуаций. Создавать собственную модель стоит, только когда данные хранятся в особом источнике, требующем особого подхода, например, когда возможна некоторая оптимизация при получении и обработке данных. Мы уже создали для обычного списка JList модель, работающую с базами данных JDBC, давайте сделаем то же самое и для раскрывающегося списка JComboBox. Такая модель на самом деле будет очень полезна и может стать многократно использующимся элементом любого приложения: как только вам понадобится наполнить раскрывающийся список элементами из базы данных, вы сможете сделать это буквально парой строк кода. Удивительно, но создатели раскрывающегося списка не удосужились обеспечить нас абстрактным классом с поддержкой слушателей вроде AbstractComboBoxModel, хотя такие классы есть для всех остальных компонентов
328 |
ГЛАВА 11 |
с моделями. Создавать «модель» с нуля излишне сложно, вместо этого унаследуем ее от стандартной модели DefaultComboBoxModel, определив нужные нам методы. (Еще одним вариантом, если вы не хотите использовать стандартную модель, может быть наследование от базового класса модели списка AbstractListModel вместе с реализацией интерфейса модели выпадающего списка.) Давайте попробуем:
//com/porty/swing/DatabaseComboBoxModel.java
//Модель раскрывающегося списка для работы с базами данных package com.porty.swing;
import javax.swing.*; import java.sql.*;
public class DatabaseComboBoxModel extends DefaultComboBoxModel {
// получение данных из запроса ResultSet
public void setDataSource(ResultSet rs, int column) throws SQLException {
//очистим список removeAllElements();
//добавим новые элементы из базы данных while ( rs.next() ) {
//получаем строки из столбца column
addElement(rs.getString(column));
}
}
}
Здесь мы пошли по пути наименьшего сопротивления, максимально опираясь на уже имеющиеся возможности стандартной модели DefaultComboBoxModel. Мы не стали менять хранилище для данных и переопределять методы модели, а просто добавили новый метод setDataSource(), который получает результат запроса к базе данных ResultSet, удаляет все имеющиеся в модели элементы и заполняет ее новыми элементами из базы данных, получая их из столбца с определенным номером (его нужно указать в качестве второго параметра того же метода setDataSource()). Благодаря максимальному использованию механизмов стандартной модели, все делается за нас: при добавлении новых элементов методом addElement() модель будет оповещать об этом присоединенных слушателей, так что пользователь сможет своими глазами увидеть, как происходит заполнение раскрывающегося списка. Заметьте, что класс новой модели мы разместили в пакете com.porty. swing, откуда его проще извлечь.
Проверим работу нашей новой модели. Давайте напишем небольшое приложение, которое присоединяется к некоторой базе данных JDBC и полученные из нее данные показывает в раскрывающемся списке JComboBox. Вот что получается:
//DatabaseComboBox.java
//Пример использования модели DatabaseComboBoxModel import javax.swing.*;
import com.porty.swing.DatabaseComboBoxModel;
Списки |
329 |
import java.sql.*; import java.awt.*;
public class DatabaseComboBox extends JFrame {
//параметры подключения к базе данных private static String
dsn = "jdbc:odbc:Library", uid = "",
pwd = "";
//наша модель
private DatabaseComboBoxModel cbm; public DatabaseComboBox() {
super("DatabaseComboBox"); setDefaultCloseOperation(EXIT_ON_CLOSE); // настраиваем соединение с базой данных
Connection conn = null; try {
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver"); // объект-соединение с базой данных
conn = DriverManager.getConnection(dsn, uid, pwd); Statement st = conn.createStatement();
ResultSet rs = st.executeQuery( "select * from readers.csv");
// передаем данные в модель
cbm = new DatabaseComboBoxModel(); cbm.setDataSource(rs, 2); rs.close();
}catch (Exception ex) {
throw new RuntimeException(ex);
}
//присоединяем модель к списку setLayout(new FlowLayout()); add(new JComboBox(cbm));
//выводим окно на экран setSize(300, 200); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new DatabaseComboBox(); } });
330 |
ГЛАВА 11 |
}
}
Роль базы данных у нас играет некоторая гипотетическая база данных JDBC для библиотеки книг без имени пользователя и пароля. Для присоединения к ней нам нужно, прежде всего, загрузить драйвер JDBC, мы задействуем стандартный драйвер JDBCODBC, поставляющийся вместе с пакетом JDK. После загрузки драйвера мы можем воспользоваться услугами класса DriverManager для создания соединения Connection с базой данных, и в случае успеха через созданное соединение выполнять запросы и получать данные. Полученные в результате запроса данные (в виде объекта ResultSet) мы передаем в нашу новую модель DatabaseComboBoxModel (дополнительно нужно указать номер столбца, данные которого станут элементами списка), после чего остается присоединить ее к раскрывающемуся списку, добавить последний в окно и вывести его на экран. Для запуска программы со своей базой данных вам нужно соответствующим образом изменить название базы данных, имя пользователя и пароль, указать другой драйвер, если это необходимо для подключения к вашей базе данных, и выбрать таблицу, в которой имеется столбец с текстовыми данными, указав ее в тексте запроса. Запустив правильно настроенную программу с примером, вы сможете увидеть, как раскрывающийся список выводит элементы из базы данных, на самом деле даже и не подозревая об этом: все детали по сбору и обработке данных, как это и должно быть, остаются в модели. Не забудьте, что манипулировать любыми моделями в Swing после их присоединения к компонентам можно только из потока рассылки событий, так как они напрямую влияют на визуальное состояние компонентов.
Продуманноеиспользованиеспециальныхмоделей,такихкакDatabaseComboBoxModel, придает вашему приложению удивительную гибкость и легкость в расширении и поддержке. Вы сможете тщательно отделить части, работающие с данными (это части, соединяющиеся с базами данных и передающие результаты моделям), от частей, заведующих пользовательским интерфейсом (это компоненты пользовательского интерфейса, отображающие данные моделей и незаметно для вас позволяющие пользователю полностью изменять их, если это необходимо). Одну и ту же модель можно задействовать в нескольких частях сложного пользовательского интерфейса, не заботясь о правильном отображении данных, а при обработке итоговых данных знать, что все нужное скрыто именно в ней.
Внешний вид списка
Внешний вид элементов, хранящихся в раскрывающемся списке JComboBox, также легко и полно настраивается, как и внешний вид элементов обычного списка JList. Более того, для вывода элементов обоих списков используется отображающий объект ListCellRenderer, знакомый нам по списку JList. На самом деле, было бы странно вводить для раскрывающегося списка новый отображающий объект — как и в случае со списком обычным, от него требуется лишь вывести некоторый элемент, находящийся на определенной позиции списка (про этот элемент также известно, выделен ли он и есть ли у него фокус ввода). Более того, список элементов в выпадающем списке выводит именно обычный список JList, обернутый в панель прокрутки и выведенный на экран во всплывающем окне.
Таким образом, все, что мы говорили про внешний вид обычных списков JList, в равной степени относится и к раскрывающимся спискам JComboBox. Поэтому мы со спокойной душой можем считать, что уже в совершенстве владеем самой изощренной техникой отображения элементов раскрывающегося списка (если вы пропустили раздел, посвященный внешнему виду списков JList, самое время к нему вернуться). Давайте рассмотрим пример, в котором используем возможности как стандартного отобра-
Списки |
331 |
жающего объекта списков DefaultListCellRenderer, так и нашего специального объекта ImageListCellRenderer, способного отображать одновременно значки и текст. Итак:
//ComboBoxRendering.java
//Отображение элементов раскрывающегося списка import javax.swing.*;
import com.porty.swing.*;
import java.awt.*;
public class ComboBoxRendering extends JFrame { // данные для первого списка
private String[] textData = { "<html><code>Первый", "<html><b>Жирный", "<html><font color=red>Красный", "<html><em>Выразительный" };
// значки
private |
ImageIcon bullet1 = |
new |
ImageIcon("Server16.gif"); |
private |
ImageIcon bullet2 = |
new |
ImageIcon("Host16.gif"); |
// данные для второго списка со значками |
|
private |
ImageListElement[] iconData = { |
new |
ImageListElement(bullet1, "Основной Сервер"), |
new |
ImageListElement(bullet1, "Дополнительный"), |
new |
ImageListElement(bullet2, |
|
"<html><b><em>Машина директора") |
};
public ComboBoxRendering() { super("ComboBoxRendering"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//создаем пару раскрывающихся списков
JComboBox combo1 = new JComboBox(textData); JComboBox combo2 = new JComboBox(iconData);
//наш специальный отображающий объект combo2.setRenderer(new ImageListCellRenderer());
//добавляем списки в окно
setLayout(new FlowLayout()); add(combo1);
add(combo2);
// выводим окно на экран setSize(350, 200); setVisible(true);
332 |
ГЛАВА 11 |
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new ComboBoxRendering(); } });
}
}
В данном примере мы создаем два несложных раскрывающихся списка JComboBox, используя для хранения данных два массива textData и iconData. Первый список, создаваемый на основе массива строк textData, демонстрирует возможности стандартного отображающего объекта списков DefaultListCellRenderer. Как вы помните, стандартный отображающий объект унаследован от обычной надписи JLabel и способен отображать не только обычный текст, но и текст в формате HTML, а также значки. Значки мы для первого списка не используем, оставляя их для списка второго, а вот возможности HTML применяем «что есть сил», так что элементы первого списка спутать друг с другом будет непросто. Второй список использует в качестве данных объекты ImageListElement, созданные нами в разделе «Списки JList» и позволяющие хранить вместе и значок, и текст элемента списка, причем текст все также может быть приукрашен всеми доступными нам средствами HTML. Не забывайте только при применении объектов ImageListElement устанавливать подходящий отображающий объект ImageListCellRenderer
методом setRenderer()9.
После создания списков и незамысловатой настройки мы добавляем их в окно и выводим его на экран. Запустив программу с примером, вы сможете убедиться, что элементы списка теперь выглядят совсем иначе.
У вашей фантазии нет никаких ограничений: все, что связано с выводом элементов списка на экран, прорисовывается в отображающем объекте. Написав собственный отображающий объект, примерно так, как мы это сделали для объекта с текстом и значками ImageListCellRenderer, вы сможете любым образом расцветить свои списки. Более того, отображающие объекты «развязывают вам руки» при работе с моделями списка, позволяя хранить в них данные совершенно произвольных типов, даже самых сложных и запутанных. Предоставив в дополнение к таким данным отображающий объект, который знает, что именно из этих данных следует вывести на экран, вы получаете возможность
9 И снова путаница: почему в обычном списке метод для смены отображающего объекта называется setCellRenderer(), а в списке JComboBox — setRenderer()? Похоже, что при создании двух похожих списков их разработчики не удосужились как следует скооперироваться. К счастью, в библиотеке Swing таких «проколов» мало, и работать с ней все же приятно.
Списки |
333 |
использовать модель не только как «подносчик снарядов» для отображения элементов списка, но и как универсальное хранилище любых данных (которые к тому же легко разделять между видами и показывать пользователю).
Редактирование
Как мы уже знаем, раскрывающийся список JComboBox позволяет пользователю не только выбирать что-либо в списке заранее имеющихся альтернатив, но и вводить собственные значения — это его основное отличие от обычного списка JList и его основное преимущество. Правда, по умолчанию раскрывающийся список редактирования не допускает, но в нескольких предыдущих примерах мы уже увидели, как включить для списка режим редактирования: этим управляет свойство editable. Установив последнее в true, вы предоставляете пользователю возможность самого широкого выбора: он сможет либо выбрать один из представленных в списке элементов, либо ввести собственное значение. Обратите внимание на маленький нюанс: редактировать элементы раскрывающегося списка нельзя, они всегда остаются теми, какими были при создании списка, можно лишь дополнительно к ним вводить новые значения.
Редактированием нового значения в списке JComboBox занимается отдельный объект, обязанности его описаны в интерфейсе ComboBoxEditor. Стандартная реализация этого интерфейса — класс с названием BasicComboBoxEditor из пакета javax.plaf.basic. В данном классе для редактирования применяется простое текстовое поле JTextField, которое прекрасно подходит для обычных строк10. Чаще всего возможностей стандартного объекта для редактирования вполне достаточно для обычных раскрывающихся списков, не использующих в качестве элементов какие-либо экзотические данные. Вы могли убедиться в этом на предыдущих примерах, где в дополнение к имеющимся в списке строкам можно было ввести собственную произвольную строку текста. Увы, но стандартный объект для редактирования хорошо справляется лишь с простыми строками: стоит только элементам списка стать немного сложнее, как идиллия нарушается. Даже такая несложная возможность, как обработка HTML-текста, уже не поддерживается стандартным объектом, который выводит его в виде кода с тегами, а внезапное предложение заняться написанием HTML-текста вручную вряд ли восхитит пользователя. Поэтому, если вы храните в раскрывающемся списке нестандартные данные, особым образом показываете их (даже если это «до боли» знакомый нам HTML-текст) и планируете сделать список редактируемым, придется заняться написанием своего объекта для редактирования, благо это не слишком сложно. В качестве варианта можно попробовать потоньше настроить стандартный отображающий объект BasicComboBoxEditor, «вырезая» из элемента списка все HTML-теги при передаче его для редактирования. Для этого будет достаточно унаследовать от него и обрабатывать проходящие через редактор элементы.
Впрочем, никто не заставляет нас оставаться в рамках стандартного объекта для редактирования и лишь немного настраивать его работу. Совсем несложно написать собственный объект для редактирования с нуля; давайте так и поступим, предоставив в качестве альтернативы простому HTML-коду, «очищенному» от тегов, объект для редактирования, позволяющий редактировать именно HTML-текст, со всеми его красочными атрибутами и широкими возможностями. С редактированием HTML нам поможет текстовый компонент JEditorPane, специально для этого и предназначенный. Нам остается только «втиснуть» его в рамки интерфейса ComboBoxEditor. Оказывается, это очень просто. Итак:
10 Нетрудно понять, что объект для редактирования занимается не только собственно редактированием, но и отображает выбранный элемент, постоянно находясь на экране. За отображение остальных элементов, находящихся в выпадающем меню, как и прежде отвечает объект
ListCellRenderer.
334 |
ГЛАВА 11 |
//com/porty/swing/HTMLComboBoxEditor.java
//Полнофункциональный редактор для списка
//JComboBox, использующего HTML
package com.porty.swing; import javax.swing.*; import java.awt.*; import java.awt.event.*;
public class HTMLComboBoxEditor implements ComboBoxEditor {
// редактор для HTML
private JEditorPane htmlEditor; public HTMLComboBoxEditor() {
htmlEditor =
new JEditorPane("text/html", ""); htmlEditor.setBorder(BorderFactory.
createEtchedBorder());
}
// возвращает редактор
public Component getEditorComponent() { return htmlEditor;
}
//сигнал выбрать весь текст и приступить
//к редактированию
public void selectAll() { htmlEditor.selectAll(); htmlEditor.requestFocus();
}
//возвращает редактируемый элемент public Object getItem() {
return htmlEditor.getText();
}
//получает новый элемент для редактирования public void setItem(Object item) {
htmlEditor.setText(item.toString());
}
//методы для присоединения слушателей
//ActionListener, который оповещаются
//об окончании редактирования
public void addActionListener( ActionListener e) { }
