Некоторое время назад мне в голову пришла мысль создать библиотеку для быстрой генерации форм на 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