360 |
ГЛАВА 12 |
Более того, данные свойства зависят от того внешнего вида, который вы используете, и если они работают для внешнего вида Swing по умолчанию (Metal), это не значит, что они работают для других внешних видов. Всегда, если вы собираетесь применить внутренние настройки внешнего вида, проверьте, работают ли они в вашей ситуации и со всеми внешними видами, которые вы планируете использовать.
Ну и напоследок обратите внимание на «процесс», в котором обновляется значение модели индикатора, — это обычный поток Thread; в нем с некоторой задержкой текущее значение модели увеличивается до максимального. Как вы помните, для каждого индикатора процесса такой процесс должен быть, и он должен постоянно обновлять значение индикатора, в противном случае пользователь просто не поймет, что же вы хотели ему сообщить неподвижным индикатором. В отличие от одного из предыдущих примеров, где мы применили инструмент SwingWorker, здесь создается самый банальный поток. С одной стороны, создать его проще, меньше параметров и всего один метод run(). С другой стороны, любые действия с нашим интерфейсом и его моделями мы должны выполнять из потока рассылки событий, а для этого приходится вызывать громоздкий метод invokeLater() очереди событий EventQueue (мы вызываем его через класс SwingUtilities). В него, как правило, приходится передавать внутренний анонимный класс, реализующий интерфейс Runnable, и чтобы последний смог получить доступ к переменным и объектам, их необходимо объявлять неизменными (final). Код получается немного грязноватым, и применение SwingWorker все же дает более приличный результат. До тех пор пока в Java нет другого способа передать фрагмент кода кроме как обернуть его в интерфейс Runnable, код обычного потока в Swing-приложении выглядит не совсем элегантно, и нам приходится с этим смириться.
Счетчики JSpinner
Счетчики JSpinner позволяют пользователю сделать выбор из набора альтернатив, которые он в поиске нужного значения может «прокручивать» в любую сторону. Счетчик вместе с полем внешне похож на раскрывающийся список JComboBox: на экране в поле виден только выбранный в данный момент элемент, но если раскрывающийся список показывает набор альтернатив в выпадающем меню, то счетчик набора своих элементов вообще не показывает, предлагая вместо этого пользователю перейти к следующему или предыдущему значению с помощью маленьких кнопок со стрелками. Такое свойство счетчика позволяет ему хранить неограниченное количество альтернатив (списки должны иметь конечный набор альтернатив, потому что они выводят его на экран либо сразу, как JList, либо в выпадающем меню, как JComboBox).
Данные счетчика хранятся в его модели, описываемой интерфейсом SpinnerModel. Модель эта довольно проста: она позволяет узнать текущее значение счетчика, следующее и предыдущее значения, сменить текущее значение новым, а также поддерживает списки слушателей ChangeListener, которые оповещаются, когда меняется текущее значение модели. В модели счетчика JSpinner нет и намека на методы, определяющие размер списка или получающие позицию элемента списка. Есть только три значения, и следующим значением может быть все что угодно. Благодаря такой модели данных счетчик позволяет организовать выбор из практически произвольного диапазона данных, тем не менее в нем остаются удобные ограничения списка (такие ограничения уменьшают количество ошибок при вводе данных пользователем: последний всегда может увидеть, что предлагается в качестве следующей или предыдущей альтернативы). Более того, в зависимости от настройки счетчик может допускать ввод произвольных значений.
Счетчики и хранимые ими данные довольно необычны, так что создавать их с помощью простых конструкторов не слишком удобно. Давайте сразу же посмотрим, какие стандартные модели для счетчиков JSpinner имеются в Swing. Таких моделей целых три: модель SpinnerListModel организует выбор из конечного числа элементов (при использо-
Диапазоны значений |
361 |
вании этой модели счетчик становится оригинальной копией списка JComboBox), модель NumberSpinnerModel дает возможность сделать из этого элемента интерфейса «настоящий» счетчик (позволяющий выбирать только цифры из определенного диапазона), наконец, модель SpinnerDateModel, более экзотичная и сложная, чем первые две модели, позволяет организовать гибкий выбор дат. К третьей модели мы вернемся немного позже, а сейчас рассмотрим небольшой пример, в котором используем счетчик и первые две модели:
//UsingSpinnerModels.java
//Использование стандартных моделей счетчика import javax.swing.*;
import java.awt.*;
public class UsingSpinnerModels extends JFrame { // набор данных для счетчика
private String[] data = {
"Холодно", "Прохладно", "Тепло", "Жарко"
};
public UsingSpinnerModels() { super("UsingSpinnerModels"); setDefaultCloseOperation(EXIT_ON_CLOSE);
//модель счетчика для выбора из набора данных
SpinnerModel list = new SpinnerListModel(data); JSpinner spinner1 = new JSpinner(list);
//модель счетчика для выбора целых чисел
SpinnerModel numbers = new SpinnerNumberModel( 4, 0, 100, 1);
JSpinner spinner2 = new JSpinner(numbers);
//добавим счетчики в панель содержимого setLayout(new FlowLayout()); add(spinner1);
add(spinner2);
//выводим окно на экран
setSize(400, 300); setVisible(true);
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {
public void run() { new UsingSpinnerModels(); } });
}
}
В примере создается небольшое окно, в котором разместятся два счетчика JSpinner. Первый создается на основе модели SpinnerListModel, которая позволяет организовать вы-
362 |
ГЛАВА 12 |
бор из нескольких альтернатив, в точности повторяя поведение раскрывающегося списка JComboBox. Задать элементы для этой модели можно либо в виде массива (в примере мы так и поступаем), либо в виде любого динамического списка с данными List (к примеру, можно использовать список ArrayList). Нетрудно сменить список элементов уже после того, как вы создали модель или даже успели присоединить ее к счетчику: для этого предназначен метод модели setList() (заметьте, что передавать данные в виде массива позволяет только конструктор модели). Созданную модель мы передаем в конструктор счетчика, так что последний будет знать, что именно надо отображать (модель, как мы уже давно привыкли, можно задать и после создания счетчика с помощью метода setModel()).
Второй счетчик использует модель для выбора чисел SpinnerNumberModel. В конструкторе этой модели требуется задать четыре значения: начальное значение (у нас оно равно четырем), минимальное и максимальное значения, между которыми должен лежать выбор пользователя (в нашем примере — ноль и сто), и, наконец, приращение, на которое будет увеличиваться или уменьшаться текущее значение при выборе пользователем следующего или предыдущего значения, соответственно. Приращение в нашем примере равно единице. У модели SpinnerNumberModel есть несколько перегруженных конструкторов: для выбора целых чисел (int), чисел с плавающей запятой (double), и, наконец, произвольных чисел, представленных объектами Number, сравнение этих объектов выполняется с помощью задаваемых в конструкторе объектов Comparable4. Как нетрудно видеть, мы использовали конструктор, позволяющий организовать выбор целых чисел. Как и в случае с предыдущей моделью, вы можете менять числовые диапазоны модели и после ее создания и присоединения (правда, в таком случае можно задействовать только объекты Number и Comparable).
Запустив программу с примером, вы увидите, как работают два созданных нами счетчика, обратите внимание, что оба допускают редактирование выбранного в данный момент значения. Вы даже можете ввести в поле число, не попадающее в заданный нами для модели диапазон, только в этом случае кнопки перехода к следующему или предыдущему значению перестанут работать. Прокручивая первый счетчик, вы столкнетесь с не слишком приятной ситуацией: поле счетчика имеет размер, соответствующий первому элементу («Холодно»), и следующий элемент в него не поместится, что для пользователя будет совсем не кстати. Более того, при смене элементов разной длины поле счетчика динамически меняет свой предпочтительный размер5, что вполне может сделать поведение вашего интерфейса непредсказуемым.
4 Объект, реализующий интерфейс Comparable, поддерживает сравнение себя с другими объектами, обычно такого же типа. В интерфейсе Comparable определен один метод с названием compareTo(), который возвращает целое число: положительное, если данный объект больше объекта, с которым он сравнивается, отрицательное, если он меньше, и ноль, если объекты равны. В конструктор SpinnerNumberModel можно передавать не любые объекты, реализующие Comparable, а подклассы Number, хотя это и не афишируется интерактивной документацией.
5 Как видно, счетчик подводит нас дважды: при выводе окна на экран он имеет размер, подходящий для размещения первого элемента, а после смены элемента меняет свой размер, «подгоняя» его под новый элемент. Однако пользователь не увидит смены размера счетчика до тех пор пока не произойдет проверка корректности контейнера (к примеру, это может произойти при изменении размеров окна или после добавления нового компонента), и это еще больше увеличит так нежелательный в пользовательских интерфейсах эффект неожиданности.
Диапазоны значений |
363 |
Логично было бы предположить, что в классе JSpinner имеется какой-либо метод, позволяющий задать наилучшую длину поля счетчика (аналогично методу setPrototypeValue() списка JComboBox), но увы, такого метода нет6. Так что использование счетчика для выбора одного из нескольких элементов (с помощью модели SpinnerListModel) не слишком удобно, особенно в случае небольшого количества этих элементов, с такими ситуациями гораздо лучше справится раскрывающийся список JComboBox. Да и пользователю будет намного легче справиться с задачей поиска, когда он видит хотя бы малую часть множества, из которого следует выбирать значение. Правда, у счетчика есть приятное дополнение — он пытается автоматически дополнить текст, который вы набираете в нем, если он частично совпадает с одним из элементов списка.
С другой стороны, счетчик для выбора чисел идеально справляется со своей ролью: он ограничивает диапазон доступных чисел, позволяет быстро найти нужное пользователю число и, наконец, прекрасно справляется с вводом новых значений, не позволяя пользователю вводить ничего, кроме чисел нужного формата. Всегда, когда у вас в приложении возникает необходимости ввода чисел, применяйте счетчик: он намного эффективнее простого текстового поля.
Выбор дат
У счетчиков JSpinner есть и третья модель с названием SpinnerDateModel, позволяющая организовать гибкий выбор дат из определенного диапазона. Учитывая отсутствие в Swing стандартного компонента-«календаря»7, данная модель находит в большинстве программ довольно широкое применение. Для использования модели для выбора дат необходимы некоторые знания о том, как в Java проводятся манипуляции датами. Классы для таких манипуляций находятся в пакете java.util, модель SpinnerDateModel использует два класса: класс Date описывает некоторый момент во времени (с точностью до миллисекунды), а класс Calendar позволяет менять даты по любому из параметров, както: день недели, день месяца, год, месяц, или даже эра.
Для того чтобы организовать с помощью класса SpinnerDateModel выбор дат, требуется совсем немного: задать с помощью объекта Date текущее значение, с помощью объектов Comparable указать минимальную и максимальную даты, между которыми должен лежать выбор пользователя (если задать эти объекты равными null, то выбор будет неограничен), и указать, по какому полю календаря будет осуществляться прокрутка. Рассмотрим пример:
//ChoosingDates.java
//Выбор дат с помощью SpinnerDateModel import javax.swing.*;
import java.util.*; import java.awt.*;
public class ChoosingDates extends JFrame { public ChoosingDates() {
super("ChoosingDates"); setDefaultCloseOperation(EXIT_ON_CLOSE);
6 На самом деле способ «твердо» задать длину поля счетчика все-таки есть, хотя и не самый элегантный. Мы узнаем о нем чуть позже, когда будем обсуждать редакторы для счетчиков.
7 Хотя в наборе дополнительных компонентов SwingX «календарь» есть (он называется JXDatePicker), и счетчик для выбора дат не так актуален, если вы решитесь использовать это дополнение.
364 |
ГЛАВА 12 |
//настраиваем модель для выбора дня месяца
SpinnerModel monthDay = new SpinnerDateModel(
new Date(), null, null, Calendar.DAY_OF_MONTH); JSpinner spinner1 = new JSpinner(monthDay);
//модель для выбора месяца, с ограничениями
SpinnerModel month = new SpinnerDateModel(
new Date(), new MinDate(), new MaxDate(), Calendar.MONTH); JSpinner spinner2 = new JSpinner(month);
//добавляем списки в панель
setLayout(new FlowLayout()); add(spinner1); add(spinner2);
// выводим окно на экран setSize(350, 300); setVisible(true);
}
// вспомогательный объект для проверок дат
private Calendar calendar = Calendar.getInstance(); // проверяет минимальную дату (по году)
class MinDate extends Date implements Comparable<Date> { public int compareTo(Date d) {
calendar.setTime(d);
int year = calendar.get(Calendar.YEAR); // год не меньше 2005
return (year < 2005) ? 1 : -1;
}
}
// проверяет максимальную дату (по году)
class MaxDate extends Date implements Comparable<Date> { public int compareTo(Date d) {
calendar.setTime(d);
calendar.get(Calendar.YEAR);
int year = calendar.get(Calendar.YEAR); // год не больше 2011
if ( year > 2011 ) return -1; else return 1;
}
}
public static void main(String[] args) { SwingUtilities.invokeLater(
new Runnable() {