2010-07-15 14 views
9

Est-ce que quelqu'un a déjà essayé d'utiliser Swing pour construire un environnement de rendu multi-tampon approprié sur lequel des éléments d'interface utilisateur Swing peuvent être ajoutés?JTextFields au-dessus du dessin actif sur JPanel, problèmes de thread

Dans ce cas, j'ai un rectangle rouge d'animation dessiné sur un arrière-plan. L'arrière-plan n'a pas besoin d'être mis à jour à chaque image, donc je le rends sur une image tamponnée et je redessine seulement la partie nécessaire pour effacer l'emplacement précédent du rectangle. Voir le code complet ci-dessous, cela prolonge l'exemple donné par @trashgod dans un thread précédent, here.

Jusqu'ici tout va bien; animation fluide, faible utilisation du processeur, pas de scintillement.

Puis j'ajoute un JTextField au Jpanel (en cliquant sur n'importe quelle position sur l'écran), et je me concentre dessus en cliquant dans la zone de texte. Effacer l'emplacement précédent du rectangle échoue maintenant à chaque clignotement du curseur, voir l'image ci-dessous. Je suis curieux si quelqu'un a une idée de la raison pour laquelle cela pourrait arriver (Swing n'étant pas thread-safe? L'image étant peinte de façon asynchrone?) Et dans quelle direction chercher des solutions possibles.

C'est sous 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 

} 
} 

Répondre

18

NewTest étend JPanel; mais parce que vous n'êtes pas peindre chaque pixel sur chaque appel à paintComponent(), vous devez appeler la méthode de super-classe et d'effacer l'ancien dessin:

@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: Comme vous le soulignez, le réglage de la couleur d'arrière-plan dans le constructeur exclut le besoin de remplir le panneau en paintComponent(), tandis que super.paintComponent() permet aux champs de texte de fonctionner correctement. Comme vous le constatez, la solution de contournement proposée est fragile. Au lieu de cela, simplifiez le code et optimisez comme justifié. Par exemple, vous n'avez pas besoin de la complication des encarts, des tampons supplémentaires et d'un écouteur de composant.

Addendum 2: Notez que super.paintComponent() appelle la méthode update() du délégué de l'interface utilisateur, "qui remplit le composant spécifié avec sa couleur d'arrière-plan (si sa propriété opaque est true)." Vous pouvez utiliser setOpaque(false) pour exclure cela.

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

Merci pour votre réponse. Si vous ajoutez super.paintComponent (g) et exécutez l'application de test fournie, vous verrez que ce n'est malheureusement pas une solution. La méthode paintComponent par défaut de JPanel efface son arrière-plan, ce qui oblige à redessiner l'image d'arrière-plan en entier, ce qui annule le but du pré-rendu de l'image d'arrière-plan. J'ai l'impression que redessiner seulement la zone nécessaire devrait être possible, mais comme il semble, il y a un problème avec redessiner le champ de texte dans un fil différent de redessiner l'image de fond qui provoque ces artefacts de dessin .. – Mattijs

+0

En ce qui concerne l'addenda: Merci beaucoup d'avoir pris le temps de plonger dans ce domaine. Je réalise que votre point, étant qu'il n'y a aucun moyen que le JTextFields automatiquement redessiner fonctionnera correctement avec le redessin actif * sans * en utilisant super.paintComponent (g), est correct. Néanmoins, l'utilisation de super.paintComponent (g) implique que la fenêtre entière doit être redessinée sur chaque image d'animation, ce qui rend l'utilisation du processeur dépendante de la taille de la fenêtre, ce que je voudrais éviter. Donc ma question est répondue, mais mon problème est toujours debout. Je vais faire un nouveau post à ce sujet. – Mattijs

+0

@Mattijs: L'utilisation de setOpaque (false) permet d'éviter le remplissage. J'ai mis à jour l'exemple pour montrer le temps de peinture. La différence est considérable, mais elle doit être mise en balance avec l'effort de gestion des mises à jour. – trashgod

2

Je trouve une solution de contournement. Ce que je pense était en train de se produire: chaque fois qu'un JTextfield doit être mis à jour (à chaque clignotement du curseur), paintComponent() surchargé de JPanel est appelé, mais avec un argument graphique différent de repaint(). Ainsi, à chaque clignotement du curseur, mon rectangle a été effacé et redessiné sur une mauvaise occurrence de Graphics, laissant le Graphics vu à l'écran invalide.

Est-ce que cela a un sens? Si c'est le cas, n'est-ce pas un inconvénient étrange dans Swing? De toute façon, en gardant un booléen (activeRedraw) d'où l'invocation vient, il semble que j'ai réussi à contourner ce problème.Il semble donc que j'ai finalement trouvé un moyen de faire du dessin actif sans repeindre toute la zone de l'écran sur chaque image, ce qui signifie une faible utilisation de la CPU indépendamment de la taille de la fenêtre!

Code complet ici:

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

Remarque: sous Windows, le comportement est à nouveau différent et la solution de contournement n'est pas suffisante. – Mattijs

+0

J'ai élaboré dans ma réponse. – trashgod