Java 2D - JLabel 仅部分绘制在 JList 中

Java 2D - JLabel only partially painted inside JList

我正在尝试在 Swing 中进行 2D 绘画,我想在空的 JList 中间画一个 JLabel
因此我想出了:

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

然而,当文本到达列表的边界时,它似乎被截断了:

如果我增加裁剪矩形的宽度,标签就会被完全绘制。
我究竟做错了什么?这样画画好吗?


完成最小示例:

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

计算文本大小很困难,而且有很多陷阱。 JListJTable 等使用渲染器窗格而不是直接在单元格渲染器上调用 paint 是有原因的。

只有当组件是框架层次结构的一部分时,它才能正确确定文本的大小。那是因为只有这样组件才会对当前显示设备使用正确的 GraphicsEnvironmentGraphicsEnvironment 包含布局文本所需的信息,如抗锯齿支持、字距调整、小数字体支持等。

所以正确的方法是将标签添加到 JList :

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

    {
      add(emptyLabel); // <---- add label
      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();
        }
      }
    }
  }

文本布局在最好的时候是棘手的,越能避免它越好(恕我直言)。

就我个人而言,我避免使用自定义绘画路线,但这就是我,尝试一些更简单的方法。

通过使用 PropertyChangeListener 来监视 ListModel 何时更改,以及使用自定义 ListDataListener 来监视 ListModel 本身何时更改,您可以创建类似的结果。

这里的一个补充是我将文本包装在 <html> 标签中,这将触发“单词”包装。

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

        }

    }
}