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

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

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

Диапазоны значений

355

дом setStringPainted(true), и задаем нестандартный текст с помощью метода setString(). Текст, несмотря даже на то, что индикатор является вертикальным, может быть порядочного размера: текст прорисовывается так же, как и сам индикатор, то есть вертикально. Впрочем, стоит заметить, что читать в таком положении длинный текст не слишком удобно (может заболеть шея). Длинный текст лучше оставить для горизонтальных индикаторов, а в вертикальных ограничиться чем-то лаконичным, вроде символа процентов.

После настройки модели и индикаторов нужно добавить последние в панель содержимого нашего окна и вывести окно на экран. Так мы и делаем. Если больше ничего не предпринимать, индикаторы процесса застынут на отметке в пять процентов (помните, текущее значение модели изначально у нас равно пяти, а максимальное — ста, так что все справедливо) и будут малоинтересны. Действительно, в любом приложении, использующем индикаторы, должен быть некоторый процесс, по мере выполнения которого текущее значение индикатора непрерывно обновляется. Роль такого процесса в нашем примере сыграет внутренний класс LongProcess, созданный на основе инструмента SwingWorker и, следовательно, представляющий собой отдельный поток выполнения с возможностью вызова промежуточных действий в графическом потоке рассылке событий. В реальных приложениях процессом может быть загрузка данных из сети или файла с диска, установка компонентов или сетевых соединений, и еще много самых разных действий. Мы сразу же запускаем нашу задачу (вызывая метод execute()), и параллельно с потоком рассылки событий начинает выполняться ее метод doInBackground(). Работа задачи проста: пока текущее значение модели не превышает максимального (то есть пока наш воображаемый процесс не закончен), в цикле while текущее значение увеличивается на единицу, после чего процесс «засыпает» на случайный промежуток времени, а затем все повторяется. Случайный промежуток простоя процесса внесет в пример свою изюминку: вы увидите, как индикатор то бежит быстрее, то замедляется. Обратите внимание, что нам не приходится заботиться о синхронизации: мы применяем метод publish() для вызова кода из потока рассылки событий, передавая туда текущее значение для модели, и работаем с моделью, а не с компонентом, так что модель сама оповестит все присоединенные к ней виды о том, что пора обновить изображение. Просто и эффектно. Не забывайте, что работа с моделью, несмотря на то, что вы не работаете с графическими компонентами напрямую, подразумевает отсылку событий этим компонентам, и, соответственно, их обновление. Так что с моделями работать необходимо только из потока рассылки событий.

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

356

ГЛАВА 12

Таблица 12.2. Основные свойства индикаторов JProgressBar

 

 

Свойства

Описание

orientation

Управляеториентациейиндикатора. Ориентацияможетбытьвертикальнойили

 

горизонтальной. Чаще всего это свойство задается при создании индикатора

 

инеменяетсявпоследствии. Еслижевыменяетеориентациюдлякомпонента,

 

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

 

контейнера: смена ориентации повлияет на расположение компонентов

stringPainted

Позволяет включить или выключить прорисовку текста на индикаторе про-

 

цесса. По умолчанию прорисовка текста выключена. Если вы ее включаете,

 

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

 

щее, какая часть процесса закончена (значения берутся из модели). Сме-

 

нить текст позволяет следующее свойство

string

Задаеттекст, которыйбудетпрорисовыватьсянаиндикаторепроцесса. Неза-

 

бывайте только включать прорисовку текста: по умолчанию она отключена

value, maximum,

Эти свойства позволяют манипулировать данными модели Bounded-

minimum

RangeModel напрямуюспомощьюсамогоиндикатора(модельвлюбомслучае

 

есть, данные свойства просто делегируют ей свою работу). Управляют соот-

ветственно текущим, максимальным и минимальным значениями. Благодаря

им (а также благодаря конструкторам JProgressBar, которые сразу позволяют

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

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

Когда ничего не ясно

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

//IndeterminateProgressBars.java

//Индикаторы в состоянии "неопределенности" import javax.swing.*;

import java.awt.*;

public class IndeterminateProgressBars extends JFrame { public IndeterminateProgressBars() {

Диапазоны значений

357

super("IndeterminateProgressBars"); setDefaultCloseOperation(EXIT_ON_CLOSE); // неопределенный индикатор

JProgressBar progress = new JProgressBar(0, 100); progress.setIndeterminate(true); progress.setStringPainted(true);

// добавляем его в окно и выводим на экран setLayout(new FlowLayout());

add(new JLabel("Соединение:")); add(progress);

setSize(300, 200); setVisible(true);

}

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

new Runnable() { public void run() {

new IndeterminateProgressBars(); } });

}

}

В панель содержимого окна мы добавляем индикатор JProgressBar, у которого нет даже самой простой отдельной модели: нам помогает конструктор, позволяющий задать минимальное и максимальное значения, а стандартная модель будет создана автоматически, за кулисами. Большего нам не понадобится: индикатор в состоянии неопределенности не использует никаких данных модели. Как видно из примера, состояние «неопределенности» включается свойством indeterminate. Никаких действий предпринимать больше не требуется: индикатор сам позаботится о том, чтобы вид полосы периодически менялся, изображая продолжающуюся деятельность процесса и подбадривая этим пользователя. Дополнительно мы включили прорисовку текста свойством stringPainted, он появится даже в «неопределенном состоянии» (по умолчанию будет изображаться выполненная в процентах часть процесса — в зависимости от значения, установленного в модели). Однако чаще всего «неопределенное» состояние не использует стандартное отображение в процентах, как дополнительное подтверждение того, что на данный момент информация о выполнении процесса отсутствует. Лучше заменить строку на свою собственную, которая подчеркнет что процесс занимает неопределенное время. Запустив программу с примером, вы сможете оценить, как смотрится «неопределенная» ситуация в исполнении индикатора JProgressBar.

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

358

ГЛАВА 12

Небольшие хитрости

Индикаторы процесса на самом деле очень просты, и работа с ними сводится в основном к обновлению текущего значения модели. Даже их внешний вид толком не настраивается (изменить можно лишь отображаемую индикатором строку). Впрочем, с помощью некоторых незамысловатых свойств UI-представителя индикаторов JProgressBar, не попавших в класс самого компонента, вы сможете добиться более изысканных визуальных эффектов, в некоторых случаях больше отвечающих стилю вашего приложения. Свойства эти отвечают за вид полосы индикатора. Рассмотрим небольшой пример, в котором эти свойства будут использоваться для достижения нового внешнего вида:

//ProgressBarTricks.java

//Небольшие хитрости индикаторов процесса import javax.swing.*;

import java.awt.*;

public class ProgressBarTricks extends JFrame { // максимальное значение индикатора

private final int MAX = 100; public ProgressBarTricks() { super("ProgressBarTricks");

setDefaultCloseOperation(EXIT_ON_CLOSE);

//настроим параметры для UI-представителей

UIManager.put("ProgressBar.cellSpacing", 2); UIManager.put("ProgressBar.cellLength", 6);

//стандартная модель

final DefaultBoundedRangeModel model =

new DefaultBoundedRangeModel(0, 0, 0, MAX);

//создадим простой индикатор процесса

//на основе полученной модели

JProgressBar progress = new JProgressBar(model);

//добавим его в окно setLayout(new FlowLayout()); add(progress);

//выводим окно на экран setSize(300, 200); setVisible(true);

//создадим "процесс"

Thread process = new Thread(new Runnable() { public void run() {

//увеличиваем текущее значение модели до

//достижения максимального значения

int value = 0;

while ( ++value <= MAX ) {

Диапазоны значений

359

final int passValue = value; SwingUtilities.invokeLater(

new Runnable() { public void run() {

model.setValue(passValue); }});

try { Thread.sleep(200);

} catch (Exception ex) { }

}

}

}); // запустим поток

process.start();

}

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

new Runnable() {

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

}

}

Здесь мы создаем небольшое окно, в котором разместится незатейливый индикатор процесса. Данные этому индикатору будет поставлять стандартная модель, начальное значение ее равно нулю, минимальное также равно нулю, а максимальное значение равно ста (вы можете видеть, как мы задаем значения в конструкторе модели). Самое интересное происходит в паре строк, которые настраивают внутренние свойства UIпредставителей индикаторов процесса (а мы знаем, что именно эти представители и выполняют фактическую прорисовку компонента на экране). Свойства UI-представителей всех компонентов Swing для любого внешнего вида хранятся в специальной таблице класса UIManager, и мы можем менять их, используя методы put(). Оказывается, что с помощью данных свойств можно тонко настроить внешний вид индикаторов процесса (точнее, перемещающейся в них полосы). Свойство с названием cellLength управляет тем, какой длины будут ячейки полосы индикатора (полоса индикатора не обязательно прорисовывается сплошной, она может состоять из набора ячеек одинакового размера), а свойство cellSpacing задает расстояние между ячейками (если это расстояние приравнять нулю, то полоса будет сплошной). Запустив программу с примером, вы увидите, как описанные два свойства влияют на внешний вид индикатора процесса (в современных пользовательских интерфейсах более популярны именно такие, ячеистые, индикаторы). Правда, стоит иметь в виду, что данные свойства действуют, только если в индикаторе процесса не прорисовывается информационная строка. Если строка есть, то полоса индикатора будет сплошной в любом случае.

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() {