How to keep a single column from being reordered in a JTable?
I have a JTable
and I need to be able to reorder the columns. However I want the first column to not be able to be re-ordered. I used the following to enable reordering:
table.getTableHeader().setReorderingAllowed(true);
The columns can now be reordered including the first column which I don't want. Is there any way to lock the first column?
I have seen some solutions that use two tables with the first column being in a separate table, but maybe there's a better/simpler way.
This is the solution that I used to prevent the 1st column from being re-ordered
private int columnValue = -1;
private int columnNewValue = -1;
tblResults.getColumnModel().addColumnModelListener(new TableColumnModelListener()
{
public void columnAdded(TableColumnModelEvent e) {}
public void columnMarginChanged(ChangeEvent e) {}
public void columnMoved(TableColumnModelEvent e)
{
if (columnValue == -1)
columnValue = e.getFromIndex();
columnNewValue = e.getToIndex();
}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
});
tblResults.getTableHeader().addMouseListener(new MouseAdapter()
{
@Override
public void mouseReleased(MouseEvent e)
{
if (columnValue != -1 && (columnValue == 0 || columnNewValue == 0))
tblResults.moveColumn(columnNewValue, columnValue);
columnValue = -1;
columnNewValue = -1;
}
});
Cheers,
Nearly 4 years later, there's still no optimal solution in sight anywhere.
Yet another suboptimal approach to prevent dragging of the first column (and other columns over the first) is to intercept the mouseEvents before the mouseInputListener installed by the uidelegate can handle them (similar to a recent QA).
The collaborators
- a custom MouseMotionListener which delegates all events to the originally installed, except the dragged if it would lead to another column above the first
- replace the original with the custom
- update the replacement whenever the LAF is changed (because the original is controlled by the ui). This requires subclassing of JTableHeader and do the wiring in updateUI
The custom MouseInputListener:
/**
* A delegating MouseInputListener to be installed instead of
* the one registered by the ui-delegate.
*
* It's implemented to prevent dragging the first column or any other
* column over the first.
*/
public static class DragHook implements MouseInputListener {
private JTableHeader header;
private MouseListener mouseDelegate;
private MouseMotionListener mouseMotionDelegate;
private int maxX;
public DragHook(JTableHeader header) {
this.header = header;
installHook();
}
/**
* Implemented to do some tweaks/bookkeeping before/after
* passing the event to the original
*
* - temporarily disallow reordering if hit on first column
* - calculate the max mouseX that's allowable in dragging to the left
*
*/
@Override
public void mousePressed(MouseEvent e) {
int index = header.columnAtPoint(e.getPoint());
boolean reorderingAllowed = header.getReorderingAllowed();
if (index == 0) {
// temporarily disable re-ordering
header.setReorderingAllowed(false);
}
mouseDelegate.mousePressed(e);
header.setReorderingAllowed(reorderingAllowed);
if (header.getDraggedColumn() != null) {
Rectangle r = header.getHeaderRect(index);
maxX = header.getColumnModel().getColumn(0).getWidth()
+ e.getX() - r.x -1;
}
}
/**
* Implemented to pass the event to the original only if the
* mouseX doesn't lead to dragging the column over the first.
*/
@Override
public void mouseDragged(MouseEvent e) {
TableColumn dragged = header.getDraggedColumn();
int index = getViewIndexForColumn(header.getColumnModel(), dragged);
// dragged column is at second position, allow only drags to the right
if (index == 1) {
if (e.getX() < maxX) return;
}
mouseMotionDelegate.mouseDragged(e);
}
//-------- delegating-only methods
@Override
public void mouseReleased(MouseEvent e) {
mouseDelegate.mouseReleased(e);
}
@Override
public void mouseClicked(MouseEvent e) {
mouseDelegate.mouseClicked(e);
}
@Override
public void mouseEntered(MouseEvent e) {
mouseDelegate.mouseEntered(e);
}
@Override
public void mouseExited(MouseEvent e) {
mouseDelegate.mouseExited(e);
}
@Override
public void mouseMoved(MouseEvent e) {
mouseMotionDelegate.mouseMoved(e);
}
//------------ un-/install listeners
protected void installHook() {
installMouseHook();
installMouseMotionHook();
}
protected void installMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseMotionDelegate = l;
listeners[i] = this;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void installMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l.getClass().getName().contains("TableHeaderUI")) {
this.mouseDelegate = l;
listeners[i] = this;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
public void uninstallHook() {
uninstallMouseHook();
uninstallMouseMotionHook();
}
protected void uninstallMouseMotionHook() {
MouseMotionListener[] listeners = header.getMouseMotionListeners();
for (int i = 0; i < listeners.length; i++) {
MouseMotionListener l = listeners[i];
if (l == this) {
listeners[i] = mouseMotionDelegate;
}
header.removeMouseMotionListener(l);
}
for (MouseMotionListener l : listeners) {
header.addMouseMotionListener(l);
}
}
protected void uninstallMouseHook() {
MouseListener[] listeners = header.getMouseListeners();
for (int i = 0; i < listeners.length; i++) {
MouseListener l = listeners[i];
if (l == this) {
listeners[i] = mouseDelegate;
}
header.removeMouseListener(l);
}
for (MouseListener l : listeners) {
header.addMouseListener(l);
}
}
}
Usage which survives switching of LAF, f.i.:
JTable table = new JTable(new AncientSwingTeam()) {
@Override
protected JTableHeader createDefaultTableHeader() {
JTableHeader header = new JTableHeader(getColumnModel()) {
DragHook hook;
@Override
public void updateUI() {
if (hook != null) {
hook.uninstallHook();
hook = null;
}
super.updateUI();
hook = new DragHook(this);
}
};
return header;
}
};
I think that you need to override the columnMoved()
method in TableColumnModelListener
. the TableColumnModelEvent
class has a getFromIndex()
method that you should be able to look at to determine if it's your fixed column, and then you should be able to cancel the event.
Hope that helps. A
First you need to define a better and simpler way. What don't you like about the 2 table approach?
You can't use a TableColumnModelListener, because the event is fired "after" the column has already been moved.
The code for dragging the column is found in the BasicTableHeaderUI. So you could try overriding the code there, but then you would need to do it for all LAFs.
The above code invokes JTableHeader.getReorderingAllowed() on a mousePressed event to determine if column reordering is allowed. I guess you could override that method in the JTableHeader and perhaps use the MouseInfo class to get the current mouse location to determine if it was over the first column and then return false. But then now you would also need to create a custom JTable that uses the custom table header.
Of course with either of the above suggestions you might be able to prevent the first column from being moved. But don't forget you also need to prevent the 2nd column from being inserted before the first column. I don't believe there is a short simple answer to the question.
Fixed Column Table is my version of how this would be imlemented with two tables. Is it better? I don't know, but it is simple since its only a single line of code to use it.
I had the same issue, and I was searching about it. So far I found two ways of doing that.
- The "if I was rewriting it myself" method : Modifying the base classes from Java.
TableColumn
would need a new property, like the "resizingAllowed", it would need the "reorderingAllowed".
From this, the modifications take place in BasicTableHeaderUI
:
There is already :
private static boolean canResize(TableColumn column,
JTableHeader header) {
return (column != null) && header.getResizingAllowed()
&& column.getResizable();
}
It would need too :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& column.getReorderable();
}
(Note that if you don't want the first column only to not move, you can do without changing the TableColumns :
private static boolean canMove(TableColumn column,
JTableHeader header) {
return (column != null) && header.getReorderingAllowed()
&& header.getColumnModel().getColumnIndex(column.getIdentifier()) != 0;
}
)
After, two places to modify in the MouseInputListener
:
- in the
mousePressed
, calling thecanMove()
instead of theheader.getReorderingAllowed()
. This ensures that a column which shouldn't be moved, won't be. -
But this is not enough, we need to prevent the immobile columns from being moved during dragging another one. You need to change the
mouseDragged
, too, when it is getting the "newColumnIndex" :if (0 < newColumnIndex && newColumnIndex < cm.getColumnCount())
You need to add the condition if this new index can be moved, for example using the "canMove()" method. This way, when you will drag a column to this immobile one, you will still drag it, but it won't swap them.
Note that this method would require you to explicitly set the UI for the JTableHeader used for your JTable, which is not really ideal. But this is the most adapted though, as it deals with the problem on the place it is supposed to.
- The "Let's try to block the normal behavior with what we actually have" method : Not modifying the UI, this method focus on the JTableHeader to block the commands made by the UI.
First, to block dragging the first column, we need a subclass from JTableHeader, with this overridden method :
@Override
public void setDraggedColumn(TableColumn pAColumn)
{
int lIndex = -1;
if (pAColumn != null)
lIndex = getColumnModel().getColumnIndex(pAColumn.getIdentifier());
if (lIndex != 0)
super.setDraggedColumn(pAColumn);
}
This will prevent a user from dragging the first column. But like described earlier, this is only one part of the problem, we need to prevent another dragged column from swapping with this first one.
So far, I don't have a correct method for this. I tried by subclassing the TableColumnModel, and overriding the moveColumn()
method :
@Override
public void moveColumn(int pColumnIndex, int pNewIndex)
{
//Move only if the first column is not concerned
if (pColumnIndex =! 0 && pNewIndex != 0)
super.moveColumn(pColumnIndex, pNewIndex);
}
But this won't work, as the UI will update anyway the mouse position in the mouseDragged
method, you will have a jump from your dragged column to another place.
So I'm still searching, and wonder if someone has propositions concerning this part.