Pull to refresh

Библиотека рефлексивной генерации Swing-форм

Java *
Sandbox
Некоторое время назад мне в голову пришла мысль создать библиотеку для быстрой генерации форм на Java Swing. Расскажу, как я к этому пришел.

Постановка задачи


Наверняка, многим приходится периодически реализовывать редактирование java-bean'ов при помощи Swing-компонент. Например, различных справочников. JDK консервативен, и не предоставляет «из коробки» чего-то, что сильно бы облегчило жизнь разработчика. Для каждого случая нужно писать что-то наподобие:
GridBagPanel panel = new GridBagPanel(){{
    add( new JLabel( "Name" ), 0, 0 );
    add( new JTextField( person.getName() ) {{
        getDocument().addDocumentListener( new DocumentListener() {
            public void insertUpdate( final DocumentEvent e ) {
                person.setName( getText() );
            }

            public void removeUpdate( final DocumentEvent e ) {
                person.setName( getText() );
            }

            public void changedUpdate( final DocumentEvent e ) {
                person.setName( getText() );
            }
        } );
    }}, 0, 1 );
// и так для каждого поля ...
}};

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

Есть разные подходы, как облегчить себе жизнь.
  • «Рисовалки» форм для IDE. Например, NetBeans позволяет строить формы и связывать представление с данными, пользуясь инструментарием пакета java.beans.
    Для отслеживания изменений в ваших бинах будет сгенерирован дополнительный код. Для построения графических компонентов также будет сгенерирован код. Весь этот код имеет свойство раздуваться и становиться неудобочитаемым. Процесс создания форм описан здесь.
  • Библиотеки рефлексивной генерации форм, такие как JGoodies (пример использования тут) и Metawidget (пример использования здесь).
    Как правило, привязка идет к строковым названиям свойств бинов. Поэтому корректность привязки может нарушиться при рефакторинге.
    Также в Metawidget расположение компонентов задается косвенно, т. е. нужно задать, например, некий XML, вместо того, чтобы добавлять компоненты на панель напрямую.
    Также в Metawidget неясно, например, как заворачивать компоненты в другие панели, рамки которых требуется подсветить при валидации.

Движимый жаждой изобретения велосипедов, я решил написать минималистскую библиотеку генерации форм, которая бы позволяла делать:
  • создание форм по умолчанию за пару строк кода
  • обычное управление расположением компонент на панели, как будто готовые компоненты у вас уже есть — по сути, инъекцию компонент, привязанных к свойствам бина
  • контроль за результатами валидации; изменять обработку валидации, реализовав интерфейс
  • добавлять новые типы компонентов, реализовав интерфейс
  • по возможности, проверять привязку компилятором

Опишу, как должен выглядеть код, использующий эту библиотеку


Форма по умолчанию


Тут все просто: даем класс, получаем форму.
Form<Person> form = FormBuilder.map( Person.class ).buildForm();
myFrame.add( form.asComponent() );
Person person = new Person();
person.setName( "john smith" );
// ... further initialization
form.setValue( person );

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


Наверное, в 80% случаев, для каждого поля требуется всего 2 компонента: ярлычок с названием и собственно редактор. Классический подход таков: спрашиваем название поля, получаем компонент.
Form<Person> form =
    FormBuilder.map( Person.class ).with( new PropertyNameBeanMapper<Person>() {
            @Override
            public JComponent mapBean( PropertyNameContext<Person> ctx ) {
                JPanel panel = new JPanel( new BorderLayout() );
                panel.add( ctx.label( "name" ), BorderLayout.NORTH );
                panel.add( ctx.editor( "name" ), BorderLayout.CENTER );
                return panel;
            }
    } ).buildForm();

А нельзя ли заменить строки на что-то более надежное? Например, так:
Form<Person> form =
    FormBuilder.map( Person.class ).with( new SampleBeanMapper<Person>() {
            @Override
            protected JComponent mapBean( Person sample, SampleContext<Person> ctx ) {
                Box box = Box.createHorizontalBox();
                box.add( ctx.label( sample.getName() ) );
                box.add( ctx.editor( sample.getName() ) );
                return box;
            }
    } ).buildForm();

Если в качестве sample подать динамический прокси (пример использования библиотеки CGLIB), который бы сообщал контексту, какие методы вызваны, то этот код станет вполне работоспособным. Подход не потокобезопасен, но ведь это GUI.

Маппинг


C ctx.label() все ясно — он должен возвращать JLabel. А что должен возвращать ctx.editor()? Внутри контекста должен существовать некий маппинг, который для каждого поля в соответствии с его типом подберет ему редактор. Но подобрать мало. А если это пользовательский компонент? Есть сразу несколько моментов, которые должны быть сконфигурированы.
  • Класс значений
  • Операция создания компонента
  • Задание и получение значения
  • Обработка изменений в компоненте (с возможностью валидации их)

Например, для стороннего компонента календаря эта конфигурация будет выглядеть так:
class DateToDateChooserMapper implements TypeMapper<JDateChooser, Date> {
    public Class<Date> getValueClass() {
        return Date.class;
    }

    public JDateChooser createEditorComponent() {
        return new JDateChooser();
    }

    public Date getValue( final JDateChooser editorComponent ) {
        return editorComponent.getDate();
    }

    public void setValue(final JDateChooser editorComponent,
                         final Date value) {
        editorComponent.setDate( value );
    }

    public void handleChanges(final JDateChooser editorComponent,
                              final ChangeHandler changeHandler) {
        editorComponent.getDateEditor()
                       .addPropertyChangeListener( "date", new PropertyChangeListener() {
            public void propertyChange( final PropertyChangeEvent evt ) {
                changeHandler.onChange( BackgroundMarker.INSTANCE );
            }
        } );
    }
}


Здесь changeHandler служит, чтобы дать системе знать об изменениях в компоненте. По умолчанию, происходит валидация бина с использованием Hibernate Validator. О ее результатах оповещается ValidationMarker, в данном случае, BackgroundMarker, который решает, как изменить внешний вид провалидированных компонентов.

Если нужно задать маппинг для всех полей данного типа, код выглядит просто:
Form<Person> form =
    FormBuilder.map( Person.class ).use( new StringToTextAreaMapper() ).buildForm();

Для конкретных полей, — несколько сложней:
Form<Person> form = FormBuilder.map( Person.class )
                               .useForProperty( "description", new StringToTextAreaMapper() )
                               .buildForm();

Опять же, нельзя ли воспользоваться динамическими прокси, и привязывать к методам вместо строк, обеспечив проверку типов? Можно, хотя и несколько громоздко:
Form<Person> form =
    FormBuilder.map( Person.class ).useForGetters( new GetterMapper<Person>() {
            @Override
            public void mapGetters( Person beanSample, GetterConfig config ) {
                config.use( beanSample.getDescription(), new StringToTextAreaMapper() );
            }
    } ).buildForm();


Заключение


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

Вики с более подробным описанием библиотеки и мавеновский репозиторий находятся на Google Code: code.google.com/p/swing-formbuilder
Исходники я перенес на GitHub: github.com/aeremenok/swing-formbuilder
Tags:
Hubs:
Total votes 10: ↑8 and ↓2 +6
Views 3.4K
Comments Comments 5