1

I am trying to find the most efficient way to create a dynamic java graphical application. I want to build a large screen, with many different parts, all of which re-drawn or updated using a distinct thread, such that the screen looks "alive". However, my initial attempt at doing that was horrible, the screen got very slow, buggy etc - so I figured I need to create different modules (JPanels), each of which contains other graphical parts (lines, circles, etc), and each distinct JPanel being redrawn separately (when needed), instead of the whole main panel (or frame).

So I have written a small demo program - my program contains a single window, with multiple panels, wrapped in my object called "MyPanel" - each such MyPanel contains several drawn lines (I have a Line object), all lines starting from the top-left corner and have different lengths and angles). Each distinct MyPanel has a different line color (see this image).

Initial Screen

I instantiate several worker threads, each designated for one MyPanel - the workers wait for 5 seconds, then try to re-draw all lines in the following manner:

  • Remove all existing lines from the JPanel (MyPanel).
  • Create new lines with different angles and lengths.
  • redraw the JPanel (MyPanel) by invoking super.repaint() this is the entire purpose, to update only this panel, have it redraw itself with all of its sub-parts, and not the entire program

However, something weird happens: when the panels are re-drawn, each one is redrawn in a way that probably contains all other MyPanels too, or mirrors the main screen somehow - its very unclear what exactly happens here. Also, all "background opacity" of the panels is gone (see this image).

Screen after

Before I attach my code, let me say that it uses a null LayoutManager. I know this is a big "no no" in terms of efficiency, modularity and whatnot. However I don't have a choice since I need to create a very graphically complicated and exact demo quickly, which only serves as a proof-of-concept, so for now, all of these flaws are negligible. I know it's horrible design-wise, it hurts me too, but that's the only way I can make it on time.

Here is the code - what happens? and how can I efficiently re-draw different parts of the program if not using this way? note I cannot "repaint over existing lines with the background color", since there is a background image in my main program.

Any help would be appreciated!

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 * Displays the main windows (this is the "JFrame" object).
 */
public class GUI extends JFrame
{
/**
 * A customized panel which contains several lines with different coordinates, all starting from
 * the top left corner of the panel with coordinates (1,1). The object contains a method which
 * removes all drawn lines from the panel, then redraws lines with different vectors.
 */
public static class MyPanel extends JPanel
{
    private List<Line> _lines;

    private Color _color;

    private int _facet;

    private int _numLines;

    public MyPanel(int facet, int numLines, Color color)
    {
        _facet = facet;
        _color = color;
        _numLines = numLines;
        _lines = new ArrayList<>();

        super.setLayout(null);
        createLines();
    }

    public void createLines()
    {
        for(Line line : _lines)
        {
            remove(line);
        }

        _lines.clear();

        Random r = new Random();

        for(int i = 0; i < _numLines; i++)
        {
            int lengthX = r.nextInt(_facet) + 1;
            int lengthY = r.nextInt(_facet) + 1;

            Line line = new Line(1, 1, 1 + lengthX, 1 + lengthY, 1, _color);

            line.setBounds(1, 1, 1 + lengthX, 1 + lengthY);
            super.add(line);

            _lines.add(line);
        }

        super.repaint();
    }
}

/**
 * Represents a line, drawn with antialiasing at a given start and end coordinates
 * and a given thickness.
 */
public static class Line extends JPanel
{
    private int _startX;
    private int _startY;
    private int _endX;
    private int _endY;

    private float _thickness;
    private Color _color;

    public Line(int startX, int startY, int endX, int endY, float thickness, Color color)
    {
        _startX = startX;
        _startY = startY;
        _endX = endX;
        _endY = endY;

        _thickness = thickness;
        _color = color;
    }

    public void paint(Graphics g)
    {
        Graphics2D g2d = (Graphics2D)g;

        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        g2d.setColor(_color);
        g2d.setStroke(new BasicStroke(_thickness));
        g2d.drawLine(_startX, _startY, _endX, _endY);
    }
}

/**
 * Stores all "MyPanel" panels of the GUI.
 * The "MyPanels" are rectangular panels containing lines of the same color
 * (different color across different panels).
 */
public List<MyPanel> panels;

public GUI()
{
    setSize(800, 800);
    setLayout(null);
    setTitle("Y U no work??");

    panels = new ArrayList<>();

    // The starting positions (x,y) of the "MyPanel"s. All panels are squares of
    // height = 300 and width = 300.
    int[][] coords = {{1, 1}, {100, 100}, {200, 100}, {50, 300}, {300, 300}, 
                      {0, 400}, {300, 400}, {350, 250}, {370, 390}};

    // The colors of the lines, drawn in the panels.
    Color[] colors = {Color.RED, Color.GREEN, Color.BLUE, Color.ORANGE, Color.CYAN,
                      Color.MAGENTA, Color.YELLOW, Color.PINK, Color.darkGray};


    for(int i = 0; i < colors.length; i++)
    {
        MyPanel panel = new MyPanel(300, 50, colors[i]);
        panel.setBackground(new Color(0, 0, 0, 0));
        // Set the *exact* start coordinates and width/height (null layout manager).
        panel.setBounds(coords[i][0], coords[i][1], 300, 300);
        add(panel);
        panels.add(panel);
    }
}

/**
 * A runnable used to instantiate a thread which waits for 5 seconds then redraws
 * the lines of a given "MyPanel".
 */
public static class Actioner implements Runnable
{
    private MyPanel _panel;

    public Actioner(MyPanel panel)
    {
        _panel = panel;
    }

    public void run()
    {
        while(true)
        {
            try
            {
                Thread.sleep(5000);
            }
            catch(Exception e) {}

            _panel.createLines();
        }
    }
}

public static void main(String[] args)
{
    GUI GUI = new GUI();

    EventQueue.invokeLater(() ->
    {
        GUI.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        GUI.setVisible(true);
    });

    // Create all operating threads (one per "MyPanel").
    for(MyPanel panel : GUI.panels)
    {
        new Thread(new Actioner(panel)).start();
    }
}

}

amirkr
  • 173
  • 3
  • 15
  • 3
    1) Custom painting is done by overriding paintComponent(...), not paint(). 2) The first statement should be `super.paintComponent(g)` to clear the background. 3) Line should not extend JPanel. It should just be a class that contains information about the line to be painted. Then you keep an ArrayList of Line objects that you paint. See [Custom Painting Approaches](https://tips4java.wordpress.com/2009/05/08/custom-painting-approaches/) for an example of this approach. It shows how to paint `Colored Rectangles` from an ArrayList. – camickr Mar 18 '18 at 21:19
  • Using the above approach will solve you null layout issue as well. – camickr Mar 18 '18 at 21:25
  • Thanks for the comment! I'll look into what you wrote. I'm sure it'll help :) – amirkr Mar 19 '18 at 10:28
  • @camickr thanks again for your reply, it helped me to write better JPanel components, compiling several drawings into one paintComponent method really helped. – amirkr Apr 03 '18 at 15:05

1 Answers1

7

So, a litany of errors:

Incorrect use of custom painting...

This...

public void paint(Graphics g)
{
    Graphics2D g2d = (Graphics2D)g;

    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    g2d.setColor(_color);
    g2d.setStroke(new BasicStroke(_thickness));
    g2d.drawLine(_startX, _startY, _endX, _endY);
}

Isn't how custom paint should be done. Graphics is a shared context in Swing, it's shared among all the components been painted in any given paint pass. This means, that unless you prepared the context first, it will still contain what ever was painted to it from the last component.

Equally, it's not recommend to override paint, it's to high in the paint chain and incorrect use can cause no end of issues.

Instead, you should start with paintComponent and make sure to call it's super method in order to maintain the paint chain operations...

protected void paintComponent(Graphics g)
{
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D)g.create();

    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

    g2d.setColor(_color);
    g2d.setStroke(new BasicStroke(_thickness));
    g2d.drawLine(_startX, _startY, _endX, _endY);

    g2d.dispose();
}

If you're modifying the state of the context (in particular the transformation, but rendering hints count), you should first create a copy of the state and dispose of it when you're done. This prevents changes to the state been passed onto other components, which can cause some weird rendering issues

Have a look at Performing Custom Painting and Painting in AWT and Swing for more details

Incorrect use of opacity

This...

panel.setBackground(new Color(0, 0, 0, 0));

is not how you create a transparent component. Swing doesn't know how to handle transparent (alpha based) colors. It only deals with opaque and non-opaque components. This is achieved through the use of the opaque property.

panel.setOpaque(false);

Violation of Swing threading rules...

Swing is single threaded AND NOT thread safe.

public void run()
{
    while(true)
    {
        try
        {
            Thread.sleep(5000);
        }
        catch(Exception e) {}

        _panel.createLines();
    }
}

Calling createLines in this context is running the risk of threading issues, as Swing attempts to paint the properties while they are been updated, which can lead to weird painting artefacts.

Remember, a paint pass may occur at any time, most of the time without your interaction or knowledge.

Instead, I'd recommend maybe using a SwingWorker (but it has its limitations) or ensuring that the call to createLines is done within the context of the Event Dispatching Thread, through the use of EventQueue.invokeLater or EventQueue.invokeAndWait depending on your needs

See Concurrency in Swing for more details.

More threads != more work done

Having more threads doesn't always mean you can get more done, it's a balancing act.

Personally I would start with a single thread, responsible for scheduling updates to each panel, either directly (via createLines) or indirectly by building the line information itself and passing the result to the component.

Remember, when you schedule a paint pass, Swing will attempt to optimise the painting by reducing the number of paint events and simply paint a larger area (as required). Also, when working with non-opaque components, painting any one component may require that other, overlapping, components also need to be painted.

As you expand the number of threads, consider if the threads should create the lines itself, this means, instead of wasting time in the EDT, you're performing the operations in a separate thread and then simply applying the results to the component.

Equally, more components may increase the amount of work needed to be done.

Another approach would be to have the Threads act as "producers" which generate a List of lines. A single component would then act as a "consumer" and when a new List of lines is ready, it would repaint itself.

This might need you to produce a mapping between the producer and consumer so you know which List of lines is been updated, but that's beyond the scope of the question

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Hey! Thank you so much for the detailed reply. I'm still reading and processing what you've wrote. The litany of errors was expected, I needed to write a large scale graphical demo within a short period of time, knowing nothing about swing/awt. So the directions you pointed me to really help, I'll read the material and see if it fixed my problem. Mainly, the issue is how to update many lines q rectangles q circles and other graphical objects, multiple times per second, and not have the "flicker" and lag. Thanks again! I'll write back once I've implemented it all. – amirkr Mar 19 '18 at 10:30
  • 1
    Maybe a `SwingWorker` might be more useful, as it can be used to generate new information in the background a `publish` it to be `processed` in the foreground in chunks - this frees up each side of the thread fence from complexity of synchronisation – MadProgrammer Mar 19 '18 at 10:34
  • 1
    If you're having "lag" issues, then some part of the system is running into trouble. It could be GC overhead, [this demonstrates](https://stackoverflow.com/questions/14886232/swing-animation-running-extremely-slow/14902184#14902184) an evolution of an idea which took the programs ability to render around 500 objects to upwards of 5, 000 simply by trying to reduce the GC overhead. – MadProgrammer Mar 19 '18 at 10:42
  • 1
    Another idea might be to look at [BufferStrategy and BufferCapabilities](https://docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html) which allows you to take complete control (AKA active rendering) of the paint process – MadProgrammer Mar 19 '18 at 10:43
  • 1
    While more complex, another idea is to consider a "time based" animation, where certain events occur over a specified period of time, rather then at a regular interval, this allows the animation to vary it's speed (or the number of updates it needs to generate) based on external factors - I often use this concept as a preference and it can produce very complex effects through the use of a "time line" concept - this provides flexible in the fact that you can vary the "duration" and the animation system adjusts automatically around it – MadProgrammer Mar 19 '18 at 10:46
  • Hey, an update! :) I've implemented some of your and camickr's advice - used setOpaque(false) - this eliminated the weird painting issue, then made each line not as an independent JPanel, but rather had many lines drawn at the same JPanel ("MyPanel"). Still some problems persist, so I'm reading the extra material you provided. I'm sure it'll help resolve everything. A big thank you! – amirkr Mar 20 '18 at 21:06
  • Hey, I wanted to thank you again - not only did your "setOpaque(false)" solve the problem, but you also foresaw the concurrency problems I encountered a few days later and was able to solve with the guidance you linked to. Kudos! – amirkr Apr 03 '18 at 15:01