Продолжается подписка на наши издания! Вы не забыли подписаться?

Отображение информации в представлениях (view) Eclipse Часть 3

Автор: Александр Цимбал
Опубликовано: 28.04.2009

В предыдущих частях статьи рассматривался вопрос о структурированном отображении текстовой (и в некоторой степени – графической) информации в структурированных контейнерах – компонентах библиотеки JFace. При этом был создан вспомогательный программный слой, который позволил разделить сами отображаемые объекты (экземпляры типов MyColor и MySound) от их текстового представления в списке типа ListViewer, с сохранением связи между отображаемыми элементами списка и собственно иерархия классов с классом ViewElement в качестве корня этой иерархии. Для создания объектов ViewElement на основе некоторой входной информации использовались соответствующие фабрики – абстрактный класс ViewElementFactory и производные от него специализированные фабрики ViewColorElementFactory и ViewSoundElementFactory.

Кроме того, в приложении был создан очень простой контейнер объектов типа ViewElement – ViewElementsHolder. Этот контейнер пока не является полноценным контейнером по целому ряду причин, в частности, потому, что он пока позволяет только хранить статический список исходных данных для объектов ViewElement (и связанных с ними рабочих объектов MyColor и MySound). Вот сокращенный код класса ViewElementsHolder, который приводился в предыдущей части статьи:

public class ViewElementsHolder 
{
  ...
  private static String[] custElems = 
    {"red", "fa", "blue", "violet", "do", "si", "re", "green"};
  
  public Object[] getCustomElements()
  {
    return custElems;
  }
  
  public IViewElement[] getElements()
  {
    Object[] input = getCustomElements();
    IViewElement[] res = new IViewElement[input.length];
    ViewElementFactory[] factoryObjects = ViewElementFactory.getFactories();
    
    // Для простоты считаем, что все исходные данные корректны 
    for (int i = 0; i < input.length; i++)
    {
      for (int j = 0; j < factoryObjects.length; j++)
      {
        IViewElement ve = factoryObjects[j].createViewElement(input[i]);

        if (ve != null)
        {  
          res[i] = ve;
          break;
        }  
      }
    }

    return res;
  }
}

Данный код использует фиксированный список объектов (MyColor и MySound). Для более или менее реальной программы нужно обеспечить, как минимум, следующее:

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

Работа с событиями

Прежде всего, нужно отчетливо различать:

В статье «Еще раз о разработке плагинов Eclipse» довольно подробно рассказывалось о действиях Eclipse. Кратко напомним самое главное: объекты Action создаются обычно самой средой Eclipse на основании информации, находящейся в XML-дескрипторе плагина, еще до загрузки классов самого плагина (которые и содержат код для выполнения данного действия). Это же относится и к элементам, с которыми сопоставляются действия. Тем не менее, действия и элементы интерфейса можно создавать с помощью кода в приложении – для того, чтобы выполняемые действия соответствовали текущему состоянию программы.

Действия Eclipse, как правило, предназначены для формализации взаимодействия пользователя с приложением. Если в программе взаимодействуют различные программные компоненты этого приложения, обычно используется стандартная модель обмена сообщениями о событиях (events). Источник события создает объект «событие» и передает его слушателям, подписавшимся на это событие. Таким образом, на поверхность выходят хорошо знакомые всем Java-программистам слушатели событий, классы самих событий и интерфейсы, сопоставленные с событиями. Дополнительная хорошая новость состоит в том, что при обмене событиями между компонентами SWT и JFace вызов всех методов происходит в одном потоке – потоке UI, поэтому не нужно думать о работе в многопоточном режиме.

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

Контекстное меню выбрано в данном случае для того, чтобы связать действие (в данном случае добавление и удаление нового элемента) не с представлением в целом (меню представления) и не с рабочей средой Eclipse (workbench), а с конкретным элементом интерфейса, в нашем случае – компонентом ListViewer.

Стандартным элементом самого низкого уровня является компонент JFace ListViewer. Его API содержит несколько методов для добавления обработчиков (полезных в реальной программе) слушателей (listeners) событий.

Примером может служить событие двойного щелчка левой клавишей мыши. Рассмотрим его поподробнее.

Для обработки данного события слушатель должен предоставить интерфейс IDoubleClickListener:

package org.eclipse.jface.viewers;

public interface IDoubleClickListener 
{
  public void doubleClick(DoubleClickEvent event);
}

Слушатель, реализующий данный интерфейс, может быть зарегистрирован в ListViewer с помощью метода:

addDoubleClickListener(IDoubleClickListener) 

Для каждого метода добавления подписки на событие (addXxx) в компоненте имеется соответствующий метод удаления подписки (removeXxx).

Тип события DoubleClickEvent очень характерен для контейнеров JFace и может быть использован как образец для создания собственных событий, поэтому ниже приведен его код:

package org.eclipse.jface.viewers;

import java.util.EventObject;
import org.eclipse.core.runtime.Assert;

public class DoubleClickEvent extends EventObject 
{
  private static final long serialVersionUID = 3258408443605038133L;
  protected ISelection selection;

  public DoubleClickEvent(Viewer source, ISelection selection) 
  {
    super(source);
    Assert.isNotNull(selection);
    this.selection = selection;
  }

  public ISelection getSelection() 
  {
    return selection;
  }

  // следующий метод возвращает объект – источник события.
  public Viewer getViewer() 
  {
    return (Viewer) getSource();
  }
}

Здесь уместно остановиться на очень часто используемом при работе с контейнерами JFace классе – интерфейсе ISelection.

Собственно говоря, сам интерфейс ISelection практически бесполезен – он объявляет единственный метод isEmpty(), возвращающий TRUE, если контейнер пуст. Программисты используют производные от него интерфейс IStructuredSelection (и реализующий этот интерфейс класс StructuredSelection), а также – при работе с деревьями – интерфейс ITreeSelection. Интерфейс IStructuredSelection имеет следующий вид:

package org.eclipse.jface.viewers;

import java.util.Iterator;
import java.util.List;

public interface IStructuredSelection extends ISelection 
{
  public Object getFirstElement();
  public Iterator iterator();
  public int size();
  public Object[] toArray();
  public List toList();
}

Таким образом, этот интерфейс формализует правила работы с одним или несколькими выбранными элементами списка. Для получения выбранных элементов списка обычно используется метод getSelection().

Использование ISelection (на практике – IStructuredSelection) при работе с графическими контейнерами JFace принципиально отличается от работы с чистыми списками. При работе со списками есть отображаемые в компоненте данные и их порядковый индекс (в порядке отображения данных). При использовании контейнеров JFace с моделями данных, провайдерами и ISelection программист работает с элементами в контейнере, а не с отображаемыми данными.

Существует стандартный класс StructuredSelection, реализующий интерфейс ISelection, который можно рассматривать как оболочку вокруг произвольных объектов. Использование IStructuredSelection и StructuredSelection будет продемонстрировано ниже.

Вторым важнейшим событием при работе со списками JFace является событие, связанное с изменением выбранного элемента (неважно, каким образом). Соответствующее событие формализовано в виде типа SelectionChangedEvent, который содержит информацию об источнике события и о выбранном элементе. Соответствующий слушатель имеет название ISelectionChangedListener, а метод для его регистрации в источнике события – addSelectionChangedListener(). Как с ним работать, также будет показано в приводимом ниже примере.

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

Не следует путать слушатели ISelectionChangedListener (изменение выбранного элемента в элементах управления JFace) и ISelectionListener (изменение выбранного элемента в представлении и/или редакторе рабочей области (workbench) Eclipse).

Другие методы компонента ListViewer, такие, как add(), getElementAt() или remove(), в данном случае для нас интереса не представляют – они предназначены для работы исключительно со списком, т.е. с отображаемой информацией, и в нашем приложении это слишком низкий уровень. Основным типом для работы будет являться не список из библиотеки JFace, а созданный нами контейнер для элементов IViewElement (класс ViewElementsHolder). Тем не менее, полезным может быть вызов для ListViewer одного из методов refresh(), которые обновляют состояние списка, извлекая информацию из модели данных для этого списка (провайдеры, фильтры и сортировщики).

Сохранение состояния представления

В приложении из предыдущей части статьи ни представление, ни его компоненты (экземпляр списка ListViewer и контейнер элементов типа IViewElement) не сохраняли свое текущее состояние между запусками программы. Такое поведение далеко не всегда соответствует смыслу решаемой задачи.

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

  1. Собственно, информацию, то есть данные, обрабатываемые приложением. В разбираемом примере данные хранятся в контейнере ViewElementsHolder.
  2. Состояние представления. Примером в данном случае может служить состояние элемента управления ListView – например, выбранного элемента, параметров фильтров, сортировщиков, цветов, используемых шрифтов и т.п.

В обоих случаях в Eclipse обычно используется примерно одинаковый подход. Информация и состояние сохраняются в XML, в каталоге, к которому можно получить доступ на основе информации о настройках экземпляра Eclipse и структуры плагинов. Обычно это некоторый подкаталог в <workspace>\.metadata\.plugins. Структуру и вид сохраняемой информации определяет сам программист, используя специальный интерфейс IMemento и реализующие его классы XMLMemento и ConfigurationElementMemento.

Интерфейс IMemento объявляет методы для чтения и записи информации (в текстовом и числовом виде) в классический древовидный XML-документ. Разработчики платформы Eclipse предполагали, что у прикладных программистов, занятых решением своих задач, не возникнет необходимости ни создавать производные от IMemento интерфейсы, ни самостоятельно писать класс, реализующий этот интерфейс – все необходимое обеспечивают стандартные реализации.

package org.eclipse.ui;
public interface IMemento 
{
  // Зарезервированный для внутреннего использования ключ.
  public static final String TAG_ID = "IMemento.internal.id";

  // значение ключа – любое уникальное сочетание букв и цифр.

  // Создание дочернего (относительно «текущего элемента») элемента дерева. 
  // Аргумент метода задает нужный «тип» дочернего элемента. По сути, «тип» -
  // это новый тег в составе XML-документа. Каждой такой тег будет иметь 
  // атрибуты и их значения. Атрибут в данном случае называется «ключом». 
  // Метод возвращает созданный элемент, который может быть 
  // использован, в свою очередь, как «текущий»
  public IMemento createChild(String type);

  // Создание дочернего ветви, т.е. дочернего объекта IMemento, с явно 
  // задаваемым значением ключа (параметр id)  
  public IMemento createChild(String type, String id);

  // Получение первого дочернего элемента указанного «типа»
  public IMemento getChild(String type);

  // Получение массива всех дочерних элементов указанного типа
  public IMemento[] getChildren(String type);

  // Получение значения атрибута (по его указанному «ключу», т.е. имени)
  // как плавающего числа, целого числа или строки (или null, в случае отсутствия 
  // такого атрибута тега или ошибки преобразования типов) 
  public Float getFloat(String key);
  public Integer getInteger(String key);
  public String getString(String key);
  ...

  // Получение ID для текущего объекта IMemento
  public String getID();

  // Сохранение данных в виде «имя_атрибута=значение» для текущего элемента
  public void putFloat(String key, float value);
  public void putInteger(String key, int value);
  public void putMemento(IMemento memento);
  public void putString(String key, String value);
  ...
}

Пока остается непонятным следующий важный вопрос: как получать начальный объект типа IMemento? Сохранение информация для плагина, с одной стороны, и глобальной информации выполняется немного по-разному, поэтому рассматривать эти режимы сохранения мы будем отдельно.

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

Сохранение состояния представления

Состояние представлений и редакторов Eclipse хранится в одном файле с фиксированным расположением и именем, а именно, в файле:

<workspace>\.metadata\.plugins\org.eclipse.ui.workbench\workbench.xml. 

Это довольно большой файл, и извлечение информации из него занимает некоторое время.

Поскольку имя файла, в общем, известно (с учетом наличия сервисных функций определения параметров конфигурации самого экземпляра Eclipse), то можно было бы создать объект, реализующий интерфейс IMemento (или класс XMLMemento) на основе его имени. Но в данном случае такой необходимости нет. Да и неразумно производить разбор большого XML-файла при загрузке состояния одного (из многочисленных) компонентов.

При загрузке представления Eclipse вызывает метод:

void init(IViewSite site,IMemento memento) throws PartInitException;

интерфейса IViewPart. Этот метод реализован в классе ViewPart, который часто используется как базовый для создания собственных представлений. Реализация по умолчанию игнорирует аргумент IMemento. Поэтому напрашивается следующий подход: переопределить в классе создаваемого представления этот метод и сохранить ссылку на объект IMemento:

public class View extends ViewPart 
{
  public static final String ID = "my.view_and_info_full.view";

  private ListViewer viewer;
  private IMemento memento;
  ...

public void init(IViewSite site, IMemento memento) 
  throws PartInitException 
{

  super.init(site, memento);
  this.memento = memento;
}

Затем сохраненный объект memento может использоваться при чтении состояния. Сохранение ссылки на memento может потребоваться, так как вызов метода init() может произойти слишком рано. В этот момент еще не созданы элементы управления, и вам попросту может оказаться нечего инициализировать. Допустим, разработчик хочет сохранить вид шрифта для ListView. В этом случае удобнее считать параметры настройки шрифта и применить их уже после того, как создан сам список. Это можно сделать в коде метода createPartControl(), а не init(). Конечно, считанные данные можно просто поместить в поля представления, но проще запомнить ссылку на memento и использовать ее в createPartControl.

Запись данных предлагается производить в методе:

void saveState(IMemento memento)

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

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

public class View extends ViewPart 
{

  public static final String MEMENTO_TYPE = "MyFontSettings"; 
  public static final String MEMENTO_FONT_SIZE = "FontSize"; 
  public static final String MEMENTO_FONT_STYLE = "FontStyle"; 
  public static final String MEMENTO_FONT_NAME = "FontName"; 

  private IMemento memento;
  ...  
  public void init(IViewSite site, IMemento memento) 
      throws PartInitException 
  {
    super.init(site, memento);
    this.memento = memento;
  }

  public void saveState(IMemento memento) 
  {
    ...
    Font font = viewer.getControl().getFont();
    FontData fd = (font.getFontData())[0];
    int height = fd.getHeight();
    int style = fd.getStyle();
    String name = fd.getName();
    
    font.dispose();

    IMemento m = memento.createChild(MEMENTO_TYPE);
    m.putInteger(MEMENTO_FONT_SIZE, height);
    m.putInteger(MEMENTO_FONT_STYLE, style);
    m.putString(MEMENTO_FONT_NAME, name);  
  }

  public void createPartControl(Composite parent) 
  {
    ...
    if (memento != null)
    {
      IMemento child = memento.getChild(MEMENTO_TYPE);
      if (child != null)
      {
        Integer sizeValue = child.getInteger(MEMENTO_FONT_SIZE);

        if (sizeValue == null)
          return;

        int size = sizeValue.intValue();
        Integer styleValue = child.getInteger(MEMENTO_FONT_STYLE);

        if (styleValue == null)
          return;

        int style = styleValue.intValue();
        String fontName = child.getString(MEMENTO_FONT_NAME);

        if (fontName == null)
          return;

        Font font = viewer.getControl().getFont();
        Font listFont = new Font  (font.getDevice(), fontName, size, style);

        viewer.getControl().setFont(listFont);
      }
    }
  }
}

В результате выполнения такого кода в файле workbench.xml появится примерно такой фрагмент:

<view id="my.view_and_info_full.view" partName="View">
  <viewState>
    <MyFontSettings FontName="Tahoma" FontSize="10" FontStyle="0"/>
  </viewState>
</view>

Такой способ сохранения состояния представлений очень удобен во многих случаях. Например, для задания параметров сортировки данных в элементе управления «список», фильтров для отображения данных и пр. Реализация XMLMemento создает теги <view> и <viewState>, все остальное разработчик может определять сам. Единственное, о чем следует всегда помнить – это необходимость проверки наличия и корректности считываемых данных. В данном примере такие проверки были минимальны. Однако в реальных приложениях их не стоит опускать.

Сохранение информации приложения

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

XMLMemento связан с редактированием данных в xml-дереве. Для реализации XMLMemento была взята модель DOM, предложенная консорциумом W3C.

XMLMemento, помимо методов, объявленных в интерфейсе IMemento, имеет следующие основные статические методы, которые играют роль конструкторов:

public static XMLMemento createReadRoot(Reader reader);
public static XMLMemento createWriteRoot(String type);

Прежде всего, бросается в глаза некоторая асимметричность методов. createReadRoot принимает на вход Reader, а createWriteRoot не принимает ссылку на Writer. Writer нужно передать методу:

public void save(Writer writer);

объекта XMLMemento, возвращаемого методом createWriteRoot.

Разработчик должен определить, в каком xml-файле хранить информацию. Поскольку файлы для сохранения информации указываются программистом явно, нет проблем создать соответствующие объекты Reader и Writer (точнее, FileReader и FileWriter). В качестве параметров конструкторов этих классов могут быть указаны просто строки, содержащие имена нужных файлов, или объекты типа java.io.File (или java.io.FileDescriptor).

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

Класс плагина Eclipse Plugin (который прямо или косвенно является предком всех плагинов) содержит метод getStateLocation() возвращает локальный путь к каталогу для хранения состояния данного установленного плагина в виде объекта, реализующего интерфейс IPath. Этот объект с помощью вызова метода toFile() интерфейса IPath преобразуется в объект типа File. Правда, для того, чтобы вызвать этот метод, нужен сам плагин, и не всегда именно тот, в котором вызывается метод getStateLocation().

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

  1. Для каждого плагина сохранять ссылку на этот плагин (например, в коде конструктора класса плагина) и предусмотреть для этого класса статический метод, возвращающий данную ссылку.
  2. Воспользоваться стандартным методом Platform.getPlugin(), который в качестве параметров требует задания уникального идентификатора нужного плагина.

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

Пример сохранения/восстановления данных приложения и состояния представления

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

Итак, мы зададим жесткое соответствие между элементами, которые могут появиться в списке, и их ключами. Под «ключами» будем понимать строки, подобные «green» или «fa», знакомые нам по предыдущему примеру:

class ViewColorElementFactory extends ViewElementFactory 
{
  private static String[] COLORS = 
    { "red", "orange", "yellow", "green", "blue", "violet" };
  ...
}

Эти ключи уже хранятся в объекте ViewElementAbstract как поле inpObject. Нужно только добавить в интерфейс IViewElement еще один метод для доступа к этому полю (выделен жирным):

import org.eclipse.core.runtime.IAdaptable;

// Интерфейс, используемый при отображении всех элементов списка
public interface IViewElement extends IAdaptable 
{
  // Строка для вывода информации об объекте  
  String getPrintName();

  // Группа, к которой относится объект  
  int getCategory ();

  // Порядковый номер в группе (например, для сортировки)  
  int getIndex ();

  // Объект, для отображения которого создан данный объект    
  Object getBaseObject ();

  // Значение "ключа", на основе которого создается объект
  Object getObjectKey ();
}

Реализация этого метода тривиальна – он просто возвращает соответствующее поле.

Перейдем к методу getElements() класса ViewElementHolder, который в схеме MVC компонентной модели JFace играет роль провайдера исходных данных для хранения и отображения.

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

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

public class ViewElementsHolder 
{
  private static ViewElementsHolder veHolder;  // уже было ранее
  private Collection listElements;             // контейнер
  ...
}

С учетом наличия этого контейнера код метода getElements() очень упрощается:

  public IViewElement[] getElements()
  {
    if (listElements == null)
      return null;

    IViewElement[] res = new IViewElement[listElements.size()];
    listElements.toArray(res);
    return res;
  }

Данные могут попадать в контейнер двумя способами:

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

public class ViewElementsHolder 
{
    public boolean isPresent(String elemKey)
    {
      return findExistingElement(elemKey) != null;
    }
    
    public IViewElement findExistingElement(String elemKey)
    {
      if (listElements != null)
      {
        Iterator i = listElements.iterator();

        while (i.hasNext())
        {
          IViewElement e = (IViewElement)i.next();
          String key = (String)e.getObjectKey(); 

          if (key.equals(elemKey))
            return e;
        }
        
      }

      return null;
    }
    
    public IViewElement addNewElement(String elemKey)
    {
      IViewElement res = null;
      ViewElementFactory[] factoryObjects = ViewElementFactory.getFactories();
      
      if (!isPresent (elemKey))
      {
        // Для простоты считаем, что все исходные данные корректны
        for (int j = 0; j < factoryObjects.length; j++)
        {
          IViewElement ve = factoryObjects[j].createViewElement(elemKey);

          if (ve != null)
          {  
            res = ve;

            if (listElements == null)
              listElements = new ArrayList<IViewElement>(64);

            listElements.add(res);
            fireElementAdded(res);            
            break;
          }
        }
      }

      return res;
    }

    public boolean removeElement(String elemKey)
    {
      IViewElement elem = findExistingElement(elemKey);

      if (elem == null)
        return false;

      fireElementRemoved(elem);
      listElements.remove(elem);
      return true;
    }

Методы fire..., выделенные жирным и курсивом, будут объяснены позже, пока на них можно не обращать внимания.

Теперь можно приступить к реализации метода сохранения данных saveData(), который вызывается в обработчике события saveState нашего представления, и метода loadData() загружающего данные, который вызывается в обработчике события createPartControl.

В реальных приложениях не стоит сохранять и загружать данные приложения в событиях инициализации и выгрузки представлений, так как эти данные могут отображаться более чем в одном представлении, и вы можете получить нежелательное дублирование этих данных в памяти. Лучше воспользоваться более глобальными событиями (например, событиями, связанными с загрузкой/выгрузкой проекта) или реализовать фабрику или синглтон-объект, которые предоставляют доступ к одному экземпляру данных. – Прим.ред.

public class ViewElementsHolder 
{
  private static ViewElementsHolder veHolder;
  private Collection listElements;
  ...  
  private static final String LIST_TAG = "ListElements";
  private static final String LIST_ELEM_TAG = "ListElement";
  private static final String LIST_ELEM_KEY_TAG = "ElementKey";
  ...
    public void saveData()
    {
      if ((listElements == null) || (listElements.isEmpty()))
        return;

      XMLMemento memento = XMLMemento.createWriteRoot(LIST_TAG);
      
      Iterator i = listElements.iterator();

      while (i.hasNext())
      {  
        IViewElement e = (IViewElement)i.next();
        IMemento child = memento.createChild(LIST_ELEM_TAG); 
        child.putString(LIST_ELEM_KEY_TAG, (String)e.getObjectKey());
      }

      FileWriter writer = null;

      try 
      {  
        writer = new FileWriter (getStateFileLocation());
        memento.save(writer);
      }
      catch (IOException e) 
      {
        e.printStackTrace();
      }
      finally 
      {
        try 
        {
          if (writer != null)
            writer.close();
        }
        catch (IOException ex) {
          ex.printStackTrace();
        }
      }
    }
    
    public void loadData ()
    {
      if (listElements == null)
        listElements = new ArrayList<IViewElement>(64);

      FileReader reader = null;

      try
      {
        reader = new FileReader (getStateFileLocation());
        IMemento memento = XMLMemento.createReadRoot(reader);
        IMemento[] children = memento.getChildren(LIST_ELEM_TAG);

        if (children != null)
          listElements.clear();

        for (int i = 0; i < children.length; i++)
        {
          String key = children[i].getString(LIST_ELEM_KEY_TAG);

          if (key != null)
            addNewElement(key);
        }
      }
      catch (Exception e)
      {
        e.printStackTrace();
      }
      finally
      {
        try 
        {
          if (reader != null)
            reader.close();
        }
        catch (Exception e)
        {
          e.printStackTrace();
        }
      }
    }

В реальных приложениях также нежелательно помещать код сохранения данных, использующий зависимые от хост-приложения (в данном случае от Eclipse) интерфейсы, в код контейнеров (коллекций), так как это уменьшает возможность повторного использования ваших решений (вследствие нарушений принципов MVC). – Прим. ред.

Для задания имени файла, в котором сохраняются данные, используется сервисный метод:

  public File getStateFileLocation() 
  {
    IPath path = VAIActivator.getDefault().getStateLocation();
    path.append("data.xml");
    return path.toFile();
  }

Разработчик, разумеется, может выбрать любой каталог и любое имя файла.

В результате в файле data.xml будет содержаться примерно такая информация:

<?xml version="1.0" encoding="UTF-8"?>
<ListElements>
  <ListElement ElementKey="green"/>
  <ListElement ElementKey="red"/>
  <ListElement ElementKey="mi"/>
  <ListElement ElementKey="sol"/>
  <ListElement ElementKey="do"/>
  <ListElement ElementKey="blue"/>
</ListElements>

Пока нет обработки событий, ввод тестовых данных может быть осуществлен просто добавлением нужных данных в этот файл.

Теперь можно перейти к обработке событий.

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

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

Нам не нужно создавать интерфейс-слушатель, определять класс события и обеспечивать возможность регистрации получателей события в источнике – все это уже сделано. Нам нужно только написать класс, реализующий этот интерфейс, а затем зарегистрировать объект-получатель.

public class View extends ViewPart 
{
...
  class ListSelectionChangedAction implements ISelectionChangedListener 
  {
    @Override
    public void selectionChanged(SelectionChangedEvent event) 
    {
      IStructuredSelection sel = (IStructuredSelection)event.getSelection();

      if (sel.size() == 1) // выделен один элемент
      {
        IViewElement elem = (IViewElement)sel.getFirstElement();
        Object o = elem.getBaseObject();

        if (o instanceof MyColor)
        {
          Color c = ((MyColor)o).getColor((String)elem.getObjectKey());
          ListViewer lv = (ListViewer)event.getSource();
          lv.getControl().setBackground(c);
        }
      }
    }
  }
...
}

Для получения цвета как объекта Color мы обращаемся к сервисной функции класса MyColor. Как это реализовано – в данном случае не очень интересно. Читатель может сам попробовать различные способы создания объекта Color на основе строки вида «yellow» или «violet».

Осталось только создать объект класса ListSelectionChangedAction и добавить его в список слушателей оповещений об изменении выделенного элемента.

Код метода createPartControl() нужно изменить следующим образом:

public void createPartControl(Composite parent)
{
  viewer = new ListViewer(parent, SWT.H_SCROLL | SWT.V_SCROLL);
  viewer.setContentProvider(new MyContentProvider());
  viewer.setLabelProvider(new MyLabelProvider());

  ViewElementsHolder holder = ViewElementsHolder.getElementsHolder();
  holder.loadData();
  viewer.setInput(holder);  
  viewer.setSorter(new MyViewerSorter());
    
  // Обработка события "изменение выбора элемента"    
  viewer.addPostSelectionChangedListener(new ListSelectionChangedAction());
  ...
}

При создании контекстного меню мы будем использовать еще одно стандартное событие.

Теперь можно перейти к событию, которое мы полностью определяем сами. Это событие связано с добавлением и удалением элементов в контейнере ViewElementHolder.

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

import java.util.EventObject;

public class ViewElementsHolderEvent extends EventObject 
{
  private IViewElement element;

  public ViewElementsHolderEvent(ViewElementsHolder source, IViewElement elem)
  {
    super (source);
    element = elem;
  }
  
  public IViewElement getElement ()
  {
    return element;
  }
}

Объект «событие» содержит, во-первых, контейнер, который порождает это событие, а, во-вторых, добавляемый/удаляемый элемент (как мы договорились, данные удаляются и добавляются поодиночке).

Далее необходим интерфейс-слушателя для этого события:

public interface IViewElementsHolderListener 
{
  public void elementAdded (ViewElementsHolderEvent event);
  public void elementRemoved (ViewElementsHolderEvent event);
}

Класс, реализующий этот интерфейс, может выглядеть так:

class ViewElementsHolderAction implements IViewElementsHolderListener 
{
  @Override
  public void elementAdded(ViewElementsHolderEvent event) 
  {
    ListViewer lv = View.getListViewer();
    lv.refresh();
    IStructuredSelection sel = new StructuredSelection(event.getElement());
    lv.setSelection(sel, true);
  }

  @Override
  public void elementRemoved(ViewElementsHolderEvent event) 
  {
    View.getListViewer().refresh();
  }
}

Обратите внимание, как в методе elementAdded() добавленный элемент становится выделенным.

Теперь нужно создать экземпляр(ы) класса ViewElementsHolderAction и зарегистрировать их в качестве слушателей экземпляров контейнера – ViewElementHolder. Поскольку таких слушателей может быть много, нужно завести в контейнере не ссылку на слушателя, а список таких ссылок, а также создать методы для регистрации слушателей и отмены такой регистрации:

public class ViewElementsHolder 
{
  private List holderListeners = new ArrayList();
  ...
  public void addViewElementsHolderListener (IViewElementsHolderListener lst)
  {
    if (!holderListeners.contains(lst))
      holderListeners.add(lst);
  }
  
  public void removeViewElementsHolderListener(
                IViewElementsHolderListener lst)
  {
    holderListeners.remove(lst);
  }
}

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

public class ViewElementsHolder 
{
  ...
  public void fireElementAdded (IViewElement elem)
  {
    ViewElementsHolderEvent event = new ViewElementsHolderEvent (this, elem);
    Iterator i = holderListeners.iterator();

    while (i.hasNext())
      ((IViewElementsHolderListener)i.next()).elementAdded(event);
  }

  public void fireElementRemoved (IViewElement elem)
  {
    ViewElementsHolderEvent event = new ViewElementsHolderEvent (this, elem);
    Iterator i = holderListeners.iterator();

    while (i.hasNext())
      ((IViewElementsHolderListener)i.next()).elementRemoved(event);
  }
}

Слушатели создаются и регистрируются все в том же методе createPartControl():

...

  // Обработка события от контейнера элементов (добавлен/удален)
  holder.addViewElementsHolderListener(new ViewElementsHolderAction());

Осталось обеспечить пользователю средства интерактивного ввода/удаления данных. В данном примере для этого динамически будут создаваться действия (actions), а именно: удаление выбранного элемента и добавление тех элементов (из фиксированного списка), которые еще не присутствуют в контейнере ListViewer. Кроме этого, будет создано и контекстное меню для вызова этих действий.

Задача распадается на два этапа:

Начнем с создания действий:

// Классы действий для выпадающих меню
    
class RemoveElementAction extends Action 
{
  private View view;

  public RemoveElementAction (String text, View view)
  {
    super(text);
    this.view = view;
  }
      
  public void run ()
  {
    IStructuredSelection sel = 
      (IStructuredSelection)v.getListViewer().getSelection();

    if (sel.size() == 1)
    {
      IViewElement elem = (IViewElement)sel.getFirstElement();
      String elemKey = (String)elem.getObjectKey();
      ViewElementsHolder.getElementsHolder().removeElement(elemKey);
    }
  }
}

class AddElementAction extends Action 
{
  private View view;

  public AddElementAction (String text, View view)
  {
    super(text);
    this.view = view;
  }
      
  public void run ()
  {
    ViewElementsHolder.getElementsHolder().addNewElement(super.getText());
  }
}

Теперь нужно создать (в createPartConrol()) нужное число экземпляров этих действий:

  private static String[] COLORS = 
    { "red", "orange", "yellow", "green", "blue", "violet"};
  private RemoveElementAction removeAction;
  private AddElementAction[] addAction;

  // Создание действий, которые будут вызываться из контекстного меню
    
  removeAction = new RemoveElementAction ("Delete", this);
  addAction    = new AddElementAction [COLORS.length];

  for (int i = 0; i < COLORS.length; i++)
    addAction[i] = new AddElementAction (COLORS[i], this);

Осталось только сопоставить эти действия с элементами контекстного меню. Для этого нужно реализовать нужным нам образом стандартный интерфейс IMenuListener, который объявляет единственный метод – menuAboutToShow(). Класс, реализующий этот интерфейс, может выглядеть следующим образом:

class DynamicMenuCreationAction implements IMenuListener 
{
  @Override
  public void menuAboutToShow(IMenuManager manager) 
  {
    ViewElementsHolder holder = ViewElementsHolder.getElementsHolder();

    for (int i = 0; i < COLORS.length; i++)
    {
      dAction[i].setEnabled(!holder.isPresent(COLORS[i]));
      manager.add(addAction[i]);
    }

    manager.add (new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
    manager.add(removeAction);
  }
}

Используется этот класс так (в коде все того же метода createPartControl()):

  // Создание контекстного меню для списка
  MenuManager mmgr = new MenuManager();

  // признак того, что меню перед отображением строится заново каждый раз
  mmgr.setRemoveAllWhenShown(true);

  // Построение меню (в стандартном методе для интерфейса IMenuListener)
  mmgr.addMenuListener(new DynamicMenuCreationAction());
  Menu menu = mmgr.createContextMenu(viewer.getControl());
  viewer.getControl().setMenu(menu);

Созданное представление при работающей программе может выглядеть так:


Заключение

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


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

Copyright © 1994-2016 ООО "К-Пресс"