2010-07-15 7 views
9

Qualcuno ha mai provato a utilizzare Swing per costruire un ambiente di rendering multi-buffer corretto in cima al quale è possibile aggiungere gli elementi dell'interfaccia utente di Swing?JTextFields sopra il disegno attivo su JPanel, problemi di threading

In questo caso ho un rettangolo rosso animato disegnato su uno sfondo. Lo sfondo non ha bisogno di essere aggiornato ogni fotogramma, quindi lo renderò su una BufferedImage e ridisegnerò solo la parte necessaria per cancellare la posizione precedente del rettangolo. Vedi il codice completo qui sotto, questo estende l'esempio dato da @trashgod in una discussione precedente, here.

Fin qui tutto bene; animazione fluida, basso utilizzo della CPU, assenza di sfarfallio.

Quindi aggiungo un JTextField al Jpanel (facendo clic su qualsiasi posizione sullo schermo) e ci concentriamo su di esso facendo clic all'interno della casella di testo. Cancellare la posizione precedente del rettangolo ora non riesce su ogni lampeggiamento del cursore, vedere l'immagine qui sotto.

Sono curioso di sapere se qualcuno ha un'idea del perché questo potrebbe accadere (Swing non è thread-safe? L'immagine viene dipinta in modo asincrono?) E in che direzione cercare le possibili soluzioni.

Questo è in Mac OS 10.5, Java 1,6

JPanel redraw fail http://www.arttech.nl/javaredrawerror.png

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 

} 
} 

risposta

18

NewTest estende JPanel; ma perché non stai dipingendo ogni pixel su ogni chiamata a paintComponent(), è necessario richiamare il metodo della super-classe e cancellare il vecchio disegno:

@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); 
    ... 
} 

Addendum: Come si nota, impostare il colore di sfondo nel costruttore preclude la necessità di riempire il pannello in paintComponent(), mentre super.paintComponent() consente il corretto funzionamento dei campi di testo. Come si osserva, la soluzione proposta è fragile. Invece, semplificare il codice e ottimizzare come garantito. Ad esempio, potresti non aver bisogno della complicazione di inset, buffer extra e un listener di componenti.

Addendum 2: nota che super.paintComponent() chiama il metodo update() del delegato dell'interfaccia utente "che riempie il componente specificato con il colore di sfondo (se la sua proprietà opaca è vera)." È possibile utilizzare setOpaque(false) per precludere questo.

Animation Test

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); 
     } 
    } 
} 
+0

Grazie per la risposta. Se aggiungi super.paintComponent (g) ed esegui l'applicazione di prova che ho fornito, vedrai che sfortunatamente questa non è una soluzione. Il metodo paintComponent predefinito di JPanel cancella il suo sfondo, il che rende necessario ridisegnare l'intera immagine di sfondo, principalmente annullando lo scopo di pre-rendering dell'immagine di sfondo. Ho la sensazione che sia possibile ridisegnare solo l'area necessaria, ma a quanto pare, c'è qualche problema nel ridisegnare il campo di testo in un thread diverso rispetto al ridisegnare l'immagine di sfondo che causa questi artefatti. – Mattijs

+0

Riguardo all'addendum: molte grazie ancora per aver dedicato del tempo a tuffarsi in questo. Mi rendo conto che il tuo punto, essendo che non c'è modo che i JTextFields che si ridisegnano automaticamente funzionino correttamente insieme al ridisegno attivo * senza * usando super.paintComponent (g), è corretto. Tuttavia, l'uso di super.paintComponent (g) implica che l'intera finestra debba essere ridisegnata su ogni frame di animazione, rendendo l'utilizzo della CPU dipendente dalla dimensione della finestra, che è ciò che vorrei impedire. Quindi la mia domanda ha una risposta, ma il mio problema è ancora in piedi. Farò un nuovo post su questo. – Mattijs

+0

@Mattijs: l'utilizzo di setOpaque (false) eviterà il riempimento. Ho aggiornato l'esempio per mostrare il tempo di pittura. La differenza è notevole, ma deve essere valutata rispetto allo sforzo di gestire gli aggiornamenti. – trashgod

2

Ho trovato una soluzione.

Quello che penso stava accadendo: ogni volta che un JTextField deve essere aggiornato (cioè su ogni intermittenza del cursore), paintComponent sovrascritto di JPanel() si chiama, ma con un argomento grafica diversa rispetto a quando chiamato da repaint(). Così su ogni lampeggiamento del cursore, il mio rettangolo è stato cancellato e ridisegnato su un'istanza Graphics sbagliata, lasciando invariata la grafica visualizzata sullo schermo.

Ha senso? Se lo fa, non è questo uno strano inconveniente in Swing?

Ad ogni modo, mantenendo un booleano (activeRedraw) da dove proviene il richiamo, sembra che sia riuscito a risolvere questo problema.Così sembra che finalmente ho trovato un modo per fare il disegno attivo senza ridipingere l'intera area dello schermo su ogni fotogramma, il che significa che l'utilizzo della cpu basso indipendentemente dalla dimensione della finestra!

completo del codice qui:

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 

    } 
} 
+0

Nota: su Windows, il comportamento è ancora una volta diverso e la soluzione alternativa non è sufficiente. – Mattijs

+0

Ho elaborato la mia risposta. – trashgod