Java,如何绘制不断变化的图形?

8

我以前没有做过这个,所以显然我不擅长。在一个表单上,当前鼠标位置周围64个像素会被画得稍微大一些。问题是,它'有点'太慢了,而且我不知道从哪里开始修复。

除此之外,我创建了一个线程,当它完成时不断调用更新图形,并显示一个小的fps文本,以展示绘制速度有多快。

图像示例:(图像来自Eclipse中的字母'a')

alt text

代码示例:

@SuppressWarnings("serial")
public static class AwtZoom extends Frame {
    private BufferedImage image;
    private long timeRef = new Date().getTime();
    Robot robot = null;

    public AwtZoom() {
        super("Image zoom");
        setLocation(new Point(640, 0));
        setSize(400, 400);
        setVisible(true);
        final Ticker t = new Ticker();

        this.image = (BufferedImage) (this.createImage(320, 330));
        addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                t.done();
                dispose();
            }
        });
        try {
            robot = new Robot();
        } catch (AWTException e) {
            e.printStackTrace();
        }
        t.start();
    }

    private class Ticker extends Thread {
        public boolean update = true;
        public void done() {
            update = false;
        }
        public void run() {
            try {

                while (update == true) {
                    update(getGraphics());
                    // try {
                    // Thread.sleep(200);
                    // } catch (InterruptedException e) {
                    // e.printStackTrace();
                    // return;
                    // }
                }
            } catch (Exception e) {

                update=false;
            }
        }
    }

    public void update(Graphics g) {
        paint(g);
    }

    boolean isdone = true;
    public void paint(Graphics g) {
        if (isdone) {
            isdone=false;
            int step = 40;
            Point p = MouseInfo.getPointerInfo().getLocation();

            Graphics2D gc = this.image.createGraphics();

            try {

                for (int x = 0; x < 8; x++) {
                    for (int y = 0; y < 8; y++) {
                        gc.setColor(robot.getPixelColor(p.x - 4 + x, p.y
                                - 4 + y));
                        gc.fillOval(x * step, y * step, step - 3, step - 3);
                        gc.setColor(Color.GRAY);
                        gc.drawOval(x * step, y * step, step - 3, step - 3);
                    }
                }

            } catch (Exception e) {
                e.printStackTrace();
            }
            gc.dispose();
            isdone = true;
            iter++;
        }
        g.drawImage(image, 40, 45, this);
        g.setColor(Color.black);
        StringBuilder sb = new StringBuilder();
        sb.append(iter)
                .append(" frames in ")
                .append((double) (new Date().getTime() - this.timeRef) / 1000)
                .append("s.");
        g.drawString(sb.toString(), 50, 375);
    }

    int iter = 0;
}

修改内容如下:
* 添加了"gc.dispose();"代码
* 添加了"isdone"变量,确保重绘不会比必要的更频繁
* 添加此链接到thrashgod源码重写中
* 添加此链接到thrashgod源码重写2中


为什么不在鼠标移动事件发生时才重新绘制? - Eran Harel
你为什么在“paint”期间调用垃圾收集器? - Nate W.
真的想要绘制鼠标附近的内容,并希望它可以在鼠标所在的任何位置工作(尽可能快,即使在视频上)。我认为你发现的问题是,绘图事件也可能不是由线程t调用的。我认为这不是问题,但无论如何都添加了“isdone”来修复它。 - Margus
一些常规指针:(1)您应该只在事件分派线程上绘制,但似乎您正在Ticker线程上绘制。(2)您不需要“done”变量,因为单个线程不能同时运行两个代码片段。(3)尝试将其分成模型(一个8 * 8颜色数组)和视图(当更新模型时要求重新绘制)。 (4)考虑使用repaint(x,y,w,h),它指定了剪辑区域,因此重新绘制更快。 - Steve McLeod
我进行了一些分析 - 在我的 Mac 上,robot.getPixelColor(..) 的速度非常慢。这是一个寻找改进的好起点。 - Steve McLeod
4个回答

15
以下是需要翻译的内容:

这是我进行的重大改写,以下是值得注意的几个更改:

  • 我将检测像素颜色的任务与绘图任务分开处理
  • 我用robot.createScreenCapture(...)替换了robot.getPixelColor(...),一次获取所有64个像素,而不是一个一个获取
  • 我引入了智能剪辑-只有需要重新绘制的部分才会被重新绘制。
  • 我修复了线程问题,使模型和视图的所有更新都发生在事件分派线程上

计时器持续运行。当它检测到像素颜色发生变化时(无论是由于鼠标移动到不同区域还是鼠标下方的像素发生变化),它会准确地检测到发生了什么变化,更新模型,然后请求视图重新绘制。这种方法对人眼来说更新速度非常快。289次屏幕更新累计用时1秒。

这对安静的周六晚上是一次愉快的挑战。

import javax.swing.*;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Ellipse2D;
import java.awt.image.BufferedImage;

public class ZoomPanel extends JPanel {

    private static final int STEP = 40;
    private int iter = 0;
    private long cumulativeTimeTaken = 0;


    public static void main(String[] args) {
        final JFrame frame = new JFrame("Image zoom");

        final ZoomPanel zoomPanel = new ZoomPanel();
        frame.getContentPane().add(zoomPanel);
        final Ticker t = new Ticker(zoomPanel);

        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent we) {
                t.done();
                frame.dispose();
            }
        });
        t.start();

        frame.setLocation(new Point(640, 0));
        frame.pack();
        frame.setVisible(true);
    }

    private final Color[][] model = new Color[8][8];

    public ZoomPanel() {
        setSize(new Dimension(400, 400));
        setMinimumSize(new Dimension(400, 400));
        setPreferredSize(new Dimension(400, 400));
        setOpaque(true);
    }

    private void setColorAt(int x, int y, Color pixelColor) {
        model[x][y] = pixelColor;
        repaint(40 + x * STEP, 45 + y * STEP, 40 + (x * STEP) - 3, 45 + (y * STEP) - 3);
    }

    private Color getColorAt(int x, int y) {
        return model[x][y];
    }

    public void paintComponent(Graphics g) {
        long start = System.currentTimeMillis();
        if (!SwingUtilities.isEventDispatchThread()) {
            throw new RuntimeException("Repaint attempt is not on event dispatch thread");
        }
        final Graphics2D g2 = (Graphics2D) g;
        g2.setColor(getBackground());
        try {

            for (int x = 0; x < 8; x++) {
                for (int y = 0; y < 8; y++) {
                    g2.setColor(model[x][y]);
                    Ellipse2D e = new Ellipse2D.Double(40 + x * STEP, 45 + y * STEP, STEP - 3, STEP - 3);
                    g2.fill(e);
                    g2.setColor(Color.GRAY);
                    g2.draw(e);
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
        iter++;
        g2.setColor(Color.black);
        long stop = System.currentTimeMillis();
        cumulativeTimeTaken += stop - start;
        StringBuilder sb = new StringBuilder();
        sb.append(iter)
                .append(" frames in ")
                .append((double) (cumulativeTimeTaken) / 1000)
                .append("s.");

        System.out.println(sb);
    }

    private static class Ticker extends Thread {

        private final Robot robot;

        public boolean update = true;
        private final ZoomPanel view;

        public Ticker(ZoomPanel zoomPanel) {
            view = zoomPanel;
            try {
                robot = new Robot();
            } catch (AWTException e) {
                throw new RuntimeException(e);
            }
        }

        public void done() {
            update = false;
        }

        public void run() {
            int runCount = 0;
            while (update) {
                runCount++;
                if (runCount % 100 == 0) {
                    System.out.println("Ran ticker " + runCount + " times");
                }
                final Point p = MouseInfo.getPointerInfo().getLocation();

                Rectangle rect = new Rectangle(p.x - 4, p.y - 4, 8, 8);
                final BufferedImage capture = robot.createScreenCapture(rect);

                for (int x = 0; x < 8; x++) {
                    for (int y = 0; y < 8; y++) {
                        final Color pixelColor = new Color(capture.getRGB(x, y));

                        if (!pixelColor.equals(view.getColorAt(x, y))) {
                            final int finalX = x;
                            final int finalY = y;
                            SwingUtilities.invokeLater(new Runnable() {
                                public void run() {
                                    view.setColorAt(finalX, finalY, pixelColor);
                                }
                            });
                        }
                    }
                }

            }
        }

    }

}

1
+1 我冒昧地使用了 createScreenCapture() 来加快我的替代代码的速度。 - trashgod
谢谢,伙计!这就是我想要的吗? - Arthur Alunts

7
如果您不介意使用Swing,这个示例展示了如何快速放大从Icon获得的BufferedImage。在您的情况下,您需要一个8x8的BufferedImage,它在mouseMoved()中被填充机器人看到的像素。
补充说明:这是你示例的左上角的快照。
补充说明:

缩放本身并不重要...

慢的部分是从桌面获取像素; 缩放很小。如果您只想查看各种动画技术,请查看此示例
补充说明:由于获取单个像素很慢,并且@Steve McLeod建议的createScreenCapture()方法很快,所以这就是我所追求的想法。您可以看到它也更新得更平滑。请注意,释放鼠标按钮可以查看捕获的颜色。 Zoom.png
import java.awt.AWTException;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Robot;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;

/** @see https://dev59.com/lVDTa4cB1Zd3GeqPKJCQ */
public class Zoom extends JPanel implements MouseMotionListener {

    private static final int SIZE = 16;
    private static final int S2 = SIZE / 2;
    private static final int SCALE = 48;
    private BufferedImage img;
    private Robot robot;

    public Zoom() {
        super(true);
        this.setPreferredSize(new Dimension(SIZE * SCALE, SIZE * SCALE));
        img = new BufferedImage(SIZE, SIZE, BufferedImage.TYPE_INT_RGB);
        try {
            robot = new Robot();
        } catch (AWTException e) {
            e.printStackTrace(System.err);
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        g.drawImage(img, 0, 0, getWidth(), getHeight(), null);
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        Point p = e.getPoint();
        int x = p.x * SIZE / getWidth();
        int y = p.y * SIZE / getHeight();
        int c = img.getRGB(x, y);
        this.setToolTipText(x + "," + y + ": "
            + String.format("%08X", c));
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        int x = e.getXOnScreen();
        int y = e.getYOnScreen();
        Rectangle rect = new Rectangle(x - S2, y - S2, SIZE, SIZE);
        img = robot.createScreenCapture(rect);
        repaint();
    }

    private static void create() {
        JFrame f = new JFrame("Click & drag to zoom.");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        Zoom zoom = new Zoom();
        f.add(zoom);
        f.pack();
        f.setVisible(true);
        zoom.addMouseMotionListener(zoom);
    }

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

            @Override
            public void run() {
                create();
            }
        });
    }
}

你是想放大组件还是桌面? - trashgod
缩放本身并不重要,只需要快速输入以更改图形。 - Margus
我添加了链接到您的代码重写,这样您就可以看到我做了什么,并且我所说的“没有明显差异”。 - Margus
@Margus:我认为你会发现这个新版本更快。 - trashgod
@trashgod的帖子中的一部分可以被认为是更好的教程,但这里没有可用的赞。 - mKorbel
显示剩余3条评论

0
将此方法添加到 Paint 方法中:
public void clear(Graphics g, Color currentColor) {
    g.setColor(backgroundColor);
    g.fillRect(0, 0, width, height);
    g.setColor(currentColor);
    int delay = 5; //milliseconds

    ActionListener taskPerformer = new ActionListener() {
    public void actionPerformed(ActionEvent evt) { } };

    new Timer(delay, taskPerformer).start();
} //run this right before you draw something

好的,使用定时器来减慢延迟,而不是线程,那样是不好的。


paint() 方法在事件分派线程上被调用... 在其中添加 Thread.sleep() 不是一个好的做法。 - rolfl
只是为了减缓绘图速度,以避免闪烁。 - Dylan Turner
1
然后考虑使用Swing计时器:http://docs.oracle.com/javase/7/docs/api/javax/swing/Timer.html ... 永远不要在事件分派线程上休眠;-)(几乎所有意义上的“永远不要”) - rolfl
我在那里添加了一个计时器而不是新线程。现在,为什么那样做不好?当我使用它时它可以工作。 - Dylan Turner
请参阅http://docs.oracle.com/javase/tutorial/uiswing/concurrency/dispatch.html - 事件分派线程上的任何延迟都会影响应用程序的响应能力,可能导致事件积压。 - rolfl
谢谢您告诉我这个。我不会再使用它了。 - Dylan Turner

-1

简单地使用时间延迟循环。您可以通过调整 i 的限制来微调延迟。这还使您能够通过一些尝试来调整转换速度。

for(long i=0;i<=100000000000;i++);

canvas.repaint();

这对我来说非常好用,而且不需要使用缓冲图像。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接