2

Similar to JButton showing up in the wrong spot after repaint is called, but the responses to that question only addressed the fact that he wasn't using a layout manager, while I am using a layout manager (poorly), so it didn't really help, unfortunately.

Details

Intent of the Program:

To show a preview of a 1920x1080 grid (scaled down to 640x480 to save space), stretching and shrinking as you change the height of each square, width of each square, and the number of columns it'll have. (You specify a number of total squares to be in the grid first, so the number of rows is inferred by the program.)

Structure:

  • One top-level JFrame.
  • Contains two JPanels: the Grid, and the sidebar, using a BorderLayout to snap them to the east and west sides.
  • Sidebar is one JPanel containing all of the JComponents in a Y-Axis aligned BoxLayout.
  • Grid extends JComponent, and uses Graphics.drawLine() to draw the grid.
  • Each component in the sidebar calls Grid.repaint() when changed to update the grid.

Current UI, with the two main JPanels outlined in red.

The Problem

Whenever I change any of the components and thus call Grid.repaint():

  1. The grid doesn't clear, resulting in multiple lines appearing;
  2. All of the sidebar components get painted at the top-left corner, while still showing/functioning on the sidebar;
  3. The grid resizes itself to be wider than normal for some reason.

Current UI, but borked.

What I've Tried

  1. Changing the repaint() region to be a rectangle that only covers the grid,
  2. Checking the documentation for anything about this,
  3. Google,
  4. Asking you guys.

The Code

Reprex: (Simplified to "Press the button to reproduce", while still keeping the essence of the potential problem areas.)

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class BuildGridGUI2
{
    static final int WIDTH_MIN = 100;
    static final int WIDTH_MAX = 2000;
    static final int WIDTH_INIT = 520;
    
    static final int HEIGHT_MIN = 100;
    static final int HEIGHT_MAX = 2000;
    static final int HEIGHT_INIT = 300;
    
    int widthOfFrames = 255;
    int heightOfFrames = 255;
    int numCols = 3;
    int numFrames = 1;
    
    
    private final JComponent makeUI()
    {
        JPanel p = new JPanel();
        
        
        p.setLayout(new BorderLayout());
        
        Grid g = new Grid();
        JComponent j = makeSideMenu(g);
        
        g.setBorder(BorderFactory.createCompoundBorder(
              BorderFactory.createLineBorder(Color.red),
              g.getBorder()));
        
        
        j.setBorder(BorderFactory.createCompoundBorder(
                  BorderFactory.createLineBorder(Color.red),
                  j.getBorder()));
        
        
        p.add(j, BorderLayout.EAST);
        p.add(g, BorderLayout.WEST);
        
        
        return p;
    }
    
    private final JComponent makeSideMenu(Grid grid)
    {
        JPanel p = new JPanel();
        
        JLabel numColsSelectorLabel = new JLabel("Frames Per Column");
        JSpinner numColsSelectorField = new JSpinner();
        
        JLabel widthLabel = new JLabel("Width per Frame (Pixels)");
        JSpinner widthSpinner = new JSpinner();
        JSlider widthSlider = new JSlider(WIDTH_MIN, WIDTH_MAX, WIDTH_INIT);
        
        JLabel heightLabel = new JLabel("Height per Frame (Pixels)");
        JSpinner heightSpinner = new JSpinner();
        JSlider heightSlider = new JSlider(BuildGridGUI2.HEIGHT_MIN, BuildGridGUI2.HEIGHT_MAX, BuildGridGUI2.HEIGHT_INIT);
        JButton confirmButton = new JButton("Confirm");
        
        
        
        numColsSelectorField.setEditor(new JSpinner.NumberEditor(numColsSelectorField));
        numColsSelectorField.setMaximumSize(numColsSelectorField.getPreferredSize());
        
        
        
        widthSlider.setMajorTickSpacing(300);
        widthSlider.setMinorTickSpacing(20);
        widthSlider.setPaintTicks(true);
        widthSlider.setPaintLabels(true);
        
        
        
        widthSpinner.setEditor(new JSpinner.NumberEditor(widthSpinner));
        widthSpinner.setPreferredSize(new Dimension(70, 30));
        widthSpinner.setMaximumSize(widthSpinner.getPreferredSize());
        
        
        
        
        heightSlider.setMajorTickSpacing(300);
        heightSlider.setMinorTickSpacing(20);
        heightSlider.setPaintTicks(true);
        heightSlider.setPaintLabels(true);
        
        
        
        heightSpinner.setEditor(new JSpinner.NumberEditor(heightSpinner));
        heightSpinner.setPreferredSize(new Dimension(70, 30));
        heightSpinner.setMaximumSize(heightSpinner.getPreferredSize());
        
        
        confirmButton.addActionListener(new ActionListener() {
            @Override public void actionPerformed(ActionEvent e)
            {
                widthOfFrames = 200;
                grid.refresh();
            }
        });
        
        
        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
        p.setPreferredSize(new Dimension(300, 480));
        p.setSize(new Dimension(300, 480));
        
        numColsSelectorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        numColsSelectorField.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthSlider.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightSlider.setAlignmentX(Component.CENTER_ALIGNMENT);
        confirmButton.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthSpinner.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightSpinner.setAlignmentX(Component.CENTER_ALIGNMENT);
        
        
        p.add(Box.createRigidArea(new Dimension(300, 30)));
        p.add(numColsSelectorLabel);
        p.add(Box.createRigidArea(new Dimension(300, 5)));
        p.add(numColsSelectorField);
        p.add(Box.createRigidArea(new Dimension(300, 25)));
        p.add(widthLabel);
        p.add(Box.createRigidArea(new Dimension(300, 3)));
        p.add(widthSpinner);
        p.add(widthSlider);
        p.add(Box.createRigidArea(new Dimension(300, 25)));
        p.add(heightLabel);
        p.add(Box.createRigidArea(new Dimension(300, 3)));
        p.add(heightSpinner);
        p.add(heightSlider);
        p.add(Box.createRigidArea(new Dimension(300, 45)));
        p.add(confirmButton);
        
        
        return p;
    }
    
    
    private static void createAndShowGUI() {
        BuildGridGUI2 b = new BuildGridGUI2();
        JFrame mainFrame = new JFrame("Grid Builder");
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainFrame.setSize(940, 480);
        mainFrame.getContentPane().add(b.makeUI());
        mainFrame.setResizable(false);
        mainFrame.setVisible(true);
    }
    
    public static void main(String... args)
    {
        SwingUtilities.invokeLater(new Runnable() {
            @Override public void run() {
                createAndShowGUI();
            }
        });
    }
    
    
    class Grid extends JComponent
    {
        static final int CANVAS_WIDTH = 640;
        static final int CANVAS_HEIGHT = 480;
        
        private final double conversionRatio = 4.0/9.0; // 1080p scaled down to 480p preview
        
        private int squareHeight, squareWidth, numColumns, numRows;
        
        
        public Grid()
        {
            setOpaque(true);
            setSize(CANVAS_WIDTH, CANVAS_HEIGHT); // trying to get the size to work properly.
            setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
        }
        
        @Override
        public Dimension getPreferredSize()
        {
            return new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT);
        }
        
        @Override
        public void paintComponent(Graphics g)
        {
            
            super.paintComponent(g);
            
            updateVars();
            
            int k;
            
            for (k = 0; k < numColumns; k++)
            {
                // @param: (x1, y1), (x2, y2)
                g.drawLine(k * squareWidth, 0, k * squareWidth, CANVAS_HEIGHT);
            }
            
            for (k = 0; k < numRows; k++)
            {
                g.drawLine(0, k * squareHeight, CANVAS_WIDTH, k * squareHeight);
            }
        }
        
        public void refresh()
        {
            // Attempting to set the repaint area to only the grid region
            // because CTRL+F is free and parameters are not
            repaint(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        }
        
        public void updateVars()
        {
            this.squareWidth = (int)(
                    (double)BuildGridGUI2.this.widthOfFrames
                    *
                    conversionRatio);
            
            this.squareHeight = (int)(
                    (double)BuildGridGUI2.this.heightOfFrames
                    *
                    conversionRatio);
            
            this.numColumns = BuildGridGUI2.this.numCols;
            
            this.numRows = (int)(
                    (
                            (double)BuildGridGUI2.this.numFrames
                            /
                            numColumns
                    )
                    + 0.5);
        }
        
    }
    
}

Actual Source Code (Not strictly relevant, but if you're in the mood for code review then I'd love to learn better coding conventions.)

Thank you!

ABadHaiku
  • 65
  • 1
  • 6
  • *".. if you're in the mood for code review then I'd love to learn better coding conventions."* That's what [Code Review](https://codereview.stackexchange.com) is for. – Andrew Thompson Feb 03 '21 at 07:16

2 Answers2

1

I made a few changes to the code you posted.

  • I changed class Grid such that it extends JPanel and not JComponent, since custom painting is usually done on a JPanel.
  • I added a instance member variable grid with type Grid, to class BuildGridGUI2 rather than creating one and sending it as a parameter to method makeSideMenu.

Here is your code with my modifications (and my preferred coding style). It simply solves your reported problem and nothing more.

import java.awt.*;
import java.awt.event.*;

import javax.swing.*;

public class BuildGridGUI2 {
    static final int WIDTH_MIN = 100;
    static final int WIDTH_MAX = 2000;
    static final int WIDTH_INIT = 520;

    static final int HEIGHT_MIN = 100;
    static final int HEIGHT_MAX = 2000;
    static final int HEIGHT_INIT = 300;

    int widthOfFrames = 255;
    int heightOfFrames = 255;
    int numCols = 3;
    int numFrames = 1;
    Grid  grid;

    private final JComponent makeUI() {
        JPanel p = new JPanel();
        p.setLayout(new BorderLayout());
        grid = new Grid();
        JComponent j = makeSideMenu();
        grid.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.red),
                                                          grid.getBorder()));

        j.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.red),
                                                       j.getBorder()));
        p.add(j, BorderLayout.EAST);
        p.add(grid, BorderLayout.WEST);
        return p;
    }

    private final JComponent makeSideMenu() {
        JPanel p = new JPanel();
        JLabel numColsSelectorLabel = new JLabel("Frames Per Column");
        JSpinner numColsSelectorField = new JSpinner();
        JLabel widthLabel = new JLabel("Width per Frame (Pixels)");
        JSpinner widthSpinner = new JSpinner();
        JSlider widthSlider = new JSlider(WIDTH_MIN, WIDTH_MAX, WIDTH_INIT);
        JLabel heightLabel = new JLabel("Height per Frame (Pixels)");
        JSpinner heightSpinner = new JSpinner();
        JSlider heightSlider = new JSlider(BuildGridGUI2.HEIGHT_MIN,
                                           BuildGridGUI2.HEIGHT_MAX,
                                           BuildGridGUI2.HEIGHT_INIT);
        JButton confirmButton = new JButton("Confirm");
        numColsSelectorField.setEditor(new JSpinner.NumberEditor(numColsSelectorField));
        numColsSelectorField.setMaximumSize(numColsSelectorField.getPreferredSize());
        widthSlider.setMajorTickSpacing(300);
        widthSlider.setMinorTickSpacing(20);
        widthSlider.setPaintTicks(true);
        widthSlider.setPaintLabels(true);
        widthSpinner.setEditor(new JSpinner.NumberEditor(widthSpinner));
        widthSpinner.setPreferredSize(new Dimension(70, 30));
        widthSpinner.setMaximumSize(widthSpinner.getPreferredSize());

        heightSlider.setMajorTickSpacing(300);
        heightSlider.setMinorTickSpacing(20);
        heightSlider.setPaintTicks(true);
        heightSlider.setPaintLabels(true);

        heightSpinner.setEditor(new JSpinner.NumberEditor(heightSpinner));
        heightSpinner.setPreferredSize(new Dimension(70, 30));
        heightSpinner.setMaximumSize(heightSpinner.getPreferredSize());

        confirmButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                widthOfFrames = 200;
                grid.refresh();
            }
        });

        p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
        p.setPreferredSize(new Dimension(300, 480));
        p.setSize(new Dimension(300, 480));

        numColsSelectorLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        numColsSelectorField.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthSlider.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightSlider.setAlignmentX(Component.CENTER_ALIGNMENT);
        confirmButton.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightLabel.setAlignmentX(Component.CENTER_ALIGNMENT);
        widthSpinner.setAlignmentX(Component.CENTER_ALIGNMENT);
        heightSpinner.setAlignmentX(Component.CENTER_ALIGNMENT);

        p.add(Box.createRigidArea(new Dimension(300, 30)));
        p.add(numColsSelectorLabel);
        p.add(Box.createRigidArea(new Dimension(300, 5)));
        p.add(numColsSelectorField);
        p.add(Box.createRigidArea(new Dimension(300, 25)));
        p.add(widthLabel);
        p.add(Box.createRigidArea(new Dimension(300, 3)));
        p.add(widthSpinner);
        p.add(widthSlider);
        p.add(Box.createRigidArea(new Dimension(300, 25)));
        p.add(heightLabel);
        p.add(Box.createRigidArea(new Dimension(300, 3)));
        p.add(heightSpinner);
        p.add(heightSlider);
        p.add(Box.createRigidArea(new Dimension(300, 45)));
        p.add(confirmButton);
        return p;
    }

    private static void createAndShowGUI() {
        BuildGridGUI2 b = new BuildGridGUI2();
        JFrame mainFrame = new JFrame("Grid Builder");
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        mainFrame.setSize(940, 480);
        mainFrame.getContentPane().add(b.makeUI());
        mainFrame.setResizable(false);
        mainFrame.setVisible(true);
    }

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

    class Grid extends JPanel {
        static final int CANVAS_WIDTH = 640;
        static final int CANVAS_HEIGHT = 480;

        private final double conversionRatio = 4.0 / 9.0; // 1080p scaled down to 480p preview

        private int squareHeight, squareWidth, numColumns, numRows;

        public Grid() {
            setOpaque(true);
            setSize(CANVAS_WIDTH, CANVAS_HEIGHT); // trying to get the size to work properly.
            setPreferredSize(new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT));
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(CANVAS_WIDTH, CANVAS_HEIGHT);
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            updateVars();
            int k;
            for (k = 0; k < numColumns; k++) {
                // @param: (x1, y1), (x2, y2)
                g.drawLine(k * squareWidth, 0, k * squareWidth, CANVAS_HEIGHT);
            }
            for (k = 0; k < numRows; k++) {
                g.drawLine(0, k * squareHeight, CANVAS_WIDTH, k * squareHeight);
            }
        }

        public void refresh() {
            // Attempting to set the repaint area to only the grid region
            // because CTRL+F is free and parameters are not
            repaint(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
        }

        public void updateVars() {
            this.squareWidth = (int) ((double) BuildGridGUI2.this.widthOfFrames * conversionRatio);
            this.squareHeight = (int) ((double) BuildGridGUI2.this.heightOfFrames * conversionRatio);
            this.numColumns = BuildGridGUI2.this.numCols;
            this.numRows = (int) (((double) BuildGridGUI2.this.numFrames / numColumns) + 0.5);
        }
    }
}

One tip: Try to use specific sub-classes of JComponent rather than JComponent. For example, I suggest changing the return type of method makeSideMenu() to JPanel rather than JComponent since that method actually returns a JPanel.

Abra
  • 19,142
  • 7
  • 29
  • 41
1

All of the sidebar components get painted at the top-left corner, while still showing/functioning on the sidebar;

JComponent is an abstract class that all Swing components extend from. It has no default painting logic.

Therefore invoking super.paintComponent(...) does not really do anything.

In particular it does not clear the background of the component before doing custom painting. This will result in the painting artifacts that you see.

Any time you extend JComponent your logic should be something something like:

protected void paintComponent(Graphics g)
{
    super.paintComponent(g);

    //  clear background

    g.setColor( getBackground() );
    g.fillRect(0, 0, getWidth(), getHeight());

    //  do custom painting

    g.setColor( getForeground() );
    ...
}

Note: this is the reason the many people override JPanel for simple custom painting as mentioned by Abra. The paintComponent(...) method of the JPanel will clear the background by default.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • Thanks, this makes total sense! I've pretty much been extrapolating from the Oracle docs examples and other questions on here where they only like to return JComponent, and I didn't know enough about Swing to dispute it. – ABadHaiku Feb 05 '21 at 06:00
  • Do you know why the grid panel becomes wider after being repainted? I can't figure out what would cause that. – ABadHaiku Feb 05 '21 at 23:35
  • 1
    Don't use setSize(). You don't know what the size should be. For example the frame has a border and a title bar. Just use the `pack()` statement before the setVisible(...) statement. – camickr Feb 05 '21 at 23:55