简体   繁体   中英

Can't paint a custom component in Swing correctly

I know Swing very poorly, so sorry for stupid question. What I need to do is to make my custom components (they are ancestors of JPanel ) with painting performed from the buffer ( BufferedImage instance). It is a requirement, because the painting procedure might be very heavy, so paintComponent method is overridden to draw from that buffer and return imediately (but if you know better ways to tell Java not to repaint the object over 200-300 times a second consuming all the CPU - I would appreciate, maybe there are some public methods to mark regions of Graphics context as "not changed", so Swing does not try to repaint them).

What is wrong with my code that it simply does not draw data on overlapping JPanels. I have made a reproducable example of the core of the problem. See the code below:

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;
}

This is a sample class that does nothing but saving his coordinates to draw at and full dimensions (which I want to be equal to its parent dimensions, that's a requirement again because I need to support visible overflowing content to be able to be displayed) and drawing a red rectangle with half of the 100% opacity.

Here is the outer code that uses it:

    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();

What is expected to be:

想要什么

What is displayed when I remove my buffer and draw in paintComponent method directly:

直接油漆

What is displayed when I don't fill the background (here the background has system L&F color, but I need the color of element's parent (in my example it is white)

直接上漆,无背景填充

What is displayed when I drraw from my buffer

在此处输入图片说明

The last one is completely amuzing. The red rectangle is partly cut off from the right and from the bottom, and the full image (including the white background) is clipped from the (0, 0) coordinate of the parent panel. The second object is not displayed at all. The coordinates in the console are absolutely OK.

I feel there is something I have to do with Swing to tell it not to perform its "magic" upon guessing what to paint and what not to paint. But how can I do that?

UPDATE:

I found that even if I don't call draw() method of my child components after I added them, they still hide their parent's background (which should not happen, bacase JPanels are meant to be opaque). So the core problem is that I can't make my Dr objects have opaque background (eg no background) where nothing is painted in them (it also explains why I see only the first rectangle - the z-order in Swing seems to be reversed, eg the last added component is painted at the bottom, as it was the farthest from us).

So, what I "think" you have is a fundamental misunderstanding how, well, everything works in Swing.

You "seem" to be having an issue with how the layout management system works, how the painting system works and how the coordinate system works

Let's start with your Dr component.

You seem to want to develop overlapping components, which can show the child components below them, but you use setOpaque(true); , which means that the component won't be see through

This...

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);
}

Seems odd to me. You define the BufferedImage to be sized as width by height , but then use full_width and full_height to fill it...it seems, to me, to make more sense to do it the other way round

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

Okay, important lesson in Swing, you don't control the paint process, so don't try. Swing has a well documented and well established painting process that provides well defined hooks into which you can inject your custom painting (ie paintComponent ). If you need "control", then you need to look towards using a BufferStrategy , which will give you complete control to define you're own painting process.

Okay, so what's the answer?

Well, that's not so straight forward, because I'm not 100% sure I understand what the problem is you're trying to solve is.

But, let's start with the Dr panel...

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;
}

So, here, I've changed it so that the panel will be positioned at the x , y position you pass to the constructor and will be sized to the fw and fh properties.

In the draw method, I then create a BufferedImage sized to the component's current size and paint ... what ever ... based on the width and height properties ... questions are raised about why we have to sizes, but, there it is...

The buffer is then draw to the top/left position of the component, this is important, Swing's coordinate system is based around the component itself, so 0x0 is always the top/left corner of the component, the coordinate system has nothing to do with the parent container.

ps- The red rectangle is for debugging purposes, you don't need it.

I then use...

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);
    }
});

And voila, overlapping components...

重叠组件

Now, I strongly recommend that you stop and go have a read through:

And, my gut feeling is, you probably really don't want separate components, what you want is one component which can paint many buffers

I finally solved the issue.

The problem was in the code that I didn't post here.

I added my Dr JPanels into Block JPanel. Block had repaint method overridden to draw from the buffer, but hadn't overridden paintComponent method in current version. It also had method forceRepaint to rebuild the buffer, and methods draw(Graphics g) and draw() to draw on the internal buffer or on the passed Graphics context, respectively. paint method wasn't overridden either.

As a result, after I added my children with call to add , Swing called the repaint on my Block object. But my version of repaint simply updated the internal buffer, and that was all. The result on the screen didn't change in any way.

So that was just a logical error.

The solution was to re-implement the overridden version of paintComponent in Block class, a class whose instance is the root object where I add elements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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