【问题标题】:Can't paint a custom component in Swing correctly无法在 Swing 中正确绘制自定义组件
【发布时间】:2018-09-02 18:41:32
【问题描述】:

我对 Swing 的了解很差,对于愚蠢的问题,我深表歉意。 我需要做的是通过缓冲区(BufferedImage 实例)执行绘制来制作我的自定义组件(它们是JPanel 的祖先)。这是一项要求,因为绘制过程可能非常繁重,因此 paintComponent 方法被覆盖以从该缓冲区中绘制并立即返回(但如果您知道更好的方法来告诉 Java 不要每秒重绘对象超过 200-300 次消耗所有的 CPU - 我会很感激,也许有一些公共方法可以将图形上下文区域标记为“未更改”,因此 Swing 不会尝试重新绘制它们)。

我的代码有什么问题,它根本没有在重叠的 JPanel 上绘制数据。我已经为问题的核心做了一个可重现的例子。请看下面的代码:

public class Dr extends JPanel {

    public Dr(int x, int y, int w, int h, int fw, int fh) {
        left = x;
        top = y;
        width = w;
        height = h;
        full_width = fw;
        full_height = fh;
        setOpaque(true);
    }

    public void draw() {
        if (buffer == null && width > 0 && height > 0) {
            buffer = new BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
        }
        if (buffer == null) return;
        Graphics g = buffer.getGraphics();
        setBounds(0, 0, full_width, full_height);
        Graphics2D g2d = (Graphics2D)g;
        g2d.setBackground(Color.WHITE);
        g2d.clearRect(0, 0, full_width, full_height);

        g2d.setColor(new Color(255, 0, 80, 128));

        g2d.fillRect(left, top, width, height);
        System.out.println(left + ", " +  top);
    }

    @Override
    public void repaint() {
        draw();
    }

    @Override
    public void paintComponent(Graphics g) {
        g.drawImage(buffer, 0, 0, null);
    }

    private BufferedImage buffer;

    private int left;
    private int top;
    private int width;
    private int height;
    private int full_width;
    private int full_height;
}

这是一个示例类,它只保存要绘制的坐标和完整尺寸(我希望它等于其父尺寸,这又是一个要求,因为我需要支持可见的溢出内容才能成为显示)并绘制一个具有 100% 不透明度一半的红色矩形。

这是使用它的外部代码:

    Dr dr1 = new Dr(4, 4, width-10, 80, width-2, height-2);
    Dr dr2 = new Dr(4, 88, width-10, 80, width-2, height-2);
    root.add(dr1);
    root.add(dr2);
    dr1.setBounds(0, 0, width-2, height-2);
    dr2.setBounds(0, 0, width-2, height-2);
    dr1.draw();
    dr2.draw();

预计会是什么:

当我删除缓冲区并直接在paintComponent 方法中绘制时显示什么:

不填充背景时显示的内容(此处背景具有系统 L&F 颜色,但我需要元素父级的颜色(在我的示例中为白色)

当我从缓冲区中绘制时显示的内容

最后一个非常有趣。红色矩形从右侧和底部部分切掉,整个图像(包括白色背景)从父面板的(0, 0)坐标处剪裁。第二个对象根本不显示。控制台中的坐标是绝对OK的。

我觉得我必须与 Swing 做一些事情来告诉它不要在猜测要绘制什么和不绘制什么时执行它的“魔法”。但是我该怎么做呢?

更新:

我发现即使我在添加子组件后不调用draw() 方法,它们仍然会隐藏其父组件的背景(这不应该发生,bacase JPanel 是不透明的)。所以核心问题是我不能让我的Dr 对象具有不透明的背景(例如没有背景),其中没有任何内容(这也解释了为什么我只看到第一个矩形 - Swing 中的 z 顺序似乎反转,例如最后添加的组件被绘制在底部,因为它离我们最远)。

【问题讨论】:

  • 1.不要像那样覆盖repaint。系统可能会调用该方法,而您覆盖它的方式违反了 Liskov 替换原则。 2. 在您覆盖paintComponent 时调用super.paintComponent(g);。 3. 你在draw 中调用setBounds 的方式看起来很可疑,但我不知道你的意图。直接在组件上设置边界很少是正确的,当然当调用者只是期望重绘时也不正确。相反,您应该使用布局管理器,然后使用ComponentListener 响应系统所做的大小更改(如果需要)。
  • 如果您使用 minimal, complete example 更新您的问题,我们可以更轻松地提供帮助,因此我们不必猜测您的其余代码在做什么。
  • “以及你覆盖它的方式违反了 Liskov 替换原则” - 为什么?当系统调用repaint方法时,可以进行全力repaint。这就是我想要的。你认为我会经常重绘而不需要更新缓冲区吗?
  • 调用 super.paintComponent(g) 完全没有做任何事情。在我看来,我根本不应该使用 Swing 机制(我正在制作浏览器渲染引擎)。其中一个想法是将图层(BufferedImage 实例)存储在特殊管理器的 Vector 中,并让我的面板按照 z-index 的顺序绘制这些图层。并且根本不要将子块元素添加为其父元素的子元素(作为嵌套的 JPanel) - 无论如何,我通过覆盖事物来破坏 Swing 机制。
  • 查看repaint 的文档。你的覆盖做一些不同的事情。您至少需要致电super.repaint(),但您可能不应该在该方法中创建图像和东西。您还应该阅读oracle.com/technetwork/java/painting-140037.html

标签: java swing


【解决方案1】:

所以,我“认为”你对 Swing 中的一切如何运作存在根本性的误解。

您“似乎”对布局管理系统的工作方式、绘画系统的工作方式以及坐标系的工作方式有疑问

让我们从您的 Dr 组件开始。

你好像想开发重叠组件,可以显示下面的子组件,但是你使用setOpaque(true);,表示组件不会被看穿

这...

public void draw() {
    if (buffer == null && width > 0 && height > 0) {
        buffer = new BufferedImage(width, height, java.awt.image.BufferedImage.TYPE_INT_ARGB);
    }
    if (buffer == null) return;
    Graphics g = buffer.getGraphics();
    setBounds(0, 0, full_width, full_height);
    Graphics2D g2d = (Graphics2D)g;
    g2d.setBackground(Color.WHITE);
    g2d.clearRect(0, 0, full_width, full_height);

    g2d.setColor(new Color(255, 0, 80, 128));

    g2d.fillRect(left, top, width, height);
    System.out.println(left + ", " +  top);
}

对我来说似乎很奇怪。您将BufferedImage 定义为height 的大小为width,然后使用full_widthfull_height 来填充它......在我看来,以另一种方式做更有意义圆形

@Override
public void repaint() {
    draw();
}

好的,Swing 中的重要一课,你无法控制绘制过程,所以不要尝试。 Swing 有一个有据可查和完善的绘制过程,它提供了定义明确的钩子,您可以将自定义绘制注入其中(即paintComponent)。如果你需要“控制”,那么你需要考虑使用BufferStrategy,它可以让你完全控制来定义你自己的绘画过程。

好的,那么答案是什么?

嗯,这不是那么直截了当,因为我不能 100% 确定我理解您要解决的问题是什么。

但是,让我们从Dr 面板开始...

public class Dr extends JPanel {

    public Dr(int x, int y, int w, int h, int fw, int fh) {
        left = x;
        top = y;
        width = w;
        height = h;
        full_width = fw;
        full_height = fh;
        setOpaque(false);

        setBounds(x, y, fw, fh);
    }

    public void draw() {
        if (buffer == null && width > 0 && height > 0) {
            buffer = new BufferedImage(getWidth(), getHeight(), java.awt.image.BufferedImage.TYPE_INT_ARGB);
        }
        Graphics g = buffer.getGraphics();
        Graphics2D g2d = (Graphics2D) g;

        g2d.setColor(new Color(255, 0, 80, 128));

        g2d.fillRect(0, 0, width, height);
        g2d.dispose();
    }

    @Override
    public void paintComponent(Graphics g) {
        draw();
        g.drawImage(buffer, 0, 0, this);
        g.setColor(Color.RED);
        g.drawRect(0, 0, getWidth() - 1, getHeight() - 1);
    }

    private BufferedImage buffer;

    private int left;
    private int top;
    private int width;
    private int height;
    private int full_width;
    private int full_height;
}

因此,在这里,我对其进行了更改,以便面板将定位在您传递给构造函数的xy 位置,并将调整为fwfh 属性的大小。

draw 方法中,然后我创建一个大小为组件当前大小的BufferedImage 并绘制...什么...基于widthheight 属性...提出了问题关于为什么我们必须调整尺寸,但是,就是这样......

然后将缓冲区绘制到组件的左上角位置,这很重要,Swing 的坐标系是基于组件本身的,所以0x0 始终是组件的左上角,坐标系与父容器无关。

ps-红色矩形是调试用的,不需要。

然后我使用...

EventQueue.invokeLater(new Runnable() {
    @Override
    public void run() {
        try {
            UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
            ex.printStackTrace();
        }

        JFrame frame = new JFrame("Testing");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(new JLayeredPane());
        frame.add(new Dr(10, 10, 180, 180, 200, 200));
        frame.add(new Dr(100, 100, 180, 180, 200, 200));
        frame.setSize(400, 400);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
});

瞧,重叠的组件...

现在,我强烈建议您停下来仔细阅读:

而且,我的直觉是,你可能真的不想要单独的组件,你想要的是一个可以绘制许多缓冲区的组件

【讨论】:

  • 感谢您的回答,我会尝试使用您的代码,但可能很难集成它,因为当我这样做时,它可能会因为我的其他错误代码而突然停止工作。我将阅读有关 BufferStrategy 的信息,好的 :)
  • "但是然后使用 full_width 和 full_height 来填充它" - 不,你错过了一些东西,事实上恰恰相反。我使用widthheight 绘制矩形,使用full_widthfull_height 调整组件大小。
  • "在它们下面,但是你使用 setOpaque(true),这意味着组件不会被看穿" - 对不起,我的错误。应该是setOpaque(false),当谈到“true”和“false”时,我经常混淆“opacity”和“transparency”这两个词,尽管我经常在 CSS 中使用“opacity”属性,这对我来说很明显。但是,更正那行代码并没有解决任何问题。
  • 关于坐标系——我知道它是如何工作的。但我也发现我不能显示超出组件边界的东西,所以这就是我需要自己的坐标系的原因。这就是为什么我需要 2 个宽度和 2 个高度。现在我认为这是一个坏主意,因为如果我的所有图层都具有完整大小的视口(例如屏幕、窗口等),它将消耗 很多 内存。我不需要浪费内存来存储透明像素,因为只有少数 HTML 块会溢出内容(正常布局,或绝对或相对定位)。
  • 我应该更好地将图层保存为不同大小的缓冲区,保持它们的坐标。我的意思是,我需要做 Swing 做的事情,但没有 Swing(在这里真的没有帮助,只会制造很多问题)。
【解决方案2】:

我终于解决了这个问题。

问题出在我没有在此处发布的代码中。

我将Dr JPanel 添加到Block JPanel。 Block 重写了 repaint 方法以从缓冲区中绘制,但在当前版本中没有重写 paintComponent 方法。它还具有方法forceRepaint 来重建缓冲区,方法draw(Graphics g)draw() 分别在内部缓冲区或传递的图形上下文上绘制。 paint 方法也没有被覆盖。

因此,在我通过调用add 添加我的孩子之后,Swing 在我的Block 对象上调用了repaint。但是我的repaint 版本只是更新了内部缓冲区,仅此而已。屏幕上的结果没有任何变化。

所以这只是一个逻辑错误。

解决方案是在Block 类中重新实现paintComponent 的覆盖版本,该类的实例是我添加元素的root 对象。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-18
    • 1970-01-01
    • 1970-01-01
    • 2015-03-14
    相关资源
    最近更新 更多