javaswingpaintcomponentz-ordercustom-painting

Custom painting and Z-order: query re painting order


So, I want to Z-order some components in a JFrame.

The component(s):

public class aBLUEBox extends JPanel{
    int xPos = 19;
    int yPos = 20;
    int width = 10;
    int height = 80;
    
    public void paintBox(Graphics g){
         g.setColor(Color.BLUE);
         g.fillRect(xPos,yPos,width,height);
    }
}

The frame:

public class CreateWindow extends JFrame{
    CreateWindow(){
        this.setTitle("Layering Test");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        
        this.setSize(1920/2,1080/2);
        this.setLocationRelativeTo(null);
        this.setResizable(false);
        
        this.setVisible(true);
    }
}

Adding components into frame/Main class:

public class LayerMain {
    CreateWindow window;

    static aBLUEBox BLUEBox;
    static aREDBox REDBox; //A Different Component just like aBLUEBox, but with an altered PaintBox() method which paints a red box instead of a blue one.

    PanelRenderer RendererP;
    
    LayerMain(){
        BLUEBox = new aBLUEBox();
        REDBox = new aREDBox();
        
        RendererP = new PanelRenderer();  //holds the PaintComponent Method. Class for this is shown below.
        
        window = new CreateWindow();
        window.add(BLUEBox);
        window.add(REDBox);

        window.setComponentZOrder(BLUEBox, 0);
        window.setComponentZOrder(REDBox, 0); //puts red on 0, moving blue up to 1.
//So now, BLUEBox's Z-order is 1, thus BLUEBox is on top of REDBox.

        System.out.println("Z-order of blue = " + window.getComponentZOrder(BLUEBox)); //Prints 1
        System.out.println("Z-order of Red = " + window.getComponentZOrder(REDBox)); //Prints 0

        window.add(RendererP);

        RendererP.repaint();  //Should Paint both box's.
     }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() { 
                new LayerMain();
            }
        });
    }
}

Then, I want to render out those component using the repaint() invocation.

Renderer:

public class PanelRenderer extends JPanel{

    public void paintComponent(Graphics g){ 
        super.paintComponent(g); 
        //JPanels:
        LayerMain.BLUEBox.paintBox(g);  //Paints Blue first, not that it should matter.
        LayerMain.REDBox.paintBox(g);   //Paints Red Second, not that it should matter.

        System.out.println("PaintComponent invoked.");
    }
}

What should render on top/below should correspond to the Z-index of the component in the frame. (Component at index 1 should render on top of component an index 0, for example)

However, when the renderer (JPanel) is added onto the window (JFrame), and paintComponent is invoked, nothing occurs. Literally nothing paints.

Commenting out the Z-order code in the main class makes it so that at least something does in fact paint, but red paints on top of blue (because in the PaintComponent method red is painted last, and thus on top), which is not what I want.

//window.setComponentZOrder(BLUEBox, 0);
//window.setComponentZOrder(REDBox, 0); //puts red on 0, moving blue up to 1.

Why do the components display in the order that they are called within the paintComponent, and not displayed in the order that they are set to within the JFrame?

MRE / SSCCE

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

public class LayerMain {
    CreateWindow window;

    static ColoredBox blueBox;
    //A Different Component just like aBLUEBox, but with an altered 
    // PaintBox() method which paints a red box instead of a blue one.
    static ColoredBox redBox; 

    PanelRenderer rendererP;

    LayerMain(){
        blueBox = new ColoredBox(Color.BLUE);
        redBox = new ColoredBox(Color.RED);

        //holds the PaintComponent Method. Class for this is shown below.
        rendererP = new PanelRenderer();  

        window = new CreateWindow();
        window.add(blueBox);
        window.add(redBox);

        window.setComponentZOrder(blueBox, 0);
        window.setComponentZOrder(redBox, 0); //puts red on 0, moving blue up to 1.
        //So now, blueBox's Z-order is 1, thus blueBox is on top of redBox.

        System.out.println("Z-order of blue = " + window.getComponentZOrder(blueBox)); //Prints 1
        System.out.println("Z-order of Red = " + window.getComponentZOrder(redBox)); //Prints 0

        window.add(rendererP);

        rendererP.repaint();  //Should Paint both box's.
     }
    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            new LayerMain();
        });
    }
}

class PanelRenderer extends JPanel{

    @Override
    public void paintComponent(Graphics g){ 
        super.paintComponent(g); 
        //JPanels:
        LayerMain.blueBox.paintBox(g);  //Paints Blue first, not that it should matter.
        LayerMain.redBox.paintBox(g);   //Paints Red Second, not that it should matter.

        System.out.println("PaintComponent invoked.");
    }
}

class CreateWindow extends JFrame{
    CreateWindow(){
        this.setTitle("Layering Test");
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);

        this.setSize(1920/2,1080/2);
        this.setLocationRelativeTo(null);
        this.setResizable(false);

        this.setVisible(true);
    }
}

class ColoredBox extends JPanel {
    int xPos = 19;
    int yPos = 20;
    int width = 10;
    int height = 80;
    Color color;
    
    ColoredBox(Color color) {
        super();
        this.color = color;
    }

    public void paintBox(Graphics g){
         g.setColor(color);
         g.fillRect(xPos,yPos,width,height);
    }
}

Solution

  • Ok, no. This is most defiantly not how you should be trying to perform custom painting or using custom components. You are not responsible for the rendering of the components, so your PanelRenderer makes no sense, nor should you really be using static this way, it's just lazy and error prone.

    Either make it 100% custom painting or 100% component orientated, not both

    Why do the components display in the order that they are called within the PaintComponent, and not displayed in the order that they are set to within the JFrame?

    Because you're not responsible for painting the components and you are messing with the system

    Component/ZOrder example

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private JPanel blueBox;
            private JPanel redBox;
    
            private JPanel top, bottom;
    
            public TestPane() {
                blueBox = makeBox(Color.BLUE);
                redBox = makeBox(Color.RED);
    
                setLayout(new GridBagLayout());
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridx = 0;
                gbc.gridy = 0;
                // Filler
                add(emptyBox(), gbc);
    
                gbc.gridwidth = 2;
                gbc.gridheight = 2;
                gbc.fill = GridBagConstraints.BOTH;
                add(blueBox, gbc);
    
                gbc.gridx++;
                gbc.gridy++;
                add(redBox, gbc);
    
                // Filler
                gbc.gridx++;
                gbc.gridy++;
                gbc.gridwidth = 1;
                gbc.gridheight = 1;
                gbc.fill = GridBagConstraints.NONE;
                add(emptyBox(), gbc);
    
                JButton flip = new JButton("Flip");
                flip.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        setComponentZOrder(bottom, 0);
                        JPanel temp = top;
                        top = bottom;
                        bottom = temp;
                        repaint();
                    }
                });
    
                gbc.gridx = 0;
                gbc.gridy = 4;
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                add(flip, gbc);
    
                top = blueBox;
                bottom = redBox;
            }
    
            protected JPanel emptyBox() {
                JPanel box = new JPanel() {
                    @Override
                    public Dimension getPreferredSize() {
                        return new Dimension(50, 50);
                    }
                };
                box.setOpaque(false);
                return box;
            }
    
            protected JPanel makeBox(Color color) {
                JPanel box = new JPanel() {
                    @Override
                    public Dimension getPreferredSize() {
                        return new Dimension(100, 100);
                    }
                };
                box.setBackground(color);
                return box;
            }
    
        }
    
    }
    

    Custom painting

    import java.awt.BorderLayout;
    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.EventQueue;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private List<Box> boxes = new ArrayList<Box>(2);
    
            public TestPane() {
                boxes.add(new Box(Color.BLUE, 0, 0));
                boxes.add(new Box(Color.RED, 50, 50));
    
                setLayout(new BorderLayout());
    
                JButton flip = new JButton("Flip");
                flip.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent e) {
                        // Ok, this is a cheat as I only have to boxes,
                        // if you had more, you'd need to move the box you 
                        // higher in the rendering order closer to the end 
                        // of the list
                        Collections.reverse(boxes);
                        repaint();
                    }
                });
                add(flip, BorderLayout.SOUTH);
            }
    
            @Override
            public Dimension getPreferredSize() {
                return new Dimension(150, 150);
            }
    
            @Override
            protected void paintComponent(Graphics g) {
                super.paintComponent(g);
    
                // Cheat, I should looping over the box array to calculate the
                // area the boxes actuall occupy
                int x = (getWidth() - 150) / 2;
                int y = (getHeight() - 150) / 2;
    
                Graphics2D g2d = (Graphics2D) g.create();
                g2d.translate(x, y);
    
                for (Box box : boxes) {
                    box.paint(g2d);
                    x += 50;
                    y += 50;
                }
                g2d.dispose();
            }
        }
    
        public class Box {
    
            private Color color;
    
            private int x, y;
    
            public Box(Color color, int x, int y) {
                this.color = color;
                this.x = x;
                this.y = y;
            }
    
            public void paint(Graphics2D g2d) {
                g2d.setColor(color);
                g2d.fillRect(x, y, 100, 100);
            }
        }
    
    }