I don't like scrollbars for their clumsiness. I wrote a custom JLabel
extension that dynamically trims the label's text on each paint()
call and appends with an ellipsis
package StretchableLabel;
import lombok.SneakyThrows;
import javax.swing.*;
import java.awt.*;
public class StretchableLabel extends JLabel {
private String originalText;
public StretchableLabel(String text) {
super(text);
this.originalText = text;
}
@Override
public void setText(String text) {
this.originalText = text;
String displayedText = trimTextIfNecessary(text);
super.setText(displayedText);
}
private String trimTextIfNecessary(String text) {
if (getFont() == null) return text;
FontMetrics fontMetrics = getFontMetrics(getFont());
int textWidth = fontMetrics.stringWidth(text);
int parentWidth = getParent().getWidth();
if (textWidth > parentWidth) {
return trimText(text, parentWidth, fontMetrics);
} else {
return text;
}
}
private String trimText(String text, int parentWidth, FontMetrics fontMetrics) {
int textLimit = parentWidth / fontMetrics.charWidth('m');
return text.substring(0, textLimit) + "...";
}
@SneakyThrows
@Override
protected void paintComponent(Graphics g) {
String displayedText = trimTextIfNecessary(originalText);
super.setText(displayedText);
super.paintComponent(g);
}
}
package StretchableLabel;
import org.apache.commons.lang3.StringUtils;
import javax.swing.*;
public class Main {
public static void main(String[] args) {
JFrame frame = new JFrame("StretchableLabel Demo");
JPanel panel = new JPanel();
StretchableLabel label = new StretchableLabel((StringUtils.repeat("long label ", 20)));
panel.add(label);
frame.add(panel);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
Drawbacks:
getText()
should return the same value that was set with setText()
(an original string). However, getText()
returns an actual, displayed text while setText()
sets a trimmed text. I can't override getText()
since it's invoked by paintComponent()
-- unless I check the caller which is tricky. It's important to intercept any setText()
calls, as I do, so that direct text setting is not possible by the class's clientsm
is too conservative. It would be more precise to check the right substring length by applying some binary search algorithm on the string (though it would be worse from the performance standpoint)How do I properly implement my idea?
You are implementing a feature that does already exist. Apparently, there is a bug in the Basic/Metal Look&Feel that makes it calculating with wrong sizes when no font has been set.
There are two simple solutions to this
Set a font explicitly
public static void main(String[] args) {
JFrame frame = new JFrame("StretchableLabel Demo");
JLabel label = new JLabel("long label ".repeat(20));
label.setFont(new Font(Font.DIALOG, 0, 16));
frame.add(label, BorderLayout.CENTER);
frame.pack();
frame.setSize(frame.getWidth() >> 1, frame.getHeight());
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
}
Use the system (i.e. Windows) Look&Feel
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}
catch(ReflectiveOperationException | UnsupportedLookAndFeelException e) {
e.printStackTrace();
}
JFrame frame = new JFrame("StretchableLabel Demo");
JLabel label = new JLabel("long label ".repeat(20));
frame.add(label, BorderLayout.CENTER);
frame.pack();
frame.setSize(frame.getWidth() >> 1, frame.getHeight());
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
}
In both cases you get the intended feature, clipping the text and appending an ellipsis, for free. Keep in mind that you have to set up the layout manager such that they will shrink the JLabel
when there’s not enough space.