I have a small program, which is able to load components during the runtime. I would like to write a small API for those components. The main program should recognize properties from the component and create for each property a swing component. My idea was to use annotations, something like:
@Property(name = "X", PropertyType.Textfield, description = "Value for X")
private int x;
What do you think about it? Any problems with that? Are there similar implementations or other options? Thanks for all advices and hints!
Update Please, notice that I'm not able to use third party libraries.
Update I would like to create an abstract class, which is able to create swing components based on the attributes from the concert class. The abstract class controls the presentation. For example (pseudo code):
public class A {
/**
* Create swing component based on the concret class
* In this example class A should create a jTextField with a jLable "Cities". So I have not to create each component manuel,
* If the text field changed the attribute for cities should set (My idea was with propertyChangesSupport).
*/
}
public class B extends A {
@Property(name = "Cities", PropertyType.Textfield, description = "Number of cities")
private int cities;
}
In terms of actually getting the annotations into the layout, you could do something like this:
public class A extends JPanel {
A() {
this.setLayout(new FlowLayout());
addProperties();
}
private void addProperties() {
for (Field field : this.getClass().getDeclaredFields()) {
Property prop = field.getAnnotation(Property.class);
if (prop != null) {
createAndAddComponents(prop);
}
}
}
private void createAndAddComponents(Property prop) {
JLabel jLabel = new JLabel(prop.name());
this.add(jLabel);
switch (prop.type()) {
case Textfield:
JTextField jTextField = new JTextField();
this.add(jTextField);
case ...
...
}
}
}
Of course, that won't look fancy because I'm using FlowLayout
to avoid clutter in the example. For the value of the JTextField
, I'm not sure what direction you want to take. You could add DocumentListener
s that will directly update the related Field
object or something, but I'm not sure what the requirements are. Here's one idea:
jTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
try {
int value = Integer.parseInt(jTextField.getText());
field.setInt(A.this, value);
} catch (NumberFormatException e1) {
} catch (IllegalArgumentException e1) {
} catch (IllegalAccessException e1) {
}
}
......
});
And you could also do some PropertyChangeSupport in there when you set the value.
I made a runnable example using GroupLayout and PropertyChangeSupport that actually looks okay...
The code is somewhat messy (particularly my use of GroupLayout, sorry), but here it is in case you need something to look at.. I hope I'm not making this too cluttered...
public class B extends A {
@Property(name = "Cities", type = PropertyType.Textfield, description = "Number of cities")
private int cities;
@Property(name = "Cool Cities", type = PropertyType.Textfield, description = "Number of cool cities")
private int coolCities;
public static void main(String[] args) {
final B b = new B();
b.addPropertyChangeListener("cities", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println("Cities: " + b.cities);
}
});
b.addPropertyChangeListener("coolCities", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
System.out.println("Cool Cities: " + b.coolCities);
}
});
JFrame frame = new JFrame();
frame.add(b);
frame.setVisible(true);
}
}
public class A extends JPanel {
//Need this retention policy, otherwise the annotations aren't available at runtime:
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
String name();
PropertyType type();
String description();
}
public enum PropertyType {
Textfield
}
A() {
addProperties();
}
private void addProperties() {
GroupLayout layout = new GroupLayout(this);
this.setLayout(layout);
layout.setAutoCreateContainerGaps(true);
layout.setAutoCreateGaps(true);
Group column1 = layout.createParallelGroup();
Group column2 = layout.createParallelGroup();
Group vertical = layout.createSequentialGroup();
for (Field field : this.getClass().getDeclaredFields()) {
field.setAccessible(true); // only needed for setting the value.
Property prop = field.getAnnotation(Property.class);
if (prop != null) {
Group row = layout.createParallelGroup();
createAndAddComponents( prop, column1, column2, row, field );
vertical.addGroup(row);
}
}
Group horizontal = layout.createSequentialGroup();
horizontal.addGroup(column1);
horizontal.addGroup(column2);
layout.setHorizontalGroup(horizontal);
layout.setVerticalGroup(vertical);
}
private void createAndAddComponents(Property prop, Group column1, Group column2, Group vertical, final Field field) {
JLabel jLabel = new JLabel(prop.name() + " (" + prop.description() + ")");
column1.addComponent(jLabel);
vertical.addComponent(jLabel);
switch (prop.type()) {
case Textfield:
final JTextField jTextField = new JTextField();
jTextField.getDocument().addDocumentListener(new DocumentListener() {
@Override
public void insertUpdate(DocumentEvent e) {
updateValue();
}
@Override
public void changedUpdate(DocumentEvent e) {
updateValue();
}
@Override
public void removeUpdate(DocumentEvent e) {
updateValue();
}
private void updateValue() {
try {
int value = Integer.parseInt(jTextField.getText());
field.setInt(A.this, value);
firePropertyChange(field.getName(), "figure out old", value);
} catch (NumberFormatException e1) {
} catch (IllegalArgumentException e1) {
} catch (IllegalAccessException e1) {
}
}
});
column2.addComponent(jTextField);
vertical.addComponent(jTextField, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE, GroupLayout.PREFERRED_SIZE);
}
}
}
Hope this helps!
In my opinion (I know I shouldn't write opinions but only facts on stackoverflow answers, but here the question is "what do you think about it?"... :-) ) in this solution you are mixing Model and View information in class B (particularly having that PropertyType.Textfield in your annotation). I know MVC pattern is not a law (even Android Adapter breaks MVC contract and it's ok anyway :-) ), but it is generally considered good practice.
If I'm getting you right you have class A that provides the view (swing components) for class B. So A is a view and B is a model. For this reason I wouldn't have B to extend A because it breaks the "IS A" relationship (B extends A means "B is a A" and in your case this would mean B is a view while it is not).
As a different option, did you consider investigating an approach based on JavaBeans and PropertyEditors? it should be more in line with the regular management of "runtime-loaded" components in swing. See http://docs.oracle.com/javase/tutorial/javabeans/advanced/customization.html
I wrote a simple example just to show you what I mean:
Here is my class B:
import java.awt.Color;
public class B {
private int x = 0;
private Color c = Color.BLUE;
private Address address;
public B() {
address = new Address();
address.setCity("Liverpool");
address.setState("England");
address.setStreet("Penny Lane");
address.setNumber("1");
address.setZip("12345");
}
public int getX() {
return x;
}
public void setX(int x) {
this.x = x;
}
public Color getC() {
return c;
}
public void setC(Color c) {
this.c = c;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
And this is my test GUI:
import java.awt.GridLayout;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JTextField;
public class Test extends JFrame {
/**
*
*/
private static final long serialVersionUID = 1L;
public Test(Object b) {
super("Test");
initGUI(b);
}
private void initGUI(Object b) {
Class<?> c = b.getClass();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridLayout(0, 2));
try {
BeanInfo info = Introspector.getBeanInfo(c);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
try {
Method readMethod = pd.getReadMethod();
Object val = readMethod.invoke(b, new Object[0]);
// Or you may want to access the field directly... but it has to be not-private
// Field f = c.getDeclaredField(pd.getName());
// Object val = f.get(b);
//
// You can use annotations to filter... for instance just consider annotated
// fields/methods and discard the others...
// if (f.isAnnotationPresent(Property.class)) {
// ...
java.beans.PropertyEditor editor = java.beans.PropertyEditorManager.findEditor(val.getClass());
if (editor != null) {
editor.setValue(val);
add(new JLabel(pd.getDisplayName()));
if (editor.supportsCustomEditor()) {
add(editor.getCustomEditor());
} else {
String[] tags = editor.getTags();
if (tags != null) {
add(new JComboBox<String>(tags));
} else {
if (editor.getAsText() != null) {
add(new JTextField(editor.getAsText()));
}else{
add(new JPanel());
}
}
}
}
} catch (SecurityException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (InvocationTargetException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
} catch (IntrospectionException ex) {
ex.printStackTrace();
}
}
public static void main(String[] args) {
B b = new B();
Test t = new Test(b);
t.pack();
t.setVisible(true);
}
}
PropertyEditorManager provides a default editor for int and for Color. What about my class Address?
public class Address {
private String city;
private String state;
private String zip;
private String street;
private String number;
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
}
I provide a custom editor for class Address (PropertyEditorManager uses different strategies to discover class editors... I used naming convention) This is my AddressEditor class:
import java.awt.Component;
import java.awt.FlowLayout;
import java.beans.PropertyEditorSupport;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class AddressEditor extends PropertyEditorSupport {
@Override
public boolean supportsCustomEditor() {
return true;
}
@Override
public Component getCustomEditor() {
Address value = (Address) getValue();
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
panel.add(new JTextField(value.getStreet()));
panel.add(new JTextField(value.getNumber()));
panel.add(new JTextField(value.getCity()));
panel.add(new JTextField(value.getState()));
panel.add(new JTextField(value.getZip()));
return panel;
}
}
This way B (and also Address) are just models and editors are provided to customize the views.
Well... this was just to respond to your question about other options... I hope I didn't go too much out of track... :-)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With