Some time ago I had the idea to create a library for fast generation of forms in Java Swing. I'll tell you how I came to this.
Formulation of the problem
Surely, many people have to periodically implement java-bean editing using a Swing component. For example, various reference books. JDK is conservative, and does not provide something “out of the box” that would greatly facilitate the life of a developer. For each case, you need to write something like:
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 );
There is already a lot of code here. But you may still need to add validation to the handler.
There are different approaches how to make your life easier.
- "Drawing" forms for IDE. For example, NetBeans allows you to build forms and associate a view with data using the java.beans toolkit.
To track changes in your bins, additional code will be generated. To build graphical components, code will also be generated. All this code tends to swell and become unreadable. The process of creating forms is described here . - Reflexive form generation libraries, such as JGoodies (use case here ) and Metawidget (use case here ).
As a rule, the binding goes to the string names of the properties of the beans. Therefore, the correctness of the binding can be broken when refactoring.
Also in Metawidget, the location of the components is specified indirectly, that is, you need to specify, for example, some XML, instead of adding components to the panel directly.
Also in Metawidget it is not clear, for example, how to wrap components in other panels, the frames of which are to be highlighted during validation.
Driven by the thirst for the invention of bicycles, I decided to write a minimalist form generation library that would allow me to do:
- creating default forms for a couple of lines of code
- the usual control of the location of components on the panel, as if you already have ready-made components - in fact, an injection of components tied to the properties of the bean
- control over validation results; modify validation processing by implementing interface
- add new component types by implementing the interface
- whenever possible, check the binding by the compiler
I will describe how the code using this library should look
Default form
Everything is simple: we give a class, we get a form.
Form<Person> form = FormBuilder.map( Person.class ).buildForm(); myFrame.add( form.asComponent() ); Person person = new Person(); person.setName( "john smith" );
Setting the location of components
Probably, in 80% of cases, each field requires only 2 components: a label with the name and the editor itself. The classic approach is: ask the name of the field, we get the component.
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();
Is it possible to replace strings with something more reliable? For example:
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();
If you submit a dynamic proxy as a sample (
an example of using the CGLIB library ), which would tell the context which methods are called, then this code will become fully functional. The approach is not thread safe, but this is a GUI.
')
Mapping
With ctx.label () everything is clear - it should return a JLabel. And what should ctx.editor () return? Within the context, there must be a certain mapping, which the editor will select for him for each field according to its type. But pick up a little. And if this is a custom component? There are several things that need to be configured.
- Class of values
- Component creation operation
- Setting and getting the value
- Processing changes in the component (with the possibility of validating them)
For example, for a
third-party calendar component, this configuration would look like this:
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 ); } } ); } }
Here, changeHandler serves to let the system know about changes in the component. By default, the bean is validated using the Hibernate Validator. Its results are notified by the ValidationMarker, in this case, the BackgroundMarker, which decides how to change the appearance of the validated components.
If you need to set the mapping for all fields of this type, the code looks simple:
Form<Person> form = FormBuilder.map( Person.class ).use( new StringToTextAreaMapper() ).buildForm();
For specific fields, it is somewhat more complicated:
Form<Person> form = FormBuilder.map( Person.class ) .useForProperty( "description", new StringToTextAreaMapper() ) .buildForm();
Again, is it possible to use dynamic proxies, and bind to methods instead of strings, providing type checking? It is possible, although somewhat cumbersome:
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();
Conclusion
I wrote such a library some time ago, and now I decided to submit it to the community court. Now I am making an adapter of this library for Scala, but for now it turns out to be rather trivial. Perhaps I will describe this in the next post.
A wiki with a more detailed description of the library and the mavenovsky repository is located on Google Code:
code.google.com/p/swing-formbuilderI transferred
source codes to GitHub:
github.com/aeremenok/swing-formbuilder