Wednesday, May 18, 2011

 

Desktop Java GUI programming made easy -- An amateur's learning path on Groovy SwingBuilder and Miglayout - part 3

Solve the puzzle


The thread in the mailing list is very intrigue. Actually with current Groovy 1.8.0 distribution, the original poster should already have the right answer. The question was posted on 2009, so it’s possible that it didn’t work at that time, but worked now with some Groovy updates. No matter what, I’ll try to explain everything based on current Groovy status.

The question myself have actually is different from the post in mailing list. My original question is how to use Miglayout with SwingBuilder. The normal answer should be: just like any other layout. Normally the layout will be a value for named parameter inside the container (), like this:

 
         panel ( layout : new BorderLayout ( ) ) {
           label ( constraints : BorderLayout.NORTH ,  text : SU.isEventDispatchThread ( ) ? 'EDT-friendly' : 'Deadlock-friendly' )
           button ( constraints : BorderLayout.CENTER , text : 'hello', actionPerformed : {
               println ( 'button pushed' )
             }) 
         }

 

If the component need some layout constraints, the constraints will go to the component line just like code above. So a MigLayout example will be

 

swing.build {

frame(pack: true, show: true, defaultCloseOperation: WC.EXIT_ON_CLOSE, layout: new MigLayout("fill")) {

panel() {

label(text: "Configuration", constraints: "")

}

}

}

 

It’s same with the example of BorderLayout. I call this method “use layout as parameter”.

There is also another method available, which write layout into an independent line inside the builder process, so the layout line was processed by builder just like any other component. This time the layout name should be registered and recognized by SwingBuilder. You also need to use named parameters in this method, just like any component. I call this “use layout as a line”.

Let’s have a look at the original question in the mailing list:

I would like to use other layout managers with the SwingBuilder as easily as for example the GridLayout:
 
SwingBuilder.build {
frame(title:'test frame', pack:true, show:true) {
gridLayout(cols:2, rows:3)
label('hello world')
/* other components ... */
}
}
 
But with MigLayout I need to write something like this:
SwingBuilder.build {
frame(title:'test frame', pack:true, show:true, layout: new MigLayout('ins dialog', '[][]', '')) {
label('hello world')
/* other components ... */
}
}

I already tried it with SwingBuilder.registerFactory('migLayout', new LayoutFactory(MigLayout)) but I had no success.

Could anyone please help me how to get other layouts the groovy way?

The first example with gridLayout used the method of “use layout as a line”. He already has Miglayout worked with method “use layout as parameter”, but he want to use it with the method of “use layout as a line”.

The answer 1 said he needs to write a Factory. But answer 2 said he can just register a bean factory. This method did work, like following

import groovy.swing.SwingBuilder

import net.miginfocom.swing.MigLayout

import groovy.swing.factory.LayoutFactory

import javax.swing.WindowConstants as WC

def swing = new SwingBuilder()

swingBuilder.registerBeanFactory('migLayout', MigLayout)

swing.build {

frame(id: 'eventScrollPanel', pack: true, show: true, defaultCloseOperation: WC.EXIT_ON_CLOSE) {

migLayout(layoutConstraints:"fill,debug", columnConstraints:"", rowConstraints:"")

panel() {

label(text:"test")

}

}

}

What happened here? Line 2 generated a LayoutFactory from MigLayout, registered that factory as migLayout. After this migLayout can be used just like gridLayout inside the build process. It will be recognized as a layout Object, just like gridLayout.

The answer 3 used another method to register factory, which didn’t use Bean factory but regular Factory, and Layout Factory was used to convert MigLayout. This is also the method the original poster tried but failed, which should work actually. We only need to replace line 2 with

swing.registerBeanFactory('migLayout', MigLayout)

But the example provided in answer 2 will not work

SwingBuilder seingBuilder = new SwingBuilder
swing.registerFactory('migLayout', new LayoutFactory(MigLayout))


swingBuilder.build {
frame(title:'test frame', pack:true, show:true,
layout: new migLayout(layoutConstraints:'ins dialog',
columnConstraints:'[][]', rowConstraints:'')) {
label('hello world')
}

You either use layout as a parameter, which requires

1. Use original layout constructor and parameters, no named parameters

2. Put layout as a value of named parameter “layout”, inside the parameters () of a container

Or use layout as a line, which requires

1. Use registered factory name, not the original constructor. Also use named parameters.

2. Put the line inside builder as an independent line.

The example code in answer 2 mixed the two methods so will not work.

First, new migLayout() will not work because there is no migLayout class. If you are going to use migLayout, you have to take the method of use layout as line. If are going to put layout in parameters, then you have to use MigLayout. And you don’t need the line of registerFactory too.

In summary, I prefer the method “use layout in parameter”. Because

1. No need to register factory, and no need to use named parameters, which are rather long for MigLayout.

2. If you put layout as a line into a container’s {}, it’s not guaranteed that all the components in that container will be after this line. What will happen if a component was declared before the layout line? Better go with another method to avoid this problem.

The widget() usage

If you remember, the blog “Groovy Builders, JCR (and MigLayout to the rescue)” used widget in his code, what’s that for? Although we talked about the usage of widget, his usage is not exactly same, because it was not used in a customized component:

def queryTextField = swing.textArea(rows: 5, columns: 20)

queryTextField.text = "/jcr:root/blueprint/element(*,nt:unstructured)"

def gui = swing.frame(title: 'JCRQuery', defaultCloseOperation: WC.EXIT_ON_CLOSE) {

panel(layout: new MigLayout("fill")) {

widget(queryTextField, constraints: 'grow')

button(text: 'Query', constraints: 'span, aligny bottom', actionPerformed: {

resultTextField.text = queryAction(queryTextField.text, valueTextField.text)

})

Here the queryTextField is a regular textArea. But it was built outside of frame first. I noticed it was still built by SwingBuilder methods. So when he need to put the component into the layout, he cannot use the regular syntax, because the component is already there, how can you declare the same component again? He also cannot use the variable name “queryTextField” inside the frame directly, because it is not a registered component. So he used widget to pass through the object and parameter, just pretend it was a customized component, which did work.

Of course, if there is no special reason, the line about “queryTextField” can be put inside the builder process, like this:

def gui = swing.frame(title: 'JCRQuery', defaultCloseOperation: WC.EXIT_ON_CLOSE) {

panel(layout: new MigLayout("fill")) {

textArea (id:”queryTextField”, constraints: 'grow')

queryTextField.text = "/jcr:root/blueprint/element(*,nt:unstructured)"

button(text: 'Query', constraints: 'span, aligny bottom', actionPerformed: {

resultTextField.text = queryAction(queryTextField.text, valueTextField.text)

})

We added an id parameter so that we can reference the component. The queryTextField.text line actually can be put inside the parameter field, but we can write it independently to show any other operations you need for queryTextField can be done this way. Of course, in some cases the operations better be done outside the builder process, then you still need the widget method.

Nested MigLayout


MigLayout is Grid based. This represents a different approach from many of the Swing Layout. Some people may have the habit to divide a complex UI into sections, put a panel for each section, and then put components into each panel with a special layout for that panel. This divide and conquer method looks intuitive, and it may be the only way to build a complex UI with the limited Swing Layout methods.

However, Miglayout don’t need nested layout to do a complex UI. Most complex Ui can be built without nested layout by grid, cell split and span, flow change etc. This method has many advantages compare to nested layout. Although it’s logical to put separate section into separate panels, but you have no control on the relationships among the components across the panel boundary. User may expect different components in different sections aligned, which is easily achieved with Miglayout’s grid. You can also adjust them by column or row together.

Still, in some case, you may need a nested component, how should you do it?

If the component inside the component don’t need any layout options, for example you want to put a list into a scrollPane. Then you can just write

scrollPane(constraints: "spany 1,wrap,grow"){list(id: 'fontsList', model: fontsModel)}

Here we defined the constraints for the scrollPane, and put the list as the child node of scrollPane with the {} syntax.

I met a combined problem of nesting and outside variable recently. At first I have a list declared outside the builder process, so I used widget method:

def fileList = new JList()

//inside the builder process

widget(fileList, constraints: "hmin 140, spany 5,grow")

Later I found I need to add a scrollPane to this list, how should I write it?

Through experiment, it should be like this

scrollPane(id:'fPane', constraints: "hmin 140, spany 5,grow" ){widget(fileList)}

So the constraints go to scrollPane now, and the list will be inside the closure of the scrollPane, which is wrapped by widget too.

If you do need some control for components inside the container, you can nest the MigLayout like this, make sure you are clear who the constraints’ receiver is.

frame(id: 'eventScrollPanel', pack: true, show: true, defaultCloseOperation: WC.EXIT_ON_CLOSE, layout: new MigLayout("fill", "", "")) {

panel(layout: new MigLayout("fill,debug", "", ""),constraints:"align right") {

label(text:"test",constraints:"align right")

}

}

So we have a top level MigLayout declared in frame, a sublevel MigLayout declared in panel. We wrote the component constraints of panel, which is controlled by the top level layout. We also wrote the component constraints of label, which went to the sub level layout in the panel.

That’s it. I wish my journey could be a little help to you. Any comment is welcome!

Update: I uploaded the source code of my pet project here, you can check them as a reference example if you are interested.


 

Desktop Java GUI programming made easy -- An amateur's learning path on Groovy SwingBuilder and Miglayout - part 2

3. SwingBuilder + MigLayout

The confused searching

Now we are familiar with MigLayout, how can we use it with SwingBuilder?

There are many articles and tutorials about SwingBuilder, but most of them only showed the built-in layout. Griffon has MigLayout plugin so some articles showed MigLayout in Griffon, but that is not what we need. There was this identical question in the mailing list:

how can I register a LayoutManager in SwingBuilder, i. e. MigLayout?

But all the answers make me totally confused:

1.With the exception of JComponents that follow the JavaBean spec, you usually have to write a Factory to allow an object to be used as a DSL inside SwingBuilder.

2.If you can use the layout via no-args constructors, and rely on setters to configure you can just register a bean factory:

SwingBuilder seingBuilder = new SwingBuilder
swingBuilder.registerBeanFactory('MigLayout', MigLayout)

swingBuilder.build {
frame(title:'test frame', pack:true, show:true,
layout: new migLayout(layoutConstraints:'ins dialog',
columnConstraints:'[][]', rowConstraints:'')) {
label('hello world')
}


----------------------------------------------------------------------

3. This works for me

def swing = new SwingBuilder()
swing.registerFactory("MigLayout", new LayoutFactory(MigLayout))

...

scrollPane(id: 'eventScrollPanel') {
panel(id: 'eventPanel') {
MigLayout(layoutConstraints: 'wrap 10', columnConstraints: "[para]0[][100lp, fill][60lp][95lp, fill]")

}

Answer 2 didn't compile because of obvious spelling error. I changed "seingBuilder" to "swingBuilder", then I was told "unable to resolve class MigLayout" for line xxxxx . I changed it to "MigLayout", the code worked. But that means "MigLayout" was never used. I removed the registerBeanFactory line, the code still works! It seemed that there is actually no need for that line. But why would the expert said that?

And, now we know MigLayout can be used in SwingBuilder directly, how can that happen? Obvious some magic happened, but I don't know the trick of the magic!

I almost gave up at that point, until I found this blog

Groovy Builders, JCR (and MigLayout to the rescue)

I didn't try to compile it because there are some libraries unknown for me. However, once I formatted the code in IDEA for better readability, I found a mystery


def queryTextField = swing.textArea(rows: 5, columns: 20)

queryTextField.text = "/jcr:root/blueprint/element(*,nt:unstructured)"

...

def gui = swing.frame(title: 'JCRQuery', defaultCloseOperation: WC.EXIT_ON_CLOSE) {

panel(layout: new MigLayout("fill")) {

widget(queryTextField, constraints: 'grow')

button(text: 'Query', constraints: 'span, aligny bottom', actionPerformed: {

resultTextField.text = queryAction(queryTextField.text, valueTextField.text)

})

What's that "widget"? A google search took me to here, which is the old wiki page on Groovy site

The topic was beyond my interest so I didn't read it before.

What's widget?

"SwingBuilder has two 'magic' elements that pass through the value argument or the named attribute to the parent container.”

Is this the magic I'm looking for?

I took out Groovy in Action and read the SwingBuilder chapter again, this time I have a better picture of SwingBuilder magic. It turned out that I had several misunderstandings on basic concepts, which are not obvious with all the examples I read before, because they are very simple, until the question raised by using MigLayout cannot be answered by these examples!

Once I come back to the basic concepts and read all the examples carefully again, the mystery began to dissolve. I didn’t find the best syntax at once, but the method I found worked, so it kept me going a long way, until now, after more experiments and reading, I think I can answer the questions above thoroughly.


Back to the basics

First, how does SwingBuilder weave its magic?

Let's start from the simplest example of SwingBuilder. I removed any layout from it so we can see the framework itself, while I also kept some minor details neglected by some example, because it's important for practical applications. I actually generated a file template based of this as the base of any Swing program I'll write in future.

(You may note that it is different from many SwingBuilder examples. First it uses build method, which will build the UI inside the Event Dispatch Thread (EDT). I'm not very familar with threading, so I just copy it here and use it as default. The "pack: true, show: true" replaced the "frame.pack():frame.show()" in many examples. Without defaultCloseOperation: WC.DISPOSE_ON_CLOSE, the application will not terminate after you closed the window.)

import groovy.swing.SwingBuilder

import javax.swing.WindowConstants as WC

def swing = new SwingBuilder()

swing.build {

frame(title: "test", pack: true, show: true, defaultCloseOperation: WC.DISPOSE_ON_CLOSE){

label('hello')

}

}


The line of container (like frame, panel, scrollPanel) is often vastly different from line of component (like label, button), and there are several kinds of variations, so I just realized they are essentially quite similar until very recently.

SwingBuilder will build a node tree from the code inside the build section. Every component writes like this


button(parameterName:parameterValue, parameterName:parameterValue, parameterName:parameterValue)

SwingBuilder first checked "button", find that it is a registered component name (see Groovy official website for a full list, most time it is just a variation of the counterpart component name in Swing ). So the builder will generate a component object with corresponding parameters provided, put it as a node into a node tree. The pair of parameterName:parameterValue are named parameters, actually a key:value map which is used a lot in Groovy.

The container will have a line like this



panel(parameterName:parameterValue, parameterName:parameterValue) { some component lines }

It's all the same, just this time any component found inside the {} will be registered as child node of this container in the node tree. So we know in last paragraph, that component will go to its parent container too. Basically SwingBuilder use the closure syntax of {} to create nested objects, which you have to do it manually in Swing programming:

panel = new JPanel();

button = new JButton()

panel.add(button)

We can think the whole {} as a closure work as the last parameter of the container. If you remember, you can write actionPerformed logic inside the component line, which follows the same format:

button(parameterName:parameterValue, actionPerformed:{do something})

So we can see it's still the same syntax. One of the power of Groovy is you can often have many variations for doing one thing, which may confused some people, that's why I only talked with the most complete format of syntax in above examples, instead of the simpler one like these:

button()

button("this is a button")

Yes, it is simple, but what kind of magic was behind? Now you can know that the first one have no parameter, while the second one only have one parameter and it is the default one, so that string will be explained as the text on the button. If you have more parameters, you have to make effort to differentiate them so you write

button(text:"this is a button", actionPerformed:{do something})

So all of above are based on the name of the component is known. What if you have a customized component not registered in SwingBuilder, like "SuperButton" ? If SuperButton is still a subclass of JButton, you can still use the button line, together with the constructor of SuperButton, and the parameters can be understood by button, like this.

button(new SuperButton("custom constructor"), enabled:false)

Although the first parameter is a constructor for component, we can still think it as a parameter in the parameter list, only it is an Object this time.

What if your customized component doesn’t have a matching type? You have widget and container to pack them, make it just looks like other ducks. Actually most customized Swing component should subclass JComponent, and widget will return java.awt.Component. So you can see this is still similar to what happens above.

container(container:new CustomOutlookBar(), constraints:BorderLayout.WEST) {

widget(widget:new MyCustomButton())

}

Currently container and widget have no real difference. They were supposed to match container and component accordingly to give some hint about that.

After these review of basics, what do we have for the questions above?


This page is powered by Blogger. Isn't yours?

Subscribe to Posts [Atom]