Skip to main content

Freezable JTables (are they extreme?)

Posted by elevy on January 4, 2009 at 7:47 PM PST

Finally, after about 7 months I get back into writing a new blog entry. I changed jobs, and it hasn’t been until the holidays that I got a chance to get back into writing again.

This time, I am writing about a new JTable trick that I wanted to do for a while. The idea is simple; it is to allow the user to "freeze" from a column on the horizontal scrolling. When you have a table with a lot of columns sometimes you want to be able to scroll to the right, but leave the left most columns visible while scrolling. The common spreadsheet programs have that functionality.

Well, better than keep trying to describe the functionality, I think we can go ahead and give it a try. Just click the Java Webstart button to get it started. Then use your right-click to open the popup menu, and select "freeze column". From there just try scrolling horizontally and you will see how nicely you can see the columns that are farther in the right together with the very few first columns that you just froze.



(sorry it requires Java 1.6+).

How does it work?

The principle is very simple. As you figure it out by now, I am using a regular swing JTable. The trick is to put a custom component, in this case an extension of a JLabel, on top the JTable. This component, called FrozenColumns, is just of the same size as the columns we want to freeze. The paintComponent method of our custom component just prepares the Graphics object, and then calls the JTable to paint itself in our custom component graphics area.

If we just set the clip to be the first few columns until the last one we want to freeze, we can trick the JTable into believing that the current visible are in the ViewPort of the JScrollPane is the one we have frozen. In this way, the JTable will paint itself in our JLabel, doing all the hard work for us.

Everything is setup from a couple of event listeners that after opening the popup menu and the user selecting the option to freeze the columns they make the custom component of the proper size and location, and then visible.

To add a component on top of the JTable, I am using the JLayeredPanel. To do that I use the following code:



JLayeredPane pane = table.getRootPane().getLayeredPane();


...

pane.add(
frozenColumns, JLayeredPane.POPUP_LAYER);
setBoundsOnFrozenColumns();

...

frozenColumns.setVisible(true);





The object frozenColumns is an instance of the custom component, which in this case is just an inner class, and it is responsible for painting the frozen columns on top of the JTable.

The setBoundsOnFrozenColumns() method is in charge of calculating the dimensions of the frozenColumns object, and calling the setBounds method. Here is the section of the code that does that:


division = table.getCellRect(1, col, true).x

+ table.getCellRect(1, col, true).width;


int limit = scrollPane.getBounds().width

- scrollPane.getVerticalScrollBar().getBounds().width


- 2;


division = Math.min(division, limit);


JLayeredPane pane = table.getRootPane().getLayeredPane();

Point p = scrollPane.getLocationOnScreen();


SwingUtilities.convertPointFromScreen(p, pane);


Rectangle scrollPaneBounds = scrollPane.getBounds();


int headerHeight = table.getTableHeader().getBounds().height + 1;

int hScrollHeight = (scrollPane.getHorizontalScrollBar()


.isVisible()) ? scrollPane.getHorizontalScrollBar()


.getBounds().height : 0;


frozenColumns.setBounds(p.x + 1, p.y + headerHeight, division,

scrollPaneBounds.height - headerHeight – hScrollHeight - 2);





Where the division variable represents the location where the division line is going to be painted, to separate the columns that are frozen and those are not.

The variable col is set in another method, the one that handles the action event of the menu selection. The way it is set is as follows:



col = table.columnAtPoint(p);




Where p is the Point where the user performed the right click to popup the menu.

From an API perspective, I created a custom TableHeader, that when you set it to the table it configures it all. It adds the custom component to the table, and sets all the event listeners that are going to set the bounds and prepare everything for making this functionality a reality. That makes setting the frozen column functionality into your tables very simple.

Here is the sample code that does that:


FreezableTableHeader tableHeader = new FreezableTableHeader(table,

scrollPane);


table.setTableHeader(tableHeader);



The source code is available under GPL license.Download


Related Topics >>

Comments

Eli, now you have completely disabled scrolling when navigating over the "gap". Piling one trick onto another to cover what the first broke usually is a never-ending story. The problem with the fake component is that now table.getVisibleRect is incorrect, that's bound to confuse collaborators - there'll always be yet another hole waiting just around the corner ;-) The multiple-table approach (done as cleanly as possible, which is _lots_ of work as others already mentioned) is the only way I know of so far that is half-way workable. Read the "half-way" as not extendable: next logical requirement would by frozen rows, meaning 4 tables ... and/or the frozen part docked at any of the borders ... and ... :-) CU Jeanette

@Jeanette I simply meant that there is a simpler technique to create fixed columns. Of course a lot more has to be done to make it work as a full blown component. Issues you describe are solvable, but I do wish that this functionality can be part of actual JTable or JXTable.

This has been an interesting discussion to read. I did a talk a few months ago to the Austin Java Users Group, where I used this as an example case for building a custom Swing component. I used the multiple tables in JScrollPane approach, with event listeners to handle most of the navigation and column resizing issues, and while I wouldn’t say it works perfectly, it does work quite well.:) I could have addressed even more but given the time constraints, I think I hit the most important issues. The talk is available online at: http://www.vizitsolutions.com/customSwing.html The approach in this blog in neat because it is dynamic, regrettably it doesn’t appear to handle the case where frozen columns are resized too well. Thanks, Alex

hi...is asme kind of component available in J2ME...can u provide me with a sample code.. Regards

We actually did this at my old job as part of a trading application. We used a method similar to ERYZHIKOV, where we had 2 JTables representing the whole spreadsheet. We created a custom component that placed 2 JTables side by side, if the user has requested column freezing. The component handled sending events to both tables to synchronize scrolling and row selection. The user was even able to adjust the frozen columns' width, and the component would automatically resize the viewport. It was a lot of work though. I think we ended up writing close to 30 classes, maybe about 10 of them being the heaviest in terms of lines of code.

Jeanette, Thanks for your comments, I just released a newer version that addresses the issues you mentioned. Still there is a small problem, which is that after the user selects a cell, the scroll bars can be used to move the cell under my JLabel. I am pretty sure that this problem can be solved too. However, I am running out of time right now. Perhaps I will write another post explaining how I did the event management, and publishing the new code. For now, you can click again in the Java Web Start link, it will launch the latest version, and you can get the jar file to play with it as well. Thanks again, eL

Eryzhikov, definitions of "works perfectly" seem to differ widely Your approach (which is the usual hack and even recommended somewhere in the swing technical documentation) has severe problems, f.i. - need to be very careful to keep row properties (selection, row height, sorting) in synch - column selection is broken across header/main table - navigation across header and main table doesn't work at all In this (incomplete) list, typically the first can be handled with enough effort, the second isn't overly important most of the time but the last tends to be a show-stopper, mainly because the BasicTableUI is very stubborn to "adjustments" Cheers Jeanette

Much simpler solution is to put put a separate JTable with fixed columns as row header of the same scroll pane. Something like following: JViewport viewport = new JViewport(); viewport.setView(fixColumnTable); viewport.setPreferredSize(fixColumnTable.getPreferredSize()); scrollPane.setRowHeaderView(viewport); scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, fixColumnTable.getTableHeader()); This works perfectly for me.

Elie, interesting approach - though not really working (have been there ;-) One problem is the re-direct of mouseEvents on the frozen columns if they are "in reality" invisible. A kind of invers problem is cell-wise navigation which may or may not involve scrolling of a column under the fixed ones. One example to see the misbehaviour, scroll all the way to the right and click on the frozen - the focus rect isn't painted because ... well, the ui thinks we hit a cell which is below the fake. Another example, scroll all the way to the right, click on the last cell to make it the lead, then navigate by keyboard to the left: when we reach the frozen, nothing much happens except that the focus rect disappears, no scrolling to visible. Again, not much the ui can do as it thinks the lead is visible. Actually, my conclusion at the time had been that it's impossible to make work - would love to be proven wrong, of course :-) CU Jeanette