Scrolling limitation with JScrollPane and JViewport maximum sizes smaller than contents
Asked Answered
C

2

3

I have a JFrame containing a JScrollPane containing a JPanel. The JPanel contains a bunch of JTextAreas. I'm loading a lot of text into them (about 8k-10k characters).

The layout works fine, though the scrolling is a bit laggy.

The real issue is that it seems JPanel, JScrollPane and JViewport have a hard 32767 size limit, so when any JTextArea grows higher than that, it can't be scrolled any further to show the last 1/3 of the text.

Below you can see a minimal example for the problem. I used the NetBeans JFrame designer so it might be a bit lengthy but the only thing I have changed from the defaults is that the JTextAreas are direct children of the JPanel, the scrollbar policies, and slightly larger font size:

public class NewJFrame extends javax.swing.JFrame {

/**
 * Creates new form NewJFrame
 */
public NewJFrame() {
    initComponents();
}

/**
 * This method is called from within the constructor to initialize the form.
 * WARNING: Do NOT modify this code. The content of this method is always
 * regenerated by the Form Editor.
 */
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">                          
private void initComponents() {

    jScrollPane1 = new javax.swing.JScrollPane();
    jPanel1 = new javax.swing.JPanel();
    jTextArea1 = new javax.swing.JTextArea();
    jTextArea2 = new javax.swing.JTextArea();

    setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

    jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);

    jTextArea1.setColumns(20);
    jTextArea1.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N
    jTextArea1.setRows(5);

    jTextArea2.setColumns(20);
    jTextArea2.setFont(new java.awt.Font("Monospaced", 0, 14)); // NOI18N
    jTextArea2.setRows(5);
    jTextArea2.setText("Some long text...");

    javax.swing.GroupLayout jPanel1Layout = new javax.swing.GroupLayout(jPanel1);
    jPanel1.setLayout(jPanel1Layout);
    jPanel1Layout.setHorizontalGroup(
        jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel1Layout.createSequentialGroup()
            .addComponent(jTextArea1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(jTextArea2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
            .addGap(0, 0, 0))
    );
    jPanel1Layout.setVerticalGroup(
        jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addGroup(jPanel1Layout.createSequentialGroup()
            .addGroup(jPanel1Layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                .addComponent(jTextArea1, javax.swing.GroupLayout.DEFAULT_SIZE, 342, Short.MAX_VALUE)
                .addComponent(jTextArea2))
            .addGap(0, 0, 0))
    );

    jScrollPane1.setViewportView(jPanel1);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
    getContentPane().setLayout(layout);
    layout.setHorizontalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addComponent(jScrollPane1)
    );
    layout.setVerticalGroup(
        layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
        .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 96, Short.MAX_VALUE)
    );

    pack();
}// </editor-fold>                        

/**
 * @param args the command line arguments
 */
public static void main(String args[]) {
    final NewJFrame f = new NewJFrame();
    /* Set the Nimbus look and feel */
    //<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
    /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
     * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html 
     */
    try {
        for(javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels())
            if("Nimbus".equals(info.getName())) {
                javax.swing.UIManager.setLookAndFeel(info.getClassName());
                break;
            }
    }catch(ClassNotFoundException ex) {
        java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }catch(InstantiationException ex) {
        java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }catch(IllegalAccessException ex) {
        java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }catch(javax.swing.UnsupportedLookAndFeelException ex) {
        java.util.logging.Logger.getLogger(NewJFrame.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
    }
    //</editor-fold>

    /* Create and display the form */
    java.awt.EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            f.setVisible(true);
        }
    });
    StringBuilder txt = new StringBuilder();
    for(int i=0; i<10000; i++)
        txt.append(i).append("\n");
    f.jTextArea1.setText(txt.toString());
    txt = new StringBuilder();
    txt.append("JTextArea height: ").append(f.jTextArea1.getHeight()).append('\n');
    txt.append("JTextArea rows: ").append(f.jTextArea1.getRows()).append('\n');
    txt.append("JScrollPane height:").append(f.jScrollPane1.getHeight()).append('\n');
    txt.append("JViewport height:").append(f.jScrollPane1.getViewport().getHeight()).append('\n');
    txt.append("JPanel height:").append(f.jPanel1.getHeight()).append('\n');
    f.jTextArea2.setText(txt.toString());
}
// Variables declaration - do not modify                     
private javax.swing.JPanel jPanel1;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextArea jTextArea1;
private javax.swing.JTextArea jTextArea2;
// End of variables declaration                   
}

If you run this and scroll to the bottom, you would expect to see the count reach 10 000 but it only goes to 1637, and you can see the top pixels from the next line just barely show up.

I have already tried to setMaximumSize and setSize on the JPanel, the JScrollPane and its JViewport but nothing has changed. I'm also somewhat confused that even though there's 10k lines of text, some of which can be scrolled far enough to be viewed, the getRows() and getSize() methods return the original values.

What is the right way to handle this situation when I want to have a scrollable JTextArea larger than 32767?

Colonel answered 4/9, 2014 at 21:3 Comment(6)
I can't belive that JViewport can have such limitation. I've developed a table with a million rows and it could be scrolled very fine. Please provide a working example for better help.Homeless
I would imagine that the hard limit would be close to Interger.MAX_VALUE, based on the requirements of DimensionDoer
You might use this approach that leverages JTable flyweight rendering; use a custom or popup cell editor as required.Activate
@SergiyMedvynskyy and @MadProgrammer, I had thought so too, and the JTextArea default maximums are at Integer.MAX_VALUE. However, the JPanel and JScrollPane values are 32767 for some reason. Trying to change the maximum size or current size of those at any point didn't make any difference for me at all.Colonel
@Activate It's not a very large file that I'm loading (<20KB), it just has a lot of lines. I'm not so sure a table would suit what I'm trying to do, but even then, the suggested approach also puts it into a JScrollPane, so I don't see how that will avoid the issue. Either way, I also wanted to find out why I'm hitting a problem just as much as I want to find a solution.Colonel
@SergiyMedvynskyy, working example provided.Colonel
C
1

The problem had nothing to do with thread safety. After much digging, I found that the underlying window manager implementation for the Look And Feel of JPanel had a hardcoded 32767 size limit, so it didn't matter that it was sitting in a JScrollPane. The idea was to avoid a lot of extra managing of individual JScrollPanes but the limit makes it unavoidable.

Moving the contents of the JPanel into their own individual JscrollPanes resolved the issue. The scrolling is still a bit laggy for reasons unknown to me.

Colonel answered 3/4, 2015 at 20:43 Comment(0)
A
2

Your example is incorrectly synchronized in that it updates jTextArea2 on the initial thread. Note that JTextArea#setText() is no longer thread safe. The example below invokes EditorKit#read(), as suggested here, to load the same 27 MB, 176 K line file examined here. This takes a few seconds, about about twice as long as the JTable approach, but scrolling is comparable.

import java.awt.EventQueue;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;

/**
 * @see https://mcmap.net/q/541823/-scrolling-limitation-with-jscrollpane-and-jviewport-maximum-sizes-smaller-than-contents
 */
public class Test {

    private static final String NAME = "/var/log/install.log";

    private void display() {
        JFrame f = new JFrame("Test");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTextArea text = new JTextArea(24, 32);
        try {
            text.read(new BufferedReader(new FileReader(NAME)), null);
        } catch (IOException ex) {
            ex.printStackTrace(System.err);
        }
        f.add(new JScrollPane(text));
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(() -> {
            new Test().display();
        });
    }
}
Activate answered 5/9, 2014 at 17:52 Comment(13)
I'm sorry but this has very little to do with what I'm trying to do or the example I gave. I don't see how thread-safety is an issue to resetting the text in the JTextArea - I'm not using append() at all. Even if I was, I only have the main thread write to the JTextArea so I don't see why there would be a synch issue. Your example doesn't exhibit my problem but it also lacks the layout I have, so that's not really helpful.Colonel
Oops, you're right about append(), but setText() has the same problem; updated. You can use whatever layout you want. The example shows that the scroll pane can handle substantially more text than "about 8k-10k characters."Activate
+1 for the SSCCE without all the generated code. but it also lacks the layout I have The layout is the problem. Who knows what the GroupLayout is doing (I sure don't)? Why would you ever create a structure with a scrollpane/panel/textarea? Normally the text area is added directly to the scroll pane. Get rid of all the group layout code and the panel. Then just add the text area to the viewport of the scrollpane. Then add the scroll pane to the BorderLayout.WEST and the second text area to the BorderLayout.EAST.Niue
@nwod, I'm not using append() at all why not? It is far more efficient (unless you read directly to the text area as demonstrated by trashgod). Using your approach first you create a buffer and waste memory by appending large amounts of text to the buffer. Then the setText() method will parse all this text looking for new line strings.Niue
@Niue It is far more efficient how? String concatenation in java is implemented with StringBuilder anyway. Even if I just did str+"ing" the compiler would implicitly pull one out of hammerspace to do the concatenation. If you have a look, you will find that the default Document implementation in JTextArea creates one explicitly for each insertString() invocation, which is what JTextArea#append() ultimately does. I use a StringBuilder because it is more convenient for me, but in this case it is also more efficient because it avoids the extra function calls and instances.Colonel
@Niue Also, JTextArea#setText() does not "parse all this text looking for new line strings." It's actually just the superclass method JTextComponent#setText() which calls the insertString() or replace() methods of the underlying document model.Colonel
@Activate I'm sorry but I still don't see how thread-safety is an issue. Anyway, I tried calling all the swing methods on the AWT thread and implementing it your way but the issue persists. I obviously can't use whatever layout I want because mine is causing the problem. I would really appreciate it if you can show me your solution using my example, because, it seems to me, that it's really a layout issue rather than thread-safety. For various reasons, I need multiple JTextAreas that scroll together, and I want to figure out why my NetBeans-generated layout breaks the JScrolPane scrolling.Colonel
@Colonel I obviously can't use whatever layout I want because mine is causing the problem. yes you can. You are creating the GUI and there is more than one approach to create a layout. Your current approach is currently not resizing components the way you would expect so the layout is wrong. The way to fix the problem is to fix the layout.Niue
@nwod, it is not efficient because if you read a large file then you have two copies of the file in memory. One in the StringBuffer and one in the text area Document. If you load the Document one line at a time then you only have one copy of the Document. because it is more convenient for me, How is it more convenient? Instead of using the append(...) method on the buffer you use it on the text area. It is less code because you don't need to create the buffer or use the setText() method. Less code seems more convenient to me.Niue
@Niue offers this very practical perspective on correct threading; a similar view is adduced here. Once you fix the threading, you can focus on the layout. My example uses the default BorderLayout.CENTER, but GridLayout would work as well. If you want to tinker with the GUI editor's code generation properties, limit the scope of the problem, as shown here, and consider nested layouts.Activate
@Niue I'm sorry, you seem to be confusing my minimal working example that demonstrates my problem with the actual application I am writing. It is more convenient for me to use a StringBuiolder because I'm doing additional processing of the file, which is a binary file, before putting anything into the JTextAreas. I will have a copy of the raw file in memory either way, because I need to do other processing on it. Again, it's not a big file, it just ends up having to display a lot of lines in the GUI.Colonel
@Niue I asked my question hoping to find out why my layout doesn't work. Instead, I'm getting advice on how I should just try something else and forget about it... For all I know, the next thing I try to do will fail because I'm making the same mistake, whatever it is, I sure don't know. I'm asking a question about layout and getting a lecture on thread-safety. I've already tried with corrected threading, and it does not remove the layout problem, nor answer the question of what exactly is going wrong.Colonel
@nwod, I asked my question hoping to find out why my layout doesn't work - GroupLayout is used by IDE's. Most programmers don't hand code GroupLayout and therefore don't understand the details of how GroupLayout works. There is no reason to use a GroupLayout and I already gave you a working solution. I'm asking a question about layout and getting a lecture on thread-safety. yes because until a problem is solved we don't know what the problem is and we have to look at all possibilities, starting with the obvious. For all I know, the next thing I try to do will fail that is how you learn.Niue
C
1

The problem had nothing to do with thread safety. After much digging, I found that the underlying window manager implementation for the Look And Feel of JPanel had a hardcoded 32767 size limit, so it didn't matter that it was sitting in a JScrollPane. The idea was to avoid a lot of extra managing of individual JScrollPanes but the limit makes it unavoidable.

Moving the contents of the JPanel into their own individual JscrollPanes resolved the issue. The scrolling is still a bit laggy for reasons unknown to me.

Colonel answered 3/4, 2015 at 20:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.