Мир ПК, #11/1998
Постоянный адрес статьи: http://www.osp.ru/pcworld/1998/11/138.htm

Введение в библиотеку Swing: списки

Дмитрий Рамодин

16.11.1998

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

Класс JList

Класс простого списка называется несколько странно - JList. По идее должно быть JListBox. Но именно Box, т. е. само обрамление, у списка-то и отсутствует. Дизайнеры Swing пошли по иному пути, предложив использовать в качестве графического контейнера списка панель прокрутки, выполненную как объект класса JScrollPane. Этот класс весьма удобен, когда речь заходит о чем-нибудь, связанном с прокруткой на экране текста, графики и даже Java-компонентов. На долю же класса JList остаются остальные графические операции и обработка данных, подлежащих отображению. Внутри класса, как и следовало ожидать, трудится класс модели данных. Вроде бы все логично, но далее, рассматривая пример, вы увидите, что все не так просто.

Класс JList имеет четыре конструктора:

JList(ListModel dataModel);
JList(java.lang.Object[] listData);
JList(java.util.Vector listData);
JList().

Первый конструктор рассчитан на то, что вы сами сформируете модель данных для списка, "зарядите" ее данными, отображаемыми в списке, и уже потом создадите экземпляр класса JList. Второй конструктор непосредственно "заряжается" пунктами данных, которые вы намерены показать пользователю в списке. Это могут быть строки, картинки или некоторые другие объекты. Вместе с третьим конструктором, которому передается заполненный объект класса Vector, первые два конструктора используются наиболее часто. И наконец, четвертый конструктор по умолчанию, без каких-либо параметров идеально подходит для динамического добавления и удаления элементов выбора.

Класс JList содержит довольно много методов (самые необходимые из них вы найдете в табл. 1). К этим методам добавляются и методы от модели, работающей внутри с JList. По умолчанию это AbstractListModel, если только вы не зададите иной модели в конструкторе. В любой момент можно получить ссылку на модель вызовом метода getModel или назначить новую модель, передав ее в качестве параметра метода setModel.

Класс JComboBox

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

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

JComboBox(ComboBoxModel aModel);
JComboBox(java.lang.Object[] items);
JComboBox(java.util.Vector items);
JComboBox().

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

Пример

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

В качестве каркаса используем базовую программу из первого занятия. Для более практичного позиционирования применяется менеджер размещения GridBaglayout.

В начале программы создаются и инициализируются все элементы, составляющие интерфейс нашей программы:

private JPanel jp = new JPanel(gbl);
private JList list = new JList(new DefaultListModel());
private JScrollPane listbox = new JScrollPane(list);
private JComboBox cbox = null;

Следует обратить внимание на вторую и третью строки. Как видите, мы подставляем в качестве параметра конструктора JList экземпляр класса DefaultListModel. С виду абсолютно бесполезный шаг, однако, не поступи мы так, некоторые весьма полезные методы по добавлению и удалению элементов списка будут недоступны. В третьей же строке показано, как на самом деле создается простой список. Ранее уже было сказано, что функции прокрутки списка теперь выполняет совершенно отдельный класс JScrollPane. Поэтому мы создаем его экземпляр, изначально добавляя список в панель прокрутки.

Конструктор класса примера прост: помимо операций, с которыми вы знакомы по предыдущим занятиям, он вызывает два метода (setupComboBox и setupList), в которых создаются и инициализируются списки, а также устанавливаются обработчики их событий.

Рассмотрим метод инициализации раскрывающегося списка setupComboBox. После установки параметров размещения компонента в окне программы создается сам компонент и его конструктору передается модель работы с данными в списке:
cbox = new JComboBox(new PictureModel());

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

Для регистрации нового выбора пользователя к раскрывающемуся списку подключается слушатель событий ItemListener:

cbox.addItemListener(
  new ItemListener() {
    public void itemStateChanged(ItemEvent ev)
    {
      if(ev.getStateChange() == ItemEvent.SELECTED)
         ((DefaultListModel)list.getModel()).addElement(
            (ImageIcon)ev.getItem());
    }
  }
);

Обработчик событий реализован как анонимный класс Java, проверяющий, какое событие пришло. Если это SELECTED, считывается ссылка на выбранную из списка картинку и она передается как параметр методу addElement, добавляющему новый элемент в простой список. Этот метод принадлежит модели данных списка, поэтому он вызывается методом getModel.

В завершение раскрывающийся список добавляется в панель окна программы:
jp.add(cbox, gbc);

Рассмотрим класс модели данных, который мы написали для правильной работы списка. Он является наследником модели данных списка по определению и реализует методы интерфейса модели данных раскрывающегося списка:
class PictureModel extends DefaultListModel implements ComboBoxModel

В классе описываются имена файлов картинок, которые появятся в списке:

String[] pics = {
      "rollover.gif",
      "disabled.gif",
      "pressed.gif",
      "normal.gif"
};

Разумеется, имена взяты для картинок, имевшихся в распоряжении автора. Вы можете задать для примера другие графические файлы. Однако следите за тем, чтобы это были файлы формата JPEG или GIF. Впоследствии нам нужно будет где-то хранить ссылки на загруженные образы картинок, поэтому мы заранее готовим массив:
private ImageIcon[] images = new ImageIcon[pics.length];

Заполнение массива происходит в конструкторе класса модели:

public PictureModel()
{
  for(int i = 0; i < pics.length; i++)
    images[i] = new ImageIcon(pics[i]);
  setSelectedItem(images[0]);
}

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

public void setSelectedItem(Object obj)
{
   selected = obj;
   fireContentsChanged(this,-1,-1);
}

Сначала ссылка на текущий выделенный объект сохраняется во внутренней частной переменной selected, затем зарегистрированные слушатели уведомляются о сделанном изменении вызовом метода fireContentChanged. Второй и третий параметры равны -1. Реально они нужны лишь для обозначения интервала выбранных пунктов при множественном выделении. В паре с setSelectedItem работает метод getSelectedItem, позволяющий получить ссылку на текущий выделенный элемент данных.

Есть еще пара обязательных методов для класса модели, которые мы реализуем. Первый возвращает размер списка выбора:

public int getSize()
{
  return images.length;
}

а второй возвращает элемент из списка выбора по переданному методу индексу:

public Object getElementAt(int index)
{
  return images[index];
}

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

Осталось рассмотреть метод, устанавливающий данные простого списка и обработчика двойного щелчка мыши:

private void setupList()
{
  gbc.gridx = 1;
  list.addMouseListener(
    new MouseAdapter() {
      public void mouseClicked(MouseEvent ev)
      {

Он довольно прост. Производится установка размещения списка на экране, после чего к списку подключается обработчик слушателя MouseListener. Кстати, похоже, что это единственный способ узнать о том, что выбрал пользователь. На мой взгляд, вычурно для библиотеки высокого уровня. Правда, у списка имеется метод locationToIndex, переводящий координаты точки, в которой была нажата кнопка мыши, в индекс выбранного элемента, но нам он не нужен. Мы просто проверяем, было ли двойное нажатие на кнопку мыши, и если - да, то просто считываем текущий выбранный объект методом getSelectedValue. Полученная ссылка на объект передается в качестве параметра методу выбора элемента раскрывающегося списка:

if(ev.getClickCount() == 2)
{
  cbox.setSelectedItem(list.getSelectedValue());

После этого распахиваем раскрывающийся список:
cbox.showPopup();
...

В конце работы простой список должен быть добавлен в панель окна программы:

jp.add(listbox, gbc);
}

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

Листинг.

import com.sun.java.swing.*;
import com.sun.java.swing.border.*;
import java.awt.*;
import java.awt.event.*;
public class Sample extends JFrame
{
  private GridBagLayout gbl = new GridBagLayout();
  private GridBagConstraints gbc = new GridBagConstraints();
  private JPanel jp = new JPanel(gbl);
  private JList list = new JList(new DefaultListModel());
  private JScrollPane listbox = new JScrollPane(list);
  private JComboBox cbox = null;
  public Sample(String caption)
  {
    super(caption);
    try
    {
      UIManager.setLookAndFeel(
         UIManager.getCrossPlatformLookAndFeelClassName());
    }
    catch(Exception e){e.printStackTrace();};
    jp.setBorder(BorderFactory.createEmptyBorder(15,15,15,15));
    setupComboBox();
    setupList();
    setContentPane(jp);
  }
  private void setupComboBox()
  {
    gbc.gridx = 0;
    gbc.gridy = 0;
    //gbc.fill = GridBagConstraints.HORIZONTAL;
    cbox = new JComboBox(new PictureModel());
    cbox.addItemListener(
      new ItemListener() {
        public void itemStateChanged(ItemEvent ev)
        {
          if(ev.getStateChange() == ItemEvent.SELECTED)
            ((DefaultListModel)list.getModel()).addElement(
               (ImageIcon)ev.getItem());
        }
      }
    );
    jp.add(cbox, gbc);
  }
  private void setupList()
  {
    gbc.gridx = 1;
    list.addMouseListener(
      new MouseAdapter() {
        public void mouseClicked(MouseEvent ev)
        {
          if(ev.getClickCount() == 2)
          {
            cbox.setSelectedItem(list.getSelectedValue());
            cbox.showPopup();
          }
        }
      }
    );
    jp.add(listbox, gbc);
  }
  public static void main(String[] args)
  {
    Sample s = new Sample(
      "\u0411\u0438\u0431\u043b\u0438\u043e\u0442\u0435\u043a\u0430 Swing");
    s.addWindowListener(new WindowAdapter()
      {
        public void windowClosing(WindowEvent e)
        {
          System.exit(0);
        }
      });
    s.pack();
    s.setResizable(false);
    s.setVisible(true);
  }
}
class PictureModel extends DefaultListModel implements ComboBoxModel
{
  String[] pics = {
        "rollover.gif",
        "disabled.gif",
        "pressed.gif",
        "normal.gif"
  };
  private ImageIcon[] images = new ImageIcon[pics.length];
  Object selected;
  public PictureModel()
  {
    for(int i = 0; i < pics.length; i++)
      images[i] = new ImageIcon(".\\Icons\\" + pics[i]);
    setSelectedItem(images[0]);
  }
  public void setSelectedItem(Object obj)
  {
     selected = obj;
     fireContentsChanged(this,-1,-1);
  }
  public Object getSelectedItem()
  {
    return selected;
  }
  public int getSize()
  {
    return images.length;
  }
  public Object getElementAt(int index)
  {
    return images[index];
  }
}
Мир ПК, #11/1998
Постоянный адрес статьи: http://www.osp.ru/pcworld/1998/11/138.htm