JTextFields поверх активного рисования на JPanel, проблемы с потоками
Кто-нибудь когда-нибудь пытался использовать Swing для создания правильной мультибуферизованной среды рендеринга, поверх которой можно добавлять элементы пользовательского интерфейса Swing?
В этом случае у меня есть анимационный красный прямоугольник, нарисованный на фоне. Фон не нужно обновлять каждый кадр, поэтому я отображаю его в BufferedImage и перерисовываю только часть, необходимую для очистки предыдущего расположения прямоугольника. Посмотрите полный код ниже, это расширяет пример, данный @trashgod в предыдущем потоке, здесь.
Все идет нормально; плавная анимация, низкое использование процессора, без мерцания.
Затем я добавляю JTextField в Jpanel (нажав на любую позицию на экране) и фокусируюсь на нем, щелкая внутри текстового поля. Очистка предыдущего местоположения прямоугольника теперь терпит неудачу при каждом моргании курсора, см. Изображение ниже.
Мне любопытно, если у кого-нибудь есть идея, почему это может произойти (Swing не является поточно-ориентированным? Изображение рисуется асинхронно?) И в каком направлении искать возможные решения.
Это на Mac OS 10.5, Java 1.6
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
}
}
2 ответа
NewTest
продолжается JPanel
; а потому что вы не рисуете каждый пиксель при каждом вызове paintComponent()
вам нужно вызвать метод суперкласса и стереть старый рисунок:
@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);
...
}
Приложение: Как вы заметили, установка цвета фона в конструкторе исключает необходимость заполнения панели в paintComponent()
, в то время как super.paintComponent()
позволяет текстовое поле (поля) функционировать правильно. Как вы заметили, предлагаемый обходной путь хрупок. Вместо этого упростите код и оптимизируйте его в соответствии с требованиями. Например, вам может не понадобиться усложнение вставок, дополнительных буферов и компонентного слушателя.
Приложение 2: Обратите внимание, что super.paintComponent()
вызывает делегата пользовательского интерфейса update()
метод, "который заполняет указанный компонент его фоновым цветом (если его непрозрачное свойство имеет значение true)". Ты можешь использовать setOpaque(false)
чтобы исключить это.
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://stackru.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);
}
}
}
Я нашел обходной путь.
То, что я думаю, происходило: всякий раз, когда необходимо обновить поле JText (т.е. при каждом моргании курсора), вызывается переопределенная функция paintComponent() JPanel, но с другим аргументом Graphics, чем при вызове repaint(). Таким образом, при каждом моргании курсора мой прямоугольник очищался и перерисовывался в неправильном экземпляре Graphics, в результате чего графика, отображаемая на экране, была недействительной.
Есть ли в этом смысл? Если да, разве это не странное неудобство в Swing?
Во всяком случае, сохраняя логическое значение (activeRedraw
) откуда взялся этот вызов, похоже, мне удалось обойти эту проблему. Похоже, я наконец-то нашел способ сделать активное рисование без перекрашивания всей области экрана в каждом кадре, что означает низкое использование процессора независимо от размера окна!
Полный код здесь:
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
}
}