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

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

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

Списки

295

Модели

Примененные нами в первом примере конструкторы класса JList, наполняющие список данными из массива или вектора, конечно, замечательны, и в тех случаях, когда вам надо быстро и просто создать небольшой список, они незаменимы. Но по настоящему удобно работать с данными компонентов Swing, в том числе и с данными списков, позволяют модели. Мы уже прекрасно знаем, что они отделяют обработку данных от представления этих данных на экране, давая программисту возможность сосредоточиться поочередно на каждой задаче и таким образом применить знаменитый лозунг «разделяй и властвуй». Более того, конструкторы класса JList создают списки которые нельзя изменять и дополнять, что как правило, весьма некстати.

Обязанности моделей списков JList описаны в интерфейсе ListModel. Интерфейс этот очень прост, требуя от вас реализовать всего четыре метода. Два метода служат для присоединения и удаления слушателей событий, происходящих при обновлении данных списка (помните, при обсуждении модели MVC мы отмечали, что модель оповещает присоединенные к ней виды при изменении данных, и это позволяет виду всегда показывать верные данные); один метод возвращает элемент, находящийся на некоторой позиции списка; еще один метод позволяет списку узнать, сколько данных в данный момент содержит модель. Несложно, но достаточно для работы списка. Заметьте, что по умолчанию модель списка не подразумевает добавления и удаления элементов.

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

//UsingListModel.java

//Использование стандартной модели списка import javax.swing.*;

import java.awt.event.*; import java.awt.*;

public class UsingListModel extends JFrame { // наша модель

private DefaultListModel dlm; public UsingListModel() {

super("UsingListModel"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//заполним модель данными dlm = new DefaultListModel(); dlm.add(0, "Кое-что"); dlm.add(0, "Кое-что еще"); dlm.add(0, "Еще немного");

//создаем кнопку и пару списков

JButton add = new JButton("Обновить");

296

ГЛАВА 11

add.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) {

dlm.add(0, "Новинка!");

}

});

JList list1 = new JList(dlm); JList list2 = new JList(dlm);

//добавляем компоненты setLayout(new FlowLayout()); add(add);

add(new JScrollPane(list1)); add(new JScrollPane(list2));

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

}

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

new Runnable() {

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

}

}

В примере мы создаем экземпляр модели DefaultListModel и используем его для хранения наших данных и вывода их на экран. Как видите, методы этой модели разительно напоминают методы стандартных контейнеров данных Java, и на самом деле всю работу по хранению данных модель DefaultListModel адресует находящемуся внутри нее вектору Vector, и даже названия методов для простоты сохранены. Мы добавляем в модель несколько элементов и передаем ее двум спискам для вывода данных на экран. Запустив программу с примером, вы увидите, как два списка, использующих одну и ту же модель, синхронно выводят одни и те же данные. В этом прелесть архитектуры MVC — неважно, откуда и как получены данные и как они будут выводиться: вы получаете данные и выбираете подходящий способ их отображения. Модели и виды сами позаботятся о том, чтобы все вовремя и правильно появлялось на экране.

Списки

297

Для демонстрации динамического поведения данных модели в панель содержимого была включена кнопка JButton. Щелкая на этой кнопке, вы будете добавлять данные в оба списка, потому что они используют одну и ту же модель, а все необходимые для обновления вида «переговоры» между моделью и списками будут проходить автоматически. Обновление экрана и проверка корректности компонентов также выполняются автоматически2. Однако помните, что динамически добавлять много данных в стандартную модель, которая уже присоединена к виду (или к нескольким видам), — нежелательная практика, ведущая к снижению производительности.

СОВЕТ

Основную часть данных подготавливайте и добавляйте в модель перед ее присоединением к виду (или видам). Это к тому же позволяет работать с моделью из отдельного потока, пока у ней нет связи с графическими компонентами. Присоединенная к виду модель оповещает его о каждом обновлении данных, и частое обновление данных может привести к серьезному снижению производительности.

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

Стандартная модель DefaultListModel очень удобна и позволяет гибко хранить и изменять любые данные. Однако бывают случаи, когда лучше определить собственную модель данных, особенно там, где данные хранятся в нестандартных структурах или их необходимо получить из необычного источника, например из сетевого соединения или базы данных. Давайте рассмотрим небольшой пример, в котором данные списка мы будем получать из простой базы данных. Это весьма вероятная ситуация в больших приложениях, данные которые почти всегда хранятся в базах данных или в серверных компонентах (таких как Enterprise JavaBeans или .NET).

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

//DatabaseListModel.java

//Модель списка, работающая с базой данных package com.porty.swing;

import javax.swing.*;

2Списки увеличатся после добавления новых элементов, и самый длинный элемент не «влезет»

впанель прокрутки после появления вертикальной полосы прокрутки, что приведет к появлению горизонтальной полосы прокрутки, крайне нежелательного гостя. На самом деле лучше составлять интерфейс так, чтобы размеров списка вместе с панелью прокрутки заранее хватало под новые элементы (в этом могут помочь подходящие менеджеры расположения и известный нам метод setPrototypeCellValue()). Внезапное изменение состояния интерфейса запутывает пользователя, и вы легко увидите это, поменяв размер окна — менеджер FlowLayout поменяет их размеры на пред-

почтительные. При работе со списками очень важно правильно определять максимальную длину их элементов — это обезопасит нас от неприятных сюрпризов.

298 ГЛАВА 11

import java.sql.*; import java.util.*;

public class DatabaseListModel extends AbstractListModel { // здесь будем хранить данные

private ArrayList<String> data = new ArrayList<String>(); // загрузка из базы данных

public void setDataSource(ResultSet rs, String column) throws SQLException {

//получаем данные data.clear();

while ( rs.next() ) { data.add(rs.getString(column));

}

//оповещаем виды (если они есть)

fireIntervalAdded(this, 0, data.size());

}

// методы модели для выдачи данных списку public int getSize() {

return data.size();

}

public Object getElementAt(int idx) { return data.get(idx);

}

}

Сама по себе модель DatabaseListModel довольно проста. Она наследует от абстрактного класса моделей списков AbstractListModel и хранит данные в динамическом массиве ArrayList. Вся «соль» заключена в методе setDataSource(), который позволяет ей получать данные из соединения с базой данных. Для правильной работы этому методу необходимо передать результат запроса к базе данных (объект ResultSet), а также название столбца, данные которого будут использованы для заполнения списка (название столбца передается в строке column). Подразумевается, что в результате выполнения запроса столбец column появится в полученных данных (в объекте ResultSet), и в нем будут храниться текстовые данные. Впрочем, можно немного доработать нашу модель, чтобы она могла работать с любым типом данных.

Заметьте, что при получении всех записей из базы данных модель оповещает об этом своих слушателей. Для оповещения присоединенных к модели слушателей (а это чаще всего виды, отображающие ее данные) мы используем возможности базового класса AbstractListModel: вызываем методы fireXXX(), каждый из которых сообщает о некотором событии, как-то: обновление элементов списка, добавление в список новых данных или удаление данных из списка (эти методы просто создают подходящие экземпляры события ListDataEvent).

Наша модель очень проста, и отправляет события о изменениях в себе напрямую, что как правило означает перерисовку списка на экране. Это означает что мы должны вызывать метод setDataSource() только из потока рассылки событий, если модель уже пе-

Списки

299

редана в список JList. Однако до присоединения к списку мы вполне можем вызвать этот метод в отдельном потоке, чем мы вскоре и воспользуемся.

Более гибкая версия созданной в этом примере модели могла бы постепенно загружать данные, оповещая об этом список, и в случае если соединение с базой данных было бы медленным (например, сетевым), пользователь мог бы воочию видеть процесс загрузки данных в список и выбрать нужный ему элемент еще до полной загрузки. Однако это потребовало бы работы метода для загрузки данных в отдельном потоке, синхронизации списка data (в классе ArrayList для ускорения работы отключена синхронизация), и оповещения присоединенных к модели слушателей только из потока рассылки событий с помощью метода invokeLater(). Это несложно, и вы можете доработать нашу простую модель в качестве простого упражнения.

Созданная модель помещается в пакет com.porty.swing: так ее будет проще импортировать и многократно использовать в других программах. Не то чтобы новая модель была очень полезна, но для начального наброска интерфейса подходит весьма хорошо.

Остается проверить работу новой модели. Давайте напишем небольшой пример с ее использованием:

//DBListModelTest.java

//Использование модели списка

//для работы с базами данных import javax.swing.*;

import java.sql.*; import java.awt.*;

import com.porty.swing.*;

public class DBListModelTest { // параметры базы данных private static String

dsn = "jdbc:odbc:Library", uid = "",

pwd = "",

query = "select * from readers.csv";

public static void main(String[] args) throws Exception {

//инициализация JDBC Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");

//объект-соединение с базой данных

Connection conn = DriverManager.getConnection(dsn, uid, pwd); Statement st = conn.createStatement();

ResultSet rs = st.executeQuery(query); // создаем модель

final DatabaseListModel dblm = new DatabaseListModel(); // загружаем данные

dblm.setDataSource(rs, "surname"); rs.close();

300 ГЛАВА 11

// интерфейс создаем в потоке рассылки событий

SwingUtilities.invokeLater( new Runnable() {

public void run() {

// присоединяем список

JList list = new JList(dblm); // помещаем список в окно

JFrame frame = new JFrame("DBList"); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(200, 200);

frame.add(new JScrollPane(list)); frame.setVisible(true);

} });

}

}

Для работы модели нам понадобится соединение с базой данных. Подойдет практически любая база данных и ее любая таблица, в которой есть столбец с текстовыми данными. Для соединения с базой данных мы задействуем интерфейс JDBC (Java Database Connectivity)3, настройка его для работы с вашей системой управления базами данных (СУБД) зависит от используемой платформы. Чаще всего это несложно и подробно описано в документации. Для правильной работы нам необходимо загрузить драйвер (мы применяем стандартный драйвер JDBC-ODBC от Sun, входящий в пакет JDK) и создать соединение с базой данных. Для этого требуется три параметра: идентификатор базы данных (Data Source Name DSN), имя пользователя (User ID UID) и его пароль (Password PWD). Эти параметры будут зависеть от базы данных и ее настроек. После этого, если все проходит успешно, производится запрос к базе данных (запрос находится в строке query, измените его так, чтобы он работал с вашей базой данных), и модель DatabaseListModel получает результат запроса ResultSet и название столбца, данные которого появятся в списке. Мы смело вызываем методы модели DatabaseListModel прямо из потока метода main(), потому что сама модель пока еще не присоединена ни к одному графическому компоненту.

Напоследок, уже из потока рассылки событий, к модели присоединяется вид — список JList, который мы помещаем в окно. Запустив приложение, вы увидите, как список выводит элементы, полученные моделью из базы данных. Использованный нами в примере подход гибок и удобен: модель выполняет все действия по получению и подготовке данных, и если у вас есть вопросы по данным, вы обращаетесь к ней. Список связывается с моделью и отображает ее информацию, и вы знаете, что для изменения внешнего вида программы нужно работать с видом, но не с данными. Все на своих местах.

Модели для больших списков

Простота моделей списков без сомнения полезна при создании списков с небольшим или средним количеством данных. Чем проще в этом случае написан код, тем он быстрее работает и тем проще его обновлять и изменять в будущем. Однако, если речь

3 Интерфейс JDBC — часть стандартной библиотеки JDK. Узнать об этом интерфейсе подробнее вы сможете в интерактивной документации пакета java.sql и на официальном сайте java.sun.com.

Списки

301

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

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

Постепенное добавление элементов в модель списка, который уже находится на экране, требует наличия отдельного потока, загружающего данные (это может быть объект SwingWorker, который мы рассматривали в главе 3). Чем реже мы будем сообщать списку о новых данных (блоками новых элементов), тем меньше он будет перерисовывать себя, и значит, тем быстрее будет отклик на действия пользователя, а это для последнего всегда важно. Сообщать списку о добавлении новых данных следует только из потока рассылки событий, иначе рано или поздно крах программы неминуем.

Список JList никогда не просит информацию об элементах списка, не видных в данный момент на экране, так же работают и остальные сложные компоненты Swing. Если приложение подразумевает работу в определенном диапазоне элементов списка, то список JList будет запрашивать информацию только о них и элементах, находящихся около них. Этот факт можно использовать для кэширования, если, например, элементы списка хранятся на диске. Все элементы большого списка невозможно хранить в памяти, так как она не безгранична, поэтому вы можете хранить их где-либо еще, в базе данных или подобном хранилище, и идея кэширования элементов, с которыми в данный момент работает пользователь, и близлежащих элементов позволит порядочно ускорить доступ к ним.

Выделение

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

Главные два метода модели выделения — setSelectionInterval() и addSelectionInterval(). Первый метод устанавливает новый интервал выделения, а второй добавляет к уже имеющемуся выделению еще один интервал выделения. Интервал выделения задается двумя целыми числами: позицией первого выделенного элемента и позицией последнего выделенного элемента. Они могут совпадать, что означает выделение одного элемента, и необязательно первое число должно быть больше второго. В методе addSelectionInterval() вы сможете провести основную работу по выделению элементов, задав именно те режимы выделения, которые вам подходят. К примеру, используемая списком JList по умолчанию модель в режиме одиночного выделения позволяет выделять только один элемент, даже если пользователь попытался выделить большой интервал из нескольких элемен-

302

ГЛАВА 11

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

Все описанные в интерфейсе ListSelectionModel возможности полностью реализованы в стандартной модели выделения DefaultListSelectionModel. Именно она используется в списках JList по умолчанию и менять ее чем-то особенным требуется редко. Она поддерживает три режима выделения элементов списка и позволяет получить исчерпывающую информацию о текущем выделении. Давайте посмотрим на примере, как с ней работать:

//ListSelectionModes.java

//Различные режимы выделения import javax.swing.*;

import java.awt.*;

public class ListSelectionModes extends JFrame { private String[] data = { "Красный", "Синий",

"Зеленый", "Желтый", "Белый"}; public ListSelectionModes() {

super("ListSelectionModes"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//заполним модель данными

DefaultListModel dlm =

new DefaultListModel(); for (String next : data)

dlm.addElement(next);

//три списка с разным типом выделения

JList list1 = new JList(dlm); list1.setSelectionMode(

ListSelectionModel.SINGLE_SELECTION); JList list2 = new JList(dlm); list2.setSelectionMode(

ListSelectionModel.SINGLE_INTERVAL_SELECTION); JList list3 = new JList(dlm);

//аналогично предыдущему вызову list3.getSelectionModel().setSelectionMode(

ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);

//добавляем компоненты

Списки

303

setLayout(new FlowLayout()); add(new JScrollPane(list1)); add(new JScrollPane(list2)); add(new JScrollPane(list3)); // выведем окно на экран setSize(300, 200); setVisible(true);

}

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

new Runnable() {

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

}

}

В примере мы заполняем стандартную модель DefaultListModel данными из массива (можно было бы передать массивы напрямую в конструкторы списков, но это не позволит нам сэкономить память) и на ее основе создаем три списка с одинаковыми элементами. Разница между списками лишь в режиме выделения элементов. Режим выделения можно сменить двумя способами: вызвав метод setSelectionMode() класса JList или вызвав метод с точно таким же названием уже для самой модели выделения. На самом деле это одно и тоже, метод класса JList просто обращается к модели выделения. В нашем распоряжении имеется три режима выделения (табл. 11.1).

Таблица 11.1. Режимы выделения элементов списка

Режим

Описание

 

 

SINGLE_SELECTION

Позволяет пользователю выбирать только один

 

элемент списка. Часто используемый режим, хотя

 

для тех же целей могут быть успешно примене-

 

ны группы переключателей или раскрывающиеся

 

списки

SINGLE_INTERVAL_SELECTION

Пользователь может одновременно выбирать не-

 

сколько элементов списка, но только смежных

MULTIPLE_INTERVAL_SELECTION

Самый гибкий режим, используемый моделью вы-

 

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

 

вольное количество элементов списка в произволь-

 

ном сочетании (в любом месте списка, поодиночке

 

или интервалами)

 

 

Запустив программу с примером, вы сможете оценить разницу в различных режимах выделения. Один элемент может быть выбран щелчком мыши или нажатием клавиш управления курсором, а интервалы и дополнительные элементы могут быть выбраны с помощью вспомогательных клавиш Shift и Ctrl. Стандартными внешними видами Swing поддерживаются и многие другие клавиши, особенно удобные для навигации в больших списках, их описание вы сможете найти в интерактивной документации Java.

304

ГЛАВА 11

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

//CustomListSelection.java

//Реализация особого режима выделения import javax.swing.*;

import java.awt.*;

public class CustomListSelection extends JFrame { private String[] data = { "Мороженное", "Курица",

"Холодное", "Горячее"}; public CustomListSelection() {

super("CustomListSelection"); setDefaultCloseOperation(EXIT_ON_CLOSE);

//настроим список и добавим его в окно

JList list = new JList(data); list.setSelectionModel(new CustomSelectionModel()); add(new JScrollPane(list));

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

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

}

// специальная модель выделения class CustomSelectionModel