суббота, 2 июля 2011 г.

Использование TreeViewer

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



Компонент TreeViewer, как и большинство остальных вещей из библиотеки JFace, очень удобен и прост в использовании. Разработчики этой библиотеки старались ко всем компонентам применить шаблон MVC, чтоб отделить три главные составляющие данных друг от друга. Еще JFace удобен тем, что инкапсулирует в себе много рутинной работы, связанной с созданием пользовательского интерфейса, выступая в роли оболочки над библиотекой SWT, что позволят сократить трудо- и время затраты над новой фичеи в UI.
Работа с компонентом TreeViewer (как говорилось выше, основанного на MVC) сводится к имплементации классов, представляющих собой эти самые модели и представления:
   - Модель. Для реализации модели необходимо имплементировать интерфейс ITreeContentProvider и, собственно говоря, создать ту модель данных, которую будет отображать вьювер.
  -  Представление. Сделать реализацию интереса ILabelProvider, для получения графического представления каждого компонента дерева. В нашем случае мы используем готовый класс LabelProvider.
Сам TreeViewer же выступает в роли контролера, обращаясь к модели и обновляя элементы интерфейсной части, наоборот и реагируя на события со стороны пользовательского интерфейса.
В качестве примера возьмем очень простую задачку, которая поможет быстро разобраться в основах использования вьювера: есть статьи, которые нужно разложить  по группам (Family, Friends и Other), и все.
К объектам модели у нас будут относиться классы Article и ArticleGroup, код которых приводится ниже:

public class Article extends Model {

    public Article(String name) {
       super(name);
    }

}

public class ArticleGroup extends Model{

    private List<Model> aritcles;   

    public ArticleGroup(String groupName) {
        super(groupName);
        this.aritcles = new ArrayList<Model>();
    }

    public Model[] getChildren() {
        return this.aritcles.toArray(new Model[aritcles.size()]);
    }

    public boolean hasChildren() {
        return this.aritcles.size() > 0;
    }

    public void addElement(Model article){
        this.aritcles.add(article);
    }
}

Для того чтоб в каждую из категорий можно было положить другуюкатегорию, и ArticelGroup и Article имеют общего предка, класс Model.

public class Model {

    private String name; 
    public ArticleGroup parent;

    public Model(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }

}
Отлично, осталось реализовать ITreeContentProvider.

public class ArticlesContentProvider implements ITreeContentProvider {

    @Override
    public void dispose() {
    }

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {       
    }

    @Override
    public Object[] getElements(Object inputElement) {       
        return getChildren(inputElement);
    }

    @Override
    public Object[] getChildren(Object parentElement) {
        if(parentElement instanceof ArticleGroup){
            return  ((ArticleGroup)parentElement).getChildren();
        }
        return new Object[0];
    }

    @Override
    public Object getParent(Object element) {
       if(element instanceof Model){
            return ((Model)element).parent;
        }
        return null;
    }

    @Override
    public boolean hasChildren(Object element) {
        if(element instanceof ArticleGroup){
            ArticleGroup group = (ArticleGroup)element;
            return group.hasChildren();
        }
        return false;
    }
}
Теперь поговорим о методах, реализованных в этом классе. Метод hasChildren() вызывается каждый раз, когда происходит раскрывание списка элементов в дереве. То есть пользователь нажимает на любой из плюсиков на дереве и отображаются все его дочерние узлы, если они есть. Так вот этот метод и проверят их наличие. Обычно дерево содержит несколько типов узлов (как минимум два - узел и лист), поэтому возврат значения которое говорит о наличии детей, должен рассчитываться индивидуально для каждого типа объекта. Если в результате возвращается true, происходит вызов метода getChildren(), на вход которому и передается этот узел, и список раскрывается. Метод getElements() работает также как и getChildren(), поэтому в зависимости от вашей доменной модели, вызов можно пернаправить с одного метода на другой. Этот метод вызывается вызовом метода setInput() на объекте TreeViewer. Метод inputChanged() вызывается при установке входного дерева. Другими словами, при каждом изменении входного дерева, inputChanged() метод будет вызван. В нашем случае реализацию можно оставить пустой.
После того как модель создана, самое время перейти к представлению, а именно реализции LabelProvider. Код представлен ниже:

public class ArticlesLabelProvider extends LabelProvider {

    public static final String ICON_FOLDER_FORMAT = "/icons/%s";

    public ArticlesLabelProvider() {
        super();
    }

    @Override
    public Image getImage(Object element) {
        if (element instanceof ArticleGroup) {
            return getImage("artcile_group_icon.gif");
        }
        if (element instanceof Article) {
            return getImage("article_icon.gif");
        }
        return null;
    }

    public Image getImage(String imageName) {
        Image image = new Image(Display.getDefault(),
                ArticlesLabelProvider.class.getClassLoader()
                        .getResourceAsStream(
                                String.format(ICON_FOLDER_FORMAT, imageName)));
        return image;
    }

    @Override
    public String getText(Object element) {
        return element.toString();
    }

}
Здесь дела обстоят еще проще чем в контент провайдере. Метод getText() возвращает строковой объект, ассоциированные с каждым узлом. В нашем случае это обычный вызов метода toString().
Метод getImage() возвращает изображение, которое будет отображено слева от надписи. Важно знать и помнить, что все изображения, загруженные в дерево приводятся к размеру первого загруженного изображения. То есть если первая иконка была размером 16х16  пикселей, а последующие за не иконки имели разрешение 25х25 - в результате все иконки сожмутся к 16х16 пикселей. И наоборот, если первое изображение было больше чем остальные - остальным не останется выбора как растянуться до размера первого. Так что имейте ввиду.
Модель и представление созданы, осталось создать создать сам TreeViewer. Для этого создадим представление ViewPart и реализуем метод createPartControl().

public class View extends ViewPart {
   
    public static final String ID = "ua.ypitomets.eclipse.example.treeviewer.view";

    @Override
    public void createPartControl(Composite parent) {
        TreeViewer viewer = new TreeViewer(parent, SWT.NONE);
        viewer.setLabelProvider(new ArticlesLabelProvider());
        viewer.setContentProvider(new ArticlesContentProvider());
        viewer.setInput(getFakeInputData());
        viewer.expandAll();
    }

   
    private Object getFakeInputData() {
        ArticleGroup root = new ArticleGroup("ROOT");
        ArticleGroup familyGroup = new ArticleGroup("Family");
        root.addElement(familyGroup);
        familyGroup.parent = root;
        Article article1 = new Article("All about as");
        familyGroup.addElement(article1);
        article1.parent = familyGroup;
        Article article2 = new Article("Our reletives");
        article2.parent = familyGroup;
        familyGroup.addElement(article2);
        Article article3 = new Article("Positive side");
        article3.parent = familyGroup;
        familyGroup.addElement(article3);
        Article article4 = new Article("Negative side");
        article4.parent = familyGroup;
        familyGroup.addElement(article4);
        ArticleGroup groupFriends = new ArticleGroup("Friends");
        root.addElement(groupFriends);
        Article article5 = new Article("Best friends");
        groupFriends.addElement(article5);
        article5.parent = groupFriends;
        Article article6 = new Article("Bad friends");
        article6.parent = groupFriends;
        groupFriends.addElement(article6);
        groupFriends.parent = root;
        ArticleGroup groupOther = new ArticleGroup("Other");
        root.addElement(groupOther);
        groupOther.parent = root;
        Article article7 = new Article("Better than animals");
        groupOther.addElement(article7);
        article7.parent = groupOther;
        Article article8 = new Article("Fruits and vegetables");
        article8.parent = groupOther;
        groupOther.addElement(article8);
        return root;
    }

    @Override
    public void setFocus() { }
}

Методы setContentProvider() и setLabelProvider() устаналивают провайдеров, которые мы реализовали. Через setInput() передаются наши данные, представляющие доменную модель, которая создается в методе getFakeInputData(). Метод expandAll() раскрывает все узлы дерева. Обратные ему мотод collapseAll() - сворачивает все узлы.
Вот что у нас в результате получилось.

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

Комментариев нет:

Отправить комментарий