Search code examples
javaswingjava-2d

Java 2D - JLabel only partially painted inside JList


I'm experimenting with 2D painting in Swing, and I'd like to paint a JLabel in the middle of an empty JList.
Thus I came up with:

public class MyJList<T> extends JList<T> {
  private final JLabel emptyLabel = new JLabel("Whatever");

  @Override
  public void paint(final Graphics g) {
    super.paint(g);

    if (getModel().getSize() == 0 && !emptyLabel.getText().isBlank()) {
      final var preferredSize = emptyLabel.getPreferredSize();
      final var x = (getWidth() - preferredSize.width) / 2;
      final var y = (getHeight() - preferredSize.height) / 2;
      final var g2 = (Graphics2D) g.create(x, y, preferredSize.width, preferredSize.height);

      try {
        emptyLabel.setBounds(0, 0, preferredSize.width, preferredSize.height);
        emptyLabel.paint(g2);
      } finally {
        g2.dispose();
      }
    }
  }
}

However when the text reaches the bounds of the list, it seems to get truncated:

enter image description here

If I increase the clipping rectangle width, the label is fully painted.
What am I doing wrong? Is this a good way to paint?


Complete minimal example:

class Main {
  public static void main(final String[] args) {
    final var myFrame = new MyFrame();
    myFrame.setVisible(true);
  }

  public static class MyFrame extends JFrame {
    public MyFrame() {
      super("Example");

      final var list = new MyJList<String>();

      final var contentPane = getContentPane();
      contentPane.setLayout(new BorderLayout());
      contentPane.add(list, BorderLayout.CENTER);

      pack();
      setMinimumSize(new Dimension(160, 200));
      setLocationRelativeTo(null);
    }
  }

  public static class MyJList<T> extends JList<T> {
    private final JLabel emptyLabel = new JLabel("Selezionare un oggetto");

    {
      emptyLabel.setEnabled(false);
    }

    @Override
    public void paint(final Graphics g) {
      super.paint(g);

      if (getModel().getSize() == 0) {
        final var preferredSize = emptyLabel.getPreferredSize();
        final var listBounds = getBounds();
        final var x = (listBounds.width - preferredSize.width) / 2;
        final var y = (listBounds.height - preferredSize.height) / 2;
        final var g2 = (Graphics2D) g.create(x, y, preferredSize.width, preferredSize.height);

        try {
          emptyLabel.setBounds(0, 0, preferredSize.width, preferredSize.height);
          emptyLabel.paint(g2);
        } finally {
          g2.dispose();
        }
      }
    }
  }
}

Solution

  • Text layout is tricky at the best of times, and the more you can avoid it, the better (IMHO).

    Personally, I avoid the custom paint route, but this is me, and try something a little more simpler.

    With the use of a PropertyChangeListener, to monitor when the ListModel changes, and a custom ListDataListener, to monitor when the ListModel itself changes, you can create a similar result.

    One addition here is the fact that I've wrapped the text in <html> tags, this will trigger the "word" wrapping.

    Example

    import java.awt.BorderLayout;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.beans.PropertyChangeEvent;
    import java.beans.PropertyChangeListener;
    import javax.swing.DefaultListModel;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    import javax.swing.JList;
    import javax.swing.JPanel;
    import javax.swing.JScrollPane;
    import javax.swing.ListModel;
    import javax.swing.SwingUtilities;
    import javax.swing.event.ListDataEvent;
    import javax.swing.event.ListDataListener;
    
    public class Test {
    
        public static void main(String[] args) {
            new Test();
        }
    
        public Test() {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    JFrame frame = new JFrame();
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            private DefaultListModel<String> model = new DefaultListModel<String>();
    
            public TestPane() {
                setLayout(new BorderLayout());
    
                JPanel panel = new JPanel(new GridBagLayout());
                JButton add = new JButton("Add");
                JButton remove = new JButton("Remove");
    
                panel.add(add);
                panel.add(remove);
    
                MyList<String> myList = new MyList<>();
                myList.setModel(model);
    
                add(new JScrollPane(myList));
                add(panel, BorderLayout.SOUTH);
    
                add.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent evt) {
                        model.addElement("Hello");
                    }
                });
                remove.addActionListener(new ActionListener() {
                    @Override
                    public void actionPerformed(ActionEvent evt) {
                        if (model.size() > 0) {
                            model.remove(0);
                        }
                    }
                });
            }
    
        }
    
        public class MyList<T> extends JList<T> {
    
            final private ModelHandler modelHandler = new ModelHandler();
    
            final private JLabel emptyLabel = new JLabel("<html>Selezionare un oggetto</html>");
    
            public MyList() {
                emptyLabel.setHorizontalAlignment(JLabel.CENTER);
                setLayout(new BorderLayout());
                add(emptyLabel);
                emptyLabel.setVisible(false);
                addPropertyChangeListener("model", new PropertyChangeListener() {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        if ((evt.getOldValue() instanceof ListModel)) {
                            ListModel model = (ListModel) evt.getOldValue();
                            model.removeListDataListener(modelHandler);
                        }
                        if ((evt.getNewValue() instanceof ListModel)) {
                            ListModel model = (ListModel) evt.getNewValue();
                            model.addListDataListener(modelHandler);
                        }
                        updateEmptyLabel();
                    }
                });
            }
    
            protected void updateEmptyLabel() {
                emptyLabel.setVisible(getModel().getSize() == 0);
            }
    
            protected class ModelHandler implements ListDataListener {
    
                @Override
                public void intervalAdded(ListDataEvent evt) {
                    updateEmptyLabel();
                }
    
                @Override
                public void intervalRemoved(ListDataEvent evt) {
                    updateEmptyLabel();
                }
    
                @Override
                public void contentsChanged(ListDataEvent evt) {
                    updateEmptyLabel();
                }
    
            }
    
        }
    }