Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scroll horizontally in JTable with Nimbus look and feel

I have a JTable that is wider than the JScrollPane it is contained in (essentially defined like this):

JTable table = new JTable(model);
// I change some things like disallowing reordering, resizing,
// disable column selection, etc.

// I set the default renderer to a DefaultTableCellRenderer
// getTableCellRendererComponent, and then changes the color
// of the cell text depending on the cell value

JPanel panel = new JPanel(new BorderLayout(0, 5));
panel.add(new JScrollPane(table), BorderLayout.CENTER);
// add other stuff to the panel
this.add(panel,  BorderLayout.CENTER);

Before I changed the look and feel from the default to Nimbus, I was able to scroll left and right in the JTable. (I like the Mac LaF, but it isn't supported on Windows, and the Windows LaF is ugly in my opinion),

I took the following code straight from the Java Tutorials:

try {
    for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
        if ("Nimbus".equals(info.getName())) {
            UIManager.setLookAndFeel(info.getClassName());
            break;
        }
    }
} catch (Exception e) {
    // If Nimbus is not available, you can set the GUI to another look
    // and feel.
}

I recompiled and ran the code without changing any of the table definition stuff above, and I couldn't scroll horizontally in the JTable anymore.

I can't seem to find anything on what would cause this. Is this the normal behavior for Nimbus, or can I change it? If so, how? or should I just try a different look and feel?

EDIT:

I discovered two things:

  1. I made a new class extending JTable to test this. I copied the code for getScrollableUnitIncrement from the JTable source, and added print statements. The orientation that is passed seems to always be SwingConstants.VERTICAL, while in the default Look and Feel (Mac Aqua or whatever), both horizontal and vertical scrolling works. I don't know why this is.

  2. Another part of the project also relies on horizontal scrolling. I tested it with both LaFs, and it worked fine in the default, but Nimbus would not allow me to scroll horizontally, either.

Could this be a bug with Nimbus?

Either way, I guess I'm going to use a different Look and Feel...

EDIT #2:

I should have mentioned this before. I am able to scroll horizontally with the scroll bar in the window, but not with my track pad or scroll wheel on my mouse.

like image 535
Matthew Denaburg Avatar asked Oct 08 '22 05:10

Matthew Denaburg


2 Answers

(Note: After writing this, I found a solution, which appears in the addendum of this post.)

To reproduce the problem, you need to make the scroll bars required. (This is why some people have trouble reproducing this bug.) This means the obvious workaround is to make your horizontal scroll bar optional. (This is not always practical.)

You will only see the bug when you drag the window's width out to more than 1200 pixels or so. Until then, the scroll bar will work fine.

And the problem only shows up in Nimbus. (It may show up in other L&Fs created from the SynthLookAndFeel, but I haven't investigated that yet.)

I've found that the spurious scroll bar thumb only shows up when you have no need to scroll, so it's just a visual bug. When you need to scroll, the scroll bar thumb will appear and will work properly, although it might not be the right size. This may be why it hasn't been fixed yet.

Here's an example where you can compare the different L&Fs. In this example, Choose Nimbus, then drag the width inward and watch how the size of the scroll bar changes. When you're wider than the background image, the spurious scroll bar will be visible. As soon as you get narrower, a valid scroll bar thumb will appear, but it will be a bit too small. As you get smaller, the scroll bar thumb will stay a constant size until you reach a certain point, (at viewport width of 1282 pixels) then it will start getting smaller like it's supposed to.

With any other L&F, as soon as you get narrower than the background image, a thumb will appear that almost fills its space. It gets smaller as the window gets smaller, like it's supposed to.

(This exercise will also reveal how Nimbus draws much more slowly than any other L&F.)

You can observe different, but still incorrect behavior by making the icon smaller. Try 800 x 450. The spurious scroll bar will appear when the viewport width is > 1035. (Viewport size is shown at the bottom of the window.)

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;
/**
 * NimbusScrollBug
 * <p/>
 * @author Miguel Muñoz
 */
public class NimbusScrollBug extends JPanel {
  private static final long serialVersionUID = -4235866781219951631L;
  private static JFrame frame;
  private static boolean firstTime = true;
  private static Point location;
  private static final UIManager.LookAndFeelInfo[] INFOS
          = UIManager.getInstalledLookAndFeels();

  private final JLabel viewPortLabel = new JLabel();

  public static void main(final String[] args) {
    makeMainFrame(new NimbusScrollBug(), "System");
  }

  public static void makeMainFrame(final NimbusScrollBug mainPanel,
                                   final String name) {
    if (firstTime) {
      installLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
    frame = new JFrame(name);
    final Container contentPane = frame.getContentPane();
    contentPane.setLayout(new BorderLayout());
    contentPane.add(mainPanel, BorderLayout.CENTER);
    contentPane.add(makeButtonPane(mainPanel), BorderLayout.LINE_START);
    frame.setLocationByPlatform(firstTime);
    frame.pack();
    frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
    frame.setVisible(true);
    if (firstTime) {
      location = frame.getLocation();
    } else {
      frame.setLocation(location);
    }
    frame.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentMoved(final ComponentEvent e) {
        location = e.getComponent().getLocation();
      }
    });
    firstTime = false;
  }

  private static JPanel makeButtonPane(final NimbusScrollBug mainPanel) {
    JPanel innerButtonPanel = new JPanel(new GridBagLayout());
    GridBagConstraints constraints = new GridBagConstraints();
    constraints.fill = GridBagConstraints.BOTH;
    constraints.gridx = 0; // forces vertical layout.
    for (final UIManager.LookAndFeelInfo lAndF : INFOS) {
      final JButton button = new JButton(lAndF.getName());
      button.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(final ActionEvent e) {
          frame.dispose();
          installLookAndFeel(lAndF.getClassName());
          makeMainFrame(new NimbusScrollBug(), lAndF.getName());
        }
      });
      innerButtonPanel.add(button, constraints);
    }
    final String version = System.getProperty("java.version");
    JLabel versionLabel = new JLabel("Java Version " + version);
    innerButtonPanel.add(versionLabel, constraints);

    JPanel outerButtonPanel = new JPanel(new BorderLayout());
    outerButtonPanel.add(innerButtonPanel, BorderLayout.PAGE_START);
    return outerButtonPanel;
  }

  private static void installLookAndFeel(final String className) {
    //noinspection OverlyBroadCatchBlock
    try {
      UIManager.setLookAndFeel(className);
    } catch (Exception e) {
      //noinspection ProhibitedExceptionThrown
      throw new RuntimeException(e);
    }
  }

  private NimbusScrollBug() {
    Icon icon = new Icon() {
      @Override
      public void paintIcon(final Component c, final Graphics g, 
                            final int x, final int y) {
        Graphics2D g2 = (Graphics2D) g;
        g2.translate(x, y);
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, 
                RenderingHints.VALUE_ANTIALIAS_ON);
        Stroke lineStroke = new BasicStroke(6.0f);
        g2.setStroke(lineStroke);
        g2.setColor(Color.white);
        g2.fillRect(0, 0, getIconWidth(), getIconHeight());
        g2.setColor(Color.RED);
        g2.drawLine(0, 0, getIconWidth(), getIconHeight());
        g2.drawLine(0, getIconHeight(), getIconWidth(), 0);
        g2.dispose();
      }

      @Override
      public int getIconWidth() {
        return 1600;
      }

      @Override
      public int getIconHeight() {
        return 900;
      }
    };
    JLabel label = new JLabel(icon);
    setLayout(new BorderLayout());
    final JScrollPane scrollPane = new JScrollPane(label,
            ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
    label.addHierarchyBoundsListener(new HierarchyBoundsAdapter() {
      @Override
      public void ancestorResized(final HierarchyEvent e) {
        viewPortLabel.setText("ViewPort Size: " 
                + scrollPane.getViewport().getSize());
      }
    });
    add(scrollPane, BorderLayout.CENTER);
    add(viewPortLabel, BorderLayout.PAGE_END);
  }
}

Addendum: Further investigation revealed the problem. The NimbusDefaults class, which creates the UIDefaults instance for Nimbus, has this line:

d.put("ScrollBar.maximumThumbSize", new DimensionUIResource(1000, 1000));

Any other look and feel uses 4096 for both values (so, for really big monitors, they will show the same behavior).

The following method, which may be used to install any look and feel, will fix this problem:

private static void installLookAndFeel(final String className) {
  //noinspection OverlyBroadCatchBlock
  try {
    Class<?> lnfClass = Class.forName(className, true,
            Thread.currentThread().getContextClassLoader());
    final LookAndFeel lAndF;
    lAndF = (LookAndFeel) lnfClass.getConstructor().newInstance();

    // Reset the defaults after instantiating, but before
    // calling UIManager.setLookAndFeel(). This fixes the Nimbus bug
    DimensionUIResource dim = new DimensionUIResource(4096, 4096);
    lAndF.getDefaults().put("ScrollBar.maximumThumbSize", dim);
    UIManager.setLookAndFeel(lAndF);
  } catch (Exception e) {
    final String systemName = UIManager.getSystemLookAndFeelClassName();
    // Prevents an infinite recursion that's not very likely...
    // (I like to code defensively)
    if (!className.equals(systemName)) {
      installLookAndFeel(systemName);
    } else {
      // Feel free to handle this any other way.
      //noinspection ProhibitedExceptionThrown
      throw new RuntimeException(e);
    }
  }
}

Of course, you can fix the problem for really big monitors by using a bigger value.

I confirmed that the vertical scroll bar has exactly the same problem, but is only seen when the window gets very large vertically. This is why this problem is usually only seen with the horizontal scroll bar.

like image 86
3 revs Avatar answered Oct 12 '22 12:10

3 revs


Based on the information you provided, I'm not able to recreate your problem (and therefore not able to help you figure out what's going wrong). Here's a sscce that works for me. Can you reproduce the problem with this example? Perhaps the problem is trickling down from a different part of the application.

public static void main(String[] args){
    try {
        for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
            if ("Nimbus".equals(info.getName())) {
                UIManager.setLookAndFeel(info.getClassName());
                break;
            }
        }
    } catch (Exception e) {
        // If Nimbus is not available, you can set the GUI to another look and feel.
    }

    //Create Frame
    JFrame frame = new JFrame("Title");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Create Table
    JTable table = new JTable(0, 2);
    ((DefaultTableModel) table.getModel()).addRow(new Object[]{"Sample Text", "Hi Mom!"});
    table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);

    // Wrap table in Scroll pane and add to frame
    frame.add(new JScrollPane(table), BorderLayout.CENTER);

    // Finish setting up the frame and display
    frame.setBounds(0, 0, 600,400);
    frame.setPreferredSize(new Dimension(600, 400));
    frame.pack();       
    frame.setVisible(true);
}
like image 24
Nick Rippe Avatar answered Oct 12 '22 10:10

Nick Rippe