When I call JTable#scrollRectToVisible, the row I want to show is hidden underneath the header in certain situations.
The rest of this question only makes sense when using the following code. This is a very simply program which I use to illustrate the problem. It shows a UI containing a JSplitPane with in the upper part some control buttons, and the lower part contains a JTable wrapped in a JScrollPane (see screenshots at the bottom of this post).
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
public class DividerTest {
private final JSplitPane fSplitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT);
private final JTable fTable;
private final JScrollPane fScrollPane;
private boolean fHideTable = false;
public DividerTest() {
fTable = new JTable( createTableModel(50));
fScrollPane = new JScrollPane(fTable);
fSplitPane.setBottomComponent(fScrollPane);
fSplitPane.setTopComponent(createControlsPanel());
fSplitPane.setDividerLocation(0.5);
}
private JPanel createControlsPanel(){
JPanel result = new JPanel();
result.setLayout(new BoxLayout(result, BoxLayout.PAGE_AXIS));
final JCheckBox checkBox = new JCheckBox("Make table invisible before adjusting divider");
checkBox.addItemListener(new ItemListener() {
#Override
public void itemStateChanged(ItemEvent e) {
fHideTable = checkBox.isSelected();
}
});
result.add(checkBox);
JButton upperRow = new JButton("Select row 10");
upperRow.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
selectRowInTableAndScroll(10);
}
});
result.add(upperRow);
JButton lowerRow = new JButton("Select row 45");
lowerRow.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
selectRowInTableAndScroll(45);
}
});
result.add(lowerRow);
JButton hideBottom = new JButton("Hide bottom");
hideBottom.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
if (fHideTable) {
fScrollPane.setVisible(false);
}
fSplitPane.setDividerLocation(1.0);
}
});
result.add(hideBottom);
JButton showBottom = new JButton("Show bottom");
showBottom.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fScrollPane.setVisible(true);
fSplitPane.setDividerLocation(0.5);
}
});
result.add(showBottom);
return result;
}
private void selectRowInTableAndScroll( int aRowIndex ){
fTable.clearSelection();
fTable.getSelectionModel().addSelectionInterval(aRowIndex, aRowIndex);
fTable.scrollRectToVisible(fTable.getCellRect(aRowIndex, 0, true));
}
public JComponent getUI(){
return fSplitPane;
}
private TableModel createTableModel(int aNumberOfRows){
Object[][] data = new Object[aNumberOfRows][1];
for( int i = 0; i < aNumberOfRows; i++ ){
data[i] = new String[]{"Row" + i};
}
return new DefaultTableModel(data, new String[]{"Column"});
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame("Test frame");
frame.getContentPane().add(new DividerTest().getUI());
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
});
}
}
Unwanted behavior
Run the above code
Press the "Select row 10": row 10 is selected and visible
Press the "Select row 45": row 45 is selected and visible
Click the "Hide bottom" button. This will adjust the divider of the JSplitPane so that only the upper panel is visible
Click the "Select row 10" button. You see of course nothing because the table is not yet visible
Click the "Show bottom" button. The divider is adjusted, but row 10 is hidden underneath the header. I expected it to be visible without needing to scroll.
Wanted behavior
Repeat the steps from above, but make sure the "Make table invisible before adjusting divider" checkbox is selected. This will call setVisible(false) on the JScrollPane around the JTable before hiding the bottom panel.
By doing this, in the last step row 10 will be visible as the top most row, which is what I want. I just do not want to turn the scrollpane invisible: in my real application, the divider is adjusted in an animated way and as such you want to keep the table visible during the animation.
Screenshots
Unwanted: row 10 is invisible after performing the aforementioned steps
Wanted: row 10 is visible after performing the aforementioned steps
Environment
I do not think it will matter, but just in case: I am using JDK7 on a Linux system.
This seems to be caused by the way how the JViewport handles the scrollRectToVisible calls for the cases that its size is smaller than the desired rectangle. It contains a (somewhat fuzzy, but probably related) comment in the JavaDocs:
Note that this method will not scroll outside of the valid viewport; for example, if contentRect is larger than the viewport, scrolling will be confined to the viewport's bounds.
I did not go though the complete code and do all the maths and check all the cases. So a warning: The following explainations contain quite same hand-waving. But a simplified description of what this means for me in this particular case:
When the bottom part is hidden (by setting the divider location accordingly), then this height of the JScrollPane and its JViewport is 0. Now, when requesting to scrollRectToVisible with a rectangle that has a height of 20 (for one table row, as an example), then it will notice that this does not fit. Depending on the current view position of the JViewport, this may cause to viewport to be scrolled so that the bottom of this rectangle is visible.
(You can observe this: Drag the divider location manually, so that approximately half of one table row is visible. When clicking the "Select row 45" button, the upper half of the row will be visible. When clicking the "Select row 10" button, then the lower half of the row will be visible)
One pragmatic solution here that seemed to work for me was to make sure that it will always scroll so that the top of the rectangle is visible (even when the rectangle does not at all fit into the viewport!). Like this:
private void selectRowInTableAndScroll(int aRowIndex)
{
fTable.clearSelection();
fTable.getSelectionModel().addSelectionInterval(aRowIndex, aRowIndex);
Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
r.height = Math.min(fScrollPane.getHeight(), r.height);
fTable.scrollRectToVisible(r);
}
But I can't promise that this will have the desired effect for you, when an animation comes into play...
Not exactly sure what the scrollRectToVisible() is doing.
You might be able to use the JViewport.setViewPosition(...) method.
Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
Point p = new Point(r.x, r.y);
fScrollPane.getViewport().setViewPosition( p );
In this case the selected row will always be shown at the top of the viewport (if possible). So the viewport will always scroll unless the selected row is current at the top. Using this approach if the first row is at the top of the viewport and you select the 10th row the viewport will scroll to display the 10th row at the top.
However, this behaviour is slightly different than using the scrollRectToVisible() method. When using the scrollRectToVisible() method the viewport when only scrolled when the rectangle is not in the visible part of the viewport. Using this approach if the first row is at the top of the viewport and you select the 10th row the viewport will NOT scroll since the 10th row is already visible in the viewport.
Don't know if this change in functionality is acceptable or not.
Note if you don't want to viewport to automatically scroll when you select a row you could try something like:
JViewport viewport = fScrollPane.getViewport();
Rectangle viewRect = viewport.getViewRect();
Rectangle r = fTable.getCellRect(aRowIndex, 0, true);
Point p = new Point(r.x, r.y);
if (! viewRect.contains(p))
viewport.setViewPosition( p );
Related
Is it possible to create a JTable like the one below from Internet Download Manager?
As you can see, it shows that the last column in the picture is the Date Added and the right side of it is like an empty column, those grid of rows on the right side is not rows and the column of it does not belong to the real columns with titles because of the selected row is ended at the Data Added column, when I tried the table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); I get this result.
So now I want the table to fills ups the empty spaces like in the first picture and still will create a horizontal scrollbar when the columns of the table reached the maximum width like below.
EDIT: Marco13 said - "Did you consider adding an empty column?"
Yes, look take a look at this
The empty column is part of all the columns, and has a horizontal scrollbar at the bottom because it is expanded over the frame, what I want to do is...
All the columns are not auto resizing except for the empty column when resizing the frame.
The empty column is fixed in the right side of the frame so when resizing, it will not leave blank pane.
The horizontal scrollbar should not appear when all columns with title are visible in the whole frame.
The empty column and rows in it cannot be selected.
You need to use below code for vertical and horizontal scrollbar:
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane .HORIZONTAL_SCROLLBAR_AS_NEEDED);
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
To resize table when JFrame resizes. You need to override the getScrollableTracksViewportWidth().
Complete code:
import java.awt.CardLayout;
import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableColumn;
public class TableExample {
JFrame frame;
JTable table;
JScrollPane scrollPane;
public TableExample(){
initComp();
}
public void initComp(){
frame = new JFrame();
frame.setSize(500, 400);
frame.setLayout(new CardLayout());
table = new JTable(100, 5){
public boolean getScrollableTracksViewportWidth(){
return getPreferredSize().width < getParent().getWidth();
}
};
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
//table.setFillsViewportHeight(true);
//table.setPreferredScrollableViewportSize(new Dimension(300, 200));
scrollPane = new JScrollPane(table);
scrollPane.setOpaque(true);
scrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane .HORIZONTAL_SCROLLBAR_AS_NEEDED);
frame.add(scrollPane);
//-----this part from doc
TableColumn column = null;
for (int i = 0; i < 5; i++) {
column = table.getColumnModel().getColumn(i);
if (i == 4) {
column.setPreferredWidth(200); //third column is bigger
} else {
column.setPreferredWidth(100);
}
}
//----
frame.setDefaultCloseOperation(frame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
public static void main(String[] args) {
new TableExample();
}
}
Output:
You can get some idea from: How to make JTable both AutoResize and horizontall scrollable? and also this question #camickr answer may help.
Lastly, if you want to adjest column size to its content width(length of the value), you need to use Table Column Adjuster
I have a group of jRadioButton that I created,
each button has an action listener that creates a JTable in a separate window.
I want that when I press another button, the frame will be cleaned and then the other JTable to be performed,
ButtonGroup group = new ButtonGroup();
JRadioButton[] jRadioButton = new JRadioButton[5];
for (int i = 0; i < 5; i++) {
jRadioButton[i] = new JRadioButton("machine "+i);
jRadioButton[i].setBounds(x, y, width, height);// x, y, width, height are place parameters
group.add(jRadioButton[i]);
frame.getContentPane().add(jRadioButton[i]);
frame.update(frame.getGraphics());//update the frame and add the buttons
}
let's say, I pressed machine 1 and a table popped up in a different window, now when I press machine 2 and a different table will pop up I want to clear the new window before the second table is shown.
So my question is, is it possible to clean a window and if yes how?
If the only difference is JTable you can just change the table's model with theTable.setModel(properJRadioButtonDependentModel).
If you have more controls you can either use a CardLayout swapping panels (for each JRadioButton instance you can create a panel and swap them)
OR
remove all controls using removeAll() method of container, add new controls and call
container.revalidate();
container.repaint();
the table creation
public void createMachineTable(int row) {
model = new DefaultTableModel(col, row);
table = new JTable(model) {
/**
*
*/
#Override
public boolean isCellEditable(int rows, int columns) {
if (columns == 0)
return false;//left side uneditable
else
return true;//right side editable
}
};
pane = new JScrollPane(table);// the new window of te table
getContentPane().add(pane);
setVisible(true);
setSize(500, 600);
getContentPane().setLayout(new FlowLayout());
setDefaultCloseOperation(1);
}
now, when pressing a different button, the following levels will be applied
pane.getViewport().remove( table );
createMachineTable(rowsNumer);
the table will be deleted and a new one will be created.
I'm having an issue creating an empty JTabbedPane where the only portion to be seen on the GUI are the row of tabs.
Everytime I add a new tab with an "empty" component, the height of the JTabbedPane increases, but why?
The current workaround is to override getPreferredSize(), but it seems kludgy to me. Comment out the overridden method to see what I mean.
Am I missing something obvious?
Background:
We need a JTabbedPane where the tabbed pane starts off with 2 tabs, but the user can add more tabs as needed, up to 10. In addition, each tab contains the same components, but with different data. The decision was made to fake the look of a JTabbedPane, by implementing an empty JTabbedPane solely for the look, and to use a single fixed JPanel whose contents will be refreshed based on the tab clicked.
(Normally, I could just recreate the JPanel n-times, but that would nightmarish for the presenter classes who control the UI, which is beyond the scope of my question.)
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class CustomTabbedPane implements Runnable
{
static final int MAX_TABS = 11; // includes the "add" tab
JPanel pnlTabs;
JTabbedPane tabbedPane;
public static void main(String[] args)
{
SwingUtilities.invokeLater(new CustomTabbedPane());
}
public void run()
{
JPanel p = buildPanel();
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(p);
frame.setSize(800,400);
frame.setVisible(true);
}
private JPanel buildPanel()
{
tabbedPane = new JTabbedPane()
{
#Override
public Dimension getPreferredSize()
{
Dimension dim = super.getPreferredSize();
dim.height = getUI().getTabBounds(this, 0).height + 1;
return dim;
}
};
tabbedPane.addTab("Tab 1", getEmptyComp());
tabbedPane.addTab("Tab 2", getEmptyComp());
tabbedPane.addTab("+", new TabCreator());
tabbedPane.addMouseListener(new MouseAdapter()
{
#Override
public void mouseClicked(MouseEvent e)
{
addTab();
}
});
JScrollPane scroll = new JScrollPane(new JTable(5,10));
JPanel p = new JPanel(new BorderLayout());
p.add(tabbedPane, BorderLayout.NORTH);
p.add(scroll, BorderLayout.CENTER);
p.setBorder(BorderFactory.createLineBorder(Color.BLUE.darker(), 1));
return p;
}
private void addTab()
{
if (tabbedPane.getSelectedComponent() instanceof TabCreator)
{
int selIndex = tabbedPane.getSelectedIndex();
if (tabbedPane.getComponentCount() < MAX_TABS)
{
if (selIndex == tabbedPane.getComponentCount()-1)
{
String title = "Tab " + (selIndex + 1);
tabbedPane.insertTab(title, null, getEmptyComp(), "", selIndex);
tabbedPane.setSelectedIndex(selIndex);
if (tabbedPane.getComponentCount() == MAX_TABS)
{
tabbedPane.setEnabledAt(MAX_TABS-1, false);
}
}
}
}
}
private Component getEmptyComp()
{
return Box.createVerticalStrut(1);
}
class TabCreator extends JLabel {}
}
Great question! But it's fairly straightforward to get a hint on what's happening.
The problem is that your content does not have a minimum width, preferred size is not set, tab placement is top/bottom and the UI is default.
Since preferred size is not set, then when the layout is revalidated the calculations of space required go into the BasicTabbedPaneUI method Dimension calculateSize(false).
That reads:
int height = 0;
int width = 0;
<other vars>
// Determine minimum size required to display largest
// child in each dimension
<actual method>
Here it calculates the minimum size to accommodate any child and stores it into height/width. In your case this yields something like 10,10 (because of the single Label tab creator I think, I didn't follow that one).
Then happens the magic:
switch(tabPlacement) {
case LEFT:
case RIGHT:
height = Math.max(height, calculateMaxTabHeight(tabPlacement));
tabExtent = preferredTabAreaWidth(tabPlacement, height - tabAreaInsets.top - tabAreaInsets.bottom);
width += tabExtent;
break;
case TOP:
case BOTTOM:
default:
width = Math.max(width, calculateMaxTabWidth(tabPlacement));
tabExtent = preferredTabAreaHeight(tabPlacement, width - tabAreaInsets.left - tabAreaInsets.right);
height += tabExtent;
}
What happens here is it sets the preferred width to be the maximum of the largest tab width and the largest child width. In your case it's around 44 for the tab text. The tabExtent is then calculated to see just how many rows of tabs are needed to support this preferred width. In your case - it's 1 extra row of tabs for each tab. That's where the extra height in preferredSize().height comes from. Essentially because for horizontal tab placement it cares about width first, then height.
How to fix:
Set a preferred size :) I know a lot of people say don't set the preferred size, but in this case this will just work. Since a preferred size is set (via actually setting it, not overriding getPreferredSize()), the code will never get to counting tabs.
Give at least one of your children a size (via setPreferredSize or overriding getPreferredSize). If one of the childrens width is that of the frame, or, say, the table at the bottom the TabbedPane will not be allocating an extra row for each tab, since a single row will fit everything.
Make your own UI for the tabbed pane. It may be easier to make your own tabbed pane though really, I've never done this.
EDIT:
After thinking about this a bit more, I realized that solution number 1 AND your own solution suffer from the flaw that, if the tabbed pane actually does require multiple rows for the tabs (hello frame resizes), bad things will happen. Don't use it.
I have window that shows log events in table.
For example, user is reading text in some row of table. When new logs come, they are added in the beginning of the table, and the row, the user was reading moves down. I have a requirement to prevent JScrollPane to scroll when new rows are added at the beginning of the table. I tried diffrent things but nothing helped. Can someone advice me how to implement this?
Thanks in advance!
Tricky task :-) The moving out happens because by default the scroll position is not adjusted in any way: adding rows above simply keeps the vertical scroll position which then points to a different row as before.
What's needed:
keep track of the visibleRect/last row before the insert
listen to model changes of type insert
calculate the new visible rect such that the old last row is scrolled back into the view
trigger the scroll
It's tricky because we need to listen to model changes. That listener is a neighbour of the modelListener registered internally by the table which updates itself on changes. So we need to be sure to act after the internal changes are done and at the same time use information before the internal update.
A dirty solution - depending on the usual sequence of swing listener notification which is last-added-first-notified (beware: DONT in production code! It will break easily, f.i. when changing the LAF) - is to gather the before-state when notified and adjust the scroll position wrapped in an invokeLater:
final DefaultTableModel model = new DefaultTableModel(20, 2);
for (int i = 0; i < model.getRowCount(); i++) {
model.setValueAt(i, i, 0);
}
final JTable table = new JTable(model);
Action action = new AbstractAction("add row at 0") {
#Override
public void actionPerformed(ActionEvent e) {
model.insertRow(0, new Object[] {});
}
};
JXFrame frame = wrapWithScrollingInFrame(table, "modify scroll");
TableModelListener l = new TableModelListener() {
#Override
public void tableChanged(TableModelEvent e) {
if (!TableUtilities.isInsert(e)) return;
final Rectangle r = table.getVisibleRect();
final int lastRow = table.rowAtPoint(
new Point(0, r.y + r.height - 5));
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
// assuming
// a) the insert was exactly 1 row
// b) was _before_ the old last row in view-coordinates
Rectangle cell = table.getCellRect(lastRow + 1, 0, true);
cell.translate(0, -r.height + cell.height);
cell.height = r.height;
table.scrollRectToVisible(cell);
}
});
}
};
model.addTableModelListener(l);
The appropriate way of getting the before-state cleanly depends on your exact context. You can keep track of the first/last row during the lifetime of the table, updating them in (one or all)
a ListSelectionListener to the row selectionModel (if the row the user is reading is always selected)
a ChangeListener to the vertical scrollBar
a ComponentListener to scrollPane's size changes
I'm currently working on a rather complex application. My job is to build parts of the GUI.
The main area is derived for JTable and contains all application relevant data. There are a few elements on top of the Table, that allow the user to control the way the data is shown in the table.
The options relevant to the task at hand are:
Changing of number of columns,
Independently changing of width of columns (not by means of JTableHeader) and
Entering one filter term per column to select specific rows of the data.
The main goal in this szenario is to create a Component (probably JTextField) for every column in the current viewsetting, which is accuratly aligned with that column (although it changes size at runtime).
First question:
The alignment doesn't work. I can't get the width of the TextFields to match the width of the columns.
How do i get it to work?
Second problem:
I want the individual filters to be chained. That is, if the user decides to enter more then one filter string, all of them should be evaluated for their respective columns and only the rows that match all filters should be shown. So far the input in a second TextField delets the first filter (which is working decently using RowFilter.regexFilter).
How do i get this to work?
Please let me know, which code snippets could be useful to you and i will be glad to post them.
Thanks in advance for any help given.
Regards, DK
I can't get the width of the
TextFields to match the width of the
columns
This example should get you started:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public class TableFilterRow extends JFrame implements TableColumnModelListener
{
private JTable table;
private JPanel filterRow;
public TableFilterRow()
{
table = new JTable(3, 5);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
JScrollPane scrollPane = new JScrollPane( table );
getContentPane().add( scrollPane );
table.getColumnModel().addColumnModelListener( this );
// Panel for text fields
filterRow = new JPanel( new FlowLayout(FlowLayout.CENTER, 0 , 0) );
for (int i = 0; i < table.getColumnCount(); i ++)
filterRow.add( new JTextField("" + i) );
columnMarginChanged( new ChangeEvent(table.getColumnModel()) );
getContentPane().add(filterRow, BorderLayout.NORTH);
}
// Implement TableColumnModelListener methods
// (Note: instead of implementing a listener you should be able to
// override the columnMarginChanged and columMoved methods of JTable)
public void columnMarginChanged(ChangeEvent e)
{
TableColumnModel tcm = table.getColumnModel();
int columns = tcm.getColumnCount();
for (int i = 0; i < columns; i ++)
{
JTextField textField = (JTextField)filterRow.getComponent( i );
Dimension d = textField.getPreferredSize();
d.width = tcm.getColumn(i).getWidth();
textField.setPreferredSize( d );
}
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
filterRow.revalidate();
}
});
}
public void columnMoved(TableColumnModelEvent e)
{
Component moved = filterRow.getComponent(e.getFromIndex());
filterRow.remove(e.getFromIndex());
filterRow.add(moved, e.getToIndex());
filterRow.validate();
}
public void columnAdded(TableColumnModelEvent e) {}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
public static void main(String[] args)
{
JFrame frame = new TableFilterRow();
frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
frame.pack();
frame.setLocationRelativeTo( null );
frame.setVisible(true);
}
}
only the rows that match all filters
should be shown
Read the JTable API and follow the link to the Swing tutorial on "How to Use Tables" where you will find the TableFilterDemo. You can easily modify the code to use "and" filters. The code would be something like:
// rf = RowFilter.regexFilter(filterText.getText(), 0);
List<RowFilter<Object,Object>> filters = new ArrayList<RowFilter<Object,Object>>(2);
filters.add(RowFilter.regexFilter(filterText.getText(), 0));
filters.add(RowFilter.regexFilter(filterText.getText(), 1));
rf = RowFilter.andFilter(filters);
This examples shares a single text field looking for the same string in multiple columns. You would obviously use your individual text fields.