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.

18 comments:

  1. Fanatastic... Very Good Article.

    Thanks & God Bless.
    Rajesh

    ReplyDelete
  2. Designing GUIs as HTML tables is lame and a bore.

    ReplyDelete
  3. Hey Anonymous, could you elaborate your comment a little bit?
    In particular, what is the relationship between HTML tables and this post?

    ReplyDelete
  4. Best Tip Ever

    Use Miglayout ;-)

    ReplyDelete
  5. Excellent set of hints. Cool that it's completly layout manager agnostic. When I looked through it, I found that I know all of them, but it costed me hours of debugging and 'change-and-try' cycles. Shame that Swing team never came with such a list. Thanks again.

    ReplyDelete
  6. @Anonymous who said "use MigLayout":
    I don't think that using MigLayout makes these practices irrelevant in any case.
    On the other hand (since you obviously want to push me in that direction), if you use DesignGridLayout, you have other benefits that MigLayout doesn't bring you (smart vertical resize is one of them).

    ReplyDelete
  7. Nice bag of tips - though some of them are tricks ;-) As all tricks are dirty by definition, I wouldn't recommend using them: instead, strive to solve the underlying issues cleanly, f.i. in a custom component zoo.

    One of my pet anti-trick is the use of any setXXSize (with XX min, pref, max, prefScrollable): my advice is to NEVER EVER do it meaning really never never never ...). Doing so drives even well-behaved layout managers nuts. Instead, make the components behave - subclass and implement the corresponding getXXSize reasonably well.

    Shameless plug: SwingX' JXTable has a visibleRowCount property which is used in calculating its prefScrollable :-)

    CU
    Jeanette

    Hmm .. can post as anonymous only? Tried the name/url but wasn't accepted

    ReplyDelete
  8. Hello Jeanette,

    personally, I'm not keen on deriving the whole set of Swing components (I have done that in some past projects and it was quite boring to maintain, not accounting for supporting those components in an IDE... but that was a long time ago, IDE are much better now).

    Thanks for the information about JXTable, I was not aware that it has visibleRowCount. Actually I don't understand why JTable doesn't have it...

    About anonymous posting, well I don't know, I already had comments from named people, maybe blogspot is a little bit picky about which kind of OpenID is accepted (of course, there's no problem with blogspot-provided OpenIDs;-))

    ReplyDelete
  9. Please, do post about Java desktop application architecture problems and best practices (like MVC, binding, actions... ), or if you will, some book recommendations. While for JavaEE we have a gazillion books/sites about best practices and architectures, for JavaSE I just can't find any. All I see are books about making java apps beautifull (like filthy rich clients) and dissecting Swing/SWT components... Maybe I should just use eclipse/NB RCP?

    ReplyDelete
  10. most of it is trivial i think, but it was a good idea to put it all together

    ReplyDelete
  11. @rcoacci: I have no experience with Eclipse/Netbeans RCP, but it looks to me that they contrive you to some kind of GUI (eg, use docking only). In addition, for Eclipse, it seems all apps developed with RCP just "look like" Eclipse, ie for developers not "typical lambda-users".
    If you love spring (which I don't), you may also take a look at spring RCP.

    ReplyDelete
  12. Yes, I agree with you about the RCPs especially the IDE look-alike. For Spring, last I saw it was unmantained. And also seems to be the case of Swing Application Framework.

    ReplyDelete
  13. This comment has been removed by a blog administrator.

    ReplyDelete
  14. This comment has been removed by a blog administrator.

    ReplyDelete
  15. Really good tips.
    You have taken lot of efforts to illustrate each point along with the screenshots. Good work !

    ReplyDelete
  16. @Sindre can you be more specific about what code you need please? I think that this post already provides enough code where needed.

    ReplyDelete
  17. really a very useful article for all of the Swing application developers.
    I want to see more articles like this..........
    this article helps me a lot............
    thanxs dude

    ReplyDelete