Некоторое время назад мне в голову пришла мысль создать библиотеку для быстрой генерации форм на Java Swing. Расскажу, как я к этому пришел.
Наверняка, многим приходится периодически реализовывать редактирование java-bean'ов при помощи Swing-компонент. Например, различных справочников. JDK консервативен, и не предоставляет «из коробки» чего-то, что сильно бы облегчило жизнь разработчика. Для каждого случая нужно писать что-то наподобие:
Уже здесь достаточно много кода. А ведь еще может понадобиться добавить в обработчик валидацию.
Есть разные подходы, как облегчить себе жизнь.
Движимый жаждой изобретения велосипедов, я решил написать минималистскую библиотеку генерации форм, которая бы позволяла делать:
Тут все просто: даем класс, получаем форму.
Наверное, в 80% случаев, для каждого поля требуется всего 2 компонента: ярлычок с названием и собственно редактор. Классический подход таков: спрашиваем название поля, получаем компонент.
А нельзя ли заменить строки на что-то более надежное? Например, так:
Если в качестве sample подать динамический прокси (пример использования библиотеки CGLIB), который бы сообщал контексту, какие методы вызваны, то этот код станет вполне работоспособным. Подход не потокобезопасен, но ведь это GUI.
C ctx.label() все ясно — он должен возвращать JLabel. А что должен возвращать ctx.editor()? Внутри контекста должен существовать некий маппинг, который для каждого поля в соответствии с его типом подберет ему редактор. Но подобрать мало. А если это пользовательский компонент? Есть сразу несколько моментов, которые должны быть сконфигурированы.
Например, для стороннего компонента календаря эта конфигурация будет выглядеть так:
Здесь changeHandler служит, чтобы дать системе знать об изменениях в компоненте. По умолчанию, происходит валидация бина с использованием Hibernate Validator. О ее результатах оповещается ValidationMarker, в данном случае, BackgroundMarker, который решает, как изменить внешний вид провалидированных компонентов.
Если нужно задать маппинг для всех полей данного типа, код выглядит просто:
Для конкретных полей, — несколько сложней:
Опять же, нельзя ли воспользоваться динамическими прокси, и привязывать к методам вместо строк, обеспечив проверку типов? Можно, хотя и несколько громоздко:
Такую библиотеку я написал некоторое время назад, а теперь решил вынести её на суд сообщества. Сейчас я делаю адаптер этой библиотеки для Scala, но пока получается довольно тривиально. Возможно, я опишу это в следующем посте.
Вики с более подробным описанием библиотеки и мавеновский репозиторий находятся на Google Code: code.google.com/p/swing-formbuilder
Исходники я перенес на GitHub: github.com/aeremenok/swing-formbuilder
Постановка задачи
Наверняка, многим приходится периодически реализовывать редактирование 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