JScrollPane minimum width inside JSplitPane - java

I'm trying out the JSplitPane with a couple of scrollable side-by-side JTables.
However I'm experiment a behavior where the JScrollPane gets shrinked too much, as per gif.
Notice the left component, that besides having a minimum width of 250px, continues shrinking.
The relevant code is
final var objetsTable = new JTable();
final var objectsScrollPane = new JScrollPane(objetsTable);
objectsScrollPane.setMinimumSize(new Dimension(250, 0));
objectsScrollPane.setPreferredSize(new Dimension(400, 300));
final var stepsTable = new JTable();
final var stepsScrollPane = new JScrollPane(stepsTable);
stepsScrollPane.setMinimumSize(new Dimension(150, 0));
stepsScrollPane.setPreferredSize(new Dimension(200, 300));
final var splitPane = new JSplitPane();
splitPane.setLeftComponent(objectsScrollPane);
splitPane.setRightComponent(stepsScrollPane);
splitPane.setResizeWeight(1.0);
How can I avoid the JScrollPanes getting shrinked too much in this case?

The getMinimumSize called on a JSplitPane returns a size which is actually taking into account the minimum size of its left and right Components, plus the divider size. So one way to maybe solve your problem would be to make your JSplitPane implement Scrollable (in order to make it respect the minimum size of itself) and add it to a JScrollPane. This way you can ensure that the minimum size is respected and when the user continues shrinking the Scrollable JSplitPane past its minimum size, then the scroll bars will show up.
Here is some working code:
import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
import javax.swing.SwingUtilities;
public class Main {
private static class MyScrollableSplitPane extends JSplitPane implements Scrollable {
private int maxUnitIncrement = 10;
public void setMaxUnitIncrement(final int pixels) {
maxUnitIncrement = pixels;
}
public int getMaxUnitIncrement() {
return maxUnitIncrement;
}
/**
* This is being constantly checked by the scroll pane instead of the
* getPreferredScrollableViewportSize...
*/
#Override
public Dimension getPreferredSize() {
final Dimension minSz = getMinimumSize(),
curSz = getSize();
curSz.width = Math.max(curSz.width, minSz.width);
curSz.height = Math.max(curSz.height, minSz.height);
return curSz;
}
/**
* This is only checked once (at the beginning).
*/
#Override
public Dimension getPreferredScrollableViewportSize() {
return super.getPreferredSize();
}
/**
* Source: https://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html .
*/
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect,
int orientation,
int direction) {
//Get the current position.
int currentPosition;
if (orientation == SwingConstants.HORIZONTAL) {
currentPosition = visibleRect.x;
} else {
currentPosition = visibleRect.y;
}
//Return the number of pixels between currentPosition
//and the nearest tick mark in the indicated direction.
if (direction < 0) {
int newPosition = currentPosition -
(currentPosition / maxUnitIncrement)
* maxUnitIncrement;
return (newPosition == 0) ? maxUnitIncrement : newPosition;
} else {
return ((currentPosition / maxUnitIncrement) + 1)
* maxUnitIncrement
- currentPosition;
}
}
/**
* Source: https://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html .
*/
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect,
int orientation,
int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return visibleRect.width - maxUnitIncrement;
} else {
return visibleRect.height - maxUnitIncrement;
}
}
#Override
public boolean getScrollableTracksViewportWidth() {
final Container parent = getParent();
return (parent instanceof JViewport) && (getMinimumSize().width < ((JViewport) parent).getWidth());
}
#Override
public boolean getScrollableTracksViewportHeight() {
final Container parent = getParent();
return (parent instanceof JViewport) && (getMinimumSize().height < ((JViewport) parent).getHeight());
}
}
private static void createAndShowGUI() {
/*Since I don't add any Components to the 'left' and 'right' panels, I am going to set the
preferred size of them. This is only for demonstrating the concept. Setting the minimum size
though is somewhat required by the JSplitPane itself.*/
final JPanel left = new JPanel();
left.setMinimumSize(new Dimension(150, 100));
left.setPreferredSize(new Dimension(200, 200));
final JPanel right = new JPanel();
right.setMinimumSize(new Dimension(300, 100));
right.setPreferredSize(new Dimension(400, 200));
final JSplitPane split = new MyScrollableSplitPane();
split.setBorder(BorderFactory.createLineBorder(Color.CYAN.darker(), 3));
split.setOrientation(JSplitPane.HORIZONTAL_SPLIT);
split.setLeftComponent(left);
split.setRightComponent(right);
final JFrame frame = new JFrame("MyScrollableSplitPane demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(new JScrollPane(split));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(Main::createAndShowGUI);
}
}

Related

Undecorated JFrame having problems with resizing

I'm making a desktop app (a Markdown editor) in Java, and I'm having problems on frame resizing. The frame I'm working on is undecorated. On the left side of the frame is a title bar. The content pane uses a FlowLayout(FlowLayout.LEFT, 0, 0) as the
default layout. My problem is when I use the following code to achive resizing function, I can only resize my frame on X and Y axis but not both sides at the same time (when you put your cursor on a vertex of the frame), and the layout isn't working. My goal is to complete a full-functioned resizing system which you can drag and drop the edges of the frame to adjust the size of it.
My Code:
package xmark.ui;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.WindowConstants;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
public class MyFrame extends JFrame {
private final TitleBar titleBar = new TitleBar(); // public class TitleBar extends JPanel
/**
* The original content pane on the frame. Is a holder of the title bar.
*/
public JPanel contentPane = (JPanel) getContentPane();
/**
* The default constructor. No needs to manually pass in any arguments
* because it automatically decorates the frame and sets up the title
* bar. There are two private methods used in the constructor: {#code setUI()}
* and {#code titleBar()}.
*/
public MyFrame() {
setUI();
titleBar();
}
private void setUI() {
// Basic method settings
setSize(new Dimension(1200, 750));
setMinimumSize(new Dimension(120, 75));
setLocationRelativeTo(null);
setUndecorated(true);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
contentPane.setBackground(UIUtilities.BASE_COLOR); // UIUtilities is a ui utility class for this app
contentPane.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
// Sizing function
contentPane.addMouseMotionListener(new MouseAdapter() {
boolean top = false;
boolean down = false;
boolean left = false;
boolean right = false;
Point draggingAnchor = null;
#Override
public void mouseMoved(MouseEvent e) {
double mouseY = e.getPoint().getY();
double mouseX = e.getPoint().getX();
if (mouseY > 1 && mouseY < 6) {
setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
top = true;
} else if (Math.abs(mouseY - getSize().getHeight()) <= 5) {
setCursor(Cursor.getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
down = true;
} else if (mouseX > 1 && mouseX < 6) {
setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
left = true;
} else if (Math.abs(mouseX - getSize().getWidth()) <= 5) {
setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
right = true;
} else {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
draggingAnchor = new Point(e.getX() + contentPane.getX(), e.getY() + contentPane.getY());
top = false;
down = false;
left = false;
right = false;
}
}
#Override
public void mouseDragged(MouseEvent e) {
Dimension dimension = getSize();
if (top) {
dimension.setSize(dimension.getWidth(), dimension.getHeight() - e.getY());
setSize(dimension);
setLocation(getLocationOnScreen().x, getLocationOnScreen().y + e.getY());
} else if (down) {
dimension.setSize(dimension.getWidth(), e.getY());
setSize(dimension);
} else if (left) {
dimension.setSize(dimension.getWidth() - e.getX(), dimension.getHeight());
setSize(dimension);
setLocation(getLocationOnScreen().x + e.getX(), getLocationOnScreen().y);
} else if (right) {
dimension.setSize(e.getX(), dimension.getHeight());
setSize(dimension);
} else {
setLocation(e.getLocationOnScreen().x - draggingAnchor.x, e.getLocationOnScreen().y - draggingAnchor.y);
}
}
});
}
private void titleBar() {
contentPane.add(titleBar);
}
public static void main(String[] args) {
new MyFrame().setVisible(true);
}
}

Scroll bar on JFrame in Java Swing

So I have a JFrame of size 500x500 on which I am drawing a line from (0,200) until (100000,200) using g.drawLine(x,y). The problem is I cannot see the entire line as there is no scroll bar. Could someone please tell me how to have a scroll bar in this particular situation to see the entire line upto the point (100000,200).
While I have usability concerns, the example below illustrates how to implement the Scrollable interface as discussed in How to Use Scroll Panes: Implementing a Scrolling-Savvy Client. Note in particular how the result of getPreferredScrollableViewportSize() differs from the result of getPreferredSize(). The unit and block increments make paging slightly easier, too.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Rectangle;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
/**
* #see https://stackoverflow.com/a/37460185/230513
*/
public class Test {
private void display() {
JFrame f = new JFrame("Test");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new JScrollPane(new DrawingPanel()));
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static class DrawingPanel extends JPanel implements Scrollable {
private static final int W = 100_000;
private static final int H = 400;
#Override
public Dimension getPreferredSize() {
return new Dimension(W, H);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.red);
g.drawLine(0, getHeight() / 2, getWidth(), getHeight() / 2);
g.drawLine(0, getHeight() / 4, 0, 3 * getHeight() / 4);
g.drawLine(W - 1, getHeight() / 4, W - 1, 3 * getHeight() / 4);
}
#Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(640, H);
}
#Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return W / 10;
} else {
return 10;
}
}
#Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if (orientation == SwingConstants.HORIZONTAL) {
return W / 10;
} else {
return 10;
}
}
#Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Test()::display);
}
}
Can you show us the code? Since it's hard to know without seeing it, however on the top of my head I think you should just create one and add it to your JFrame like so:
JTextArea ta = new JTextArea(); // Example with a JTextArea,
depends on what you have, as I said, we need to see the code
JScrollPane sp = new JScrollPane(ta); //Add it to the component
needed, in your case the drawn line i guess
jFrame.add(sp); //Add it to the frame
Ande before adding it, you can manipulate the size:
sp.setColumnHeaderView(new JLabel("header column"));
sp.setRowHeaderView(new JLabel("header row"));
sp.setPreferredSize(new Dimension(500, 300));

Too many JPanels inside a JPanel (with GridBagLayout)

So basically if I put JPanels inside a JPanel that uses GridBagLayout and I restrict the size with setPreferredSize, eventually it reaches a point where it can't hold all of them, and it exhibits the behavior shown in the attached picture:
I'm making an accordion. This is just an example to showcase the problem I'm having. Each part of the accordion can open individually and they're of arbitrary size and get added on the fly. Its easy enough to get the heights of all the individual panels and compare them against the total height, but when too many are added it exhibits the crunching behavior I've shown. This also shrinks the heights so its much more difficult to determine when the crunching has happened. I would have to cache heights and somehow pre-calculate the heights of the new parts getting added. The end goal is to remove older panels when a new panel is added and there isn't enough room for it.
Is there an easy way to determine what height something would be if it weren't constrained, or maybe a supported way to detect when such crunching has is happening (so I can quickly thin it out before it gets painted again)? An option that makes GridBagLayout behave like some other layouts and overflow into hammerspace instead of compressing would work too.
Code for example:
import java.awt.*;
import java.awt.event.*;
import javaisms.out;
import javax.swing.*;
public class FoldDrag extends JLayeredPane {
public TexturedPanel backingPanel = new TexturedPanel(new GridBagLayout(),"data/gui/grayerbricks.png");
static JPanel windowbase=new JPanel();
static JPanel restrictedpanel=new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
public FoldDrag() {
JButton addpan = new JButton("Add things");
windowbase.add(addpan);
windowbase.add(restrictedpanel);
restrictedpanel.setBackground(Color.red);
restrictedpanel.setPreferredSize(new Dimension(200,200));
gbc.weighty=1;
gbc.weightx=1;
gbc.gridx=0;
gbc.gridy=0;
gbc.gridheight=1;
gbc.gridwidth=1;
gbc.fill=GridBagConstraints.HORIZONTAL;
addpan.addActionListener(new ActionListener() {
int number=0;
#Override
public void actionPerformed(ActionEvent e)
{
number++;
gbc.gridy=number;
JPanel tmppanel = new JPanel();
tmppanel.setPreferredSize(new Dimension(100,30));
if(number%3==0)
tmppanel.setBackground(Color.blue);
if(number%3==1)
tmppanel.setBackground(Color.yellow);
if(number%3==2)
tmppanel.setBackground(Color.green);
restrictedpanel.add(tmppanel,gbc);
restrictedpanel.validate();
}
});
windowbase.setVisible(true);
}
private static void createAndShowUI() {
JFrame frame = new JFrame("DragLabelOnLayeredPane");
frame.getContentPane().add(windowbase);
FoldDrag thedrag=new FoldDrag();
windowbase.add(thedrag);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setPreferredSize(new Dimension(300,300));
frame.pack();
frame.setResizable(true);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
out.active=true;
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
createAndShowUI();
}
});
}
}
EDIT: Seems I didn't describe my version of the accordion very well. Here's a link.
You have particular requirement which may be better served through the use of it's layout manager. This provides you the ability to control every aspect of the layout without the need to resort to hacks or "work arounds" which never quite work or have bizarre side effects
public class AccordionLayout implements LayoutManager {
// This "could" be controlled by constraints, but that would assume
// that more then one component could be expanded at a time
private Component expanded;
public void setExpanded(Component expanded) {
this.expanded = expanded;
}
public Component getExpanded() {
return expanded;
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
}
#Override
public Dimension preferredLayoutSize(Container parent) {
Dimension size = minimumLayoutSize(parent);
if (expanded != null) {
size.height -= expanded.getMinimumSize().height;
size.height += expanded.getPreferredSize().height;
}
return size;
}
#Override
public Dimension minimumLayoutSize(Container parent) {
int height = 0;
int width = 0;
for (Component comp : parent.getComponents()) {
width = Math.max(width, comp.getPreferredSize().width);
height += comp.getMinimumSize().height;
}
return new Dimension(width, height);
}
#Override
public void layoutContainer(Container parent) {
Insets insets = parent.getInsets();
int availableHeight = parent.getHeight() - (insets.top + insets.bottom);
int x = insets.left;
int y = insets.top;
int maxSize = 0;
Dimension minSize = minimumLayoutSize(parent);
if (expanded != null) {
minSize.height -= expanded.getMinimumSize().height;
// Try an honour the preferred size the expanded component...
maxSize = Math.max(expanded.getPreferredSize().height, availableHeight - minSize.height);
}
int width = parent.getWidth() - (insets.left + insets.right);
for (Component comp : parent.getComponents()) {
if (expanded != comp) {
comp.setSize(width, comp.getMinimumSize().height);
} else {
comp.setSize(width, maxSize);
}
comp.setLocation(x, y);
y += comp.getHeight();
}
}
}
And the runnable example...
This goes to the enth degree, creating a specialised component to act as each "fold", but this just reduces the complexity of the API from the outside, meaning, you just need to think about the title and the content and let the rest of the API take care of itself
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.LineBorder;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private AccordionLayout layout;
public TestPane() {
layout = new AccordionLayout();
setLayout(layout);
AccordionListener listener = new AccordionListener() {
#Override
public void accordionSelected(Component comp) {
layout.setExpanded(comp);
revalidate();
repaint();
}
};
Color colors[] = {Color.RED, Color.BLUE, Color.CYAN, Color.GREEN, Color.MAGENTA, Color.ORANGE, Color.PINK, Color.YELLOW};
String titles[] = {"Red", "Blue", "Cyan", "Green", "Magenta", "Orange", "Pink", "Yellow"};
for (int index = 0; index < colors.length; index++) {
AccordionPanel panel = new AccordionPanel(titles[index], new ContentPane(colors[index]));
panel.setAccordionListener(listener);
add(panel);
}
}
}
public class ContentPane extends JPanel {
public ContentPane(Color background) {
setBackground(background);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(100, 100);
}
}
public interface AccordionListener {
public void accordionSelected(Component comp);
}
public class AccordionPanel extends JPanel {
private JLabel title;
private JPanel header;
private Component content;
private AccordionListener accordionListener;
public AccordionPanel() {
setLayout(new BorderLayout());
title = new JLabel("Title");
header = new JPanel(new FlowLayout(FlowLayout.LEADING));
header.setBackground(Color.GRAY);
header.setBorder(new LineBorder(Color.BLACK));
header.add(title);
add(header, BorderLayout.NORTH);
header.addMouseListener(new MouseAdapter() {
#Override
public void mouseClicked(MouseEvent e) {
AccordionListener listener = getAccordionListener();
if (listener != null) {
listener.accordionSelected(AccordionPanel.this);
}
}
});
}
public AccordionPanel(String title) {
this();
setTitle(title);
}
public AccordionPanel(String title, Component content) {
this(title);
setContentPane(content);
}
public void setAccordionListener(AccordionListener accordionListener) {
this.accordionListener = accordionListener;
}
public AccordionListener getAccordionListener() {
return accordionListener;
}
public void setTitle(String text) {
title.setText(text);
revalidate();
}
public String getText() {
return title.getText();
}
public void setContentPane(Component content) {
if (this.content != null) {
remove(this.content);
}
this.content = content;
if (this.content != null) {
add(this.content);
}
revalidate();
}
public Component getContent() {
return content;
}
#Override
public Dimension getMinimumSize() {
return header.getPreferredSize();
}
#Override
public Dimension getPreferredSize() {
Dimension size = content != null ? content.getPreferredSize() : super.getPreferredSize();
Dimension min = getMinimumSize();
size.width = Math.max(min.width, size.width);
size.height += min.height;
return size;
}
}
public class AccordionLayout implements LayoutManager {
// This "could" be controled by constraints, but that would assume
// that more then one component could be expanded at a time
private Component expanded;
public void setExpanded(Component expanded) {
this.expanded = expanded;
}
public Component getExpanded() {
return expanded;
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
}
#Override
public Dimension preferredLayoutSize(Container parent) {
Dimension size = minimumLayoutSize(parent);
if (expanded != null) {
size.height -= expanded.getMinimumSize().height;
size.height += expanded.getPreferredSize().height;
}
return size;
}
#Override
public Dimension minimumLayoutSize(Container parent) {
int height = 0;
int width = 0;
for (Component comp : parent.getComponents()) {
width = Math.max(width, comp.getPreferredSize().width);
height += comp.getMinimumSize().height;
}
return new Dimension(width, height);
}
#Override
public void layoutContainer(Container parent) {
Insets insets = parent.getInsets();
int availableHeight = parent.getHeight() - (insets.top + insets.bottom);
int x = insets.left;
int y = insets.top;
int maxSize = 0;
Dimension minSize = minimumLayoutSize(parent);
if (expanded != null) {
minSize.height -= expanded.getMinimumSize().height;
// Try an honour the preferred size the expanded component...
maxSize = Math.max(expanded.getPreferredSize().height, availableHeight - minSize.height);
}
int width = parent.getWidth() - (insets.left + insets.right);
for (Component comp : parent.getComponents()) {
if (expanded != comp) {
comp.setSize(width, comp.getMinimumSize().height);
} else {
comp.setSize(width, maxSize);
}
comp.setLocation(x, y);
y += comp.getHeight();
}
}
}
}
Now, if you're really up for a challenge, you could use something a animated layout proxy and do something like...
The end goal is to remove older panels when a new panel is added and there isn't enough room for it
I would guess that after you add a panel you compare the preferred height with the actual height. When the preferred height is greater you have a problem and you remove components as required.
So then the next problem is to use a layout manager that doesn't change the heights of the panels. This can still be done with the GridBagLayout. You just need to override the getMinimumSize() method to return the getPreferredSize() Dimension.
Each part of the accordion can open individually and they're of arbitrary size and get added on the fly
You might want to consider using the Relative Layout. You can add components whose preferred size will be respected. So you will be able to check when the preferred height is greater than the actual height.
Then you can also add components that will be sized based on the amount of space left in the panel. These would be your expanding panels.
So in your example you example when you expand an item you could configure that component to take up the entire space available. If you expand two items then they would each get half the space available.
Maybe something like this:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ExpandingPanel extends JPanel
{
private JPanel expanding;
public ExpandingPanel(String text, Color color)
{
setLayout( new BorderLayout() );
JButton button = new JButton( text );
add(button, BorderLayout.NORTH);
expanding = new JPanel();
expanding.setBackground( color );
expanding.setVisible( false );
add(expanding, BorderLayout.CENTER);
button.addActionListener( new ActionListener()
{
#Override
public void actionPerformed(ActionEvent e)
{
expanding.setVisible( !expanding.isVisible() );
Container parent = ExpandingPanel.this.getParent();
LayoutManager2 layout = (LayoutManager2)parent.getLayout();
if (expanding.isVisible())
layout.addLayoutComponent(ExpandingPanel.this, new Float(1));
else
layout.addLayoutComponent(ExpandingPanel.this, null);
revalidate();
repaint();
}
});
}
private static void createAndShowGUI()
{
RelativeLayout rl = new RelativeLayout(RelativeLayout.Y_AXIS);
rl.setFill( true );
JPanel content = new JPanel( rl );
content.add( new ExpandingPanel("Red", Color.RED) );
content.add( new ExpandingPanel("Blue", Color.BLUE) );
content.add( new ExpandingPanel("Green", Color.GREEN) );
JFrame frame = new JFrame("Expanding Panel");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add( content);
frame.setLocationByPlatform( true );
frame.setSize(200, 300);
frame.setVisible( true );
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
createAndShowGUI();
}
});
}
}
You can tell something is "crunched" when panel.getPreferredSize().height != panel.getHeight() and panel.getPreferredSize().width != panel.getWidth()

JList Individually Sized Cells

I have a horizontally arranged JList. All of the items in the list are of significantly different sizes, and by default the renderer scales each item to the size of the largest item in the list. I have attempted to implement a custom renderer as follows, but each item in the list remains the same size. Any advice?
The following is the ListCellRenderer:
package ui.wizards;
import java.awt.Component;
import java.awt.Dimension;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.ListCellRenderer;
import javax.swing.SwingConstants;
public class WizHistoryCellRenderer extends JLabel
implements ListCellRenderer<String>
{
private static final long serialVersionUID = -3650608268225337416L;
JList<? extends String> list;
#Override
public Component getListCellRendererComponent(JList<? extends String> list,
String value, int index, boolean isSelected, boolean cellHasFocus)
{
this.list = list;
int width = this.getFontMetrics(this.getFont())
.stringWidth((String) value);
int height = 20;
setText(value);
if (list != null)
{
if (index == list.getSelectedIndex())
showSelected();
else
showUnselected();
}
setMaximumSize(new Dimension((int) (1.1 * width), height));
setPreferredSize(new Dimension((int) (1.1 * width), height));
setHorizontalAlignment(SwingConstants.CENTER);
setOpaque(true);
return this;
}
private void showSelected()
{
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
}
private void showUnselected()
{
setBackground(list.getBackground());
setForeground(list.getForeground());
}
}
Try calling the JList method setFixedCellHeight, with -1 as the argument. This causes JList to use the preferred height returned by any custom renderer when drawing the list items.
I think you could try to subclass javax.swing.plaf.basic.BasicListUI and override protected void paintCell(Graphics g, int row, Rectangle rowBounds, ListCellRenderer cellRenderer, ListModel dataModel, ListSelectionModel selModel, int leadIndex) and maybe some other methods performing cell size calculation.
Then use JList.setUI(ListUI) to apply your custom UI. The UI is responsible for drawing the JList using ListCellRenders. Please excuse that I do not provide a complete example as it is presumably a lot of work tweaking all aspects of the custom drawing process.
You will need 3 things:
A scale factor for each individual cell. You may for example add a scale factor variable of type double in each entry of the list.
A custom ListCellRenderer.
Make the method BasicListUI#updateLayoutState() public.
Follows self-contained code (read the comments for more info):
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon;
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.ListCellRenderer;
import javax.swing.plaf.basic.BasicListUI;
public class JListIndipendentCellSizes {
//Configuration constants:
private static final int ICON_SIZE = 20;
private static final double SCALE_STEP_SIZE = 0.125; //Smaller values of this makes zooming slower. Greater values makes zooming faster.
//Simple Icon implementation for demonstration purposes.
public static class TJIcon implements Icon {
private final Color rectColor, ovalColor;
public TJIcon(final Color rectColor, final Color ovalColor) {
this.rectColor = rectColor;
this.ovalColor = ovalColor;
}
#Override
public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
g.setColor(rectColor);
g.fillRect(x, y, getIconWidth(), getIconHeight());
g.setColor(ovalColor);
g.fillOval(x, y, getIconWidth(), getIconHeight());
}
#Override public int getIconWidth() { return ICON_SIZE; }
#Override public int getIconHeight() { return ICON_SIZE; }
}
//A simple list entry. Contains a text, an icon and (on top of them all) the scale factor:
public static class TJListEntry {
private final String text;
private final Icon icon;
private double scaleFactor;
public TJListEntry(final String text,
final Icon icon) {
this.text = text;
this.icon = icon;
scaleFactor = 1;
}
public String getText() {
return text;
}
public Icon getIcon() {
return icon;
}
public double getScaleFactor() {
return scaleFactor;
}
public void zoomIn() {
scaleFactor = scaleFactor + SCALE_STEP_SIZE;
}
public void zoomOut() {
scaleFactor = Math.max(scaleFactor - SCALE_STEP_SIZE, SCALE_STEP_SIZE); //Do not allow underflow.
}
public void resetZoom() {
scaleFactor = 1;
}
}
public static class TJListCellRenderer extends JLabel implements ListCellRenderer<TJListEntry> {
private double currentScaleFactor;
public TJListCellRenderer() {
//Ensure every pixel is painted starting from the top-left corner of the label:
super.setVerticalAlignment(JLabel.TOP);
super.setHorizontalAlignment(JLabel.LEFT);
//We need to do this, because the scaling in paintComponent() is also relative to the top-left corner.
}
#Override
public void paintComponent(final Graphics g) {
//setRenderingHints here? Probably for ANTIALIAS...
((Graphics2D)g).scale(currentScaleFactor, currentScaleFactor); //Let's scale everything that is painted afterwards:
super.paintComponent(g); //Let's paint the (scaled) JLabel!
}
#Override
public Dimension getPreferredSize() {
final Dimension superPrefDim = super.getPreferredSize(); //Handles automatically insets, icon size, text font, etc.
final double w = superPrefDim.width * currentScaleFactor, //And we just scale the preferred size.
h = superPrefDim.height * currentScaleFactor; //And we just scale the preferred size.
return new Dimension((int)w + 1, (int)h + 1); //Add one extra pixel to spare.
}
#Override
public Component getListCellRendererComponent(final JList<? extends TJListEntry> list, final TJListEntry value, final int index, final boolean isSelected, final boolean cellHasFocus) {
currentScaleFactor = value.getScaleFactor(); //Probably the most important step.
setIcon(value.getIcon()); //Could be a loaded ImageIcon here (i.e. image)!
setText(value.getText());
if (isSelected) {
setBackground(list.getSelectionBackground());
setForeground(list.getSelectionForeground());
} else {
setBackground(list.getBackground());
setForeground(list.getForeground());
}
setOpaque(true);
return this;
}
}
public static class TJListUI extends BasicListUI {
#Override
public void updateLayoutState() {
super.updateLayoutState(); //Just make the following method public.
/*Note: this is not really needed here:
The method could remain protected, but in the case you want this
code to be a bit more reusable, then you shall make it public.*/
}
}
public static void main(final String[] args) {
final TJListEntry[] entries = new TJListEntry[]{new TJListEntry("This is a sample text.", new TJIcon(Color.BLACK, Color.WHITE)),
new TJListEntry("This is a longer sample text.", new TJIcon(Color.GREEN, Color.RED)),
new TJListEntry("Small text", new TJIcon(Color.LIGHT_GRAY, Color.BLUE))};
final TJListUI ui = new TJListUI();
final JList<TJListEntry> list = new JList<>(entries);
list.setUI(ui); //Important step! Without setting our UI, it won't work (at least as I have looked for).
list.setCellRenderer(new TJListCellRenderer()); //Important step! Without setting our custom ListCellRenderer you will have to implement your own ListUI probably.
final JScrollPane scroll = new JScrollPane(list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
final JButton buttonZoomIn = new JButton("Zoom in selected cells"),
buttonZoomOut = new JButton("Zoom out selected cells"),
buttonResetZoom = new JButton("Reset zoom of selected cells");
buttonZoomIn.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).zoomIn();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonZoomOut.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).zoomOut();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
buttonResetZoom.addActionListener(e -> {
for (final int i: list.getSelectedIndices())
list.getModel().getElementAt(i).resetZoom();
ui.updateLayoutState(); //Read the preferred sizes from the cell renderer.
list.revalidate(); //Update the JScrollPane.
list.repaint(); //Repaint the list.
});
final JPanel buttons = new JPanel(); //FlowLayout.
buttons.add(buttonZoomIn);
buttons.add(buttonZoomOut);
buttons.add(buttonResetZoom);
final JPanel panel = new JPanel(new BorderLayout());
panel.add(buttons, BorderLayout.PAGE_START);
panel.add(scroll, BorderLayout.CENTER);
final JFrame frame = new JFrame("Independent JList cell sizes demo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
And a screenshot:
To run it on your own, just select at least one index, and play with the buttons.

How to remove gap in java swing label of large size

in my application I have label which has a font size over 200. This label contains big up and down (irregular)gap. How can I remove it ?
This is my code:
package Core;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
public class LabelDemo extends JPanel {
public LabelDemo() {
super(new GridBagLayout());
JLabel label2;
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
// Create the other labels.
label2 = new JLabel("Text-Only Label");
label2.setBorder(BorderFactory.createTitledBorder("aaaaaaaa"));
label2.setFont(new Font("Verdana", Font.PLAIN, (int) 220));
// label2.setBorder(new EmptyBorder(-50, 0, 0, 0));
// Add the labels.
add(label2, c);
}
/**
* Create the GUI and show it. For thread safety, this method should be invoked from the event dispatch thread.
*/
private static void createAndShowGUI() {
// Create and set up the window.
JFrame frame = new JFrame("LabelDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Add content to the window.
frame.add(new LabelDemo());
// Display the window.
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
// Schedule a job for the event dispatch thread:
// creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Turn off metal's use of bold fonts
UIManager.put("swing.boldMetal", Boolean.FALSE);
createAndShowGUI();
}
});
}
}
I also try my last post: How to change gap in swing label and experiment with insets but this looks different in linux and windows
Is there some better way how to remove this gap ?
JDigit may give you some ideas:
It override's paintComponent() to down-sample a high-resolution BufferedImage and control the geometry.
It uses setBorderPainted(false) to set the borderPainted property.
It uses a FocusHandler for custom highlighting.
Addendum: As noted here, the underlying problem is the font's leading, defined in FontMetrics as being included in the font's height. As suggested in a comment by #Guillaume Polet, you can render the text wherever you want in your own JComponent. TextLayout, discussed here, can be used to calculate the bounds, as shown below.
Pros:
Absolute control over placement.
Geometry of TexteLayout bounds based on FontMetrics.
Cons:
No Icon support.
No HTML support.
Note that the JComponent authors "recommend that you put the component in a JPanel and set the border on the JPanel."
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.font.FontRenderContext;
import java.awt.font.TextLayout;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
/**
* #see https://stackoverflow.com/a/16014525/230513
*/
public class UnleadedTest {
private static class Unleaded extends JComponent {
private Font font = new Font("Verdana", Font.PLAIN, 144);
private FontRenderContext frc = new FontRenderContext(null, true, true);
private String text;
private TextLayout layout;
private Rectangle r;
public Unleaded(String text) {
this.text = text;
calcBounds();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(r.width, r.height);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
calcBounds();
layout.draw(g2d, -r.x, -r.y);
}
private void calcBounds() {
layout = new TextLayout(text, font, frc);
r = layout.getPixelBounds(null, 0, 0);
}
}
private void display() {
JFrame f = new JFrame("Unleaded");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Unleaded label = new Unleaded("Unleaded");
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createTitledBorder("Title"));
panel.add(label);
f.add(panel);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
new UnleadedTest().display();
}
});
}
}
The "right way" to do this would be to extend "BasicLabelUI" and override the "protected String layoutCL()" method. This is the method that is responsible for laying out everything inside the label and is called when the "getPreferredSize()" of the JLabel is called. So this method determines the height the component is going to be.
If you drill down deep enough you'll see that the height is determined by the following line in the SwingUtilities:1021 class (which is used by layoutCL):
textR.height = fm.getHeight();
So the label is not causing the white space, the font is. The label just conforms to what the FontMetrics object says is the maximum height of the font for that size.
The easiest way would probably be to cheat; Force the size calculation to do something it shouldn't. Below is your example with a custom LabelUI component which you can experiment on. For example if you force the variable to 'dy' to '-40' the text will be at the top. If you want to make something more durable you could check all the leters in the string of the label, measure their maximum height and use that in the layoutCL method. But thats more work obviously.
package Core;
import sun.swing.SwingUtilities2;
import javax.swing.*;
import javax.swing.plaf.LabelUI;
import javax.swing.plaf.basic.BasicLabelUI;
import javax.swing.text.View;
import java.awt.*;
public class LabelDemo extends JPanel {
public LabelDemo() {
super(new GridBagLayout());
JLabel label2;
GridBagConstraints c = new GridBagConstraints();
c.gridx = 0;
c.gridy = 0;
// Create the other labels.
label2 = new JLabel("Text-Only Label");
label2.setVerticalAlignment(SwingUtilities.TOP);
label2.setVerticalTextPosition(SwingUtilities.TOP);
label2.setUI(SkinnyLabelUI.createUI(label2));
label2.setBorder(BorderFactory.createTitledBorder("aaaaaaaa"));
label2.setFont(new Font("Verdana", Font.PLAIN, (int) 220));
// label2.setBorder(new EmptyBorder(-50, 0, 0, 0));
// Add the labels.
add(label2, c);
}
/**
* Create the GUI and show it. For thread safety, this method should be
* invoked from the event dispatch thread.
*/
private static void createAndShowGUI() {
// Create and set up the window.
JFrame frame = new JFrame("LabelDemo");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Add content to the window.
frame.add(new LabelDemo());
// Display the window.
frame.pack();
frame.setVisible(true);
}
public static void main(String[] args) {
// Schedule a job for the event dispatch thread:
// creating and showing this application's GUI.
SwingUtilities.invokeLater(new Runnable() {
public void run() {
// Turn off metal's use of bold fonts
UIManager.put("swing.boldMetal", Boolean.FALSE);
createAndShowGUI();
}
});
}
private static class SkinnyLabelUI extends BasicLabelUI {
private static final SkinnyLabelUI labelUI = new SkinnyLabelUI();
public static LabelUI createUI(JComponent c) {
return labelUI;
}
protected String layoutCL(
JLabel label,
FontMetrics fm,
String text,
Icon icon,
Rectangle viewR,
Rectangle iconR,
Rectangle textR) {
int verticalAlignment = label.getVerticalAlignment();
int horizontalAlignment = label.getHorizontalAlignment();
int verticalTextPosition = label.getVerticalTextPosition();
int horizontalTextPosition = label.getHorizontalTextPosition();
if (icon != null) {
iconR.width = icon.getIconWidth();
iconR.height = icon.getIconHeight();
} else {
iconR.width = iconR.height = 0;
}
/* Initialize the text bounds rectangle textR. If a null
* or and empty String was specified we substitute "" here
* and use 0,0,0,0 for textR.
*/
boolean textIsEmpty = (text == null) || text.equals("");
int lsb = 0;
int rsb = 0;
/* Unless both text and icon are non-null, we effectively ignore
* the value of textIconGap.
*/
int gap;
View v;
if (textIsEmpty) {
textR.width = textR.height = 0;
text = "";
gap = 0;
} else {
int availTextWidth;
gap = (icon == null) ? 0 : label.getIconTextGap();
if (horizontalTextPosition == SwingUtilities.CENTER) {
availTextWidth = viewR.width;
} else {
availTextWidth = viewR.width - (iconR.width + gap);
}
v = (label != null) ? (View) label.getClientProperty("html") : null;
if (v != null) {
textR.width = Math.min(availTextWidth,
(int) v.getPreferredSpan(View.X_AXIS));
textR.height = (int) v.getPreferredSpan(View.Y_AXIS);
} else {
textR.width = SwingUtilities2.stringWidth(label, fm, text);
lsb = SwingUtilities2.getLeftSideBearing(label, fm, text);
if (lsb < 0) {
// If lsb is negative, add it to the width and later
// adjust the x location. This gives more space than is
// actually needed.
// This is done like this for two reasons:
// 1. If we set the width to the actual bounds all
// callers would have to account for negative lsb
// (pref size calculations ONLY look at width of
// textR)
// 2. You can do a drawString at the returned location
// and the text won't be clipped.
textR.width -= lsb;
}
if (textR.width > availTextWidth) {
text = SwingUtilities2.clipString(label, fm, text,
availTextWidth);
textR.width = SwingUtilities2.stringWidth(label, fm, text);
}
textR.height = fm.getHeight();
System.out.println("font height: " + textR.height);
}
}
/* Compute textR.x,y given the verticalTextPosition and
* horizontalTextPosition properties
*/
if (verticalTextPosition == SwingUtilities.TOP) {
if (horizontalTextPosition != SwingUtilities.CENTER) {
textR.y = 0;
} else {
textR.y = -(textR.height + gap);
}
} else if (verticalTextPosition == SwingUtilities.CENTER) {
textR.y = (iconR.height / 2) - (textR.height / 2);
} else { // (verticalTextPosition == BOTTOM)
if (horizontalTextPosition != SwingUtilities.CENTER) {
textR.y = iconR.height - textR.height;
} else {
textR.y = (iconR.height + gap);
}
}
if (horizontalTextPosition == SwingUtilities.LEFT) {
textR.x = -(textR.width + gap);
} else if (horizontalTextPosition == SwingUtilities.CENTER) {
textR.x = (iconR.width / 2) - (textR.width / 2);
} else { // (horizontalTextPosition == RIGHT)
textR.x = (iconR.width + gap);
}
// WARNING: DefaultTreeCellEditor uses a shortened version of
// this algorithm to position it's Icon. If you change how this
// is calculated, be sure and update DefaultTreeCellEditor too.
/* labelR is the rectangle that contains iconR and textR.
* Move it to its proper position given the labelAlignment
* properties.
*
* To avoid actually allocating a Rectangle, Rectangle.union
* has been inlined below.
*/
int labelR_x = Math.min(iconR.x, textR.x);
int labelR_width = Math.max(iconR.x + iconR.width,
textR.x + textR.width) - labelR_x;
int labelR_y = Math.min(iconR.y, textR.y);
int labelR_height = Math.max(iconR.y + iconR.height,
textR.y + textR.height) - labelR_y;
int dx, dy;
if (verticalAlignment == SwingUtilities.TOP) {
dy = viewR.y - labelR_y;
} else if (verticalAlignment == SwingUtilities.CENTER) {
dy = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2));
} else { // (verticalAlignment == BOTTOM)
dy = (viewR.y + viewR.height) - (labelR_y + labelR_height);
}
if (horizontalAlignment == SwingUtilities.LEFT) {
dx = viewR.x - labelR_x;
} else if (horizontalAlignment == SwingUtilities.RIGHT) {
dx = (viewR.x + viewR.width) - (labelR_x + labelR_width);
} else { // (horizontalAlignment == CENTER)
dx = (viewR.x + (viewR.width / 2))
- (labelR_x + (labelR_width / 2));
}
/* Translate textR and glypyR by dx,dy.
*/
textR.x += dx;
textR.y += dy;
iconR.x += dx;
iconR.y += dy;
if (lsb < 0) {
// lsb is negative. Shift the x location so that the text is
// visually drawn at the right location.
textR.x -= lsb;
textR.width += lsb;
}
if (rsb > 0) {
textR.width -= rsb;
}
return text;
}
}
}
changing the border offset might help:
int OFFSET_TOP=50,OFFSET_BOTTOM=50;
label.setBorder(new TitledBorder(TITLE){
#Override
public Insets getBorderInsets(Component c, Insets insets){
return new Insets(insets.top - OFFSET_TOP, insets.left, insets.bottom - OFFSET_BOTTOM, insets.right);
}
});

Categories

Resources