I have a situation like this.
I have scrollpane whose viewportView is a JPanel
And that JPanel has the layout as BoxLayout. In this panel I add one class which extends JPanel and that class contains JComponents.
So while running an application, the JComponents are shown in the JScrollPane.
This is how my ScrollPane is formed.
The problem here is, When the data exceeds more than around 750 rows, The scrollbar starts giving problems.
When scrolling up or down by mouse wheel, scroll doesnot move smoothly, It suddenly stops in the middle and again starts, say it has a jerky movement.
my Question is how can i get the smooth mouse movement in this scenario.
My scrollPane is like this
public JScrollPane getScrollPane() {
if (scrollPane == null) {
scrollPane = new JScrollPane();
scrollPane.setSize(new Dimension(1000, 433));
scrollPane.setLocation(new Point(10, 10));
scrollPane.setColumnHeaderView(getHeaderOfRowPanel());
scrollPane
.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
scrollPane.setViewportView(getScrollPanel());
scrollPane
.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.getVerticalScrollBar().setUnitIncrement(
unitIncrement);
}
return scrollPane;
}
private JPanel getScrollPanel() {
if (scrollPanel == null) {
scrollPanel = new JPanel();
scrollPanel.setBorder(null);
scrollPanel.setLayout(new BoxLayout(getScrollPanel(),
BoxLayout.Y_AXIS));
}
return scrollPanel;
}
private class RowPanel extends JPanel {
//My components are here ..
//I add this Panel in scrollPanel
}
Have look at JScrollBar.setUnitIncrement, beacuse bunch of JPanels in the JScollPane has un_natural scrolling in compare with JList, JTable or JTextArea
example
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class JScrollBarUnitIncrement {
public static void main(String[] args) {
final JFrame f = new JFrame("");
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(2000, 1));
for (int i = 0; i != 2000; i++) {
JButton btn = new JButton("Button 2");
panel.add(btn);
}
final JScrollPane sPane = new JScrollPane(panel);
final int increment = 50;
sPane.getVerticalScrollBar().setUnitIncrement(increment);
KeyStroke kUp = KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0);
KeyStroke kDown = KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0);
sPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(kUp, "actionWhenKeyUp");
sPane.getActionMap().put("actionWhenKeyUp", new AbstractAction("keyUpAction") {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final JScrollBar bar = sPane.getVerticalScrollBar();
int currentValue = bar.getValue();
bar.setValue(currentValue - increment);
}
});
sPane.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(kDown, "actionWhenKeyDown");
sPane.getActionMap().put("actionWhenKeyDown", new AbstractAction("keyDownAction") {
private static final long serialVersionUID = 1L;
public void actionPerformed(ActionEvent e) {
final JScrollBar bar = sPane.getVerticalScrollBar();
int currentValue = bar.getValue();
bar.setValue(currentValue + increment);
}
});
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(sPane);
f.pack();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
f.setVisible(true);
}
});
}
private JScrollBarUnitIncrement() {
}
}
It is never good to populate such huge no. of rows in JScrollPane. Because, the visible portion is only around let's say 20 to 30 rows in viewport depending on the height of the scrollpane and the height of your RowPanel. So, why to populate such huge rows at once ? The problem with the smoothness is because there might be exception (see the console ). So, resolve this, I see two options for you. One is to use pagination and another is to allow users to enter some search criteria to filter out the unwanted records.
As #mKorbel notes, both JTable and JList implement Scrollable for convenient scroll increments, and they both use the flyweight pattern for rendering speed. If you can't use either component directly, you can still use the patterns. The tutorial includes Scrollable examples, and there's a CellRendererPane example here.
Related
I've spent a while creating an sscce from my larger program, I hope it's small enough!
I've a JSplitPane with a table on top, and below is a JPanel.
The bottom panel contains smaller JPanels, or 'entries'. As the number of entries grows, the bottom SplitPane takes up the space of the top pane.
Dimension dim = getPreferredSize();
setPreferredSize(dim);
In the first class, uncommenting this code solves the problem, but I've no idea why. I'd like to understand how to deal with resizing better if anyone can help? (I've also got to deal with the text area expanding horizontally, which is one of the reasons some of the constraints might look a little odd)
Here's the bottom panel:
package example;
import java.awt.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
import javax.swing.border.Border;
public class BottomPanel extends JPanel{
private JLabel summary = new JLabel();
private EntryPanel entryPanel = new EntryPanel();
public BottomPanel() {
//Dimension dim = getPreferredSize();
//setPreferredSize(dim);
setLayout(new GridBagLayout());
entryPanel.setBorder(BorderFactory.createLineBorder(Color.RED));
setComponents();
}
private void setComponents() {
Border test = BorderFactory.createLineBorder(Color.BLUE);
JScrollPane jsp = new JScrollPane(entryPanel);
JPanel dummy1 = new JPanel();
JPanel dummy2 = new JPanel();
JPanel dummy3 = new JPanel();
dummy1.setBorder(test);
dummy2.setBorder(test);
dummy3.setBorder(test);
int row = 0;
add(dummy1, new GBC(0,row).setWeight(0,0));
// new row
row++;
add(summary, new GBC(1,row).setAnchor(GBC.LINE_START));
// new row
row++;
add(jsp, new GBC(1,row).setWeight(50,70).setFill(GBC.BOTH));
// new row
row++;
add(dummy2, new GBC(2,row).setWeight(0,0));
}
private class EntryPanel extends JPanel{
private List<JPanel> entries = new ArrayList<>();
private int row = 0;
private EntryPanel(){
setLayout(new GridBagLayout());
for(int x = 0; x < 20; x++)
createEntryItem("TEST", "test text");
}
private void createEntryItem(String s, String text){
Border test = BorderFactory.createLineBorder(Color.GREEN);
JPanel entryItem = new JPanel();
entryItem.setBorder(test);
entryItem.setLayout(new GridBagLayout());
JLabel title = new JLabel(s);
JTextArea details = new JTextArea(text);
entryItem.add(title, new GBC(0,0).setAnchor(GBC.LINE_START));
entryItem.add(details, new GBC(0,1).setAnchor(GBC.LINE_START));
entryItem.add(new JPanel(), new GBC(1,0).setWeight(100,0));
entries.add(entryItem);
displayEntry(entryItem);
}
private void displayEntry(JPanel entry){
JPanel dummy = new JPanel();
add(entry, new GBC(0,row).setAnchor(GBC.LINE_START).setFill(GBC.HORIZONTAL));
add(dummy, new GBC(1,row).setWeight(100, 0));
row++;
}
}
}
and the containing frame and panel:
package example;
import java.awt.*;
import javax.swing.*;
public class Example extends JFrame{
private GraphicsDevice gd = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice();
private int width = (int)((gd.getDisplayMode().getWidth())*0.5);
private int height = (int)((gd.getDisplayMode().getHeight())*0.75);
private JPanel mainPanel = new JPanel();
private JSplitPane sp;
private JTable table = new JTable();
private BottomPanel bp = new BottomPanel();
public Example () {
setSize(width,height);
setLayout(new BorderLayout());
sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,new JScrollPane(table), bp);
add(sp);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setVisible(true);
}
public static void main(String[] args){
SwingUtilities.invokeLater(new Runnable(){
public void run(){
Example ex = new Example();
}
});
}
}
and the GridBagHelper:
package example;
import java.awt.GridBagConstraints;
public class GBC extends GridBagConstraints{
public GBC(int gridx, int gridy){
this.gridx = gridx;
this.gridy = gridy;
}
public GBC setAnchor(int anchor){
this.anchor = anchor;
return this;
}
public GBC setFill(int fill){
this.fill = fill;
return this;
}
public GBC setWeight(double weightx, double weighty){
this.weightx = weightx;
this.weighty = weighty;
return this;
}
}
Any help or advice would be really appreciated - I'm self taught and am struggling a fair bit!
Thanks
I'm too tired to write a proper answer, but I'm glancing through this and one of the first issues I see is setSize(width,height) inside the constructor for Example. It's better to call pack() on the JFrame and let it size everything itself automatically.
I think pack can be a bit unpredictable with a JScrollPane, though. From memory, I've had it do some weird things like collapse the scroll pane to its minimum size.
Using pack on your code example seems to attempt to size it around the sum of the panels in BottomPanel. (The sum would be taller than my screen resolution, though, so I can't tell what the exact behavior is. It caps the height of the window at the height of my screen.)
A more elegant way to do what you're trying to do is like this:
public Example () {
// setSize(width,height);
// setLayout(new BorderLayout());
sp = new JSplitPane(JSplitPane.VERTICAL_SPLIT,new JScrollPane(table), bp);
sp.setResizeWeight(2.0 / 3.0);
sp.setPreferredSize(width, height);
setContentPane(sp);
// add(sp);
pack();
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setVisible(true);
}
I used setResizeWeight to achieve the split pane proportions and set the preferred size of the pane instead of the window.
This is pretty similar to the result that you like but it's a little more proper. It could be that one of the more experienced Swing users here knows a better way to use JScrollPane which e.g. doesn't involve an explicit setPreferredSize.
I guess my question is why getting the preferredSize, and then setting the preferredSize makes a difference?
If we dig down through the code, you'll find...
public void setPreferredSize(Dimension preferredSize) {
Dimension old;
// If the preferred size was set, use it as the old value, otherwise
// use null to indicate we didn't previously have a set preferred
// size.
if (prefSizeSet) {
old = this.prefSize;
}
else {
old = null;
}
this.prefSize = preferredSize;
prefSizeSet = (preferredSize != null);
firePropertyChange("preferredSize", old, preferredSize);
}
You can the have a look at how getPreferredSize works...
public boolean isPreferredSizeSet() {
return prefSizeSet;
}
/**
* Gets the preferred size of this component.
* #return a dimension object indicating this component's preferred size
* #see #getMinimumSize
* #see LayoutManager
*/
public Dimension getPreferredSize() {
return preferredSize();
}
/**
* #deprecated As of JDK version 1.1,
* replaced by <code>getPreferredSize()</code>.
*/
#Deprecated
public Dimension preferredSize() {
/* Avoid grabbing the lock if a reasonable cached size value
* is available.
*/
Dimension dim = prefSize;
if (dim == null || !(isPreferredSizeSet() || isValid())) {
synchronized (getTreeLock()) {
prefSize = (peer != null) ?
peer.getPreferredSize() :
getMinimumSize();
dim = prefSize;
}
}
return new Dimension(dim);
}
This will return the user set preferredSize if it was set, over calculating its own.
surely i'm assigning a value that was already in place?
But you call setPreferredSize before any components are added to the container
If you change the code to something like...
Dimension dim = getPreferredSize();
System.out.println(dim);
setPreferredSize(dim);
it will print out something like java.awt.Dimension[width=10,height=10], so, yeah, small...
But, if you use something like...
public BottomPanel() {
setLayout(new GridBagLayout());
entryPanel.setBorder(BorderFactory.createLineBorder(Color.RED));
setComponents();
Dimension dim = getPreferredSize();
System.out.println(dim);
setPreferredSize(dim);
}
it prints out java.awt.Dimension[width=105,height=710]
The basic moral to the story is, don't call setPreferred/Minimum/MaximumSize unless you have a very, very good reason to and even then, consider overriding getPreferredSize instead. It's no one else's responsibility to calculate the size of the component, but the componet itself
I'm trying to understand how Java.awt works (we need to create a GUI without GUI editor)
the following code does not show 2 TextAreas:
Frame fr = new Frame("Parser");
Panel buttons = new Panel();
Panel inputText = new Panel();
Panel outputText = new Panel();
String here = new String ("Insert code here...");
TextArea input = new TextArea(here, 9, 96, TextArea.SCROLLBARS_VERTICAL_ONLY);
TextArea output = new TextArea(here, 9,96,TextArea.SCROLLBARS_VERTICAL_ONLY);
public Window(){
fr.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent we) {
fr.dispose();
}
}
);
fr.setSize(700, 400);
fr.setLocation(200,100);
fr.setResizable(false);
fr.add(buttons);
fr.add(inputText);
fr.add(outputText);
buttons.setBounds(new Rectangle(0,0,700,60));
buttons.setBackground(new Color(200,200,200));
inputText.setBounds(new Rectangle(0,60,700,170));
inputText.setBackground(new Color(255,255,255));
inputText.add(input);
outputText.setBounds(new Rectangle(0,230,700,170));
outputText.setBackground(new Color(200,200,200));
outputText.add(output);
}
Obtained result:
Expected result:
Your code does not respect the layout managers that your containers are using. I believe that AWT Frames use a BorderLayout by default (edit: yes they do, per the Frame API. Suggestions:
In general avoid AWT for Swing which has much greater power and flexibility, although it too is showing its age, just less so than AWT.
Read up on and use layout managers in a smart way to do your heavy lifting for you. Here it looks like a BoxLayout could help you.
Avoid use of null layouts. While yes, that could offer you a quick and easy fix for your current code, it leads to the creation of very inflexible GUI's that while they might look good on one platform look terrible on most other platforms or screen resolutions and that are very difficult to update and maintain.
Avoid setting bounds, sizes or locations of any components, and again let the components and their container's layout managers set the sizes for you.
The Layout Manager Tutorial
For example:
import java.awt.BorderLayout;
import java.awt.GridLayout;
import javax.swing.*;
public class MyWindow extends JPanel {
private static final int ROWS = 10;
private static final int COLS = 50;
private static final String[] BUTTON_NAMES = { "Monday", "Tuesday",
"Wednesday", "Thursday", "Friday" };
private static final int GAP = 3;
private JTextArea inputTextArea = new JTextArea(ROWS, COLS);
private JTextArea outputTextArea = new JTextArea(ROWS, COLS);
public MyWindow() {
JPanel buttonPanel = new JPanel(new GridLayout(1, 0, GAP, 0));
for (String btnName : BUTTON_NAMES) {
buttonPanel.add(new JButton(btnName));
}
outputTextArea.setFocusable(false);
outputTextArea.setEditable(false);
setBorder(BorderFactory.createEmptyBorder(GAP, GAP, GAP, GAP));
setLayout(new BoxLayout(this, BoxLayout.PAGE_AXIS));
add(buttonPanel);
add(putInTitledScrollPane(inputTextArea, "Input Text"));
add(putInTitledScrollPane(outputTextArea, "Output Text"));
}
private JPanel putInTitledScrollPane(JComponent component,
String title) {
JPanel wrapperPanel = new JPanel(new BorderLayout());
wrapperPanel.setBorder(BorderFactory.createTitledBorder(title));
wrapperPanel.add(new JScrollPane(component));
return wrapperPanel;
}
private static void createAndShowGui() {
MyWindow mainPanel = new MyWindow();
JFrame frame = new JFrame("MyWindow");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.getContentPane().add(mainPanel);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
Which displays as:
Use of layout managers gives you much greater ease when it comes to changing or enhancing your GUI. For example, since I'm setting my JTextArea's width with a COL constant, if I change the COL constant, the whole GUI widens, even the buttons and the button JPanel, since the layout managers are handling all the sizing. With your code, you'd have to manually change the width of every component added to the GUI, which is prone to bug creation.
Because you are manually laying out your components, you are needed to set layout to null (setLayout(null);)
so before adding any component add this line in your code.
fr.setLayout(null);
Now you will get this :
I've looked around a while and also played around trying to add multiple panels to a JTabbedPane.
My question is: Is it possible to add the same Jpanel to multiple TabbedPanes. Everything way that I tried, it doesn't seem to work correctly. This is how it it works.
public MainGUI() {
JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.TOP);
getContentPane().add(tabbedPane, BorderLayout.CENTER);
JEditorPane instructionalEditorPane = new JEditorPane();
tabbedPane.addTab("Instructional", instructionalEditorPane);
JPanel codePanel = new JPanel();
JPanel drawPanel = new JPanel();
JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, codePanel, drawPanel);
splitPane.setResizeWeight(0.75);
tabbedPane.addTab("Code Panel", splitPane);
JEditorPane unifiedInstPane = new JEditorPane();
JPanel unifiedCodePanel = new JPanel();
JPanel unifiedDrawPanel = new JPanel();
JSplitPane unifiedSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, unifiedCodePanel, unifiedDrawPanel);
unifiedSplitPane.setResizeWeight(0.75);
JSplitPane unifiedPanel = new JSplitPane(JSplitPane.VERTICAL_SPLIT,unifiedInstPane, unifiedSplitPane);
unifiedPanel.setResizeWeight(0.40);
tabbedPane.addTab("Unified Tab", unifiedPanel);
}
What I would like to do is just add the instructionalEditorPane and the splitPane to multiple tabbedPanes but when I do I loose the original Individual tabbedPanes. If I have to I can do it this way but I would then have to write to both the unifiedInstPane & the instructionalEditorPane to keep them updated. I would also have to do this for the 2 splitPanes which have the codePanel and drawPanels embedded. This will make it harder to keep all the panels in sync.
Any suggestions?
"Is it possible to add the same Jpanel to multiple TabbedPanes." -- no. You can only add a component to one container at a time. Your JPanels should share models but use unique components. The model will likely be a non-GUI class of your creation.
For example, here's a very simplistic rendering of my recommendations:
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import javax.swing.text.PlainDocument;
public class MainGui2 extends JPanel {
private static final int TAB_COUNT = 3;
private JTabbedPane tabbedPane = new JTabbedPane();
private PlainDocument doc = new PlainDocument();
private Action btnAction = new ButtonAction("Button");
public MainGui2() {
for (int i = 0; i < TAB_COUNT; i++) {
tabbedPane.add("Tab " + (i + 1), createPanel(doc, btnAction));
}
setLayout(new BorderLayout());
add(tabbedPane);
}
private JPanel createPanel(PlainDocument doc, Action action) {
JTextArea textArea = new JTextArea(doc);
textArea.setColumns(40);
textArea.setRows(20);
JPanel panel = new JPanel();
panel.add(new JScrollPane(textArea));
panel.add(new JButton(action));
return panel;
}
private class ButtonAction extends AbstractAction {
public ButtonAction(String title) {
super(title);
}
#Override
public void actionPerformed(ActionEvent evt) {
try {
String text = "Button Pressed!\n";
doc.insertString(doc.getLength(), text, null);
} catch (BadLocationException e) {
e.printStackTrace();
}
}
}
private static void createAndShowGui() {
JFrame frame = new JFrame("MainGui2");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new MainGui2());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGui();
}
});
}
}
Better would be to create a formal model class that gets injected into each view, each tabbed pane's individual panes.
Edit
You state in comment:
Yes I can fix that by making calls to the instances but then I'm back to my original problem of having to make calls to each instance to affect a change in all the panel. Say for example I have a drawing panel and I need to call repaint(), I would have to make a call to 2 different instances to get both tabbedPanes to update. Is there any way around this?
Yes, the solution is to use an MVC, or model-view-control, structure. Your model holds your overall program logic, the views are what the user sees, and the control interacts between the two.
Consider having your model notify either the control or the views that its been changed, and then this stimulates a repaint an all observer views.
I am a beginer at programing and i wanted to add a scroll panel to a JTextArea so i tried to research tutorials online. i followed the examples but its not working can someone plz tell me what i am doing wrong. thank you so much
public View(Model model) {
this.model = model;
setBounds(100,50, 800, 400);
setDefaultCloseOperation(EXIT_ON_CLOSE);
Container c = getContentPane();
addDisplay(c);
addButtons(c);
addTxt(c);
}
private void addDisplay(Container c){
JPanel p = new JPanel();
addTxt2(p);
addTxt(p);
add(p, "North");
}
private void addTxt(JPanel p){
txt = new JTextArea(15, 35);
txt.setBackground(Color.BLACK);
txt.setForeground(Color.WHITE);
txt.setEditable(true);
JScrollPane scroll= new JScrollPane (txt);
p.add(scroll);
}
Always invoke revalidate and repaint after adding any components to a JPanel
p.add(scroll);
p.revalidate();
p.repaint();
From the use of setBounds, it appears that there is no layout manager in use. Don't use absolute positioning (null layout). By default, components have a size of 0 x 0 so will not appear unless their size is set. A layout manager should be used here instead.
Post an SSCCE for better help sooner
You have to set the bounds of your scroll setBounds(int, int, int, int)
and define the area of your JTextArea
Here's an example:
public class ScrollingTextArea extends JFrame {
JTextArea txt = new JTextArea();
JScrollPane scrolltxt = new JScrollPane(txt);
public ScrollingTextArea() {
setLayout(null);
scrolltxt.setBounds(3, 3, 300, 200);
add(scrolltxt);
}
public static void main(String[] args) {
ScrollingTextArea sta = new ScrollingTextArea();
sta.setSize(313,233);
sta.setTitle("Scrolling JTextArea with JScrollPane");
sta.show();
}
}
I've found it here
edit: now solved, but can't mark as accepted for two days
In my class I have a JScrollPanel and that has a JPanel inside of it too.
My code resembles something like this:
import java.awt.Color;
import java.awt.Container;
import javax.swing.*;
public class MyClass {
private JPanel p;
private JScrollPane s;
private Container contentPane;
public MyClass(Container contentPane) {
this.contentPane = contentPane;
this.p = new JPanel();
this.p.setBackground(Color.WHITE);
BoxLayout boxLayout = new BoxLayout(this.p, BoxLayout.Y_AXIS);
this.p.setLayout(boxLayout);
this.s = new JScrollPane(this.p);
this.s.setSize(400, 364);
this.contentPane.add(this.s);
}
public final JLabel makeJLabel(String message) {
JLabel jLabel = new JLabel("<html><p style=\"padding-left:15px;padding-right:15px;width:280px;\">" + message.replaceAll("(\r\n|\n)", "<br />") + "</p></html>");
/*
some stuff here to calculate pref/max size and add an imageicon
*/
p.add(jLabel);
this.p.revalidate();
this.s.revalidate(); //just added because the above line made no effect
scrollToBottom();
return jLabel;
}
public void scrollToBottom() {
JScrollBar vertical = s.getVerticalScrollBar();
vertical.setValue(vertical.getMaximum());
}
}
Elsewhere in my class I have a method which adds a JLabel to the JPanel. This actual method is quite long, so I wont post it all, but this is the code which adds it to the panel: p.add(jLabel1);
All of these JLabels are added in a vertical fashion thanks to the Box Layout.
After the JLabel has been added to the JPanel I want the JScrollPane to scroll to the bottom. But this can't be done until after the JPanel has actually been drawn (painted?) onto JPanel. Otherwise I get this result:
So what I want to do is add some form of listener to the JPanel which detects when my JLabel has been painted to it, so that I can tell my JScrollPane to scroll to the bottom. I have already written a method which scrolls the pane to the bottom, but I don't have anywhere suitable to call it from yet.
Does anyone have any ideas on this please? Thanks.
I'm assuming you just want the label to be visible in the scrollpane so I would gues you should be able to do something like:
panel.add( label );
panel.revalidate();
label.scrollRectToVisible( label.getBounds() );
Or if you really do want to just scroll bo the bottom then you would do something like:
panel.revalidate();
scrollPane.getVerticalScrollBar().setValue( getVerticalScrollBar().getMaximum() );
Both of these answers assume the GUI is already visible.
The first part of Rob's answer is the way to go - the missing piece is to wrap the scrollRectToVisible into SwingUtilities.invokeLater. Doing so delays the scrolling until all pending events are processed, that is until all internal state is updated. A code snippet (in swingx test support speak, simply replace the frame creation and scrollpane wrapping with manually created code)
final JComponent panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
for(int i = 0; i < 10; i++)
panel.add(new JLabel("initial message " + i));
JXFrame frame = wrapWithScrollingInFrame(panel, "scroll to bottom");
Action action = new AbstractAction("addMessage") {
int count;
#Override
public void actionPerformed(ActionEvent e) {
final JLabel label = new JLabel("added message " + count++);
panel.add(label);
panel.revalidate();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
label.scrollRectToVisible(label.getBounds());
}
});
}
};
frame.add(new JButton(action), BorderLayout.SOUTH);
show(frame);
Scratched my head for ages over this, but after asking the question I finally figured it out.
To solve my problem all I need to do was listen for a change in value in the JScrollPane's scroll bar. If it changed, done some calculations, and scroll to the bottom if necessary.
Care has to be taken to ensure that you're not overriding the user moving the scroll bar however.
In particular you're looking at the track which is an AdjustmentEvent. This event is also fired when a user moves the scroll bar.
In order to allow the user to scroll without forcibly scroll it to the bottom, I always keep track of the maximum scroll bar value. If when track is fired the new max value is higher than the current one then a new item has been added and we should think about scrolling to the bottom. If the values are equal then the user is scrolling the scroll bar and we do nothing.
The event listeners can be found on this website and can be make to work very easily: Listening for Scrollbar Value Changes in a JScrollPane Container
import java.awt.Color;
import java.awt.Container;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.*;
public class MyClass {
private JPanel p;
private JScrollPane s;
private Container contentPane;
private int scrollBarMax;
public MyClass(Container contentPane) {
this.contentPane = contentPane;
this.p = new JPanel();
this.p.setBackground(Color.WHITE);
BoxLayout boxLayout = new BoxLayout(this.p, BoxLayout.Y_AXIS);
this.p.setLayout(boxLayout);
this.s = new JScrollPane(this.p);
this.s.setSize(400, 364);
this.contentPane.add(this.s);
this.s.getVerticalScrollBar().addAdjustmentListener(new AdjustmentListener(){
#Override
public void adjustmentValueChanged(AdjustmentEvent evt) {
if (evt.getValueIsAdjusting()) {
return;
}
if (evt.getAdjustmentType() == AdjustmentEvent.TRACK) {
if (scrollBarMax < s.getVerticalScrollBar().getMaximum()) {
if ((s.getVerticalScrollBar().getValue() + s.getVerticalScrollBar().getSize().height) == scrollBarMax) {
//scroll bar is at the bottom, show the last added JLabel
scrollBarMax = s.getVerticalScrollBar().getMaximum();
scrollToBottom();
} else {
//scroll bar is not at the bottom, user has moved it
scrollBarMax = s.getVerticalScrollBar().getMaximum();
}
}
}
}
});
scrollBarMax = s.getVerticalScrollBar().getMaximum();
}
public final JLabel makeJLabel(String message) {
JLabel jLabel = new JLabel("<html><p style=\"padding-left:15px;padding-right:15px;width:280px;\">" + message.replaceAll("(\r\n|\n)", "<br />") + "</p></html>");
/*
some stuff here to calculate pref/max size and add an imageicon
*/
p.add(jLabel);
scrollBarMax = s.getVerticalScrollBar().getMaximum();
return jLabel;
}
public void scrollToBottom() {
JScrollBar vertical = s.getVerticalScrollBar();
vertical.setValue(vertical.getMaximum());
}
}