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