4

Still relatively new to Swing but after a couple of hours of search, I couldn't find the answer online, hence this post (sorry if already answered and I overlooked it).

I'm using JFreeChart in a Swing application. Some charts are relatively heavy (180k data points) and the JFreeChart's ChartPanel needs ~6 seconds to do its first paintComponent().

Hence I would like to show a "Please wait" message in a dialog while the component paints (no need to show progress with a SwingWorker). I tried to override the paintComponent method but unfortunately the message never appears on screen (I guess the thread directly goes into painting the chart, without taking the time to paint the dialog).

My code looks like this:

public class CustomizedChartPanel extends ChartPanel{

private static final long serialVersionUID = 1L;
private JDialog dialog = null;
boolean isPainted = false;

public CustomizedChartPanel(JFreeChart chart) { super(chart); }

@Override
public void paintComponent(Graphics g) {
    //At first paint (which can be lengthy for large charts), show "please wait" message
    if (! isPainted){
        dialog = new JDialog();
        dialog.setUndecorated(true);
        JPanel panel = new JPanel();
        panel.add(new JLabel("Please wait"));
        dialog.add(panel);
        dialog.pack();
        GuiHelper.centerDialog(dialog); //Custom code to center the dialog on the screen
        dialog.setVisible(true);
        dialog.repaint();
    }

    super.paintComponent(g);

    if (! isPainted){
        isPainted = true;
        dialog.dispose();
            super.repaint();
        }
}
}

Any pointer on how to solve this / best practices would be very much appreciated!

Thanks, Thomas


UPDATE:

Thanks for the hints & debate: very helpful.

I started implementing the suggested solution with the invokeLater() as I'm fearing that the JLayer solution won't work since it's also running on the EDT.

Unfortunately I'm having a null pointer exception when the paintComponent() is called by the invokeLater().

My code looks like this:

    @Override
public void paintComponent(Graphics graph) {
    //At first paint (which can be lengthy for large charts), show "please wait" message
    if (! isPainted){
        isPainted = true;
        dialog = new JDialog();
        dialog.setUndecorated(true);
        JPanel panel = new JPanel();
        panel.add(new JLabel("Please wait"));
        panel.add(new JLabel("Please wait !!!!!!!!!!!!!!!!!!!!!!!!!!!!!"));
        dialog.add(panel);
        dialog.pack();
        GuiHelper.centerDialog(dialog); //Custom code to center the dialog on the screen
        dialog.setVisible(true);
        dialog.repaint();
        RunnableRepaintCaller r = new RunnableRepaintCaller(this, graph, dialog);
        SwingUtilities.invokeLater(r);
    }
    else super.paintComponent(graph); //NULL POINTER EXCEPTION HERE (invoked by runnable class)
}

And the runnable class is:

public class RunnableRepaintCaller implements Runnable{
private ChartPanel target;
private Graphics g;
private JDialog dialog;

public RunnableRepaintCaller(ChartPanel target, Graphics g, JDialog dialog){
    this.target = target;
    this.g = g;
    this.dialog = dialog;
}

@Override
public void run() {
    System.out.println(g);
    target.paintComponent(g);
    dialog.dispose();
}
}

Again, any pointer would be much appreciated !

Thomas

Tom
  • 1,375
  • 3
  • 24
  • 45
  • The simplest thing to do is to just set a busy cursor and reset to normal when finished... – lbalazscs Jul 31 '12 at 19:21
  • @user1235867 Take a look at my answer below. It uses SwingWorker but this is your only way out. I cannot have a better suggestion. Also, in paintComponent you should be performing only "painting" operations and not modify the UI nor call invokeLater. – Guillaume Polet Jul 31 '12 at 20:23

4 Answers4

5

Here is an example and/but it uses SwingWorker. You should seriously consider using this because if somehow the OS invalidates your frame and the loading of your JFreeChart is done on the EDT (Event Dispatching Thread), then your GUI will look frozen.

It also allows you to give better user feedback while you are processing the data. (sorry if the code is a bit long but most of the interesting code is in the initUI and the SwingWorker).

Note: instead of the dialog, you could use the JLayer (if you use Java 7), but this was unnecessary in my example.

The code is highly inspired from http://www.vogella.com/articles/JFreeChart/article.html

/**
 * This code was directly taken from: http://www.vogella.com/articles/JFreeChart/article.html
 * All credits goes to him for this code.
 * 
 * Thanks to him.
 */

import java.util.List;

import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.plot.PiePlot3D;
import org.jfree.data.general.DefaultPieDataset;
import org.jfree.data.general.PieDataset;
import org.jfree.util.Rotation;

public class PieChart extends JFrame {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                initUI();
            }
        });
    }

    protected static void initUI() {
        // First we create the frame and make it visible
        final PieChart demo = new PieChart("Comparison");
        demo.setSize(500, 270);
        demo.setVisible(true);
        // Then we display the dialog on that frame
        final JDialog dialog = new JDialog(demo);
        dialog.setUndecorated(true);
        JPanel panel = new JPanel();
        final JLabel label = new JLabel("Please wait...");
        panel.add(label);
        dialog.add(panel);
        dialog.pack();
        // Public method to center the dialog after calling pack()
        dialog.setLocationRelativeTo(demo);

        // allowing the frame and the dialog to be displayed and, later, refreshed
        SwingWorker<JFreeChart, String> worker = new SwingWorker<JFreeChart, String>() {

            @Override
            protected JFreeChart doInBackground() throws Exception {
                publish("Loading dataset");
                // simulating the loading of the Dataset
                try {
                    System.out.println("Loading dataset");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // This will create the dataset 
                PieDataset dataset = demo.createDataset();
                publish("Loading JFreeChart");
                // simulating the loading of the JFreeChart
                try {
                    System.out.println("Loading JFreeChart");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // based on the dataset we create the chart
                JFreeChart chart = demo.createChart(dataset, "Which operating system are you using?");
                // we put the chart into a panel
                return chart;
            }

            @Override
            protected void process(List<String> chunks) {
                label.setText(chunks.get(0));
                dialog.pack();
                dialog.setLocationRelativeTo(demo);
                dialog.repaint();
            }

            @Override
            protected void done() {
                try {
                    // Retrieve the created chart and put it in a ChartPanel
                    ChartPanel chartPanel = new ChartPanel(this.get());
                    // add it to our frame
                    demo.setContentPane(chartPanel);
                    // Dispose the dialog.
                    dialog.dispose();
                    // We revalidate to trigger the layout
                    demo.revalidate();
                    // Repaint, just to be sure
                    demo.repaint();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

        };
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                     worker.execute();
            }
        });
        dialog.setVisible(true);
    }

    public PieChart(String applicationTitle) {
        super(applicationTitle);
    }

    /** * Creates a sample dataset */

    private PieDataset createDataset() {
        DefaultPieDataset result = new DefaultPieDataset();
        result.setValue("Linux", 29);
        result.setValue("Mac", 20);
        result.setValue("Windows", 51);
        return result;

    }

    /** * Creates a chart */

    private JFreeChart createChart(PieDataset dataset, String title) {

        JFreeChart chart = ChartFactory.createPieChart3D(title, // chart title
                dataset, // data
                true, // include legend
                true, false);
        PiePlot3D plot = (PiePlot3D) chart.getPlot();
        plot.setStartAngle(290);
        plot.setDirection(Rotation.CLOCKWISE);
        plot.setForegroundAlpha(0.5f);
        return chart;

    }

}
Guillaume Polet
  • 47,259
  • 4
  • 83
  • 117
  • This doesn't work if the dialog is modal because execution will wait at 'dialog.setVisible(true);'. I want to use modal to prevent users from interacting with other widgets while things load. – Ryan May 07 '14 at 15:01
  • @Ryan Rightly spotted. Just move the setVisible(true) after the call to `worker.execute()`. – Guillaume Polet May 08 '14 at 06:50
  • @"Guillaume Polet" Now you've introduced a race condition. You have no guarantee that the worker thread won't run to completion before dialog.setVisible(true) is even called. If this happens all kinds of weird things may happen such as the modal dialog pops up after the work has already done and now the program is stuck. – Ryan May 09 '14 at 15:30
  • @Ryan while possible, it's very unlikely to happen since by essence you dispatch long running task in a Swing Worker, not something that happens within a short time. To be totally complete and sure that no race condition occurs, we can move the `execute()`call in an invokeLater block ensuring that the dialog is first shown before the worker executes. – Guillaume Polet May 12 '14 at 06:53
  • @"Guillaume Polet" I don't think your fix is right either. I think you need to move the dialog.setVisible(true) to an invokeLater in the doInBackground method to establish the correct happens-before relationship. Oh, and saying it is unlikely to happen doesn't inspire confidence in a program. I'd rather it never happen. – Ryan May 12 '14 at 13:59
  • @Ryan What do you mean by "I don't think your fix is right either"? I actually think it is the right call. If you move the `setVisible` inside the `doInBackground` wrapped in an `invokeLater`, then you run into the race condition again while my solution actually prevents this by ensuring that first the dialog is made visible and when that is the case, the next Swing event will be executed, ie, my call to `worker.execute()` (when a dialog is visible, Swing events are still dispatched). – Guillaume Polet May 12 '14 at 14:57
  • @"Guillaume Polet" Calling execute in a invokeLater makes no since in that execute results in a separate thread of execution. Your solution also doesn't establish the happens-before relationship needed to guarantee that the dialog is visible before the done method is called. The only guarantee you have is that doInBackground will execute before "done" is queued up on the EDT. Therefore you must queue up the setVisible(true) call on the EDT while in doInBackground. Once doInBackground is complete the "done" method will be queued up for the EDT as well. – Ryan May 12 '14 at 15:38
  • @Ryan, while I completely agree with your first two comments and the fact that "very unlikely to happen" can still happen and should be avoided at all costs, I must disagree on your last comments. Calling execute() in an invokeLater and calling `dialog.setVisible(true)` right after it (within the same dispatched event) will guarantee that the dialog is visible before `execute` (and thus doInBackground) is called. If we choose your option, I see one default in there: doInBackground can complete before the dialog is even visible (although done will never be called before dialog.setVisible()) – Guillaume Polet May 12 '14 at 17:59
  • @"Guillaume Polet" Calling invokeLater while already in the EDT is a no-op, but is beside the point. It is still possible for the background worker process to run to completion before setVisible(true) is ever called in your solution. In my solution you call setVisible(true) from within invokeLater as the first statement of doBackground and you are guaranteed that the setVisible(true) call is queued before the "done" method is called (done is queued to run on the EDT automatically by the SwingWorker after doInBackground is complete). – Ryan May 12 '14 at 18:23
  • @Ryan calling invokeLater within the EDT is not a no-op (it's actually a common pattern used to do something "after the current event", please read the source and javadoc of `invokeLater`: `Toolkit.getEventQueue().postEvent(new InvocationEvent(Toolkit.getDefaultToolkit(), runnable));`). It pushes an `InvocationEvent` on the AWT-Event queue that will, when dispatched, invoke the provided `Runnable`, hence guaranteeing that the execute is indeed invoked after the `setVisible(true)` call (just like you are guaranteed in your solution that setVisible is called before done). – Guillaume Polet May 12 '14 at 20:29
  • @"Guillaume Polet" I stand corrected. Both of our solutions would work. – Ryan May 12 '14 at 20:42
2

You can use JLayer as explained here. This is specifically for a busy indicator as you want.

Further more you can keep the JPanel with setEnabled(false) till your data completely loads. This prevents unwanted clicks on the JPanel.

Community
  • 1
  • 1
Nitin Chhajer
  • 2,299
  • 1
  • 24
  • 35
  • 1
    +1 notice JLayer is about Java7, for Java6 have to look at JXLayer, JLayer is based on JXLayer, as an alternative is GlassPane, notice Glasspane is behing AWT Components, to required to use Swing JComponents – mKorbel Jul 31 '12 at 18:59
  • @GuillaumePolet This may be possible not sure until tried. But instead of frozen it will appear to move slower than usual. And this behaviour is seen in most of the application. If the `LayerUI` is made a private class a flag can be used to indicate to kill the thread, and the `CustomizedChartPanel` can be used for displaying all the charts. This also boosts the GUI. – Nitin Chhajer Jul 31 '12 at 19:19
  • 1
    @NitinChhajer No, if the JFreeChart is loaded on the EDT, this will not work, as any repaint events (even if triggered from a javax.swing.Timer since they also rely on InvocationEvent) will not occur until the JFreechart has finished loading. – Guillaume Polet Jul 31 '12 at 19:28
0

It's been a long time since I've done anything in Java, but as far as I know, the repaint() method does not actually cause any drawing to happen. It simply flags the control as needing to be redrawn at the soonest possible opportunity. You need to call the paint() method directly if you want a component to be drawn immediately.

Taedrin
  • 445
  • 1
  • 4
  • 16
-2

You need to start the waiting Dialog in a new Thread. I don't know how you create your chart, but here is a sample

SwingUtilities.invokeLater(new Runnable() {
        public void run() {
             dialog = new JDialog();
             dialog.setUndecorated(true);
             JPanel panel = new JPanel();
             panel.add(new JLabel("Please wait"));
             dialog.add(panel);                
             GuiHelper.centerDialog(dialog); 
             dialog.setVisible(true);

            Thread performer = new Thread(new Runnable() {
                public void run() {
                    dialog.setVisible(false); 
                    //Here the code that prepare the chart                              
                }
        });
        performer.start();
    }
});     
Momo
  • 2,471
  • 5
  • 31
  • 52
  • 2
    -1 Your code does not make sense and it is very bad idea to perform GUI operations in another Thread than the EDT. – Guillaume Polet Aug 03 '12 at 08:46
  • I guess it's easy to say is a very bad idea without giving any references...For me what doesn't make sense is to try to performe two separated operations simultaneously : the display of the loading panel and the loading of the actual chart in the same thread. – Momo Aug 09 '12 at 02:32
  • 1
    https://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html Swing event handling code runs on a special thread known as the event dispatch thread. This is necessary because most Swing object methods are not "thread safe": invoking them from multiple threads risks thread interference or memory consistency errors. Some Swing component methods are labelled "thread safe" in the API specification; these can be safely invoked from any thread. Programs that ignore this rule may function correctly most of the time, but are subject to unpredictable errors that are difficult to reproduce. – Benas Jul 15 '16 at 14:02