简体   繁体   English

Swing中的增量图形

[英]Incremental graphics in Swing

I'm trying to get the hang of doing graphics stuff (drawing lines, etc.) in Swing. 我正试图在Swing中完成图形内容(绘制线条等)。 So far, all the tutorials I've seen declare a class that overrides paintComponent , and all the paintComponent methods do some set, specific thing, like drawing a red square (although maybe they draw it at a different location every time). 到目前为止,我见过的所有教程都声明了一个覆盖paintComponent的类,并且所有paintComponent方法都做了一些特定的事情,比如绘制一个红色正方形(尽管可能每次都在不同的位置绘制它)。 Or maybe they draw a number of lines and shapes, but the paintComponent method does everything all at once. 或者他们可能会绘制一些线条和形状,但paintComponent方法会同时执行所有操作。

I'm trying to figure out: suppose I want to draw one thing in a component, and later on draw something else on top of it without erasing what I drew before. 我想弄清楚:假设我想在一个组件中绘制一个东西,然后在它上面绘制其他东西而不删除我之前绘制的东西。 My first thought was to have my paintComponent override call a callback. 我的第一个想法是让我的paintComponent覆盖调用回调。

import java.awt.*;
import javax.swing.*;
public class DrawTest {

    private interface GraphicsAction {
        public void action (Graphics g);
    }

    private static class TestPanel extends JPanel {

        private GraphicsAction paintAction;

        public void draw (GraphicsAction action) {
            paintAction = action;
            repaint();
        }

        @Override
        public void paintComponent (Graphics g) {
            super.paintComponent (g);
            if (paintAction != null)
                paintAction.action(g);
        }
    }

    private static void createAndShowGui() {
        JFrame frame = new JFrame ("DrawTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(500,500));

        TestPanel p = new TestPanel ();
        frame.getContentPane().add(p);
        frame.pack();
        frame.setVisible(true);
        p.repaint();

        p.draw (new GraphicsAction () {
            public void action (Graphics g) {
                g.setColor(Color.RED);
                g.drawLine(5, 30, 100, 50);
            }
        });

        // in real life, we would do some other stuff and then
        // later something would want to add a blue line to the
        // diagram 

        p.draw (new GraphicsAction () {
            public void action (Graphics g) {
                g.setColor(Color.BLUE);
                g.drawLine(5, 30, 150, 40);
            }
        });

    }

    public static void main (String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGui();                 
            }
        });
    }
}

This doesn't work. 这不起作用。 A blue line shows up, but no red line. 出现蓝线,但没有红线。 I'm guessing this is because the repaint() in draw causes everything to start over when I draw the blue line, but I'm not sure; 我猜这是因为repaint()draw使一切,当我画蓝线开始了,但我不知道; anyway, I don't know how else to get paintComponent to be called. 无论如何,我不知道如何让paintComponent被调用。

Also, if I put a Thread.sleep(1000) between the two p.draw calls, I don't even see the red line for a second. 另外,如果我在两个p.draw调用之间放置一个Thread.sleep(1000) ,我甚至都看不到红线。 So I'm not at all clear on how to get my graphics to show up when I want them to. 因此,我并不清楚如何在我想要的时候显示我的图形。

I've done some searching on "incremental graphics" in Swing, but nothing that helps find a solution. 我已经在Swing中对“增量图形”进行了一些搜索,但没有任何东西可以帮助找到解决方案。 I found an Oracle article "Painting in AWT and Swing" that discusses overriding an update() method to accomplish incremental graphics, but I haven't found any actual examples of this being done. 我发现了一篇Oracle文章“在AWT和Swing中绘画”,讨论了覆盖update()方法以完成增量图形,但我还没有找到任何实际的例子。

So how would I get this to do what I want? 那么我怎么能做到这一点呢? It seems like a common enough task that there should be a simple way to do it, but I haven't found one. 这似乎是一个很常见的任务,应该有一个简单的方法来做,但我还没有找到一个。 I'm assuming it should be doable without calling getGraphics , which, based on other StackOverflow responses would be, at best, kind of gauche. 我假设它应该是可行的,而不需要调用getGraphics ,基于其他StackOverflow响应,它最多只是一种诡计。

Painting in Swing is destructive. Swing中的绘画具有破坏性。 That is, whenever a new paint cycle runs, you are expected to completely rebuild the output as per the state of the object you are painting. 也就是说,每当新的绘制周期运行时,您都应该根据要绘制的对象的状态完全重建输出。

Take a look at Painting in AWT and Swing 看看AWT和Swing中的绘画

So when you call 所以当你打电话

p.draw (new GraphicsAction () {
    public void action (Graphics g) {
        g.setColor(Color.RED);
        g.drawLine(5, 30, 100, 50);
    }
});

Followed by 其次是

p.draw (new GraphicsAction () {
    public void action (Graphics g) {
        g.setColor(Color.BLUE);
        g.drawLine(5, 30, 150, 40);
    }
});

You are basically throwing away the first action. 你基本上扔掉了第一个动作。 Ignoring how repaints are scheduled for the moment. 忽略目前如何安排重拍。 The first request says, "paint a red line", the second says, "paint a blue line", but before these actions are executed, the Graphics context is cleaned, preparing it for updating. 第一个请求说“画一条红线”,第二个请求“画一条蓝线”,但在执行这些动作之前,清理Graphics上下文,准备更新。

This is very important, as the Graphics context you are provided is a shared resource. 这非常重要,因为您提供的Graphics上下文是共享资源。 All the components painted before have used the same context, all the components painted after you will use the same context. 之前绘制的所有组件都使用了相同的上下文,在您使用相同的上下文后绘制的所有组件。 This means, if you don't "clean" the context before painting to it, you can end up with unwanted paint artifacts. 这意味着,如果在绘制之前没有“清理”上下文,则最终会出现不需要的绘制工件。

But how can you get around it?? 但你怎么能绕过它?

There are a few choices here. 这里有一些选择。

You could draw to a backing buffer (or BufferedImage ) which has it's own Graphics context, which you can add to and would only need to "paint" in your paintComponent method. 您可以绘制到具有自己的Graphics上下文的后备缓冲区(或BufferedImage ),您可以添加它,并且只需要在paintComponent方法中“绘制”。

This would mean, each time you call p.draw(...) , you would actually paint to this buffer first then call repaint . 这意味着,每次调用p.draw(...) ,您实际上会首先repaint到此缓冲区,然后调用repaint

The problem with this, is you need to maintain the size of the buffer. 这个问题是,你需要保持缓冲区的大小。 Each time the component size changes, you would need to copy this buffer to a new buffer based on the new size of the component. 每次组件大小更改时,您都需要根据组件的新大小将此缓冲区复制到新缓冲区。 This is a little messy, but is doable. 这有点乱,但是可行。

The other solution would be to place each action in a List and when required, simply loop through the List and re-apply the action whenever required. 另一个解决办法是放置在每个动作List通过并在需要时,只需循环List和在需要时重新申请的动作。

This is probably the simplest approach, but as the number of actions grow, could reduce the effectiveness of the paint process, slowing the paint process. 这可能是最简单的方法,但随着动作数量的增加,可能会降低油漆过程的有效性,从而减缓油漆过程。

You could also use a combination of the two. 您也可以使用两者的组合。 Generate a buffer when it doesn't exists, loop through the List of actions and renderer them to the buffer and simply paint the buffer in the paintComponent method. 当缓冲区不存在时生成缓冲区,遍历操作List并将它们渲染到缓冲区,并简单地在paintComponent方法中绘制缓冲区。 Whenever the component is resized, simply null the buffer and allow the paintComponent to regenerate it...for example... 每当调整组件大小时,只需将缓冲区置null并允许paintComponent重新生成它......例如......

Also, if I put a Thread.sleep(1000) between the two p.draw calls 另外,如果我在两个p.draw调用之间放置一个Thread.sleep(1000)

Swing is a single threaded framework. Swing是一个单线程框架。 That means that all updates and modifications are expected to done within the context of the Event Dispatching Thread. 这意味着所有更新和修改都应在Event Dispatching Thread的上下文中完成。

Equally, anything that blocks the EDT from running will prevent it from process (amongst other things) paint requests. 同样,任何阻止EDT运行的东西都会阻止它处理(除其他外)绘制请求。

This means that when you sleep between the p.draw calls, you are stopping the EDT from running, meaning it can't process your paint requests... 这意味着当您在p.draw调用之间sleep时,您将停止运行EDT,这意味着它无法处理您的绘制请求...

Take a look at Concurrency in Swing for more details 有关更多详细信息,请参阅Swing中的Concurrency

Updated with example 更新了示例

在此输入图像描述

I just want to point out that is really inefficient. 我只想指出这是非常低效的。 Re-creating the buffer each time invalidate is called will create a large number of short lived objects and could a significant drain on performance. 每次调用invalidate重新创建缓冲区将创建大量短期对象,并且可能会显着降低性能。

Normally, I would use a javax.swing.Timer , set to be non-repeating, that would be restarted each time invalidate was called. 通常,我会使用javax.swing.Timer ,设置为非重复,每次调用invalidate时都会重新启动。 This would be set to a short delay (somewhere between 125-250 milliseconds). 这将设置为短延迟(介于125-250毫秒之间)。 When the timer is triggered, I would simply re-construct the buffer at this time, but this is just an example ;) 当计时器被触发时,我只是在这个时候重新构造缓冲区,但这只是一个例子;)

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class DrawTest {

    private interface GraphicsAction {

        public void action(Graphics g);
    }

    private static class TestPanel extends JPanel {

        private GraphicsAction paintAction;
        private BufferedImage buffer;

        @Override
        public void invalidate() {
            BufferedImage img = new BufferedImage(
                    Math.max(1, getWidth()),
                    Math.max(1, getHeight()), BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = img.createGraphics();
            g2d.setColor(getBackground());
            g2d.fillRect(0, 0, getWidth(), getHeight());
            if (buffer != null) {
                g2d.drawImage(buffer, 0, 0, this);
            }
            g2d.dispose();
            buffer = img;
            super.invalidate();
        }

        protected BufferedImage getBuffer() {
            if (buffer == null) {
                buffer = new BufferedImage(
                        Math.max(1, getWidth()),
                        Math.max(1, getHeight()), BufferedImage.TYPE_INT_ARGB);
                Graphics2D g2d = buffer.createGraphics();
                g2d.setColor(getBackground());
                g2d.fillRect(0, 0, getWidth(), getHeight());
                g2d.dispose();
            }
            return buffer;
        }

        public void draw(GraphicsAction action) {
            BufferedImage buffer = getBuffer();
            Graphics2D g2d = buffer.createGraphics();
            action.action(g2d);
            g2d.dispose();
            repaint();
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(getBuffer(), 0, 0, this);
        }
    }

    private static void createAndShowGui() {
        JFrame frame = new JFrame("DrawTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(500, 500));

        TestPanel p = new TestPanel();
        frame.getContentPane().add(p);
        frame.pack();
        frame.setVisible(true);
        p.repaint();

        p.draw(new GraphicsAction() {
            public void action(Graphics g) {
                g.setColor(Color.RED);
                g.drawLine(5, 30, 100, 50);
            }
        });

        // in real life, we would do some other stuff and then
        // later something would want to add a blue line to the
        // diagram 
        p.draw(new GraphicsAction() {
            public void action(Graphics g) {
                g.setColor(Color.BLUE);
                g.drawLine(5, 30, 150, 40);
            }
        });

    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGui();
            }
        });
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM