I would like to have a TextField which shows by its width the maximum of allowed
input characters.
I used Font.getStringBounds to calculate the width of the maximum length.
But to my surprise the resulting width in the example looks like omitting a
complete character!
Using FontMetrics.stringWidth supplies the same width value.
Creating the textField just using the JTextField(int columns) constructor gave
a better result, but the field is still too small.
What is missing?
import java.awt.*;
import java.awt.font.*;
import java.awt.geom.*;
import javax.swing.*;
public class InputWidthTextField extends JFrame {
public InputWidthTextField() {
setSize(250, 230);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(new FlowLayout(FlowLayout.CENTER, 60, 20));
Font font= new Font(Font.MONOSPACED, Font.PLAIN, 12);
int max= 4;
JLabel lb= new JLabel("Enter up to "+max+" characters:");
add(lb);
MaxInputWidthTextField tf= new MaxInputWidthTextField(font, max);
add(tf);
setVisible(true);
}
public static void main(String... args) {
EventQueue.invokeLater(InputWidthTextField::new);
}
class MaxInputWidthTextField extends JTextField {
public MaxInputWidthTextField(Font font, int maxCount) {
super();
if (font==null)
font= getFont();
else
setFont(font);
FontMetrics fm= getFontMetrics(font);
FontRenderContext frc= fm.getFontRenderContext();
String buf= "8".repeat(maxCount);
Rectangle2D rect= font.getStringBounds(buf, frc);
Dimension dim= new Dimension((int)rect.getWidth(), (int)rect.getHeight());
setPreferredSize(dim);
System.out.println((int)rect.getWidth()+", "+(int)rect.getHeight());
System.out.println(fm.stringWidth(buf));
System.out.println(getGraphics());
}
}
}
I'm not even going to try and figure out all the things that are wrong with your code, instead, I'm going to demonstrate what you should be doing instead.
Take a look at public JTextField​(int columns)
JTextFieldpublic JTextField​(int columns) Constructs a new empty
TextField with the specified number of columns. A default model is
created and the initial string is set to null. Parameters: columns -
the number of columns to use to calculate the preferred width; if
columns is set to zero, the preferred width will be whatever naturally
results from the component implementation
So, if we do a side by side comparison, this is what we get (your field is on the bottom)
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.font.FontRenderContext;
import java.awt.geom.Rectangle2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
add(new JTextField(8), gbc);
add(new MaxInputWidthTextField(null, 8), gbc);
}
}
class MaxInputWidthTextField extends JTextField {
public MaxInputWidthTextField(Font font, int maxCount) {
super();
if (font == null) {
font = getFont();
} else {
setFont(font);
}
FontMetrics fm = getFontMetrics(font);
FontRenderContext frc = fm.getFontRenderContext();
String buf = "8".repeat(maxCount);
Rectangle2D rect = font.getStringBounds(buf, frc);
Dimension dim = new Dimension((int) rect.getWidth(), (int) rect.getHeight());
setPreferredSize(dim);
}
}
}
What I do recommend is having a look at the pre-existing implementation
The JTextField#getPreferredSize implementation looks like...
/**
* Returns the column width.
* The meaning of what a column is can be considered a fairly weak
* notion for some fonts. This method is used to define the width
* of a column. By default this is defined to be the width of the
* character <em>m</em> for the font used. This method can be
* redefined to be some alternative amount
*
* #return the column width >= 1
*/
protected int getColumnWidth() {
if (columnWidth == 0) {
FontMetrics metrics = getFontMetrics(getFont());
columnWidth = metrics.charWidth('m');
}
return columnWidth;
}
/**
* Returns the preferred size <code>Dimensions</code> needed for this
* <code>TextField</code>. If a non-zero number of columns has been
* set, the width is set to the columns multiplied by
* the column width.
*
* #return the dimension of this textfield
*/
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
if (columns != 0) {
Insets insets = getInsets();
size.width = columns * getColumnWidth() +
insets.left + insets.right;
}
return size;
}
Instead of "setting" the preferred size, I would "consider" overriding the getPreferredSize and injecting your "custom" workflow into instead
Further experimentation...
So, I did a really quick test...
Font monoFont = new Font(Font.MONOSPACED, Font.PLAIN, 13);
JTextField field = new JTextField();
field.setFont(monoFont);
FontMetrics metrics = field.getFontMetrics(getFont());
for (int i = 32; i < 127; i++) {
System.out.println(i + " " + ((char) i) + " = " + metrics.charWidth((char)i));
}
And found that every character is 8 points wide. Where as the default font uses the character m which is (in my testing) 12 points wide.
So, this got me to thinking, the really problem isn't that the getPreferredSize is wrong, it's that we actually need to "pad" the result of getColumnWidth, for example...
public class MaxInputWidthTextField extends JTextField {
public MaxInputWidthTextField(Font font, int maxCount) {
super(maxCount);
setFont(font);
}
#Override
protected int getColumnWidth() {
return super.getColumnWidth() + 1;
}
}
which can generate something like...
It's just enough to add a little white space at the trailing end of the text field and you don't need to jump through a lot of hopes to make it work.
Runnable example
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
JFrame frame = new JFrame();
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
public TestPane() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridwidth = GridBagConstraints.REMAINDER;
Font monoFont = new Font(Font.MONOSPACED, Font.PLAIN, 13);
add(new JTextField(8), gbc);
add(new MaxInputWidthTextField(monoFont, 8), gbc);
}
}
public class MaxInputWidthTextField extends JTextField {
public MaxInputWidthTextField(Font font, int maxCount) {
super(maxCount);
setFont(font);
}
#Override
protected int getColumnWidth() {
return super.getColumnWidth() + 1;
}
}
}
#Camickr
Rob, you seem to have been very strict yesterday. ;-)
But surely, not understanding is also on my side. For when I tell you that in a monospaced font both "W" and "i" have the same width, this will certainly be nothing new to you. So I don't understand why you are asking me for an MRE or why only "WWWW" didn't work for you. I can only imagine, you didn't use a monospace font,
but this is a sine qua non when calculating the maximum possible width beforehand, given a maximum character count, for any string the user will enter.
I hope, we don't see different things on our screens due to resolution or scaling differences. So I send a picture of how it looks like at my site. Whenever I enter the last character in one of the textfields, the whole string is shifted to the left, so that the first character gets partly hidden. Hence in my example the zero is always cut. You may deem this to be acceptable, but I would prefer to have each character fully visible.
import java.awt.*;
import javax.swing.*;
public class FieldWidthTest extends JFrame {
public FieldWidthTest() {
setSize(250, 230);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setLayout(new GridLayout(4, 1));
Font font= new Font(Font.MONOSPACED, Font.PLAIN, 12);
int max= 4;
for (int i=0; i<4; i++) {
JPanel p= new JPanel();
JTextField tf= new JTextField(max++);
tf.setFont(font);
p.add(tf);
add(p);
}
setVisible(true);
}
public static void main(String... args) {
EventQueue.invokeLater(FieldWidthTest::new);
}
}
#MadProgrammer
Overriding getPreferredSize(), as you suggested, worked well; but using setPreferredSize() also did the job and saves the overriding.
It was an error not to create and set a monospaced font in case the paramter was null. The solution, however, was to add 2 to the dimension's width, which worked for all font sizes. Camickr would call this justifiably "using magic numbers", but I don't know where to get this number from.
Thanks to all.
class MaxInputWidthTextField extends JTextField {
public MaxInputWidthTextField(Font monoFont, int maxCount) {
super();
if (monoFont==null) monoFont= new Font(Font.MONOSPACED, Font.PLAIN, 13);
setFont(monoFont);
FontMetrics fm= getFontMetrics(monoFont);
FontRenderContext frc= fm.getFontRenderContext();
String buf= "8".repeat(maxCount);
Rectangle2D rect= monoFont.getStringBounds(buf, frc);
// Insets insets= getMargin(); // Parameters are all 0.
Insets insets= getInsets();
int iWidth= insets.left + (int)rect.getWidth() + insets.right +2;
int iHeight= insets.top + (int)rect.getHeight() + insets.bottom;
Dimension dim= new Dimension(iWidth, iHeight);
setPreferredSize(dim);
}
}
Related
OK, So I have a program that displays a a String on a JPanel and it draws the lines of where the Ascents are, it draws a line at the Descent and halfway between, and a quarter way between. I have a combo box to change the font and the size of the String, and i have a JTextField to change the Text itself. I used actionListeners to update the String. Whenever i update the text of the String via the JTextField, the program glitches out, and shows a copy of the image of the JTextField on the top right corner of the JFrame.
Code:
package com.Patel.RichClients;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
//a class to demonstrate
public class StringAscent extends JFrame {
private static Font font;
private int fontSize = 50;
private StringPanel panel;
private JComboBox fontOptions;
private JComboBox fontSizeOptions;
private JTextField text;
// constructor
public StringAscent() {
// set the initial font to Times new Roman
font = new Font("Agency FB", Font.PLAIN, fontSize);
setVisible(true);
setSize(520, 170);
setTitle("String Ascents");
setLayout(new BorderLayout());
//set up the components
GraphicsEnvironment ge= GraphicsEnvironment.getLocalGraphicsEnvironment();
String[] fontNames = ge.getAvailableFontFamilyNames();
fontOptions = new JComboBox(fontNames);
text = new JTextField(22);
text.setText("Swing");
String[] sizeOptions = new String[50];
for (int i = 0; i < sizeOptions.length; i ++){
sizeOptions[i] = Integer.toString(i + 1);
}
fontSizeOptions = new JComboBox(sizeOptions);
panel = new StringPanel();
//set up actionListeners
fontOptions.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
JComboBox ref = (JComboBox) e.getSource();
font = new Font(fontNames[ref.getSelectedIndex()], Font.PLAIN, fontSize);
panel.repaint();
}
});
fontSizeOptions.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
JComboBox ref = (JComboBox) e.getSource();
fontSize = Integer.parseInt(sizeOptions[ref.getSelectedIndex()]);
font = new Font(font.getName(), Font.PLAIN, fontSize);
panel.repaint();
}
});
text.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e) {
panel.repaint();
}
});
//add components
add(panel, BorderLayout.NORTH);
add(fontOptions, BorderLayout.WEST);
add(text, BorderLayout.CENTER);
add(fontSizeOptions, BorderLayout.EAST);
}
public static void main(String[] args) {
Runnable showGui = new Runnable() {
public void run() {
StringAscent gui = new StringAscent();
}
};
SwingUtilities.invokeLater(showGui);
}
// inner JPanel class
private class StringPanel extends JPanel {
// constructor
public StringPanel() {
}
public Dimension getPreferredSize() {
return new Dimension(400, 100);
}
public void paintComponent(Graphics g) {
g.setColor(Color.black);
// set up the string
g.setFont(font);
// FontMetric is an object that holds information relevant to the
// rendering of the font
FontMetrics metric = g.getFontMetrics();
int ascent = metric.getAscent();
String string = text.getText();
g.drawString(string, 100, 50);
int x = 50;
// draw Ascent line
g.drawLine(x, 50 - ascent, x + 180, 50 - ascent);
// draw Ascent/2 line
g.drawLine(x, 50 - (ascent / 2), x + 180, 50 - (ascent / 2));
// draw Ascent/4 line
g.drawLine(x, 50 - (ascent / 4), x + 180, 50 - (ascent / 4));
// draw baseline line
g.drawLine(x, 50, x + 180, 50);
// draw Descent line
g.drawLine(x, 50 + metric.getDescent(), x + 180, 50 + metric.getDescent());
}
}
}
Add super.paintComponent(g) to the start of your paintComponent method
public void paintComponent(Graphics g) {
super.paintComponent(g)
g.setColor(Color.black);
//...
Basically, one of the jobs of paintComponent is to prepare the Graphics context for painting the current component. In Swing, the Graphics is a shared resource which is used by all the components within a window when been painted, so it's important to make sure that context is prepared properly before you paint on it
Take a look at Performing Custom Painting and Painting in AWT and Swing for more details about how painting works
Say I have two components, A and B, in a JPanel. I want Component A to stay left aligned, while Component B does its best to stay in the middle of the panel. I mocked up the following demo (sorry for the quality, I made it in paint):
What I am doing now is using a GridBagLayout on the JPanel, and keeping A left aligned while keeping B centered, but B stays centered within the 2nd column, so it is centered in the space remaining after A is placed, instead of centered with respect to the panel as a whole.
I cannot use any 3rd party libraries for this. Is there a way to do this using pure Swing?
Edit:
Firefly's answer is correct (as marked) but I created an SSCCE showing my original problem (first row), Hovercraft Full Of Eels' attempted solution (second row), and the Firefly's correct solution (third row). I figured it can't hurt to post it:
package stackoverflow;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.LayoutManager2;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class StackOverflowTest extends JFrame
{
public StackOverflowTest()
{
super("Stack Overflow Test");
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
JPanel testPanel = new JPanel(new GridLayout(3, 1));
// set up grid bag layout example
JPanel gridBagPanel = new JPanel(new GridBagLayout());
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = GridBagConstraints.LINE_START;
gridBagPanel.add(getA(), gridBagConstraints);
gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = GridBagConstraints.CENTER;
gridBagConstraints.weightx = 1.0;
gridBagPanel.add(getB(), gridBagConstraints);
testPanel.add(gridBagPanel);
// set up border layout panel
JPanel borderPanel = new JPanel(new BorderLayout());
borderPanel.add(getA(), BorderLayout.LINE_START);
JPanel flowPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 0, 0));
borderPanel.add(flowPanel, BorderLayout.CENTER);
flowPanel.add(getB());
testPanel.add(borderPanel);
// set up sly493 layout panel
JPanel sly493LayoutPanel = new JPanel(new Sly493LayoutManager());
sly493LayoutPanel.add(getA(), Sly493LayoutManager.LEFT);
sly493LayoutPanel.add(getB(), Sly493LayoutManager.CENTERED);
testPanel.add(sly493LayoutPanel);
// set up panel to act as the midpoint marker
JPanel midpointMarkerPanel = new JPanel()
{
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(Color.BLACK);
int w = getWidth();
int h = getHeight();
int x = w / 2;
g2.drawLine(x, 0, x, h);
g2.drawLine(x, 0, x - (h / 5), (h / 5));
g2.drawLine(x, 0, x + (h / 5), (h / 5));
}
};
midpointMarkerPanel.setPreferredSize(new Dimension(1, 50));
// setup up content pane
JPanel contentPane = new JPanel(new BorderLayout());
contentPane.add(testPanel, BorderLayout.NORTH);
contentPane.add(midpointMarkerPanel, BorderLayout.CENTER);
this.setContentPane(contentPane);
pack();
}
private JPanel getA()
{
JPanel aPanel = new JPanel(new BorderLayout());
JLabel aLabel = new JLabel("A", JLabel.CENTER);
aLabel.setFont(aLabel.getFont().deriveFont(Font.BOLD, 36));
aPanel.add(aLabel, BorderLayout.CENTER);
aPanel.setBackground(Color.RED);
aPanel.setPreferredSize(new Dimension(50, 50));
return aPanel;
}
private JPanel getB()
{
JPanel bPanel = new JPanel();
JLabel bLabel = new JLabel("B", JLabel.CENTER);
bLabel.setForeground(Color.WHITE);
bLabel.setFont(bLabel.getFont().deriveFont(Font.BOLD, 36));
bPanel.add(bLabel, BorderLayout.CENTER);
bPanel.setBackground(Color.BLUE);
bPanel.setPreferredSize(new Dimension(50, 50));
return bPanel;
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable()
{
#Override
public void run()
{
new StackOverflowTest().setVisible(true);
}
});
}
private static class Sly493LayoutManager implements LayoutManager2
{
public static final Integer LEFT = 0;
public static final Integer CENTERED = 1;
private Component leftComponent;
private Component centeredComponent;
#Override
public void addLayoutComponent(String name, Component comp)
{
}
#Override
public void removeLayoutComponent(Component comp)
{
if (leftComponent == comp)
{
leftComponent = null;
}
else if (centeredComponent == comp)
{
centeredComponent = null;
}
}
#Override
public Dimension preferredLayoutSize(Container parent)
{
Dimension d = new Dimension();
for (Component c : parent.getComponents())
{
//wide enough to stack the left and center components horizontally without overlap
d.width += c.getPreferredSize().width;
//tall enough to fit the tallest component
d.height = Math.max(d.height, c.getPreferredSize().height);
}
return d;
}
#Override
public Dimension minimumLayoutSize(Container parent)
{
return preferredLayoutSize(parent);
}
#Override
public void layoutContainer(Container parent)
{
//in this method we will:
//1) position the left component on the left edge of the parent and center it vertically
//2) position the center component in the center of the parent (as long as it would not overlap
//the left component) and center it vertically
int leftComponentWidth = leftComponent.getPreferredSize().width;
int leftComponentHeight = leftComponent.getPreferredSize().height;
int centeredComponentWidth = centeredComponent.getPreferredSize().width;
int centeredComponentHeight = centeredComponent.getPreferredSize().height;
leftComponent.setBounds(0, (parent.getHeight() - leftComponentHeight) / 2, leftComponentWidth, leftComponentHeight);
int leftComponentRightEdge = leftComponent.getX() + leftComponent.getWidth();
int centerComponentLeftEdge = (parent.getWidth() - centeredComponentWidth) / 2;
int centerComponentTopEdge = (parent.getHeight() - centeredComponentHeight) / 2;
if (leftComponentRightEdge >= centerComponentLeftEdge)
{
//Center component will "do its best" to remain in the center
//but it will not do so if it would cause it to overlap the left component
centerComponentLeftEdge = leftComponentRightEdge;
}
centeredComponent.setBounds(centerComponentLeftEdge,
centerComponentTopEdge,
centeredComponentWidth,
centeredComponentHeight);
}
#Override
public void addLayoutComponent(Component comp, Object constraints)
{
if (LEFT.equals(constraints))
{
if (leftComponent != null)
{
throw new IllegalStateException("A left component has already been assigned to this layout.");
}
leftComponent = comp;
}
else if (CENTERED.equals(constraints))
{
if (centeredComponent != null)
{
throw new IllegalStateException("A centered component has already been assigned to this layout.");
}
centeredComponent = comp;
}
else
{
throw new IllegalStateException("Unexpected constraints '" + constraints + "'.");
}
}
#Override
public Dimension maximumLayoutSize(Container target)
{
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
#Override
public float getLayoutAlignmentX(Container target)
{
return 0;
}
#Override
public float getLayoutAlignmentY(Container target)
{
return 0;
}
#Override
public void invalidateLayout(Container target)
{
}
}
}
If I'm understanding your needs correctly, you want B centered relative to the parent as a whole, not centered in the space left over after A is positioned. That makes this problem interesting and after testing the other suggested answers, I don't believe they can meet that requirement.
I'm having trouble thinking of a way to combine the built-in layout managers in a way that would achieve that. So, I've hacked up a custom implementation of LayoutManager2.
The following executable example may meet your needs. The implementation is quick and dirty and is in no way an example of a good generalized layout manager, but it appears to meet your requirements and behaves like your drawings made me think it should. I interpreted your requirement that "B does its best to stay in the middle of the panel" to mean that B should try to remain centered relative to the panel as a whole, but not at the expense of overlapping A.
package com.example;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager2;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Example {
public Example() {
JPanel a = new JPanel();
a.setBackground(Color.RED);
a.setPreferredSize(new Dimension(128, 128));
JPanel b = new JPanel();
b.setBackground(Color.BLUE);
b.setPreferredSize(new Dimension(128, 128));
JPanel panel = new JPanel(new Sly493LayoutManager());
panel.add(a, Sly493LayoutManager.LEFT);
panel.add(b, Sly493LayoutManager.CENTERED);
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
new Example();
}
private static class Sly493LayoutManager implements LayoutManager2 {
public static final Integer LEFT = 0;
public static final Integer CENTERED = 1;
private Component leftComponent;
private Component centeredComponent;
#Override
public void addLayoutComponent(String name, Component comp) { }
#Override
public void removeLayoutComponent(Component comp) {
if (leftComponent == comp) {
leftComponent = null;
} else if (centeredComponent == comp) {
centeredComponent = null;
}
}
#Override
public Dimension preferredLayoutSize(Container parent) {
Dimension d = new Dimension();
for (Component c : parent.getComponents()) {
//wide enough to stack the left and center components horizontally without overlap
d.width += c.getPreferredSize().width;
//tall enough to fit the tallest component
d.height = Math.max(d.height, c.getPreferredSize().height);
}
return d;
}
#Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
#Override
public void layoutContainer(Container parent) {
//in this method we will:
//1) position the left component on the left edge of the parent and center it vertically
//2) position the center component in the center of the parent (as long as it would not overlap
//the left component) and center it vertically
int leftComponentWidth = leftComponent.getPreferredSize().width;
int leftComponentHeight = leftComponent.getPreferredSize().height;
int centeredComponentWidth = centeredComponent.getPreferredSize().width;
int centeredComponentHeight = centeredComponent.getPreferredSize().height;
leftComponent.setBounds(0, (parent.getHeight() - leftComponentHeight) / 2, leftComponentWidth, leftComponentHeight);
int leftComponentRightEdge = leftComponent.getX() + leftComponent.getWidth();
int centerComponentLeftEdge = (parent.getWidth() - centeredComponentWidth) / 2;
int centerComponentTopEdge = (parent.getHeight() - centeredComponentHeight) / 2;
if (leftComponentRightEdge >= centerComponentLeftEdge) {
//Center component will "do its best" to remain in the center
//but it will not do so if it would cause it to overlap the left component
centerComponentLeftEdge = leftComponentRightEdge;
}
centeredComponent.setBounds(centerComponentLeftEdge,
centerComponentTopEdge,
centeredComponentWidth,
centeredComponentHeight);
}
#Override
public void addLayoutComponent(Component comp, Object constraints) {
if (LEFT.equals(constraints)) {
if (leftComponent != null) {
throw new IllegalStateException("A left component has already been assigned to this layout.");
}
leftComponent = comp;
} else if (CENTERED.equals(constraints)) {
if (centeredComponent != null) {
throw new IllegalStateException("A centered component has already been assigned to this layout.");
}
centeredComponent = comp;
} else {
throw new IllegalStateException("Unexpected constraints '" + constraints + "'.");
}
}
#Override
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
#Override
public float getLayoutAlignmentX(Container target) {
return 0;
}
#Override
public float getLayoutAlignmentY(Container target) {
return 0;
}
#Override
public void invalidateLayout(Container target) {
}
}
}
Here's a really quick and dirty way, similar to Firefly's answer - just create a JPanel with null Layout, and place the two child panels in its paintComponent method:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Example extends JPanel {
protected JPanel a;
protected JPanel b;
int size = 200;
public Example() {
setLayout( null );
a = new JPanel();
a.setBackground( Color.red );
a.setPreferredSize( new Dimension( size, size ) );
b = new JPanel();
b.setBackground( Color.blue );
b.setPreferredSize( new Dimension( size, size ) );
add( a );
add( b );
setPreferredSize( new Dimension( 4 * size, size ) );
}
#Override
public void paintComponent( final Graphics g ) {
super.paintComponent( g );
a.setBounds( 0, 0, size, size );
int w = getWidth();
int x = (w - size) / 2;
if ( x < size ) {
x = size;
}
b.setBounds( x, 0, size, size );
}
public static void main( String[] args ) {
SwingUtilities.invokeLater( new Runnable() {
#Override
public void run() {
// Create and set up the window.
JFrame jf = new JFrame();
jf.setName( "Example" );
Example item = new Example();
jf.add( item );
// Display the window.
jf.pack();
jf.setVisible( true );
jf.addWindowListener( new WindowAdapter() {
#Override
public void windowClosing( WindowEvent arg0 ) {
System.exit( 0 );
}
} );
}
} );
}
}
Have the overall container use BorderLayout
Add A to its BorderLayout.LINE_START position.
Add another FlowLayout JPanel BorderLayout.CENTER. This panel will hold B.
Add B to the JPanel above. Since FlowLayout defaults to FlowLayout.CENTER, B should be centered in this JPanel.
For my practical purposes, I did not need precise centering. And I needed right alignment. And I needed two rows. And the text on the right was much shorter than the text in the center. So the trick is:
have both rows in a separate JPanel
use GridBagLayout
the "centered" item is really the left item, but it is right-aligned.
Some code from a real app (the {} blocks used to be in different functions, but that's not important now):
JPanel myPanel = new JPanel(new GridBagLayout());
JLabel centerFirst = new JLabel("first line");
JLabel rightFirst = new JLabel("...");
{
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = GridBagConstraints.EAST;
gridBagConstraints.weightx = 0.5;
myPanel.add(centerFirst, gridBagConstraints);
}
{
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 3;
gridBagConstraints.gridy = 0;
gridBagConstraints.anchor = GridBagConstraints.EAST;
gridBagConstraints.weightx = 0.5; // you don't really need 0.5 and 0.5, it may be 0.1 and 0.1, two equal values
myPanel.add(rightFirst, gridBagConstraints);
}
JLabel centerSecond = new JLabel("second line");
JLabel rightSecond = new JLabel("...");
{
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 1;
gridBagConstraints.gridy = 1;
gridBagConstraints.anchor = GridBagConstraints.EAST;
gridBagConstraints.weightx = 0.5;
myPanel.add(centerSecond, gridBagConstraints);
}
{
GridBagConstraints gridBagConstraints = new GridBagConstraints();
gridBagConstraints.gridx = 3;
gridBagConstraints.gridy = 1;
gridBagConstraints.anchor = GridBagConstraints.EAST;
gridBagConstraints.weightx = 0.5;
myPanel.add(rightSecond, gridBagConstraints);
}
//...
// the main frame uses GridBagLayout too
JFrame frame = new JFrame();
{
GridBagLayout gbl = new GridBagLayout();
//...
frame.setLayout(gbl);
}
// myPanel: full width of the enclosing GridBagLayout
{
GridBagLayout gbl = (GridBagLayout) frame.getContentPane().getLayout();
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0; // it will occupy columns 0 and 1 of the GridBagLayout
gbc.gridy = 4; // and there will be stuff above it
gbc.gridheight = 1; // full width, therefore height=1 is enough
gbc.gridwidth = 2; // the number of columns in this GridBagLayout
gbc.fill = GridBagConstraints.HORIZONTAL;
gbl.setConstraints(myPanel, gbc);
frame.add(myPanel);
}
Box layout
container.add(componentA)
container.add(Box.createHorisontalGlue())
container.add(componentB)
container.add(Box.createHorisontalGlue())
For my application I am designing a script editor. At the moment I have a JPanel which contains another JPanel that holds the line number (positioned to the left), and a JTextArea which is used to allow users to type their code in (positioned to the right).
At the moment, I have implemented a JScrollPane on the JTextArea to allow the user to scroll through their code.
For the JPanel containing the line numbers, they increment every time the user presses the enter key.
However, the problem is that I want the same JScrollPane (the one implemented on the JTextArea) to control the scrolling of the line number JPanel as well; i.e. when the user scrolls on the JTextArea, the line number JPanel should also scroll. But since the line numbers are held in a JPanel, I cant add that component to a JTextArea.
The constructor for the JPanel class containing the JTextArea and the line number JPanel:
private ScriptEditor() {
setBackground(Color.WHITE);
lineNumPanel = new LineNumberPanel();
scriptArea = new JTextArea();
scriptArea.setLineWrap(true);
scriptArea.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 15));
scriptArea.setMargin(new Insets(3, 10, 0, 10));
JScrollPane scrollPane = new JScrollPane(scriptArea);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setPreferredSize(new Dimension(width, height));
scriptArea.addKeyListener(this);
add(lineNumPanel);
add(scrollPane);
}
The constructor for the line number JPanel which adds JLabels within itself to represent the line numbers:
public LineNumberPanel() {
setPreferredSize(new Dimension(width, height));
box = Box.createVerticalBox();
add(box);
//setup the label
label = new JLabel(String.valueOf(lineCount));
label.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 15));
//setup the label alignment
label.setVerticalAlignment(JLabel.TOP);
label.setHorizontalAlignment(JLabel.CENTER);
label.setVerticalTextPosition(JLabel.TOP);
setAlignmentY(TOP_ALIGNMENT);
box.add(label);
}
You should use JScrollPane#setRowHeaderView to set the component that will appear at the left hand side of the scroll pane.
The benefit of this is the row header won't scroll to the left as the view scrolls to the right...
The example deliberately uses line wrapping...
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Element;
import javax.swing.text.Utilities;
public class ScrollColumnHeader {
public static void main(String[] args) {
new ScrollColumnHeader();
}
public ScrollColumnHeader() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JTextArea ta = new JTextArea(20, 40);
ta.setWrapStyleWord(true);
ta.setLineWrap(true);
JScrollPane sp = new JScrollPane(ta);
sp.setRowHeaderView(new LineNumberPane(ta));
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(sp);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class LineNumberPane extends JPanel {
private JTextArea ta;
public LineNumberPane(JTextArea ta) {
this.ta = ta;
ta.getDocument().addDocumentListener(new DocumentListener() {
#Override
public void insertUpdate(DocumentEvent e) {
revalidate();
repaint();
}
#Override
public void removeUpdate(DocumentEvent e) {
revalidate();
repaint();
}
#Override
public void changedUpdate(DocumentEvent e) {
revalidate();
repaint();
}
});
}
#Override
public Dimension getPreferredSize() {
FontMetrics fm = getFontMetrics(getFont());
int lineCount = ta.getLineCount();
Insets insets = getInsets();
int min = fm.stringWidth("000");
int width = Math.max(min, fm.stringWidth(Integer.toString(lineCount))) + insets.left + insets.right;
int height = fm.getHeight() * lineCount;
return new Dimension(width, height);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
FontMetrics fm = ta.getFontMetrics(ta.getFont());
Insets insets = getInsets();
Rectangle clip = g.getClipBounds();
int rowStartOffset = ta.viewToModel(new Point(0, clip.y));
int endOffset = ta.viewToModel(new Point(0, clip.y + clip.height));
Element root = ta.getDocument().getDefaultRootElement();
while (rowStartOffset <= endOffset) {
try {
int index = root.getElementIndex(rowStartOffset);
Element line = root.getElement(index);
String lineNumber = "";
if (line.getStartOffset() == rowStartOffset) {
lineNumber = String.valueOf(index + 1);
}
int stringWidth = fm.stringWidth(lineNumber);
int x = insets.left;
Rectangle r = ta.modelToView(rowStartOffset);
int y = r.y + r.height;
g.drawString(lineNumber, x, y - fm.getDescent());
// Move to the next row
rowStartOffset = Utilities.getRowEnd(ta, rowStartOffset) + 1;
} catch (Exception e) {
break;
}
}
}
}
}
And as I just discovered, #camickr has a much more useful example, Text Component Line Number
Create an Outer Panel which holds the Line Number panel and Text Area.
Then put this new panel into the Scroll Pane so you end up with this arrangement:
Which in code is something like:
private ScriptEditor() {
setBackground(Color.WHITE);
JPanel outerPanel = new JPanel();
lineNumPanel = new LineNumberPanel();
scriptArea = new JTextArea();
scriptArea.setLineWrap(true);
scriptArea.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 15));
scriptArea.setMargin(new Insets(3, 10, 0, 10));
outerPanel.add(lineNumPanel, BorderLayout.WEST)
outerPanel.add(scriptArea, BorderLayout.CENTER)
JScrollPane scrollPane = new JScrollPane(outerPanel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setPreferredSize(new Dimension(width, height));
scriptArea.addKeyListener(this);
add(lineNumPanel);
add(scrollPane);
}
I suggest placing both components into a panel, then place that panel into the scroll pane.
I get a JFrame and i want to display a JLabel with a border in it with a padding of maybe 50px. When i set the size of the JFrame to 750, 750, and the size of the JLabel to 650, 650 and the location to 50, 50, it display it strange... Here's my code:
public class GUI {
/**
* Declarate all
*/
public int height = 750;
public int width = 750;
Dimension screen = Toolkit.getDefaultToolkit().getScreenSize();
int x = (screen.width / 2) - (width / 2); // Center horizontally.
int y = (screen.height / 2) - (height / 2); // Center vertically.
/**
* Create the GUI
*/
JFrame frame = new JFrame();
Border border = LineBorder.createBlackLineBorder();
JLabel label = new JLabel();
public GUI(){
label.setBorder(border);
label.setSize(700, 700);
label.setLocation(0, 0);
frame.getContentPane().setLayout(null);
frame.add(label);
}
public void createGUI() {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setBounds(x,y,width,height);
frame.setVisible(true);
}
}
So I think the title-bar at the top is also include in the size. In Graphics you can use getInsets(). Now is there anything like that for Swing / JFrame?
First get the pixels trimmed out by the frame.
int reqWidth = reqHeight = 750;
// first set the size
frame.setSize(reqWidth, reqHeight);
// This is not the actual-sized frame. get the actual size
Dimension actualSize = frame.getContentPane().getSize();
int extraW = reqWidth - actualSize.width;
int extraH = reqHeight - actualSize.height;
// Now set the size.
frame.setSize(reqWidth + extraW, reqHeight + extraH);
An alternate simpler way. The previous works but this is recommended.
frame.getContentPane().setPreferredSize(750, 750);
frame.pack();
Hope this helps.
EDIT:
Add this in your constructor before adding components to the frame. and to set it in the middle, use
frame.setLocationRelativeTo(null);
This will center the window on the screen.
Using setPreferredSize() is problematic, as it always overrules the component's calculation with an arbitrary choice. Instead, pack() the enclosing Window to accommodate the preferred sized of the components, as shown below.
import java.awt.Color;
import java.awt.EventQueue;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
/**
* #see https://stackoverflow.com/a/13481075/230513
*/
public class NewGUI extends JPanel {
private static final int S1 = 10;
private static final int S2 = 50;
private JLabel label = new JLabel("Hello, world!");
public NewGUI() {
label.setHorizontalAlignment(JLabel.CENTER);
Border inner = BorderFactory.createEmptyBorder(S1, S1, S1, S1);
Border outer = BorderFactory.createLineBorder(Color.black);
label.setBorder(new CompoundBorder(outer, inner));
this.setBorder(BorderFactory.createEmptyBorder(S2, S2, S2, S2));
this.add(label);
}
private void display() {
JFrame f = new JFrame("NewGUI");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(this);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new NewGUI().display();
}
});
}
}
I need to resize the font of multiple JLabel based on the scaling factor used to resize the container. To do this, I am setting the font of each JLabel to null so that they take the font of the container. It works, but it also produces strange results.
To be specific, the text seems to "lag" behind the container and sometimes it gets even truncated. I would like to avoid this behavior. Any idea how?
Example code simulating the behavior:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.geom.AffineTransform;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class TextResize implements Runnable {
public static void main(String[] args) {
TextResize example = new TextResize();
SwingUtilities.invokeLater(example);
}
public void run() {
JFrame frame = new JFrame("JLabel Text Resize");
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(800, 400));
Container container = frame.getContentPane();
container.setLayout(new BorderLayout());
final JPanel labelContainer = new JPanel(new GridBagLayout());
labelContainer.setBorder(BorderFactory.createLineBorder(Color.black));
//initial font
final Font textFont = new Font("Lucida Console", Font.PLAIN, 10).deriveFont(AffineTransform.getScaleInstance(1, 1));
labelContainer.setFont(textFont);
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH;
c.insets = new Insets(0, 10, 0, 10);
c.weightx = 1;
for (int i = 0; i < 5; i++) {
JLabel f = new JLabel("Text here with possibly looooooooong words");
f.setBorder(BorderFactory.createLineBorder(Color.green));
f.setFont(null);//take the font from parent
c.gridy = i;
labelContainer.add(f, c);
}
JSlider slider = new JSlider(0,50000,10000);
slider.addChangeListener(new ChangeListener() {
double containerWidth = labelContainer.getPreferredSize().getWidth();
double containerHeight = labelContainer.getPreferredSize().getHeight();
#Override
public void stateChanged(ChangeEvent ev) {
JSlider source = (JSlider) ev.getSource();
double scale = (double) (source.getValue() / 10000d);
//scaling the container
labelContainer.setSize((int) (containerWidth * scale), (int) (containerHeight * scale));
//adjusting the font: why does it 'lag' ? why the truncation at times?
Font newFont = textFont.deriveFont(AffineTransform.getScaleInstance(scale, scale));
labelContainer.setFont(newFont);
//print (font.getSize() does not change?)
System.out.println(scale + " " + newFont.getTransform() + newFont.getSize2D());
}
});
container.add(slider, BorderLayout.NORTH);
JPanel test = new JPanel();
test.setLayout(null);
labelContainer.setBounds(5, 5, labelContainer.getPreferredSize().width, labelContainer.getPreferredSize().height);
test.add(labelContainer);
container.add(test, BorderLayout.CENTER);
frame.pack();
frame.setVisible(true);
}
}
Picture:
http://i.stack.imgur.com/tZLOO.png
Thanks,
-s
You can use any of the following methods:
by #trashgod
by #StanislavL
by #coobird
I sort of solved the problem adding:
#Override
protected void paintComponent(Graphics g) {
final Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
g2d.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
super.paintComponent(g2d);
}
Thanks anyway.
If performance speed is an issue, then you might find the following information about the 3 methods pointed to by MKorbel above useful.
Coobird's code has some limitations if used on a multi-call basis (eg in a sizeChanged Listener or a LayoutManager)
Trashgod's method is between 2 and 4 times slower than Stanislav's (but it also is designed to fill the area BOTH directions as the OP asked in that question, so not unexpected.)
The code below improves on Stanislav's rectangle method (by starting from the current font size each time rather than reverting back to MIN_FONT_SIZE each time) and thus runs between 20 and 50 times faster than that code, especially when the window/font is large.
It also addresses a limitation in that code which only effectively works for labels located at 0,0 (as in the sample given there). The code below works for multiple labels on a panel and at any location.
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
// Improved version of http://java-sl.com/tip_adapt_label_font_size.html
public class FontResizingLabel extends JLabel {
public static final int MIN_FONT_SIZE=3;
public static final int MAX_FONT_SIZE=240;
Graphics g;
int currFontSize = 0;
public FontResizingLabel(String text) {
super(text);
currFontSize = this.getFont().getSize();
init();
}
protected void init() {
addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent e) {
adaptLabelFont(FontResizingLabel.this);
}
});
}
protected void adaptLabelFont(JLabel l) {
if (g==null) {
return;
}
currFontSize = this.getFont().getSize();
Rectangle r = l.getBounds();
r.x = 0;
r.y = 0;
int fontSize = Math.max(MIN_FONT_SIZE, currFontSize);
Font f = l.getFont();
Rectangle r1 = new Rectangle(getTextSize(l, l.getFont()));
while (!r.contains(r1)) {
fontSize --;
if (fontSize <= MIN_FONT_SIZE)
break;
r1 = new Rectangle(getTextSize(l, f.deriveFont(f.getStyle(), fontSize)));
}
Rectangle r2 = new Rectangle();
while (fontSize < MAX_FONT_SIZE) {
r2.setSize(getTextSize(l, f.deriveFont(f.getStyle(),fontSize+1)));
if (!r.contains(r2)) {
break;
}
fontSize++;
}
setFont(f.deriveFont(f.getStyle(),fontSize));
repaint();
}
private Dimension getTextSize(JLabel l, Font f) {
Dimension size = new Dimension();
//g.setFont(f); // superfluous.
FontMetrics fm = g.getFontMetrics(f);
size.width = fm.stringWidth(l.getText());
size.height = fm.getHeight();
return size;
}
protected void paintComponent(Graphics g) {
super.paintComponent(g);
this.g=g;
}
public static void main(String[] args) throws Exception {
FontResizingLabel label=new FontResizingLabel("Some text");
JFrame frame=new JFrame("Resize label font");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(label);
frame.setSize(300,300);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}