JTextFields encima del dibujo activo en JPanel, problemas de subprocesamiento

Resuelto Mattijs asked hace 14 años • 2 respuestas

¿Alguien ha intentado alguna vez utilizar Swing para construir un entorno de renderizado con múltiples buffers adecuado sobre el cual se pueden agregar elementos de la interfaz de usuario de Swing ?

En este caso tengo un rectángulo rojo animado dibujado sobre un fondo. No es necesario actualizar el fondo en cada cuadro, por lo que lo renderizo en una Imagen Buffered y vuelvo a dibujar solo la parte necesaria para borrar la ubicación anterior del rectángulo. Vea el código completo a continuación, esto amplía el ejemplo dado por @trashgod en un hilo anterior, aquí .

Hasta ahora, todo bien; animación fluida, bajo uso de CPU, sin parpadeos.

Luego agrego un JTextField al Jpanel (haciendo clic en cualquier posición en la pantalla) y me enfoco en él haciendo clic dentro del cuadro de texto. Borrar la ubicación anterior del rectángulo ahora falla cada vez que parpadea el cursor; consulte la imagen a continuación.

Tengo curiosidad por saber si alguien tiene una idea de por qué podría suceder esto (¿Swing no es seguro para subprocesos? ¿La imagen se pinta de forma asincrónica?) y en qué dirección buscar posibles soluciones.

Esto está en Mac OS 10.5, Java 1.6

Error al volver a dibujar JPanel
(fuente: arttech.nl )

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

public class NewTest extends JPanel implements 
    MouseListener, 
    ActionListener, 
    ComponentListener, 
    Runnable 
{

JFrame f;
Insets insets;
private Timer t = new Timer(20, this);
BufferedImage buffer1;
boolean repaintBuffer1 = true;
int initWidth = 640;
int initHeight = 480;
Rectangle rect;

public static void main(String[] args) {
    EventQueue.invokeLater(new NewTest());
}

@Override
public void run() {
    f = new JFrame("NewTest");
    f.addComponentListener(this);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.add(this);
    f.pack();
    f.setLocationRelativeTo(null);
    f.setVisible(true);
    createBuffers();
    insets = f.getInsets();
    t.start();
}

public NewTest() {
    super(true);
    this.setPreferredSize(new Dimension(initWidth, initHeight));
    this.setLayout(null);
    this.addMouseListener(this);
}

void createBuffers() {
    int width = this.getWidth();
    int height = this.getHeight();

    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice gs = ge.getDefaultScreenDevice();
    GraphicsConfiguration gc = gs.getDefaultConfiguration();

    buffer1 = gc.createCompatibleImage(width, height, Transparency.OPAQUE);        

    repaintBuffer1 = true;
}

@Override
protected void paintComponent(Graphics g) {
    int width = this.getWidth();
    int height = this.getHeight();

    if (repaintBuffer1) {
        Graphics g1 = buffer1.getGraphics();
        g1.clearRect(0, 0, width, height);
        g1.setColor(Color.green);
        g1.drawRect(0, 0, width - 1, height - 1);
        g.drawImage(buffer1, 0, 0, null);
        repaintBuffer1 = false;
    }

    double time = 2* Math.PI * (System.currentTimeMillis() % 5000) / 5000.;
    g.setColor(Color.RED);
    if (rect != null) {
        g.drawImage(buffer1, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, this);
    }
    rect = new Rectangle((int)(Math.sin(time) * width/3 + width/2 - 20), (int)(Math.cos(time) * height/3 + height/2) - 20, 40, 40);
    g.fillRect(rect.x, rect.y, rect.width, rect.height);
}

@Override
public void actionPerformed(ActionEvent e) {
    this.repaint();
}

@Override
public void componentHidden(ComponentEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void componentMoved(ComponentEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void componentResized(ComponentEvent e) {
    int width = e.getComponent().getWidth() - (insets.left + insets.right);
    int height = e.getComponent().getHeight() - (insets.top + insets.bottom);
    this.setSize(width, height);
    createBuffers();
}

@Override
public void componentShown(ComponentEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void mouseClicked(MouseEvent e) {
    JTextField field = new JTextField("test");
    field.setBounds(new Rectangle(e.getX(), e.getY(), 100, 20));
    this.add(field);
    repaintBuffer1 = true;
}

@Override
public void mouseEntered(MouseEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void mouseExited(MouseEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void mousePressed(MouseEvent arg0) {
    // TODO Auto-generated method stub

}

@Override
public void mouseReleased(MouseEvent arg0) {
    // TODO Auto-generated method stub

}
}
Mattijs avatar Jul 15 '10 21:07 Mattijs
Aceptado

NewTestse extiende JPanel; pero como no estás pintando cada píxel en cada llamada a paintComponent(), necesitas invocar el método de la superclase y borrar el dibujo antiguo:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    int width = this.getWidth();
    int height = this.getHeight();
    g.setColor(Color.black);
    g.fillRect(0, 0, width, height);
    ...
}

Anexo: Como observa, configurar el color de fondo en el constructor evita la necesidad de completar el panel paintComponent(), mientras super.paintComponent()permite que los campos de texto funcionen correctamente. Como puede observar, la solución propuesta es frágil. En su lugar, simplifique el código y optimícelo según sea necesario. Por ejemplo, es posible que no necesite la complicación de inserciones, búferes adicionales y un componente de escucha.

Anexo 2: Tenga en cuenta que super.paintComponent()llama al update()método del delegado de la interfaz de usuario, "que rellena el componente especificado con su color de fondo (si su propiedad opaca es verdadera)". Puede utilizar setOpaque(false)para impedir esto.

Prueba de animación

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.BufferedImage;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

/** @see http://stackoverflow.com/questions/3256941 */
public class AnimationTest extends JPanel implements ActionListener {

    private static final int WIDE = 640;
    private static final int HIGH = 480;
    private static final int RADIUS = 25;
    private static final int FRAMES = 24;
    private final Timer timer = new Timer(20, this);
    private final Rectangle rect = new Rectangle();
    private BufferedImage background;
    private int index;
    private long totalTime;
    private long averageTime;
    private int frameCount;

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                new AnimationTest().create();
            }
        });
    }

    private void create() {
        JFrame f = new JFrame("AnimationTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(this);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
        timer.start();
    }

    public AnimationTest() {
        super(true);
        this.setOpaque(false);
        this.setPreferredSize(new Dimension(WIDE, HIGH));
        this.addMouseListener(new MouseHandler());
        this.addComponentListener(new ComponentHandler());
    }

    @Override
    protected void paintComponent(Graphics g) {
        long start = System.nanoTime();
        super.paintComponent(g);
        int w = this.getWidth();
        int h = this.getHeight();
        g.drawImage(background, 0, 0, this);
        double theta = 2 * Math.PI * index++ / 64;
        g.setColor(Color.blue);
        rect.setRect(
            (int) (Math.sin(theta) * w / 3 + w / 2 - RADIUS),
            (int) (Math.cos(theta) * h / 3 + h / 2 - RADIUS),
            2 * RADIUS, 2 * RADIUS);
        g.fillOval(rect.x, rect.y, rect.width, rect.height);
        g.setColor(Color.white);
        if (frameCount == FRAMES) {
            averageTime = totalTime / FRAMES;
            totalTime = 0; frameCount = 0;
        } else {
            totalTime += System.nanoTime() - start;
            frameCount++;
        }
        String s = String.format("%1$5.3f", averageTime / 1000000d);
        g.drawString(s, 5, 16);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        this.repaint();
    }

    private class MouseHandler extends MouseAdapter {

        @Override
        public void mousePressed(MouseEvent e) {
            super.mousePressed(e);
            JTextField field = new JTextField("test");
            Dimension d = field.getPreferredSize();
            field.setBounds(e.getX(), e.getY(), d.width, d.height);
            add(field);
        }
    }

    private class ComponentHandler extends ComponentAdapter {

        private final GraphicsEnvironment ge =
            GraphicsEnvironment.getLocalGraphicsEnvironment();
        private final GraphicsConfiguration gc =
            ge.getDefaultScreenDevice().getDefaultConfiguration();
        private final Random r = new Random();

        @Override
        public void componentResized(ComponentEvent e) {
            super.componentResized(e);
            int w = getWidth();
            int h = getHeight();
            background = gc.createCompatibleImage(w, h, Transparency.OPAQUE);
            Graphics2D g = background.createGraphics();
            g.clearRect(0, 0, w, h);
            g.setColor(Color.green.darker());
            for (int i = 0; i < 128; i++) {
                g.drawLine(w / 2, h / 2, r.nextInt(w), r.nextInt(h));
            }
            g.dispose();
            System.out.println("Resized to " + w + " x " + h);
        }
    }
}
trashgod avatar Jul 15 '2010 15:07 trashgod

Encontré una solución.

Lo que creo que estaba sucediendo: cada vez que es necesario actualizar un JTextfield (es decir, en cada parpadeo del cursor), se llama al paintComponent() anulado de JPanel, pero con un argumento Graphics diferente al que llama repaint(). Por lo tanto, en cada parpadeo del cursor, mi rectángulo se borró y se volvió a dibujar en una instancia de Gráficos incorrecta, dejando los Gráficos que se ven en la pantalla no válidos.

¿Tiene esto algún sentido? Si es así, ¿no es un extraño inconveniente en Swing?

De todos modos, al mantener un valor booleano ( activeRedraw) de dónde proviene la invocación, parece que logré solucionar este problema. Parece que finalmente encontré una manera de realizar dibujos activos sin volver a pintar toda el área de la pantalla en cada cuadro, lo que significa un bajo uso de la CPU independientemente del tamaño de la ventana.

Código completo aquí:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.Timer;

public class NewTest extends JPanel implements 
    MouseListener, 
    ActionListener, 
    ComponentListener, 
    Runnable 
{

    JFrame f;
    Insets insets;
    private Timer t = new Timer(20, this);
    BufferedImage buffer1;
    boolean repaintBuffer1 = true;
    int initWidth = 640;
    int initHeight = 480;
    Rectangle rect;
    boolean activeRedraw = true;

    public static void main(String[] args) {
        EventQueue.invokeLater(new NewTest());
    }

    @Override
    public void run() {
        f = new JFrame("NewTest");
        f.addComponentListener(this);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(this);
        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
        createBuffers();
        insets = f.getInsets();
        t.start();
    }

    public NewTest() {
        super(true);
        this.setPreferredSize(new Dimension(initWidth, initHeight));
        this.setLayout(null);
        this.addMouseListener(this);
    }

    void createBuffers() {
        int width = this.getWidth();
        int height = this.getHeight();

        GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
        GraphicsDevice gs = ge.getDefaultScreenDevice();
        GraphicsConfiguration gc = gs.getDefaultConfiguration();

        buffer1 = gc.createCompatibleImage(width, height, Transparency.OPAQUE);        

        repaintBuffer1 = true;
    }

    @Override
    protected void paintComponent(Graphics g) {
        //super.paintComponent(g);
        int width = this.getWidth();
        int height = this.getHeight();

        if (activeRedraw) { 
            if (repaintBuffer1) {
                Graphics g1 = buffer1.getGraphics();
                g1.clearRect(0, 0, width, height);
                g1.setColor(Color.green);
                g1.drawRect(0, 0, width - 1, height - 1);
                g.drawImage(buffer1, 0, 0, null);
                repaintBuffer1 = false;
            }

            double time = 2* Math.PI * (System.currentTimeMillis() % 5000) / 5000.;
            g.setColor(Color.RED);
            if (rect != null) {
                g.drawImage(buffer1, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, rect.x, rect.y, rect.x + rect.width, rect.y + rect.height, this);
            }
            rect = new Rectangle((int)(Math.sin(time) * width/3 + width/2 - 20), (int)(Math.cos(time) * height/3 + height/2) - 20, 40, 40);
            g.fillRect(rect.x, rect.y, rect.width, rect.height);

            activeRedraw = false;
        }
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        activeRedraw = true;
        this.repaint();
    }

    @Override
    public void componentHidden(ComponentEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void componentMoved(ComponentEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void componentResized(ComponentEvent e) {
        int width = e.getComponent().getWidth() - (insets.left + insets.right);
        int height = e.getComponent().getHeight() - (insets.top + insets.bottom);
        this.setSize(width, height);
        createBuffers();
    }

    @Override
    public void componentShown(ComponentEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseClicked(MouseEvent e) {
        JTextField field = new JTextField("test");
        field.setBounds(new Rectangle(e.getX(), e.getY(), 100, 20));
        this.add(field);
        repaintBuffer1 = true;
    }

    @Override
    public void mouseEntered(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseExited(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mousePressed(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }

    @Override
    public void mouseReleased(MouseEvent arg0) {
        // TODO Auto-generated method stub

    }
}
Mattijs avatar Jul 16 '2010 12:07 Mattijs