Thursday, January 15, 2009

Swing UI layout: best practices

Today, I will show my best practices about designing Swing UI forms.

This post focuses exclusively on UI layout, not on other -common- UI problems such as MVC, binding, actions... I may post about all these in future posts however.

This is quite a long post, but this is partly due to screenshots showing do's and don'ts.

The best practices exposed here use my DesignGridLayout as an example, but most of them (if not all) should be suitable for most modern LayoutManagers (like GroupLayout) and even some old-fashioned ones (like GridBagLayout).

For each practice, I'll show screenshots before/after the practice along with code snippets.

Code presented here works with Java 5 (baseline alignment may require the additional swing-layout library) and Java 6.

1. Always use Baseline alignment for components that have a meaningful baseline
Most modern LayoutManagers have an option to align components in a row on their baselines.
DesignGridLayout gives you no choice: baseline alignment is automatic (and cannot be disabled).

Without baseline alignment

With baseline alignment


2. Avoid using JEditorPane and JTextPane
Most Swing components in Java 6 have a meaningful baseline. However, some seem to have no correct baseline (for no apparent good reason). JEditorPane and JTextPane are examples of such components. This means that it is impossible to have these components aligned on their baseline.
Thus, when using those components, you don't know exactly how they will be aligned with other components in the same row; this will depend on the LayoutManager you use.
DesignGridLayout aligns these components on the top of their "box", which is not very beautiful, but nothing better is possible until those components are able to return a decent baseline value.

Screenshot sample

Hence the best is to not use such components at all if possible. You should prefer JTextArea when it fits your needs (no need for rich text style).

3. Call setColumns() on JTextField and JTextArea
Most LayoutManagers use (or can use) components preferred size to perform an optimal layout.
In Swing, many components are able to determine the optimal preferred width based on their content. For instance, JLabel, JButton, JList, JTable belong to this category and for these you don't need to explicitly set the preferred width.
For JTextField and JTextArea, however, this is not the case, by calling setColumns() (or using the constructor that takes a columns argument), you make sure these components will have the right preferred width.

Without setColumns()

With setColumns(10)

4. Put all components that can vary in height in a JScrollPane
Some components allow you to display several "lines" of information (JTable, JList, JTextArea...) In most cases, it is impossible to know exactly how many lines will be displayed. Hence by putting those components in a JScrollPane you make sure the user will be able to vertically scroll to see all available data.
In addition, some components (such JTextArea) were specifically coded to be embedded in a JScrollPane, if you don't, they will look ugly (e.g. no border).

Without JScrollPane

With JScrollPane

5. Call setRows() on JTextArea
Like setColumns(), it will help optimize the preferred height for the component (by default its preferred height is equivalent to the height of 1 line of text).

Without setRows()
Note how the JTextArea looks like a simple JTextField!

With setRows(4)

6. Call setVisibleRowCount() on JList
As for JTextArea where you should call setRows(), JList has a useful method that enables you to set the preferred number of rows to be visible, which will then automatically compute the preferred height (depending on the actual content of those rows).

Without setVisibleRowCount()

With setVisibleRowCount(4)

7. Call setPreferredScrollableViewportSize() on JTable
Unfortunately, JTable does not have a setVisibleRowCount() method as in JList. Hence, you need to find another way to set a preferred size (in terms of number of rows) to the JTable but avoid that it displays "partial" rows. The default JTable preferred height shows 20 rows (independently of the actual number of rows in the model) which is generally more than what you would want to show.

The practice I show here has worked quite well for me:
static public void setTableHeight(JTable table, int rows)
{
int width = table.getPreferredSize().width;
int height = rows * table.getRowHeight();
table.setPreferredScrollableViewportSize(new Dimension(width, height));
}

Without setPreferredScrollableViewportSize()

With setPreferredScrollableViewportSize()

8. Force minimum width on JTextField
In best practice 3 above, I have shown how to set a correct preferred width for JTextField, this will allow the LayoutManager to show the panel in its preferred size with correct sizes for all fields. However, when resizing the panel, the minimum size is generally used by the LayoutManager to make sure that components never shrink smaller than this minimum size.

Unfortunately, JTextField minimum width is meaningless and generally needs to be set by hand to avoid ridiculously small fields when the user shrinks the panel width.

However, you should always avoid setting sizes in pixels to avoid bad layouts on different kinds of monitors (you should strive to be resolution independent so that your UI will look good on low and high DPI screens).

What I do is to use setColumns() again but as an intermediate step to setting the minimum width:
static public final void setTextField(JTextField field, int min, int pref)
{
field.setColumns(min);
field.setMinimumSize(field.getPreferredSize());
if (pref != min)
{
field.setColumns(pref);
}
}

Before (trying to shrink width as much as possible)

After

9. Don't use TitledBorder to separate groups of information
A lot of people use Swing TitledBorder around several sub-panels in order to separate groups of information inside a form. The major problem with this approach is that every sub-panel has its own LayoutManager, and LayoutManagers are disconnected from each other, hence you are likely to have bad alignment between sub-panels:


If you follow Karsten Lentszch's advice, you could use a JLabel and a JSeparator instead:


Here is how you can do it with DesignGridLayout:
_lblInfo.setForeground(Color.BLUE);
layout.row().left().fill().add(_lblInfo, new JSeparator());
layout.row().grid(_lblFirstName).add(_firstName);
layout.row().grid(_lblSurname).add(_surname);

layout.emptyRow();
_lblOffice.setForeground(Color.BLUE);
layout.row().left().fill().add(_lblOffice, new JSeparator());
layout.row().grid(_lblCompany).add(_company);
layout.row().grid(_lblAddress).add(_address);
layout.row().grid(_lblZip).add(_zip);
layout.row().grid(_lblCity).add(_city);

10. Set consistent sizes for all JButtons in a row
Swing automatically calculates JButton preferred size based on its content (text, icon). However, this means that all buttons in your form will have a different width!
Depending on the LayoutManager you use, you may have to individually set the preferred sizes of all buttons, based on the preferred size of the largest one.

Most modern LayoutManagers will do that for you, though. Here is an example with DesignGridLayout:
layout.row().center().add(new JButton("OK"), new JButton("Cancel"));


11. Special considerations for components spanning several rows
Some LayoutManagers (including DesignGridLayout) allow you to define components (like JList or JTable) to span several rows.

When I use such components, I make sure that their preferred height (which defines the height of the JScrollPane in which they will be embedded) is larger than the total height of the rows that are spanned. Why so? Just a matter of taste. See for yourself:

With height smaller than spanned rows

With height larger than spanned rows

Conclusion

For some of these best practices, it may prove useful to create a class with a few helper methods (such as setTableHeight() and setTextField() above) or create a factory for your components.

If you follow those best practices, you should achieve a better user experience in your UI forms. DesignGridLayout, if you use it, will take advantage of these best practices in an effective way.

Hope that this can be useful to all Swing developers. Any comments are welcome.

Wednesday, January 07, 2009

Announce: DesignGridLayout 1.1-rc1 released!

After one month of heavy work, I am proud to announce the first release candidate of DesignGridLayout 1.1.

This version brings one major new feature and fixes a few bugs:
  • new support for components to span several rows (RFE #10)
  • fixed problems with baseline alignment in JRE5 (issues #3 and #27)
  • fixed a problem with smart vertical resize of JList (issue #28)
  • fixed a potential exception that could occur in very specific layouts (issue #26)
In addition, the examples demo application has been completely rewritten in order to show all DesignGridLayout features along with description and source code. This application can now constitute a very effective way to learn DesignGridLayout from scratch in no time. It is also useful to current DesignGridLayout users who want to learn new features.

The new support for components spanning multiple rows allows you to define layouts that look like this:


The source code for that is quite straightforward:
layout.row().grid(label1).add(field1).grid(label2).add(list);
layout.row().grid(label3).add(field3).grid().spanRow();
layout.row().center().add(button);
It is important to notice that "smart vertical resize", one of DesignGridLayout unique features, is still active on components spanning multiple rows. You can see on the following screenshots the same layout as above during vertical resize (note the list always show only entire rows and never truncates any row):





Of course, you can also see this behavior "live" if you launch the examples application!

I consider this release candidate to be ready for production and, if no bugs are reported, I expect a final release in less than one month.

Enjoy!

Friday, January 02, 2009

My resolutions for 2009

Nowadays almost everybody feels the urge to claim their private resolutions for the new year that has just started.

Although I don't really feel with the same urge, I think expressing my own resolutions for 2009 in this blog will put some pressure on me to try my best following those resolutions, else one could remind these to me in one year ;-). I could also refer to this post myself to remind me what I promised myself

So here they are (in no particular order):
  • release 2 versions of DesignGridLayout (1.1 and 1.2)
  • release -finally- my guice-gui framework
  • learn how to play the guitar at least 15 minutes per day
  • learn and play with groovy (buy and read "Groovy in Action")
  • play with grails (and write some new Open Source project with it)
  • play with griffon (and see how I could possibly integrate DesignGridLayout to it;-))
  • read "Programming in Scala"
  • spend more time with my family (this resolution is quite incompatible with all others...)
  • resume my work on HiveBoard and release one new version during the year
  • don't ever download JavaFx or even mention it in any of my blog posts (damn I have just broken that resolution, OK, that's the last one)
From this, you can guess more or less where I am heading for this year.

Thursday, January 01, 2009

DesignGridLayout: row-span support soon ready!

December 2008 has been a busy month for me on DesignGridLayout.

Although not yet completely ready for a first 1.1 release candidate, the current Subversion trunk already includes some interesting changes:
  • added support for components spanning multiple rows (issue #10): this is further discussed below
  • fixed several problems with baseline alignment (only on Java 5): issues #3 and #27
  • fixed a problem with smart vertical resize of JList (issue #28)
  • fixed an exception with multi-grid feature (issue #26)
  • optimized the size of the example jar (removed all screenshots that are used during automatic tests)
  • completely refactored the examples application: see below

New showcase application

Much of my past efforts on DesignGridLayout were spent on reworking the original examples application which was overly simplistic and not a very good way to "sell" DesignGridLayout. Hence I have written from scratch a new showdown application that:
  • demonstrates all DesignGridLayout features from the basics to the more advanced uses (arranged in a tutorial-oriented way)
  • describes each feature
  • always shows the relevant source code that produces every sample
  • allows you to launch the layout in a separate frame in order to test its resize behavior
  • asks you to choose your preferred Look & Feel so that you are not limited to the old-fashioned default Metal look & feel
Here is a screenshot of this new showdown application (with Windows XP look & feel):


Now the good news is that, although there is no official release of this new DesignGridLayout application, you can already launch it today! Just click the jnlp link below:


Multiple row span

This is the main improvement to the future DesignGridLayout 1.1.
I had to slightly change the API (completely backward compatible with 1.0), as described in one previous post on this blog. As a reminder, here is a simple example:
layout.row().grid(label("lbl11")).add(field("field11")).grid(label("lbl12")).add(list());
layout.row().grid(label("lbl21")).add(field("field21")).grid().spanRow();
layout.row().center().add(button());

Note the spanRow() call on the second line of code. This states that "in this position of the current row, span the component that is in the same position on the row above".

The result looks like this:


Please note there's a catch in this snapshot: the list size does not obey the "smart vertical resize" feature of DesignGridLayout (first introduced here). That's one open point in the current source code.

This open point is to decide whether the layout should keep the "smart height" for a multi row span component or not:
  • if we don't keep it, then at preferred size, the bottom border of the list is aligned with the bottom border of the field (example above)
  • if we keep it, then there won't be such alignment, which may look quite ugly as in the snapshot below


I am currently inferring about what to do here. I wonder if this should be left as an option (new API) to the end user. Among the available options:
  1. disable smart vertical size for multi row span components (current situation)
  2. enable smart height with loss of bottom border alignment (as in snapshot above)
  3. enable smart height but keep bottom border alignment (or baseline alignment with the last visible row of the list). This means that there will be a strange vertical spacing between first and second field. I am not even sure that this is feasible actually, I'll need to investigate further.
Additional decisions would concern the form of the API to select the behavior, in particular, I have to find out a light API (that does not make the current one heavier) and which granularity to give this API, more precisely, at which level the behavior should be selectable: the whole layout, each row, or every single multi row span component? The finer the level, the heavier the API, the more complex it is to use...

The behavior of spanRow() is described in the future 1.1 javadoc (not released, but you can already build it from the Subversion trunk).

A second catch (not visible in the example above) with the "spanRow()" API is that, unfortunately, it is now possible to code (and compile) a layout that cannot be rendered at runtime, because it just makes no sense, as in the following example:
// spanrow() called on a subgrid with different number of columns
layout.row().grid(label("lbl1")).add(field("field1"), field("field2"), field("field3"));
layout.row().grid(label("lbl4")).add(field("field4")).spanRow();

The spanRow() call on the second row does not match a single component of the first row, but there are 2 candidate components to be spanned: field2 and field3.

Since there is no way to discover this problem at compile-time, DesignGridLayout performs the check at runtime and will replace each incorrect spanRow() call with a special "marker" component (with a tooltip that gives further information) to show that the original source code has to be reworked.

The following screenshot shows many examples of incorrect API usage:


This screen is also available from the showdown application.

Roadmap

Once I can progress on the open point presented above, I will be able to propose a first release candidate of version 1.1. That should be ready by mid-january hopefully. The final 1.1 version will be released 3-4 weeks after the latest stable (no open bug) release candidate.

Enjoy!