In my application, I have a two column table which will be dynamically populated through the life of the program. I have a method called updateTableWidths which, when called, does the following:
makes the first column just as wide as it needs to be to fit cell contents
makes the second column just as wide as it needs to be to fit cell contents
if the entire table is wider than its container, enables a horizontal scrollbar
otherwise, lets last column stretch to fill container
It seems to work great, but for some really strange reason it needs to be called twice (but only in some cases, i.e. when table widths are growing rather than shrinking). The SSCCE below illustrates my dilemma.
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
#SuppressWarnings("serial")
public class TableResizeTest extends JComponent {
private JFrame frame;
private JScrollPane scrollPane;
private DefaultTableModel tableModel;
private JTable table;
private JPanel panel;
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Throwable e) {
e.printStackTrace();
}
EventQueue.invokeLater(new Runnable() {
public void run() {
try {
TableResizeTest window = new TableResizeTest();
window.frame.setVisible(true);
window.frame.requestFocusInWindow();
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
public TableResizeTest() {
initialize();
}
private void initialize() {
frame = new JFrame("Test");
frame.setBounds(300, 300, 200, 300);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
tableModel = new DefaultTableModel(new Object[]{"Col_1", "Col_2"},0);
table = new JTable(tableModel);
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
table.setAlignmentY(Component.TOP_ALIGNMENT);
table.setAlignmentX(Component.LEFT_ALIGNMENT);
table.setBorder(new EmptyBorder(0, 0, 0, 0));
table.setFillsViewportHeight(true);
table.setTableHeader(null);
table.setEnabled(false);
table.getColumnModel().setColumnMargin(0);
frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));
panel = new JPanel();
frame.getContentPane().add(panel);
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
JButton btnFillTableShort = new JButton("Fill table short");
btnFillTableShort.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnFillTableShort);
JButton btnFillTableLong = new JButton("Fill table long");
btnFillTableLong.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnFillTableLong);
JButton btnUpdateTableWidths = new JButton("Update table widths");
btnUpdateTableWidths.setAlignmentX(Component.CENTER_ALIGNMENT);
panel.add(btnUpdateTableWidths);
btnUpdateTableWidths.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
updateTableWidths();
}
});
btnFillTableLong.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fillTableLong();
}
});
btnFillTableShort.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
fillTableShort();
}
});
scrollPane = new JScrollPane(table);
scrollPane.setPreferredSize(new Dimension(200, 100));
scrollPane.setMaximumSize(new Dimension(32767, 100));
frame.getContentPane().add(scrollPane);
scrollPane.setAlignmentY(Component.TOP_ALIGNMENT);
scrollPane.setBorder(new LineBorder(new Color(0, 0, 0)));
frame.pack();
}
public void fillTableShort() {
tableModel.setRowCount(0);
tableModel.addRow(new Object[]{"short", "short"});
}
public void fillTableLong() {
tableModel.setRowCount(0);
tableModel.addRow(new Object[]{"looooooooooooooooooooong", "looooooooooooooooooooong"});
}
public void updateTableWidths() {
int col_1_width = 0;
for (int row = 0; row < table.getRowCount(); row++) {
TableCellRenderer renderer = table.getCellRenderer(row, 0);
Component comp = table.prepareRenderer(renderer, row, 0);
col_1_width = Math.max (comp.getPreferredSize().width, col_1_width);
}
col_1_width += table.getIntercellSpacing().width;
col_1_width += 10;
table.getColumn("Col_1").setMinWidth(col_1_width);
table.getColumn("Col_1").setMaxWidth(col_1_width);
System.out.println("col_1_width was set to " + col_1_width);
int col_2_width = 0;
for (int row = 0; row < table.getRowCount(); row++) {
TableCellRenderer renderer = table.getCellRenderer(row, 1);
Component comp = table.prepareRenderer(renderer, row, 1);
col_2_width = Math.max (comp.getPreferredSize().width, col_2_width);
}
col_2_width += table.getIntercellSpacing().width;
int tableWidth = col_2_width + col_1_width;
if (scrollPane.getVerticalScrollBar().isVisible()) {
tableWidth += scrollPane.getVerticalScrollBar().getWidth();
}
if (tableWidth > scrollPane.getWidth()) {
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
System.out.println("Auto resize was set to AUTO_RESIZE_OFF");
} else {
table.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN);
col_2_width = scrollPane.getWidth() + col_1_width;
System.out.println("Auto resize was set to AUTO_RESIZE_LAST COLUMN");
}
table.getColumn("Col_2").setPreferredWidth(col_2_width);
table.getColumn("Col_2").setMinWidth(col_2_width);
System.out.println("col_2_width was set to " + col_2_width + "\n");
}
}
Here is the sequence of steps you can follow to reproduce the behavior:
1) Launch the app
2) Click button "Fill table short"
3) Click button "Update table widths" (in this case only one click is needed)
4) Click button "Fill table long"
5) Click button "Update table widths" (first click)
6) Click button "Update table widths" (second click)
After the second click, it displays properly. I have some debug info that prints to the console, and for both the first and second click, the method seems to be doing exactly the same thing:
col_1_width was set to 152
Auto resize was set to AUTO_RESIZE_OFF
col_2_width was set to 142
So it's not like there is some obvious difference happening the second time around as a result of something that was changed during the first run. And as I mentioned earlier, this only happens when table columns have grown, rather than shrunk.
I am really stumped here. Any ideas?
You probably need to specify the preferred size for the first column as well to work properly with the scroll. Try adding this line:
table.getColumn("Col_1").setPreferredWidth(col_1_width);
in the the block that handles Col_1 calculations in updateTableWidths method. It solved the issue in the posted SSCCE.
EDIT:
There is a dependency on maxWidth and minWidth in setPreferredWidth. Below is the code for setPreferredWidth. So the order may matter.
public void setPreferredWidth(int preferredWidth) {
int old = this.preferredWidth;
this.preferredWidth = Math.min(Math.max(preferredWidth, minWidth), maxWidth);
firePropertyChange("preferredWidth", old, this.preferredWidth);
}
Also exploring setMaxWidth indicates that it may set preferred width as well:
public void setMaxWidth(int maxWidth) {
int old = this.maxWidth;
this.maxWidth = Math.max(minWidth, maxWidth);
if (width > this.maxWidth) {
setWidth(this.maxWidth);
}
if (preferredWidth > this.maxWidth) {
setPreferredWidth(this.maxWidth);
}
firePropertyChange("maxWidth", old, this.maxWidth);
}
Try adding TableColumn#setWidth and/or TableColumn#setPreferredWidth
ie
table.getColumn("Col_1").setWidth(col_1_width);
table.getColumn("Col_1").setPreferredWidth(col_1_width);
//...///
table.getColumn("Col_2").setWidth(col_2_width);
table.getColumn("Col_2").setPreferredWidth(col_2_width);
Related
I've made a JTextArea where I input different commands and separate them by newline "\n", and if there is an error in one of the lines then I write it in a console output. Here I made a very simple, and not the best solution to make this line indication, but it's a bit buggy.
How I made it
I've defined a textArea where I can type different information/commands, and if one of the commands/lines is invalid I write it in the console just to display something for now. I basically count the lines by splitting the textArea rows up by "\n" and then count which line the error occurs in, and the left consoleLineNum is using the amount of rows in textArea, to then make a string containing all the numbers of rows+"\n".
But here my question is, is this a good enough way? If so, why/how can I make it more robust? Or how can I make this indication with line numbers, in the left? It has to increase each time the user makes a new line in the textArea.
private static void createAndShowGUI() {
//Create and set up the window.
JFrame frame = new JFrame("GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(width, height);
frame.setLayout(new BorderLayout(3,3));
/*----- Panels -----*/
JPanel panel1 = new JPanel();
JPanel panel2 = new JPanel();
JPanel panel3 = new JPanel();
//Add Components to this panel.
JScrollPane scrollPane = new JScrollPane(textArea);
JScrollPane scrollPaneOutput = new JScrollPane(consoleOutput);
textArea.setCaretPosition(textArea.getDocument().getLength());
consoleOutput.setEditable(false);
consoleLineNum.setEditable(false);
ButtonPanel_listener(buttonPanel);
textArea.addKeyListener(new KeyListener() {
#Override
public void keyTyped(KeyEvent e) {
}
#Override
public void keyPressed(KeyEvent e) {
consoleLineNum.setText("");
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= Integer.parseInt(String.valueOf(textArea.getText().split("\n").length)); i++) {
sb.append(i + " \n");
}
consoleLineNum.setText(sb.toString());
}
#Override
public void keyReleased(KeyEvent e) {
}
});
//Background
panel1.setBackground(Color.LIGHT_GRAY);
//Preferred size
panel1.setPreferredSize(new Dimension(100, 100));
scrollPane.setPreferredSize(new Dimension(200, 200));
panel2.setLayout(new BorderLayout());
panel2.add(scrollPane, BorderLayout.CENTER);
panel2.add(buttonPanel, BorderLayout.SOUTH);
panel2.add(consoleLineNum, BorderLayout.WEST);
consoleLineNum.setText(num);
panel3.setLayout(new BorderLayout());
panel3.add(drawCanvas, BorderLayout.CENTER);
panel1.setLayout(new BorderLayout());
panel1.add(scrollPaneOutput, BorderLayout.CENTER);
//textArea.addActionListener(e -> drawCanvas.drawCircle(250, 250, 200));
//Add contents to the window.
frame.add(panel2, BorderLayout.WEST);
frame.add(panel3, BorderLayout.CENTER);
frame.add(panel1, BorderLayout.SOUTH);
//Display the window.
frame.pack();
frame.setVisible(true);
}
Difference between your expected and actual results
When I hit newline/Enter, it doesn't show the number right away, only when I start typing.
Or
Here I want it to match where the user is, so if the user hit enter, and go to the next line, then the number matches and is shown right away.
If I delete all lines, except some, it still shows the numbers
Here I want it to wipe all the numbers and update it to match the amount of data in textArea.
Tried this, and it works almost as expected. The only problem is that when I delete lines, it's one behind.
#Override
public void keyPressed(KeyEvent e) {
numI = textArea.getLineCount();
if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE && numI > 0) {
numI = numI - 1;
}
if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE || e.getKeyCode() == KeyEvent.VK_ENTER){
StringBuilder sb = new StringBuilder();
for (int i = 0; i <= numI; i++) {
sb.append(i+1 + " \n");
}
consoleLineNum.setText(sb.toString());
}
}
Use a DocumentListener to listen for changes in the text of the JTextArea. The API of JTextArea already provides a method that tells you how many lines it contains, namely getLineCount(). After placing the JTextArea in a JScrollPane, set a JList as the row header for the JScrollPane.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import javax.swing.DefaultListModel;
import javax.swing.JFrame;
import javax.swing.JList;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JViewport;
import javax.swing.WindowConstants;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
public class CountLns implements DocumentListener, Runnable {
private JFrame frame;
private JList<Integer> lineNumbersList;
private JTextArea textArea;
public void changedUpdate(DocumentEvent docEvent) {
// Never called.
}
public void insertUpdate(DocumentEvent docEvent) {
handleDocEvent();
}
public void removeUpdate(DocumentEvent docEvent) {
handleDocEvent();
}
#Override
public void run() {
showGui();
}
private JScrollPane createTextArea() {
textArea = new JTextArea(20, 50);
textArea.getDocument().addDocumentListener(this);
JScrollPane scrollPane = new JScrollPane(textArea);
lineNumbersList = new JList<>(new Integer[]{1});
lineNumbersList.setBackground(Color.cyan);
lineNumbersList.setFont(textArea.getFont());
lineNumbersList.setFixedCellHeight(16);
JViewport rowHeader = new JViewport();
rowHeader.setView(lineNumbersList);
scrollPane.setRowHeader(rowHeader);
return scrollPane;
}
private void handleDocEvent() {
DefaultListModel<Integer> model = new DefaultListModel<>();
List<Integer> lineNumbers = IntStream.range(0, textArea.getLineCount())
.boxed()
.map(i -> i + 1)
.collect(Collectors.toList());
model.addAll(lineNumbers);
lineNumbersList.setModel(model);
}
private void showGui() {
frame = new JFrame();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.add(createTextArea(), BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
textArea.requestFocusInWindow();
frame.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new CountLns());
}
}
Whenever the contents of the JTextArea change, the DocumentListener counts the lines in the JTextArea and sets the JList model to contain exactly that number of elements. So if the JTextArea has 12 lines, the JList will contain all the numbers from 1 to 12.
Try doing it like this:
yourTextArea.addKeyListener(new KeyAdapter() {
#Override
public void keyTyped(KeyEvent e) {
// Returns true every time the user presses either backspace or enter.
if (e.getKeyChar() == '\n' || e.getKeyChar() == '\b') {
int lines;
// Checks if the text ends with a newline, if so,
// it adds 1 to the line count, so that would make your line
// appear even if the user just started a new line.
if (yourTextArea.getText().endsWith("\n")) {
lines = yourTextArea.getText().split("\n").length + 1;
} else {
lines = yourTextArea.getText().split("\n").length;
}
// Removes previous count.
linePanel.setText("");
// Appends a new line to the area for every line.
for (int i = 0; i < lines; i++) {
linePanel.append((i + 1) + "\n");
}
}
}
});
I did not write in Java for 1 year so sorry, if I messed up syntax
First of all, why won't you implement line wrap and line count increment by pressing Enter? It looks much more simple to me
#Override
public void keyPressed(KeyEvent e) {
if(e.getKeyCode() == KeyEvent.VK_ENTER) {
textArea.append("\n");
Integer lineCount = Integer.parseInt(consoleLineNum.getText());
Integer newLineNumber = new Integer(lineCount.intValue() + 1)
consoleLineNum.append("\n" + newLineNumber.toString())
}
}
However you still will not get actual number of lines, if some of them will be deleted. So you can also add trickier logic to your keyPressed() method
if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
// Well I actually googled this line for counting occurences
Integer linesCountActual = textArea.getText().split("\n", -1).length - 1);
Integer linesCountLeft = consoleLineNum.getText().split("\n", -1).length - 1);
if (linesCountActual > linesCountLeft) {
String oldText = consoleLineNum.getText();
String newText = oldText.substring(0, oldText.length() - 2);
consoleLineNum.setText(newText);
}
}
I have a java 8 program in which - Parent is a JFrame that has Menu, few buttons, a text field and a JTable with fixed number of non-editable rows. Number of rows and data cannot be changed dynamically.
Menu has list of UIManager.getInstalledLookAndFeels()
Initially JTable rows border are visible
If LookAndFeel change to [Nimbus javax.swing.plaf.nimbus.NimbusLookAndFeel], and then try any other LookAndFeel, rows border goes away.
I am using SwingUtilities.updateComponentTreeUI(parentFrame) to apply LnF. LnF is applying on all components including JTable, but once Nimbus LnF applied and after that choosing any other LnF, rows border went off.
As a option repaint() is not making any difference.
In image
(1) is when program starts where row borders are visible
(2) when Nimbus LnF applied
(3) LnF changed to Metal but row borders are NOT visible
Please suggest.
Sample Code:
package com.sv.runcmd;
import com.sv.core.logger.MyLogger;
import com.sv.swingui.SwingUtils;
import com.sv.swingui.component.AppExitButton;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.table.DefaultTableModel;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import static com.sv.core.Constants.SP_DASH_SP;
import static com.sv.swingui.UIConstants.EMPTY_BORDER;
public class LnFExample extends JFrame {
public static void main(String[] args) {
new LnFExample().initComponents();
}
private static final String APP_TITLE = "LnF";
private DefaultTableModel model;
private JTable tblCommands;
private JMenuBar mbarSettings;
public LnFExample() {
super(APP_TITLE);
SwingUtilities.invokeLater(this::initComponents);
}
/**
* This method initializes the form.
*/
private void initComponents() {
Container parentContainer = getContentPane();
parentContainer.setLayout(new BorderLayout());
JButton btnExit = new AppExitButton(true);
createTable();
JPanel topPanel = new JPanel(new GridLayout(2, 1));
topPanel.add(btnExit);
topPanel.setBorder(EMPTY_BORDER);
JPanel lowerPanel = new JPanel(new BorderLayout());
JScrollPane jspCmds = new JScrollPane(tblCommands);
lowerPanel.add(jspCmds);
parentContainer.add(topPanel, BorderLayout.NORTH);
parentContainer.add(lowerPanel, BorderLayout.CENTER);
btnExit.addActionListener(evt -> exitForm());
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent evt) {
exitForm();
}
});
createAppMenu();
setPosition();
}
private final MyLogger logger = MyLogger.createLogger("rc.log");
private void createAppMenu() {
mbarSettings = new JMenuBar();
JMenu menuSettings = new JMenu("Settings");
menuSettings.add(getThemesMenu());
mbarSettings.add(menuSettings);
setJMenuBar(mbarSettings);
}
public UIManager.LookAndFeelInfo[] getAvailableLAFs() {
return UIManager.getInstalledLookAndFeels();
}
public JMenu getThemesMenu() {
JMenu menu = new JMenu("Theme");
int i = 'a';
int x = 0;
for (UIManager.LookAndFeelInfo l : getAvailableLAFs()) {
JMenuItem mi = new JMenuItem((char) i + SP_DASH_SP + l.getName());
if (i <= 'z') {
mi.setMnemonic(i);
}
int finalX = x;
mi.addActionListener(e -> applyTheme(finalX, l));
menu.add(mi);
i++;
x++;
}
return menu;
}
UIManager.LookAndFeelInfo themeToApply;
public void applyTheme(int idx, UIManager.LookAndFeelInfo lnf) {
themeToApply = lnf;
SwingUtilities.invokeLater(this::applyLnF);
}
public void applyLnF() {
try {
UIManager.setLookAndFeel(themeToApply.getClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
SwingUtilities.updateComponentTreeUI(this);
}
private void createTable() {
model = SwingUtils.getTableModel(new String[]{"Col1"});
createRows();
Border borderBlue = new LineBorder(Color.BLUE, 1);
tblCommands = new JTable(model);
}
private void createRows() {
for (int i = 1; i <= 10; i++) {
model.addRow(new String[]{"Row- " + i});
}
}
private void setPosition() {
// Setting to right most position
pack();
GraphicsConfiguration config = getGraphicsConfiguration();
Rectangle bounds = config.getBounds();
Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(config);
int x = bounds.x + bounds.width - insets.right - getWidth();
int y = bounds.y + insets.top + 10;
setLocation(x, y);
setVisible(true);
}
/**
* Exit the Application
*/
private void exitForm() {
setVisible(false);
dispose();
logger.dispose();
System.exit(0);
}
}
I also get the same results using JDK11 on Windows 10.
I suspect the issue is with the UIResource interface. Note, this is just a tagging interface, there are no actual methods to implement.
It is my understanding that this interface should be implemented on properties of Swing components. For example on the Font, Border, Color, Icon properties of the various component.
Then when you invoke SwingUtilities.updateComponentTreeUI(parentFrame) all the properties that implement the UIResource will be replaced with the respective property from the new LAF.
Check out UIManager Defaults. It will list all the properties of each Swing component.
You will see that for most LAF's the properties are in instance of FontUIResource, or ColorUIResource, etc.
However, for the Nimbus LAF many properties lack the "...UIResource".
So I would suggest this is a Nimbus issue and I have no idea how to fix it.
Edit:
hard to tell while debugging if this was a Metal LaF issue ... or a Nimbus issue
It is a Nimbus issue.
Download the code from the above link and then make the following change:
SwingUtilities.updateComponentTreeUI( rootPane );
System.out.println(table.getDefaultRenderer(Object.class));
Now toggle the LAF between non NImbus and you will see that the default table renderer for the non Nimbus LAF's contain UIResource in the class name which would indicate to me that they implement the UIResource interface.
Now toggle to the Nimbus LAF. The renderer does not have UIResource in the class name.
Now toggle back to any other LAF and the rendering is incorrect because the Nimbus renderer has not been replaced with the proper LAF renderer.
Note, this is not a LAF issue. It is designed to work this way. This allows you to create a custom renderer that can be used in all LAF (unless of course you tag the renderer with the UIResource interface).
For some reason the Nimbus developers appear to not have tagged the renderers with the UIResource interface so once they are set they are not changed with the rest of the LAF.
So another solution would be to create a "wrapper" renderer that simply wraps a default renderer and invoke its default renderering logic but would also implement the UIResource interface . Then you would need to replace each default Nimbus renderer with a wrapper renderer.
As discussed in comments and by #camickr, this is probably a bug with Nimbus or Metal LaF.
However I have made a workaround that you can use for now.
Essentially I override prepareRenderer of the JTable and check if the Metal LaF is being used ( and that it wasnt set on start up or it will paint 2 borders around the JTable) and if those conditions are met we simply set the border for each row to new MetalBorders.TableHeaderBorder():
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
JComponent component = (JComponent) super.prepareRenderer(renderer, row, column);
if (UIManager.getLookAndFeel().getName().equals("Metal") && !wasMetalOnStartup) {
component.setBorder(new MetalBorders.TableHeaderBorder());
}
return component;
}
TestApp.java:
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.util.UUID;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.EmptyBorder;
import javax.swing.plaf.metal.MetalBorders;
import javax.swing.plaf.metal.MetalLookAndFeel;
import javax.swing.plaf.metal.OceanTheme;
import javax.swing.plaf.nimbus.NimbusLookAndFeel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
public class TestApp {
private JTable table;
private boolean wasMetalOnStartup = false;
public TestApp() {
setNimbusLookAndFeel();
initComponents();
}
public static void main(String[] args) {
SwingUtilities.invokeLater(TestApp::new);
}
private void initComponents() {
wasMetalOnStartup = UIManager.getLookAndFeel().getName().equals("Metal");
JFrame frame = new JFrame("TestApp");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
panel.setBorder(new EmptyBorder(10, 10, 10, 10));
// setup chmahe LaF button
JButton refreshButton = new JButton("Change L&F");
refreshButton.addActionListener((ActionEvent e) -> {
try {
if (!UIManager.getLookAndFeel().getName().equals("Nimbus")) {
setNimbusLookAndFeel();
} else {
setMetalLookAndFeel();
}
SwingUtilities.updateComponentTreeUI(frame);
} catch (Exception ex) {
}
});
table = new JTable() {
private static final long serialVersionUID = 1L;
#Override
public boolean isCellEditable(int row, int column) {
return false;
}
#Override
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
JComponent component = (JComponent) super.prepareRenderer(renderer, row, column);
if (UIManager.getLookAndFeel().getName().equals("Metal") && !wasMetalOnStartup) {
component.setBorder(new MetalBorders.TableHeaderBorder());
}
return component;
}
};
// setup JTable and custom table model with intial data
Object[][] data = getRandomData();
String[] columnNames = {"Random Data"};
DefaultTableModel model = new DefaultTableModel(data, columnNames);
table.setModel(model);
table.getColumnModel().getColumn(0).setPreferredWidth(300);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
// add components to the panel
JScrollPane pane = new JScrollPane(table);
panel.add(pane, BorderLayout.CENTER);
panel.add(refreshButton, BorderLayout.SOUTH);
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
private void setNimbusLookAndFeel() {
try {
UIManager.setLookAndFeel(new NimbusLookAndFeel());
wasMetalOnStartup = false;
} catch (Exception ex) {
}
}
private void setMetalLookAndFeel() {
try {
MetalLookAndFeel.setCurrentTheme(new OceanTheme());
UIManager.setLookAndFeel(new MetalLookAndFeel());
wasMetalOnStartup = false;
} catch (Exception ex) {
}
}
private Object[][] getRandomData() {
Object[][] data = {{UUID.randomUUID()}, {UUID.randomUUID()}, {UUID.randomUUID()}, {UUID.randomUUID()}};
return data;
}
}
Got an email reply to bug I raised from Oracle regarding this issue and they also able to reproduce it as a bug.
JDK-8258567.
Link : http://bugs.java.com/bugdatabase/view_bug.do?bug_id=JDK-8258567
I'm making a frame who needs to show labels in a Scroll Panel, but after I add the labels the scroll don't work.
JScrollPane scrollPane = new JScrollPane();
scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setBounds(1, 1, 210, 259);
panel.add(scrollPane);
JPanel roomList = new JPanel();
scrollPane.setViewportView(roomList);
roomList.setLayout(null);
int x=0;
for(String l : list) {
JLabel c = new JLabel(l+" "+x);
c.setBounds(new Rectangle(1, 1+x*11, 191, 14));
roomList.add(c);
x++;
}
I'm sure the list has more than 22.
I don't know how to google it!
Your basic problem is, you don't understand how the layout management API works, or how to replace it's functionality when you choose to discard it.
You problem starts here:
roomList.setLayout(null);
There's a lot of work going on in the background which provides a great deal of information to various parts of the API, while on the surface, the layout management API is not complex, the role it plays is.
The JScrollPane will use the component's preferredSize to determine when it should display the scrollbars. Since you've done away with this automated calculation, the JScrollPane has nothing to go on
For more information, have a look at Laying Out Components Within a Container
As a simple example...
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Rectangle;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JScrollPane scrollPane = new JScrollPane(new TestPane());
frame.add(scrollPane);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel implements Scrollable {
public TestPane() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
for (int index = 0; index < 100; index++) {
add(new JLabel("Row " + index), gbc);
gbc.gridy++;
}
}
#Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(100, 50);
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 32;
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 32;
}
#Override
public boolean getScrollableTracksViewportWidth() {
return getPreferredSize().width <= getWidth();
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}
}
This example implements the Scrollable interface, this is not always required, but is used, for this example, to provide a hint to the JScrollPane about the preferred size of the viewable area it should use, otherwise it will attempt to use the component's preferredSize.
But, as has already been suggested, there are other, simpler and more optimised solutions available to you.
If your information is simple enough, you can use a JList to list a number of values in a vertical manner, see How to use Lists for more details.
If you information is in a more complex structure, you could use a JTable, which provides a row and column style structure. See How to use tables for more information
Have you tried using jLists instead of JScrollPanes ?
They're very easily implemented, look great and work like a charm.
DefaultListModel model = new DefaultListModel();
for(String l : list) {
model.addElement(l);
}
yourList.setModel(model);
Where list is the list with the room data and yourList is the jList.
OK so I've got a JPanel with a GridLayout. Each cell of the grid then contains another JPanel.
What I'd like to be able to do is have a listener on the "underneath" JPanel which then tells me which of the "overlayed" JPanels was clicked - so I can react to it and the surrounding ones, without making the covering JPanels aware of their position (they change!!)
Is there a way of doing this - similar to Determine clicked JPanel component in the MouseListener. Event handling but I couldn't find a way of grabbing the component on top.
I could probably grab the co-oridnates and work it out using that info - but I'd rather not!!
Any help/pointers/tips would be appreciated :D
Do the same thing but use getParent() on the source. Or you can search up the hierarchy if it is deeper, even some helper methods for that:
javax.swing.SwingUtilities.getAncestorOfClass and getAncestorNamed
use putClientProperty / getClientProperty, nothing simplest around ..., you can put endless numbers of ClientProperty to the one Object
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.border.LineBorder;
public class MyGridLayout {
public MyGridLayout() {
JPanel bPanel = new JPanel();
bPanel.setLayout(new GridLayout(10, 10, 2, 2));
for (int row = 0; row < 10; row++) {
for (int col = 0; col < 10; col++) {
JPanel b = new JPanel();
System.out.println("(" + row + ", " + col + ")");
b.putClientProperty("column", row);
b.putClientProperty("row", col);
b.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
JPanel btn = (JPanel) e.getSource();
System.out.println("clicked column " + btn.getClientProperty("column")
+ ", row " + btn.getClientProperty("row"));
}
});
b.setBorder(new LineBorder(Color.blue, 1));
bPanel.add(b);
}
}
JFrame frame = new JFrame("PutClientProperty Demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(bPanel);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
MyGridLayout myGridLayout = new MyGridLayout();
}
});
}
}
I have a Swing app with a large panel which is wrapped in a JScrollPane. Users normally move between the panel's subcomponents by tabbing, so when they tab to something out view, I want the scroll pane to autoscroll so the component with input focus is always visible.
I've tried using KeyboardFocusManager to listen for input focus changes, and then calling scrollRectToVisible.
Here's an SSCCE displaying my current strategy (just copy/paste and run!):
import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
KeyboardFocusManager.getCurrentKeyboardFocusManager()
.addPropertyChangeListener("focusOwner",
new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
focused.scrollRectToVisible(focused.getBounds());
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
If you run this example, you'll notice it doesn't work very well. It does get the focus change notifications, but the call to scrollRectToVisible doesn't appear to have any effect. In my app (which is too complex to show here), scrollRectToVisible works about half the time when I tab into something outside of the viewport.
Is there an established way to solve this problem? If it makes any difference, the Swing app is built on Netbeans RCP (and most of our customers run Windows).
My comment to the other answer:
scrollRectToVisible on the component itself is the whole point of that
method ;-) It's passed up the hierarchy until a parent doing the
scroll is found
... except when the component itself handles it - as JTextField does: it's implemented to scroll horizontally to make the caret visible. The way out is to call the method on the field's parent.
Edit
just for clarity, the replaced line is
content.scrollRectToVisible(focused.getBounds());
you have to take Rectangle from JPanel and JViewPort too, then compare, for example
notice (against down-voting) for final and nice output required some work for positions in the JViewPort
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
//http://stackoverflow.com/questions/8245328/how-do-i-make-jscrollpane-scroll-to-follow-input-focus
public class FollowFocus {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
final int ROWS = 100;
final JPanel content = new JPanel();
content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
content.add(new JLabel(
"Thanks for helping out. Use tab to move around."));
for (int i = 0; i < ROWS; i++) {
JTextField field = new JTextField("" + i);
field.setName("field#" + i);
content.add(field);
}
final JScrollPane scroll = new JScrollPane(content);
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addPropertyChangeListener("focusOwner", new PropertyChangeListener() {
#Override
public void propertyChange(PropertyChangeEvent evt) {
if (!(evt.getNewValue() instanceof JComponent)) {
return;
}
JViewport viewport = (JViewport) content.getParent();
JComponent focused = (JComponent) evt.getNewValue();
if (content.isAncestorOf(focused)) {
System.out.println("Scrolling to " + focused.getName());
Rectangle rect = focused.getBounds();
Rectangle r2 = viewport.getVisibleRect();
content.scrollRectToVisible(new Rectangle(rect.x, rect.y, (int) r2.getWidth(), (int) r2.getHeight()));
}
}
});
JFrame window = new JFrame("Follow focus");
window.setContentPane(new JScrollPane(content));
window.setSize(200, 200);
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
window.setVisible(true);
}
});
}
}
Here my short summary.
Add this to your Tools class:
public static void addOnEnter(Component c, Consumer<FocusEvent> onEnter) {
FocusListener fl = new FocusListener() {
#Override
public void focusGained(FocusEvent e) {
onEnter.accept(e);
}
#Override
public void focusLost(FocusEvent e) { }
};
c.addFocusListener(fl);
}
public static void scrollToFocus(FocusEvent e) {
((JComponent) e.getComponent().getParent()).scrollRectToVisible(
e.getComponent().getBounds());
}
and use it like this:
Tools.addOnEnter(component, Tools::scrollToFocus);
component can be JTextField, JButton, ...
One major issue in your code is:
focused.scrollRectToVisible(focused.getBounds());
You are calling scrollRectToVisible on the component itself! Presumably a typo.
Make your JScrollPane a final variable and call
scrollPane.getViewport().scrollRectToVisible(focused.getBounds());
Here jtextbox is the component you want to focus and jscrollpane is your scrollpane:
jScrollpane.getVerticalScrollBar().setValue(jtextbox.getLocation().x);