A GUI Painter Friendly Table Component

Submitted by gunnar on

In the early days of Java, GUI forms were written, not drawn. They were created by writing code that instantiated components and added them to containers with various layout constraints. Then the program was run and the result could be admired. This way of working, WYGIWYG (what you get is what you get) was often quite fun, more often frustrating, and never very productive. Today we have a JavaBeans specification and integrated development environments (IDEs) with GUI painters. Some of these are doing really good jobs, considering the difficulties with layout managers and platform portability.

With most components, such as text fields and buttons, the principle of dropping them on the form, setting properties, and adding event listeners is quite sufficient. The JTable though is more problematic. It's just too complex to configure with simple property editors and also so common that you don't want to have to write a lot of code every time you use it.

You can drop a table in a JScrollPane and set a lot of properties on the JTable, but when it comes to adding and customizing columns, the GUI painter can't help you since the columns are not JavaBeans. One solution is for the GUI painter to provide an editor for the table model property, thereby letting you define columns and set a few attributes on them. However, I have never seen an editor that will allow you to customize the columns of the table with the same flexibility you have when you customize text fields on a form.

There is, however, a completely different way to go, which is the one I chose for the table component in our own class library DOI, called the DoiTable.

Design Time Behavior

The DoiTable doesn't have a table model property editor at all. In fact, when you drop it on the form it doesn't even look like a table. Instead, it behaves like a container during design time, and you fill it with columns by dropping DoiTableColumn components inside it. At runtime, though, it automatically converts itself to a JTable with all the column properties taken from the design time column components.

Figure 1 shows the design time look of a simple table with four columns. The screenshot is taken from the NetBeans form editor. When the designer drops a table on the form, it appears as a big rectangle. The designer can then give the table a label and activate tools for inserting and deleting rows by setting properties on the table. Note the "Table" label and small tool bar above the rectangle. The rectangle is the drop area for columns. During design time this area is an ordinary panel with a flow layout in which column components can be dropped and reordered. The columns must be instances of the DoiTableColumn class. If you accidentally drop some other type of component inside it, the drop area turns red.

A DoiTableColumn is a direct descendant of the DoiTextField class, which is the standard text field in the DOI library, overridden to change the design time appearance and add some properties and behavior that is specific to a table column. As you can see, I've tried to make the column components look a bit like the columns they will become at runtime. From the GUI painter's point of view, the table is just a container. Therefore, the painter will allow you to set properties on each individual column as if they were ordinary fields on a panel, which is exactly what they are, until you run the application. In Figure 1, one of the columns is selected so you can see the property sheet for it in the lower right pane. Note also that the column components retain their preferred size even if the table is too narrow to show them all on one line. The fourth column, "Logical", doesn't fit, so it's placed on a new row. This behavior is consistent with any other flow layout panel. Although I could have made them resize themselves to mimic the behavior of a JTable more closely, I decided against it to make the columns easier for the designer to work with.

This is basically how the table component presents itself to the designer. To the user, however, it looks just like a JTable in a JScrollPane, as shown in Figure 2. I'll shortly go into the details on how this conversion happens, but first a little bit about how the table component communicates with the GUI painter.

Adjusting the BeanInfo

Every JavaBean component that you can draw on a form must have a supporting BeanInfo object, which is an instance of a class that implements the java.beans.BeanInfo interface. The BeanInfo object is used by the GUI painter to determine which properties and events the bean has. Although it can be created automatically using introspection, it's usually written by the author of the bean. Writing such a class is outside the scope of this article, but there is one important feature that is often forgotten when BeanInfo classes are described: the "container delegate" property. At the time of writing it isn't even mentioned in the Java Tutorial. Without this property, all beans must fall into the following two categories:

  1. Component beans such as JTextField or JButton - you drop them in containers but you don't drop anything inside them.
  2. Simple container beans such as JPanel - they are initially empty and you can drop components inside them.

The GUI painter can tell them apart by treating empty containers as category 2 and all other beans as category 1. The DoiTable, however, falls into a third category. It isn't just a container, but a container that initially has a label, a tool bar, and an inner container for the columns. Without a special "trick" in the BeanInfo class, the GUI painter would think that the DoiTable is an ordinary component because it isn't empty and won't let you drop anything inside it. This is certainly not the behavior we want, so we must inform the GUI painter that it is a container and that it has a special place for dropping stuff. The following code excerpt from the DoiTableBeanInfo class shows how this is done: 

public BeanDescriptor getBeanDescriptor()

{

  BeanDescriptor bd =  new BeanDescriptor(itsBeanClass);

  bd.setName("DoiTable");

  bd.setValue("isContainer", Boolean.TRUE);

  bd.setValue("containerDelegate", "getColumnContainer");

  return bd;

}

The method creates a BeanDescriptor, which is an object that contains basic properties about the bean. While some of these properties have dedicated methods such as setName, others are set using the generic setValue method. In the code above, the property isContainer is set to TRUE to tell the GUI painter that although this bean isn't empty, it is still a container. We also have to tell the GUI painter which method on our bean returns the inner container by setting the property containerDelegate to the name of the method. In the DoiTable case, the method is called getColumnContainer.

Converting to Runtime Behavior

When the application is run we obviously don't want the table to look like it does in the GUI painter. Instead we want the drop area, a.k.a. the column container, to convert itself to a real JTable. This conversion happens in the method addNotify, which is called automatically on every component when it is added to a displayable container. This method may be called several times, so we must make sure the table doesn't attempt to con-vert itself more than once. Also, we don't want it to convert itself at all when we are using the table in the GUI painter. To test for design time or runtime mode, there is a method in the java.beans.Beans class called isDesignTime. This method returns true when called from a com-ponent in a GUI painter, and false otherwise.

The first thing we need to do is implement the addNotify method:

public void addNotify()

{

  super.addNotify();

  commitColumnContainer();

}

The first thing the method does is invoke the same method on the superclass to let it do whatever it needs to do, then it calls the method commitColumnContainer to do the real work. This method looks like:

public void commitColumnContainer()

{

  commitColumnContainer(false);

}

As you can see, it doesn't do much; it just delegates to another method. The reason for this is that the other method has a parameter that allows the caller to force a conversion even if we are in design time. This is useful in certain circumstances, which I'll get back to later. For now we'll look at the first few lines of the "real" commitColumnContainer method:

public void commitColumnContainer(boolean pForce)

{

  if (!pForce && Beans.isDesignTime())

    return;

  if (itsColumnContainer == null)

    return;

The method starts by checking if a conversion should happen at all by testing the force parameter and calling the isDesignTime method. If these tests are passed, it goes on to check if the table has already been converted. The column container panel is created and added to the table by the constructor and removed when the conversion is completed. This means that if it is null, the table is already converted and the method returns immediately. Now the real conversion can be done. We start off by transferring all column beans from the column container into an internal array:

  int ccc =   itsColumnContainer.getComponentCount();

  itsColumns = new DoiTableColumn[ccc];

 for (int i = 0; i < ccc; ++i) {

  DoiTableColumn column = (DoiTableColumn)itsColumnContainer.getComponent(i);

  itsColumns[i] = column;

  column.setTable(this);

}

Each column is given a reference back to the table using the setTable method of the DoiTableColumn class. This reference is used by the column to access various properties on the table that affect its behavior. Now it's time to get rid of the column container and replace it with a scroll pane:

  remove(itsColumnContainer);

  itsColumnContainer = null;

  itsScrollPane = new JScrollPane();

  add(itsScrollPane, BorderLayout.CENTER);

The scroll pane will eventually contain a JTable, but before we can create it we need a column model, the object used by Swing's JTable to represent its columns. A JTable can automatically create the column model based on its table model, but we don't want that because the DoiTableColumn objects contain much more information about the columns than is contained in an ordinary table model, e.g., preferred width in characters, resizability, label text. etc.

The below code creates a column model that contains column objects of Swing's TableColumn class, with relevant properties copied from the corresponding DoiTableColumn objects:

  TableColumnModel colmod = new DefaultTableColumnModel();

  for (int i = 0; i < ccc; ++i) {

    // Get the column bean. Skip if hidden.

    DoiTableColumn column = itsColumns[i];

    if (column.isHidden())

      continue;

    // Create a Swing column.

    TableColumn swingColumn = new TableColumn();

    // Copy properties.

    swingColumn.setHeaderValue(column.getLabelText());

    swingColumn.setResizable(column.isResizable());

    // Add to column model.

    colmod.addColumn(swingColumn);

  }

There is still one little detail before we can create the JTable. We need a table model. A JTable can't exist without a table model so we need to create one that is initially empty. This is accomplished with the following code:

  TableModel tm =   new DefaultTableModel(0, ccc);

 Now the JTable can be created and added to the scroll pane that has replaced the column container. We also tell it not to automatically create a new column model if the table model is replaced later: 

  JTable jt = new JTable(tm, colmod);

  jt.setAutoCreateColumnsFromModel(false);

  itsScrollPane.add(jt);

}

That's it. The DoiTable bean now contains a JTable within a JScrollPane instead of a column container panel. The DoiTableColumn beans still exist though, and there is an implicit association between each column bean with the corresponding Swing TableColumn object in the column model. This association will prove very useful for later enhancements, some of which I'll hint at in the next section.

I promised to mention the purpose of the pForce parameter. This parameter can be used by subclasses of the DoiTable that create and add all columns. Let's say you want to create a bean called PhoneNumberTable, with a number type column and a phone number column. This bean would add its columns in the constructor and then call commitColumnCon-tainer(true) to force the conversion to a JTable. In this case, the force parameter is necessary since the conversion must happen in design time as well as runtime.

Enhancements

The purpose of this article is to show you the principle of the column container, not how to write a full-fledged table component. To do that, I'd probably have to fill 10 issues of JDJ. For this reason the code examples shown of what really happens inside the DoiTable have been simplified. Still, I'd like to round off with a brief list of some interesting features in the real DOI classes.

Runtime Propagation of Properties

Many of the DoiTableColumn properties are automatically propagated to the JTable when changed at runtime. This allows runtime code to dynamically change the table by simply setting properties on the DoiTableColumn bean, which is much easier than doing it through the JTable. For example, the column header is updated if the label text of the column is set. This propagation is accomplished through the implicit association between the invisible column bean and the visible table column.

Runtime Synchronization of Cell Values

The DoiTable has a property called ContextRowNo that can be set programmatically. It is also updated automatically when the user selects a row. I mentioned earlier that the DoiTableColumn class is a subclass of a class called DoiTextField, which is an enhancement of JTextField. This means that a DoiTableColumn bean can have a value. The context row is used to synchronize the value of a column bean and the corresponding cell value. The designer can add a listener on a column bean that's triggered when the user edits the cell. The event handler can then access the cell value through the column bean and set a value on another cell on the same row, also through a column bean. The code for this is easier to write and maintain than using a table model listener.

Design Time Rendering

As you can see in Figure 1, the text fields in the column beans are not empty. Instead they contain a text value that reflects a few important properties (a feature inherited from the base class DoiTextField): a mandatory column has an exclamation mark suffix, a numeric column is displayed with "#", "##" or "#.#" (depending on if it is an Integer, Long, or Double), an uppercase string column uses "ABC", etc.

Smart Design Time Checking

In some circumstances, checking for design-time mode is not sufficient. Some IDEs, for example, NetBeans, have a preview function that creates a window with the form inside it where the designer can try it out. The isDesignTime method still returns true, however, which causes DoiTable to think that it's still in design mode, and it doesn't convert itself. To get around this, it has its own isDesignTime method that first calls the standard method. If it returns false we are in "real" runtime, and no further checking is necessary; if it returns false an extra check for the special preview mode is necessary. This check is IDE dependent, and in the NetBeans case it is done by searching the parent container hierarchy for the innermost frame that has a title starting with "Testing Form[". Other IDEs will most likely need variations of this technique.

Conclusion

I hope I've provided you with some ideas that you can use when you write your own beans. The same principle can naturally be applied to very different kinds of widgets, especially complex ones that are easier to design with if they are broken up into parts. The time spent on doing this is earned many times over when the end-user GUIs are designed.

Tags

Products and tools
Technologies