Java / Swing : JTextArea in a JScrollPane, how to prevent auto-scroll? - java

here's a runnable piece of code that shows what my "problem" is.
I've got a JTextArea wrapped in a JScrollPane. When I change the text of the JTextArea, the JScrollPane scrolls automatically to the end of the text and I don't want that.
Here are my requirements:
the application should not scroll vertically automatically but...
the user should be able to scroll vertically
the user should not be able to scroll horizontally
the application should never scroll horizontally
the JTextArea must not be editable
(so even if there's more text than what can fit horizontally, neither the application nor the user should be able to scroll horizontally. While vertically, only the user should be able to scroll.)
I don't know how to "fix" this: should this be fixed using JTextArea or JScrollPane methods?
Note that AFAICT this is not a duplicate at all of: JTextPane prevents scrolling in the parent JScrollPane
Here's the kinda funny example, every 200 ms it puts new text in the JTextArea and you can see the JScrollPane always scrolling automatically to the end of the text.
import javax.swing.*;
import java.awt.*;
import java.util.Random;
public final class TextInScrollPane extends JFrame {
private static final Random r = new Random( 42 );
public static void main( final String[] args ) {
final JFrame f = new JFrame();
f.setDefaultCloseOperation( EXIT_ON_CLOSE );
f.setLayout(new BorderLayout());
final JTextArea jta = new JTextArea( "Some text", 30, 30 );
jta.setEditable( false ); // This must not be editable
final JScrollPane jsp = new JScrollPane( jta );
jsp.setHorizontalScrollBarPolicy( JScrollPane.HORIZONTAL_SCROLLBAR_NEVER );
jsp.setVerticalScrollBarPolicy( JScrollPane.VERTICAL_SCROLLBAR_ALWAYS );
f.add( jsp, BorderLayout.CENTER );
f.pack();
f.setLocationRelativeTo( null );
f.setVisible(true);
final Thread t = new Thread( new Runnable() {
public void run() {
while ( true ) {
try {Thread.sleep( 200 );} catch ( InterruptedException e ) {}
final StringBuilder sb = new StringBuilder();
for (int i = 0; i < 50 + r.nextInt( 75 ); i++) {
for (int j = 0; j < r.nextInt(120); j++) {
sb.append( (char) 'a' + r.nextInt(26) );
}
sb.append( '\n' );
}
SwingUtilities.invokeLater( new Runnable() {
public void run() {
jta.setText( sb.toString() );
}
} );
}
}
});
t.start();
}
}

How to set AUTO-SCROLLING of JTextArea in Java GUI?
http://download.oracle.com/javase/1.5.0/docs/api/javax/swing/text/DefaultCaret.html#NEVER_UPDATE
Try:
JTextArea textArea = new JTextArea();
DefaultCaret caret = (DefaultCaret)textArea.getCaret();
caret.setUpdatePolicy(DefaultCaret.NEVER_UPDATE);
This should prevent the caret from automatically making the document scroll to the bottom.

Answering my own question: I'm not exactly sure this is the best way to solve my issue, but setting the JTextArea's caret using setCaretPosition(0) seems to work fine:
jta.setText( sb.toString() );
jta.jta.setCaretPosition( 0 );

You have run into a very strange behaviour in the implementation of the Document classes. I use a DefaultStyledDocument in a JTextPane inside a JScrollPane.
Now, here is the wierd thing. If I update the document on the EventQueue (like you do by scheduling a runnable to run later) the scroll pane automatically scrolls to the end.
However, the document classes claim to be thread safe and actually updatable from another thread. If I make sure to update on another thread than the EventQueue everything works fine but the scroll pane does NOT scroll to the end.
I have no explanation as to why this is so, I haven't looked in the Swing source. I have been exploiting this "feature" since 2006 and it has been consistent so far :-)

Related

GridLayout doesn't fill up the entire window

Whats the reason that a JInteralFrame with a GridLayout(x, y) doesn't fill up the entire window although I'm adding x*y buttons to it? Why is there white space around it like in the first picture below?
If i resize it a bit, I'm able to remove all the white space around the grid layout, like in the second picture below, but I do not understand why that's not always the case.
Why is there white space around it like in the first picture below?
A GridLayout assigns exactly the same width or height to every component, but for a GUI 20 (for example) components wide, it is only possible to do that every 20 pixels that the GUI is stretched. It arranges any 'left over' pixels of space to the left and right most components.
To get around that, you might instead use a GridBagLayout and adjust the weights of rows and columns to allow some components to take over the remaining space in a way that is almost unnoticeable (the difference in component sizes) to the user.
Why is there white space around it like in the first picture below?
Andrew's answer explains the problem with the GridLayout.
However I don't know if this can be fixed with any of the other standard JDK layout managers.
Check out the Relative Layout. It is a layout manager that allows you to give components a size relative to one another, so it is easy to make all components the same size. When there are extra pixels you can set the "rounding policy" to allocate the pixels to different components.
The layout is a little more complex because it can't be done with a single layout manager. In the example below you need a panel that uses the RelativeLayout for vertical layout. Then you need to create a separate panel for each row and those panels will use a RelativeLayout with a horizontal layout.
import java.awt.*;
import javax.swing.*;
public class SSCCE extends JPanel
{
SSCCE()
{
int size = 10;
setBackground( Color.RED );
Float constraint = new Float(1.0f);
RelativeLayout vertical = new RelativeLayout(RelativeLayout.Y_AXIS);
vertical.setRoundingPolicy( RelativeLayout.EQUAL );
vertical.setFill(true);
setLayout( vertical );
for (int i = 0; i < size; i++)
{
RelativeLayout horizontal = new RelativeLayout(RelativeLayout.X_AXIS);
horizontal.setRoundingPolicy( RelativeLayout.EQUAL );
horizontal.setFill(true);
JPanel row = new JPanel( horizontal );
for (int j = 0; j < size; j++)
{
row.add(new JButton(), constraint);
}
add(row, constraint);
}
}
private static void createAndShowGUI()
{
JFrame frame = new JFrame("SSCCE");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new SSCCE());
frame.pack();
frame.setLocationByPlatform( true );
frame.setVisible( true );
}
public static void main(String[] args) throws Exception
{
EventQueue.invokeLater( () -> createAndShowGUI() );
/*
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
*/
}
}

Swing is very slow with long strings

I built a simple Java program that logs in a JTextArea component.
JTextArea _log = new JTextArea();
_log.setEditable(false);
JScrollPane scrollLog = new JScrollPane(_log);
scrollLog.setPreferredSize(getMaximumSize());
add(scrollLog);
The problem is that logging like this takes 15ms on average:
public void log(String info) {
_log.append(info + "\n");
}
This is far(!) slower than logging using System.out.println. Logging takes more time than the whole running time of the algorithm!
Why is the JTextArea is so slow? Is there a way to improve it?
EDIT 1:
I am using separate thread for the algorithm, and using SwingUtilities.invokeLater to update the log in the UI.
The algorithm tread finish his work after 130ms on average, but the JTextArea finish his appends after 6000ms on avarage.
EDIT 2:
I tried to test this by use setText of string that contains 2500 charaters. In that case the operation took 1000ms on average.
I tried to use another controller then JTextArea and I get same results.
Is it hard for Swing components to deal with large strings? What can I do about it?
EDIT 3:
I just test with this code:
public class Test extends JFrame {
public Test() {
final JTextArea log = new JTextArea();
log.setEditable(false);
log.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
JScrollPane scrollLog = new JScrollPane(log);
scrollLog.setPreferredSize(getMaximumSize());
JButton start = new JButton("Start");
start.addActionListener(new ActionListener() {
#Override
public void actionPerformed(ActionEvent e) {
long start = System.nanoTime();
for (int i = 0; i < 2500; i++) {
log.append("a\n");
}
long end = System.nanoTime();
System.out.println((end - start) / 1000000.0);
}
});
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(2, 1));
panel.add(scrollLog);
panel.add(start);
add(panel);
}
public static void main(String[] args) {
Test frame = new Test();
frame.setSize(600,500);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}
The time of that for loop is 1870ms on avarage.
This is the only code that I ran (include the declaration of _log at the top of the question)
A JTextArea is not slow.
Far(!) away from System.out.println.
System.out.println() executes on a separate Thread.
The log takes more time then the hole running time of the algorithm!
So your algorithm is probably executing on the Event Dispatch Thread (EDT) which is the same Thread as the logic that appends text to the text area. So the text area can't repaint itself until the algorithm is finished.
The solution is to use a separate Thread for the long running algorithm.
Or maybe a better choice is to use a SwingWorker so you can run the algorithm and "publish" results to the text area.
Read the section from the Swing tutorial on Concurrency for more information and a working example of a SwingWorker.
Edit:
//log.setComponentOrientation(ComponentOrientation.RIGHT_TO_LEFT);
The above line is causing the problem. I get 125 for the first test and 45 when I keep clicking the button.
That property is not needed. The text is still displayed on the left side of the text pane. If you want right aligned text then you need to use a JTextPane and set the attributes of the text pane to be right aligned.
That is why you should always post an MCVE. There is no way we could have guessed from your original question that you were using that method.
Edit2:
Use the alignment feature of a JTextPane:
SimpleAttributeSet center = new SimpleAttributeSet();
StyleConstants.setAlignment(center, StyleConstants.ALIGN_CENTER);
textPane.getStyledDocument().setParagraphAttributes(0, doc.getLength(), center, false);
Now any new text you add to the document should be center aligned. You can change this to right.

Making JLable in JTextPane Undeletable

I currently have a JLabel embedded in a JTextPane using this:
import javax.swing.*;
import javax.swing.text.*;
public class MainFrame
{
JFrame mainFrame = new JFrame("Main Frame");
JTextPane textPane = new JTextPane();
public MainFrame()
{
String[] components = {"Title", "\n"};
String[] styles = {"LABEL_ALIGN", "LEFT_ALIGN"};
StyledDocument sd = textPane.getStyledDocument();
Style DEFAULT_STYLE = StyleContext.getDefaultStyleContext().getStyle(StyleContext.DEFAULT_STYLE);
Style LEFT_STYLE = sd.addStyle("LEFT_ALIGN", DEFAULT_STYLE);
StyleConstants.setAlignment(LEFT_STYLE, StyleConstants.ALIGN_LEFT);
Style CENTER_STYLE = sd.addStyle("CENTER_ALIGN", DEFAULT_STYLE);
StyleConstants.setAlignment(CENTER_STYLE, StyleConstants.ALIGN_CENTER);
JLabel titleLbl = new JLabel("Title");
Style LABEL_STYLE = sd.addStyle("LABEL_ALIGN", DEFAULT_STYLE);
StyleConstants.setAlignment(LABEL_STYLE, StyleConstants.ALIGN_CENTER);
StyleConstants.setComponent(LABEL_STYLE, titleLbl);
for(int i = 0; i < components.length; i++)
{
try
{
sd.insertString(sd.getLength(), components[i], sd.getStyle(styles[i]));
sd.setLogicalStyle(sd.getLength(), sd.getStyle(styles[i]));
}
catch(BadLocationException e)
{
e.printStackTrace();
}
}
mainFrame.add(textPane);
mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
mainFrame.setLocationRelativeTo(null);
mainFrame.setExtendedState(JFrame.MAXIMIZED_BOTH);
mainFrame.pack();
mainFrame.setVisible(true);
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(MainFrame::new);
}
}
How can I make the label un-deletable? Because whenever I hold backspace, the label ends up getting removed from the JTextPane
You might be able to use a NavigationFilter to prevent the removal of the component at the beginning of the text pane. Check out: How to make part of a JTextField uneditable for an example of this approach. In this case the label represents a single character so the prefix length would be set to 1. Or maybe you can just use the prefix concept itself and don't even use the JLabel.
Otherwise, you might be able to create a custom DocumentFilter. Check out the section from the Swing tutorial on Implementing a DocumentFilter for the basics.
So you would need to track the offset off the location of the component. Then in the remove(...) method of the filter you would need to check if you are removing data in the range of your offset. If so you would ignore the remove.
Of course the offset can dynamically change if you add or remove text before the label so you would need to manage that as well.
Or you can check out the Protected Text Component which attempts to manage all of that for you.
Why not just put your title label outside the text area? That seems more intuitive.
It looks like there's no real way to avoid this while still allowing the textarea to be editable. You could place the label above the text frame so that it occupies the same space, or above the text frame so that it behaves like a proper title.
Unfortunately, the nature of the textarea is that all of its subcomponents are editable or none of them are.

Runtime alignment of JComponents + chaining to RowFilters

I'm currently working on a rather complex application. My job is to build parts of the GUI.
The main area is derived for JTable and contains all application relevant data. There are a few elements on top of the Table, that allow the user to control the way the data is shown in the table.
The options relevant to the task at hand are:
Changing of number of columns,
Independently changing of width of columns (not by means of JTableHeader) and
Entering one filter term per column to select specific rows of the data.
The main goal in this szenario is to create a Component (probably JTextField) for every column in the current viewsetting, which is accuratly aligned with that column (although it changes size at runtime).
First question:
The alignment doesn't work. I can't get the width of the TextFields to match the width of the columns.
How do i get it to work?
Second problem:
I want the individual filters to be chained. That is, if the user decides to enter more then one filter string, all of them should be evaluated for their respective columns and only the rows that match all filters should be shown. So far the input in a second TextField delets the first filter (which is working decently using RowFilter.regexFilter).
How do i get this to work?
Please let me know, which code snippets could be useful to you and i will be glad to post them.
Thanks in advance for any help given.
Regards, DK
I can't get the width of the
TextFields to match the width of the
columns
This example should get you started:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public class TableFilterRow extends JFrame implements TableColumnModelListener
{
private JTable table;
private JPanel filterRow;
public TableFilterRow()
{
table = new JTable(3, 5);
table.setPreferredScrollableViewportSize(table.getPreferredSize());
JScrollPane scrollPane = new JScrollPane( table );
getContentPane().add( scrollPane );
table.getColumnModel().addColumnModelListener( this );
// Panel for text fields
filterRow = new JPanel( new FlowLayout(FlowLayout.CENTER, 0 , 0) );
for (int i = 0; i < table.getColumnCount(); i ++)
filterRow.add( new JTextField("" + i) );
columnMarginChanged( new ChangeEvent(table.getColumnModel()) );
getContentPane().add(filterRow, BorderLayout.NORTH);
}
// Implement TableColumnModelListener methods
// (Note: instead of implementing a listener you should be able to
// override the columnMarginChanged and columMoved methods of JTable)
public void columnMarginChanged(ChangeEvent e)
{
TableColumnModel tcm = table.getColumnModel();
int columns = tcm.getColumnCount();
for (int i = 0; i < columns; i ++)
{
JTextField textField = (JTextField)filterRow.getComponent( i );
Dimension d = textField.getPreferredSize();
d.width = tcm.getColumn(i).getWidth();
textField.setPreferredSize( d );
}
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
filterRow.revalidate();
}
});
}
public void columnMoved(TableColumnModelEvent e)
{
Component moved = filterRow.getComponent(e.getFromIndex());
filterRow.remove(e.getFromIndex());
filterRow.add(moved, e.getToIndex());
filterRow.validate();
}
public void columnAdded(TableColumnModelEvent e) {}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
public static void main(String[] args)
{
JFrame frame = new TableFilterRow();
frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
frame.pack();
frame.setLocationRelativeTo( null );
frame.setVisible(true);
}
}
only the rows that match all filters
should be shown
Read the JTable API and follow the link to the Swing tutorial on "How to Use Tables" where you will find the TableFilterDemo. You can easily modify the code to use "and" filters. The code would be something like:
// rf = RowFilter.regexFilter(filterText.getText(), 0);
List<RowFilter<Object,Object>> filters = new ArrayList<RowFilter<Object,Object>>(2);
filters.add(RowFilter.regexFilter(filterText.getText(), 0));
filters.add(RowFilter.regexFilter(filterText.getText(), 1));
rf = RowFilter.andFilter(filters);
This examples shares a single text field looking for the same string in multiple columns. You would obviously use your individual text fields.

Problem with Java GUI

I have made a java program with GUI. Now I want to add a component on the GUI where I can display whatever I want in the same way we display output through
System.out.println();
Which component I can add on the GUI and how to display content on that component.
You could define a PrintStream that prints to a JTextArea:
final JTextArea textArea = new JTextArea();
PrintStream printStream = new PrintStream( new OutputStream() {
#Override
public void write( final int b ) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
textArea.append( "" + (char )b );
textArea.setCaretPosition( textArea.getText().length() );
}
});
}
} );
System.setOut(printStream);
For just one line you can use a JLabel and set its text property. How to use JLabel: http://www.leepoint.net/notes-java/GUI/components/10labels/jlabel.html
Or if you need to print multiple lines you can use a JTextArea-box.
Its also possible to draw/paint text ontop of the GUI-panel with Java2D and the Graphics object.
You can use a JTextArea and add text to it each time you print something. Call setEditable(false) so it's read-only. Add it to a JScrollPane so it's scrollable.
Or you could use a JList and add each line as a separate list item. It won't do word wrapping but if you're displaying something akin to an event log it'll look good for that purpose.

Categories

Resources