SwingBuilder for creating Swing GUIs in Groovy - provides DSL for declarative UI construction
—
Groovy Swing provides a comprehensive data binding system that connects UI components to data models automatically, enabling declarative data synchronization.
The binding system is built around ValueModel interfaces and binding factories.
// Binding factories
def bind(Map attributes = [:])
def bindGroup(Map attributes = [:])
def bindProxy(Map attributes = [:])The foundation of the binding system for representing bindable values.
interface ValueModel {
Object getValue()
void setValue(Object value)
Class getType()
boolean isEditable()
void addValueChangeListener(PropertyChangeListener listener)
void removeValueChangeListener(PropertyChangeListener listener)
}Concrete implementations of ValueModel for different data scenarios.
// Basic value holder
class ValueHolder implements ValueModel {
ValueHolder()
ValueHolder(Object value)
ValueHolder(Object value, Class type)
}
// Property-based model
class PropertyModel implements ValueModel {
PropertyModel(Object bean, String propertyName)
}
// Nested property model
class NestedValueModel implements ValueModel {
NestedValueModel(ValueModel parentModel, String childPropertyName)
}
// Closure-based model
class ClosureModel implements ValueModel {
ClosureModel(Closure readClosure)
ClosureModel(Closure readClosure, Closure writeClosure)
}
// Form data model
class FormModel {
FormModel()
FormModel(Map initialValues)
ValueModel getValueModel(String propertyName)
void setProperty(String name, Object value)
Object getProperty(String name)
}// Basic value holder
def nameModel = new ValueHolder('John Doe', String)
nameModel.setValue('Jane Smith')
println nameModel.getValue() // 'Jane Smith'
// Property model for bean binding
class Person {
String name
int age
}
def person = new Person(name: 'Alice', age: 30)
def nameModel = new PropertyModel(person, 'name')
def ageModel = new PropertyModel(person, 'age')
// Nested property model
def addressModel = new PropertyModel(person, 'address')
def cityModel = new NestedValueModel(addressModel, 'city')
// Closure-based model
def dynamicModel = new ClosureModel(
{ -> person.name.toUpperCase() }, // Read closure
{ value -> person.name = value.toLowerCase() } // Write closure
)
// Form model
def formModel = new FormModel([
firstName: 'John',
lastName: 'Doe',
email: 'john@example.com'
])Factory methods for creating different types of data bindings.
// Simple property binding
def bind(Map attributes)
// Common attributes:
// source: Object - source object
// sourceProperty: String - property name on source
// target: Object - target object (optional, defaults to component)
// targetProperty: String - property name on target (optional)
// converter: Closure - value converter (optional)
// reverseConverter: Closure - reverse converter (optional)
// validator: Closure - validation closure (optional)def swing = new SwingBuilder()
def person = new Person(name: 'John', age: 25, email: 'john@example.com')
swing.frame(title: 'Data Binding Demo') {
panel {
gridBagLayout()
// Simple text field binding
label(text: 'Name:', constraints: gbc(gridx: 0, gridy: 0))
textField(
columns: 20,
text: bind(source: person, sourceProperty: 'name'),
constraints: gbc(gridx: 1, gridy: 0)
)
// Binding with converter
label(text: 'Age:', constraints: gbc(gridx: 0, gridy: 1))
textField(
columns: 20,
text: bind(
source: person,
sourceProperty: 'age',
converter: { it.toString() },
reverseConverter: { Integer.parseInt(it) }
),
constraints: gbc(gridx: 1, gridy: 1)
)
// Binding with validation
label(text: 'Email:', constraints: gbc(gridx: 0, gridy: 2))
textField(
columns: 20,
text: bind(
source: person,
sourceProperty: 'email',
validator: { it?.contains('@') }
),
constraints: gbc(gridx: 1, gridy: 2)
)
// Checkbox binding
checkBox(
text: 'Is Active',
selected: bind(source: person, sourceProperty: 'active'),
constraints: gbc(gridx: 1, gridy: 3)
)
}
}Binding groups for managing multiple related bindings.
def bindGroup(Map attributes = [:])
// BindGroup methods:
// bind() - Activate all bindings in group
// unbind() - Deactivate all bindings in group
// update() - Update all bindings in group
// reverseUpdate() - Reverse update all bindingsswing.frame(title: 'Binding Group Demo') {
def person = new Person()
// Create binding group
def personBindings = bindGroup()
panel {
gridLayout(rows: 4, columns: 2)
label(text: 'First Name:')
textField(text: bind(group: personBindings, source: person, sourceProperty: 'firstName'))
label(text: 'Last Name:')
textField(text: bind(group: personBindings, source: person, sourceProperty: 'lastName'))
label(text: 'Age:')
spinner(
value: bind(group: personBindings, source: person, sourceProperty: 'age'),
model: spinnerNumberModel(minimum: 0, maximum: 120)
)
button(text: 'Update All') {
actionPerformed {
personBindings.update()
}
}
button(text: 'Reset Form') {
actionPerformed {
personBindings.reverseUpdate()
}
}
}
}Proxy objects for advanced binding scenarios.
def bindProxy(Map attributes = [:])
// BindingProxy provides:
// - Delayed binding resolution
// - Dynamic target switching
// - Conditional binding activation// List binding with selection
swing.frame(title: 'List Binding') {
def items = ['Item 1', 'Item 2', 'Item 3'] as ObservableList
def selectedItem = new ValueHolder()
splitPane {
// List with items binding
scrollPane(constraints: 'left') {
list(
listData: bind(source: items),
selectedValue: bind(source: selectedItem, sourceProperty: 'value')
)
}
// Detail panel bound to selection
panel(constraints: 'right') {
label(text: bind(
source: selectedItem,
sourceProperty: 'value',
converter: { "Selected: $it" }
))
}
}
}
// Table binding with row selection
swing.frame(title: 'Table Binding') {
def tableData = [
[name: 'John', age: 30],
[name: 'Jane', age: 25]
] as ObservableList
def selectedRow = new ValueHolder()
table(
model: bind(source: tableData),
selectedElement: bind(source: selectedRow, sourceProperty: 'value')
) {
propertyColumn(header: 'Name', propertyName: 'name')
propertyColumn(header: 'Age', propertyName: 'age')
}
}Different components support different binding properties through specialized property classes.
// AbstractButton binding properties
// - selected, text, icon, enabled, visible
// JTextField binding properties
// - text, enabled, editable, visible
// JList binding properties
// - listData, selectedIndex, selectedValue, selectedValues
// JTable binding properties
// - model, selectedRow, selectedColumn, selectedElement
// JComboBox binding properties
// - items, selectedItem, selectedIndex
// JSpinner binding properties
// - value, enabled
// JSlider binding properties
// - value, minimum, maximum, enabled
// JProgressBar binding properties
// - value, minimum, maximum, string, stringPainteddef model = new FormModel([
items: ['A', 'B', 'C'],
selectedItem: 'B',
percentage: 50,
description: 'Sample text'
])
swing.panel {
vbox {
// Combo box binding
comboBox(
items: bind(source: model, sourceProperty: 'items'),
selectedItem: bind(source: model, sourceProperty: 'selectedItem')
)
// Slider with progress bar
slider(
minimum: 0, maximum: 100,
value: bind(source: model, sourceProperty: 'percentage')
)
progressBar(
minimum: 0, maximum: 100,
value: bind(source: model, sourceProperty: 'percentage'),
stringPainted: true
)
// Text area binding
scrollPane {
textArea(
rows: 5, columns: 30,
text: bind(source: model, sourceProperty: 'description')
)
}
}
}Understanding binding activation and cleanup.
// Binding lifecycle methods
// bind() - Activate binding and start synchronization
// unbind() - Deactivate binding and stop synchronization
// update() - Force immediate source-to-target update
// reverseUpdate() - Force immediate target-to-source update// Creating and managing bindings manually
def nameBinding = swing.bind(
source: person,
sourceProperty: 'name',
target: textField,
targetProperty: 'text'
)
// Activate binding
nameBinding.bind()
// Update from source
nameBinding.update()
// Update from target
nameBinding.reverseUpdate()
// Deactivate when done
nameBinding.unbind()Install with Tessl CLI
npx tessl i tessl/maven-org-apache-groovy--groovy-swing