简体   繁体   中英

Redrawing lines with Java Swing efficiently

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).

初始画面

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).

屏幕过后

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();
    }
}

}

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 Thread s 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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM