paintComponent()与paint(),以及JPanel与Canvas在绘图型GUI中的区别

16
我从这篇这篇,和这篇帖子中获得了一些有趣的想法和批评(请参见有关GUI代码的最后一篇帖子)。然而,我仍然对一些事情感到非常困惑。主要是,显示用户引入的图形的最便宜的方法是什么?
更具体地说,我在MouseDragged()方法中通过创建JPanel类的对象并使用paintComponent(getGraphics())方法(相应地使用AuxClass2AuxClass1)使用JPanel类的paintComponent()方法。
显然,使用getGraphics()paintComponent()而不是repaint()是一个坏主意,我怀疑这与内存使用有关。每次用户拖动鼠标时调用AuxClass2也不是一个好主意。
另外,JPanel vs Canvas(即swing vs awt)有点令人困惑。什么时候使用它们?
我一直在试图找到解决方法,但没有找到一个,特别是对于getGraphics()方法:还有什么其他方式可以将图形添加到面板中?
3个回答

23

重量级组件与轻量级组件

一般而言,重量级组件会与其本地对等体进行连接,而轻量级组件会共享一个本地对等体。

通常来说,混合使用重量级组件和轻量级组件是不明智的,因为可能会存在Z顺序问题以及绘制问题(即使现在已经被改进了)。

这也就是为什么你被劝阻使用 Canvas 类的原因,可能是因为你试图将其放置在一个轻量级组件上...我猜。

控制的幻觉

对于新手来说,Swing API 最大的问题之一就是认为你可以对绘制过程进行控制,实际上你不能。接受这个事实会更容易。

最好的做法就是请求重绘管理器在其最早方便的时间执行更新操作。

同时,调用 getGraphics 并不保证返回非空值。

正确的先后顺序

paintpaintComponent

这里的问题在于,paint 执行了多项重要任务,而调用 paintComponent 只是其中的一项。

在 Swing 中,我们经常被鼓励使用 paintComponent 来进行自定义绘制,因为它通常是组件的最底层,在子组件绘制之前进行绘制。

如果你重写了 paint 方法,并在调用 super.paint 之后绘制图形,那么你最终会绘制在所有内容的顶部,这并不总是期望的结果。

即使如此,子组件也可以独立于其父容器进行绘制,从而覆盖你可能添加的任何绘制效果。

有用的链接

结束语

只有被添加到与本地对等体相关联的组件上的组件才会调用其paint方法。因此,试图绘制尚未添加到容器中的组件是毫无意义的...


我有点想点赞,但是“正确的事物顺序”的解释让我有点困惑。关于“paint”的部分对我来说很清楚,但它如何与paintComponent相配合就不太清楚了(除了在子组件被绘制之前发生)。我会尝试你提供的链接,看看是否能给我更多的见解。 - searchengine27
@searchengine27 一般来说,绘制链看起来像是 paint -> paintComponent -> paintBorder -> paintChildren。容易忽略调用 super.paint 并产生奇怪的绘图结果,但是也很容易通过在自定义绘图后调用 super.paint 覆盖绘图或绘制子组件,这可能听起来像个好主意,但是,每个子组件都可以独立于父容器进行绘制,这意味着您最初绘制的任何内容都可能被覆盖。 - MadProgrammer
我确实遇到了这样的问题,但我很难弄清楚如何解决它。本质上,我有一个具有边框布局的JPanel。里面有3个JLists,其中2个有边框,一个没有,所有内容都不同。我需要JList已经被绘制并初始化了Graphics对象,以便使用Graphics来计算列表内文本和边框文本(使用带文本的Border)的宽度。问题是,我需要父JPanel根据任何列表中任何文本的最大宽度调整大小,但我似乎无法找出顺序。 - searchengine27

11
使用 BufferedImage 作为绘画表面,将其显示在一个 JLabel 中。将标签放置在一个面板的中心位置,并将其放入一个 JScrollPane 中。
根据需要调用 bufferedImage.getGraphics(),但记得在完成后调用 dispose(),然后调用 label.repaint()
在整个过程中使用 Swing 组件,并且不要覆盖任何东西。
这里有一个示例,演示如何使用图像作为绘画表面。 还有一个更好的示例 我没有说这个截图更好,而是代码更好。 ;)

1
真诚地赞赏真正程序员的艺术,点个赞! - Alex Mandelias
1
@AAAlex123 谢谢! 我正等着有人这么说呢。(检查帖子日期) 幸好我没有屏住呼吸等待。;) - Andrew Thompson

6

我一直在寻找解决办法,但并没有找到一个,特别是对于getGraphics()方法:除此之外如何将图形添加到面板中?

记得将需要绘制的内容作为变量,并在paintComponent()中使用它。

例如,在你之前的问题中尝试做的事情应该看起来像:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class PaintRectangle extends JPanel {

    private Point mouseLocation;

    public PaintRectangle() {
        setPreferredSize(new Dimension(500, 500));

        MouseAdapter listener = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                updateMouseRectangle(e);
            }

            private void updateMouseRectangle(MouseEvent e) {
                mouseLocation = e.getPoint();
                repaint();
            }

            @Override
            public void mouseDragged(MouseEvent e) {
                updateMouseRectangle(e);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                mouseLocation = null;
                repaint();
            }
        };
        addMouseListener(listener);
        addMouseMotionListener(listener);
    }

    private Rectangle getRectangle() {
        if(mouseLocation != null) {
            return new Rectangle(mouseLocation.x - 5, mouseLocation.y - 5, 10, 10);
        }
        else {
            return null;
        }
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Rectangle rectangle = getRectangle();
        if(rectangle != null) {
            Graphics2D gg = (Graphics2D) g;
            gg.setColor(Color.BLUE);
            gg.fill(rectangle);
            gg.setColor(Color.BLACK);
            gg.draw(rectangle);
        }
    }

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                frame.getContentPane().add(new PaintRectangle());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }
}

另请参见http://docs.oracle.com/javase/tutorial/uiswing/painting/


谢谢,问题是调用repaint()会擦除之前绘制的内容。有没有办法避免这种情况发生? - Alex
不行,因为你不知道哪些部分需要重新绘制(用户可能已经将另一个窗口移动到你的窗口上方),所以paintComponent应该重新绘制整个组件(在指定的剪辑区域内)。你可以使用Andrew的方法将绘制内容绘制到图像中,并在paintComponent中绘制该图像。 - Walter Laan
这是否意味着,如果我需要保留整个“画作”,例如保存到硬盘,我就不应该使用CanvasJPanel,而是应该使用BufferedImage - Alex
1
这不应该是被接受的答案。几乎没有解释,而且解释中的两个句子之一是一个不完整的句子,毫无意义,因为没有上下文可以推断出任何信息。 - searchengine27

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