Swing is very slow with long strings - java

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.

Related

Space under JTextPane text when using .setText method

I am using MigLayout 3.5.5, as the newer updates are not compatible with my older code.
Problem
When setting text to a JTextPane in a MigLayout, the JTextPane will take double the space (according to font size) IF the text I am setting the JTextPane contains space characters. It does not happen all the time, but in the specific program I am making, it happens frequently.
The program's goal is to present information in a letter-by-letter basis, so there is a button that updates the text to the next letter. However, the text bounces around, because the JTextPane is sometimes occupying more space than usual. I identified a certain pattern to the height differences.
Pattern
A new line indicates that I added a letter.
"|" represents a space character in the text.
"Space" means JTextPane is taking double the space.
Full String: "The quick brown fox jumps over the lazy dog."
T
Th
The
The|
The|q (Space)
The|qu
The|qui (Space)
The|quic
The|quick (Space)
The|quick|
Note: I stopped the pattern here, because from this point on (starting with The|quick|b), every single letter addition resulted in the JTextPane occupying double its height.
I've already tried printing out the letter-by-letter text to the console to see if there were any new line characters within the text being added, but to no avail. I also thought it might be a problem with the automatic wrapping of the JTextPane, but the text I inserted isn't quite long enough to wrap in the JFrame's size.
Here is a short example to reproduce the behavior:
public class MainFrame extends JFrame {
int currentLetter = 1;
final String FULL_TEXT = "The quick brown fox jumps over the lazy dog.";
JTextPane text;
JButton addLetter;
MainFrame() {
setSize(500, 500);
setLayout(new MigLayout("align center, ins 0, gap 0"));
addElements();
setVisible(true);
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
MainFrame application = new MainFrame();
}
});
}
private void addElements() {
text = new JTextPane();
text.setEditable(false);
text.setFont(new Font("Times New Roman", Font.BOLD, 19));
text.setForeground(Color.WHITE);
text.setBackground(Color.BLACK);
add(text, "alignx center, wmax 80%, gapbottom 5%");
addLetter = new JButton("Add Letter");
addLetter.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if (currentLetter != FULL_TEXT.length()) {
currentLetter++;
updateText();
}
}
});
add(addLetter, "newline, alignx center");
updateText();
}
private void updateText() {
String partialText = new String();
for (int letter = 0; letter < currentLetter; letter++) {
partialText += FULL_TEXT.toCharArray()[letter];
}
text.setText(partialText);
}
}
Why am I using JTextPane?
I tried using JLabel for this task, and it worked well... until the text was long enough to wrap. Then, when I used HTML within the JLabel text to wrap it, every time I updated the text, it would take time for the HTML to render and result in some pretty nasty visual effects.
Next, I tried JTextArea to disguise it as a JLabel, since it not only has line wrapping, but word wrapping as well. It was a great solution, until I found out that I couldn't use a center paragraph alignment in a JTextArea.
So I settled for a JTextPane, which will work well if only I got rid of the extra space at the bottom of it.
Thanks in advance for your help!
The solution is to append text by using the insertString() method on the StyledDocument of the JTextPane instead of using setText() on the JTextPane itself.
For example, instead doing this every time:
JTextPane panel = new JTextPane();
panel.setText(panel.getText() + "test");
You should do this:
JTextPane panel = new JTextPane();
StyledDocument document = panel.getStyledDocument();
document.insertString(document.getLength(), "test", null);
And of course you need to catch the BadLocationException.
Then the space disappears. Here's the question where I found my answer to the rendering problem: JTextPane appending a new string
The answers to those questions don't address the problem with the space, but they do show the correct way to edit text in the JTextPane.

Java code doesn't work without a println

Here is my code:
while(monster.curHp > 0)
{
System.out.println("");
if(battle.pressedButton)
{
text = Player.name + ": " + Player.curHitPoints + " " + monster.name + ": " + monster.curHp;
battle = new GUIForBattle(text,Player,monster);
}
}
The weird thing is that if I have that println line in the while loop the code will work normally and when the button is pressed we will update text to have the current status and we will redraw the GUI using the GUIForBattle class, however if I don't have that println it wont redraw. Any advice? Thank you!
Here is the GUIForBattle for more context
public class GUIForBattle extends JFrame {
boolean pressedButton = false;
public GUIForBattle(String words, player PlayerOne, Monster monster)
{
JFrame frame = new JFrame(); //frame that holds everything
JPanel Panel = new JPanel(new GridLayout(5,5)); //panel where things get added
JLabel text = new JLabel(words); // text label
JButton attack = new JButton("Attack"); //makes a button used to attack
//adding what pressing the attack button would do
attack.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
int attackAmount = PlayerOne.weaponEquipped.att;
monster.curHp = monster.curHp - attackAmount;
pressedButton = true;
}
}
);
JButton Item = new JButton("Item"); // makes a button used to use items
Item.addActionListener(
new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
//we need to make a item interface
}
});
Panel.add(text); //adds the text to the panel
Panel.add(attack);
Panel.add(Item);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 800); //setting size of frame
frame.add(Panel); //adding the panel to frame
frame.setVisible(true); //making the frame visible
}
}
Your code is inherently multi-threaded; one thread is running through that little while loop; the other is the swing application thread that will be handling your swing event handlers.
If you use shared variables like this (both threads access pressedButton) you need to make sure that variable is synchronized between threads. There are several ways of handling this, but an easy way for this particular problem would be to make the variable volatile.
If the variable is not synchronized in any way, there is no guarantee by the JVM as to when one thread will 'see' the changes made to it by the other. And typically, if you keep one thread occupied like you're doing here (this while loop is called a busy wait) it will never take the time to synchronize, and you'll never see the updates.
The println is an IO operation, meaning at some point your thread will be waiting for IO to complete. Most likely this causes the JVM to synchronize the variables, which is why you notice this difference.
In any case, relying on this without thinking about synchronization can be considered a bug.
Java threads and memory handling are a complex subject, and not something I would advise for beginners to jump in to; it could be overwhelming. Just try to avoid sharing memory between threads for now. For the moment, just run your logic in your swing application code (it's not ideal, but for some beginner code it's probably a good starting point).
When you feel ready for it, read up on the memory model and what it implies for multi-threading.

Why is Document.insertString()'s runtime not constant-time?

I am working on creating a logger to show output as part of a larger Java swing GUI. Unfortunately I was experiencing a slowdown after adding it. I have traced the problem to repeated calls of Document.insertString().
I made a test which shows this slowdown:
LogPanel.java
public class LogPanel extends JPanel{
private static final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private JEditorPane textPane;
private static int numTextRows = 50;
private SimpleAttributeSet keyWord;
private Document document;
public LogPanel() {
super(new BorderLayout());
add(makePanel(), BorderLayout.CENTER);
}
private Component makePanel() {
// Just a text area that grows and can be scrolled.
textPane = new JTextPane();
document = textPane.getDocument();
keyWord = new SimpleAttributeSet();
StyleConstants.setForeground(keyWord, Color.BLACK);
//textArea.setRows(numTextRows);
textPane.setEditable(false);
textPane.setFont(new Font("monospaced", Font.PLAIN, 12));
DefaultCaret caret = (DefaultCaret) textPane.getCaret();
caret.setUpdatePolicy(DefaultCaret.ALWAYS_UPDATE);
//Wrap the textPane in a JPanel with BorderLayout so that the text does not wrap
JPanel textPaneWrapper = new JPanel();
textPaneWrapper.setLayout(new BorderLayout());
textPaneWrapper.add(textPane);
JScrollPane areaScrollPane = new JScrollPane(textPaneWrapper);
areaScrollPane.getVerticalScrollBar().setUnitIncrement(20);
//JScrollPane areaScrollPane = new JScrollPane(textPane);
areaScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
areaScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
areaScrollPane.setPreferredSize(new Dimension(250, 250));
// builder.add(areaScrollPane, cc.xywh(1, 3, 3, 1));
StyleConstants.setBackground(keyWord, areaScrollPane.getBackground());
textPane.setBackground(areaScrollPane.getBackground());
return areaScrollPane;
}
public void appendResult(final String action, final String result, final Color color) {
Date now = new Date();
String strDate = df.format(now);
String paddedAction = String.format("%-19s", action);
StyleConstants.setForeground(keyWord, color);
try {
document.insertString(document.getLength(), strDate + " " + paddedAction + " " + result + "\n", keyWord);
} catch (BadLocationException e) {
throw new RuntimeException(e);
}
if(!textPane.hasFocus()) {
textPane.setCaretPosition(document.getLength());
}
}
public void appendResult(String action, String result) {
appendResult(action, result, Color.BLACK);
}
}
LogTester.Java
public class LogTester extends JFrame{
private LogPanel logPanel;
private JButton pushMe;
public LogTester(){
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLayout(new GridBagLayout());
logPanel = new LogPanel();
this.add(logPanel);
pushMe= new JButton("Press Me");
LogTester self=this;
pushMe.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
self.addLotsOfStuff();
}
});
this.add(pushMe);
this.pack();
this.setVisible(true);
}
public void addLotsOfStuff(){
for(int i=0; i<1000; i+=1){
long start=System.currentTimeMillis();
for(int j=0; j<1000; j+=1){
String str="This is a very long peice of text designed to test the capabilites of our log panel. Move along, nothing to see here.";
logPanel.appendResult("HERP",str);
}
long end=System.currentTimeMillis();
System.out.println(end-start);
}
}
public static void main(String args[]){
LogTester test=new LogTester();
}
}
The program above attempts to write a large number of lines to a JTextPane, which utilizes Document.insertString(). The results of this program are concerning:
For some reason, each call to this function increases the runtime of the next call: It looks linear or even mildly exponential. This might imply that all the previous contents of the document are being copied on each insert, rather than the new string being appended (in some sort of linked list fashion)
Unlike Java GUI freezing because of insertString method? I am primarily concerned with the increasing runtime of the function, not the idle time of the application. Adding threading will not help if each individual call gets very slow.
Unlike Limit JTextPane space usage I am not concerned with large memory usage. If the document is large, it will use a large amount of memory. I just don't understand why that would affect the runtime of inserting more information to the Document.
Perhaps the slowdown can be attributed to this caret position memory leak?
What parts of JTextPane or Document would I have to override in order to achieve a constant time insertString()?
Growing arrays is nearly linear, but not actually linear.
Once you exceed the size of the level 1 cache line, you use another until:
There are no free cache lines, so you have to evict an existing line cache, populating it from a portion of a level 2 cache line.
There are no free level 2 cache lines, so you have to evict one of those, populating it from a (Typically) general RAM reqeust.
Your general RAM request is too large to fit into one request, and the group of requested pages can not be co-fetched due to their count or memory chip locations.
This means that, even for linear operations, really small stuff runs much, much faster than the same linear algorithm dealing with a much larger amount of data.
So when deciding on if an algorithm is O(n) or otherwise, it's important to remember that such a decision is based on a fundamental understanding of the expectations of your computing model. Big-O notation assumes that all RAM fetches are equal in time, and computations (operations) are also equal in time. Under such constraints, comparing the count of the operations to the amount of data still makes sense; but, assuming the wall clock time is exact doesn't.

visualizing JPanel change within a loop

I am new to Java swing programming. I want to make a frame which will appear red and blue in turn one after another. So, I took 2 child JPanel, 1 for red and other for blue, and a for-loop. On each iteration I remove one panel from parent panel and add another. But, when I run the program it only shows the last state of the frame.
Can anyone explain why? And what's the intended approach to make a program work like that?
My code:
public class Test2 extends JFrame {
public Test2() {
JPanel Red = new JPanel(new BorderLayout());
JPanel Blue = new JPanel(new BorderLayout());
//...initialize Red and Blue
Red.setBackground(Color.red);
Blue.setBackground(Color.blue);
Red.setPreferredSize(new Dimension(200,200));
Blue.setPreferredSize(new Dimension(200,200));
JPanel panel = new JPanel(new BorderLayout());
panel.setPreferredSize(new Dimension(200,200));
add(panel);
pack();
setTitle("Border Example");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
int M = 1000000; //note that, I made a long iteration to not finish the program fast and visualize the effect
for(int i=0;i<M;i++)
{
if(i%(M/10)==0) System.out.println(i); //to detect whether the program is running
if(i%2==0)
{
panel.removeAll();
panel.repaint();
panel.revalidate();
panel.add(Red,BorderLayout.CENTER);
}
else
{
panel.removeAll();
panel.repaint();
panel.revalidate();
panel.add(Blue,BorderLayout.CENTER);
}
}
}
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
Test2 ex = new Test2();
ex.setVisible(true);
}
});
}}
Don't use a loop. Swing will only repaint the frame once the entire loop has finished executing.
Instead you need to use a Swing Timer. When the Timer fires you invoke your logic. Read the section from the Swing tutorial on How to Use Swing Timers.
Here is a simple example of a Timer that simply displays the time every second: Update a Label with a Swing Timer
Also, don't remove/add panels. Instead you can use a Card Layout and sway the visible panel. Again read the tutorial on How to Use CardLayout.
Basically you don't need to use a while (or any other) loop, Swing only paints once it has finished that loop then repaint the GUI.
As stated before by #camickr on his answer, you could try a Swing Timer; here's an example that does exactly what you want.
From your comment on another answer:
Could you please explain why "repaint" does not work in a loop? And why is the Timer working without a "repaint"?
Swing is smart enough to know it doesn't needs to repaint in a loop, instead it will repaint once it the loop finishes, if you read the tutorial on Swing Custom Paint on the step 3 it says:
"Swing is smart enough to take that information and repaint those sections of the screen all in one single paint operation. In other words, Swing will not repaint the component twice in a row, even if that is what the code appears to be doing."
And Timer will repaint it, because it's not running on the EDT but in it's own Thread
I would suggest to take in one step at a time.
First make it run without changing panels / colors.
Now it doesn't because this
public final void Test2() {
is a method (which is never used) and not a constructor.
Change to a constructor declaration like :
public Test2() {
to make the program do something. Then you can go to the next step.
Also use Java naming conventions (like blue instead of Blue).

How to loop back to beginning of program

I am trying to create a while loop with this condition. (a > 1) So basically. every time a is set to greater than 1, it will close the JFrame I created and then start the program over. My problem is that, when I try edit the integer "a" from within an action listener, it doesn't recognize that it has already been declared. This is somewhat difficult to actually describe, so here is my code.
public class TestBox {
public static void main(String[] args) {
int a = 2;
while(a > 1){
a = 0;
JFrame frame = new JFrame("Test Box");
frame.setSize(1200, 800);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setResizable(true);
frame.setLayout(new GridLayout(6, 6, 1, 1));
JPanel panelAOne = new JPanel();
JPanel panelATwo = new JPanel();
JPanel emptySpace = new JPanel();
JLabel labelAOne = new JLabel();
labelAOne.setFont(new Font("Aerial", Font.ITALIC, 21));
labelAOne.setText("Welcome to the Test Box!");
JLabel labelATwo = new JLabel();
labelATwo.setFont(new Font("Aerial", Font.ITALIC, 21));
labelATwo.setText("Where would you like to go?");
JLabel emptyLabel = new JLabel("stuff goes here");
JButton buttonAOne = new JButton("Colors");
panelAOne.add(labelAOne);
panelAOne.add(labelATwo);
panelATwo.add(buttonAOne);
emptySpace.add(emptyLabel);
frame.add(panelAOne);
frame.add(buttonAOne);
frame.add(emptySpace);
frame.setVisible(true);
buttonAOne.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
a = 2;
//If this code is left as is, a GUI will open endlessly until it crashes the computer
}
}
});
}
}
You look to be trying to shoe-horn a linear console program into a Swing event-driven GUI program or visa versa, and you really don't want to do that, and I would recommend that you re-structure your program. Instead, show your GUI, get the input, and re-request the input in the GUI if it's wrong. Don't use a console-type program while loop for this.
So, for instance if the ActionListener finds that the input is not valid, clear the text in the JTextField by calling setText("") on the JTextField, show the user a JOptionPane informing them of the error in input and await another press of the button. But leave the JFrame displayed.
Edit: I don't even see a JTextField in your code, so I'm not sure where the user is supposed to enter input.
Your scheme cannot work, at least not reliably. At the end of the first iteration of your loop, local variable a still has the value set at the top of the loop (0), so the loop exits and the application's main thread dies. The GUI will continue to run, as it does so in a separate thread (the AWT's event-dispatch thread (EDT)), but even if you re-wrote your code so that an analog of variable a could be modified by your ActionListener, nobody who cares would still be paying attention by that point. (Probably. You have multiple threads sharing data without any synchronization, so really the behavior of your program is not well defined.)
GUI programming is fundamentally different from console programming, as Hovercraft Full Of Eels pointed out. It requires a significant mental adjustment to move from one to the other, but the basic paradigm of GUI programming is that everything your program does is a response to an event. Thus, if you want some sort of re-spawning behavior then you should obtain it by registering a listener for the appropriate event, and having it perform the work you want.

Categories

Resources