GridBagLayout gridwidth doesn't work as expected - java

I'm working with the java swing LayoutManager GridBagLayout, and ran into this problem. I want a layout like this
ACC
BB
But get a layout like this
ACC
B
A and B take up the same number of columns despite B having a gridwidth of 2 where A's gridwidth is 1. I don't think there can be a vanishingly small column between A,B and C because C starts in column 1. The problem does not occur if C's gridwidth is 1 instead of 2. I'm baffled by the output.
Why is this happening / how can I fix this?
JFrame test = new JFrame();
test.setSize(800,800);
test.setLayout(new GridBagLayout());
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
GridBagConstraints c;
c = new GridBagConstraints();
c.gridx=0;
c.gridy=0;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("A"), c);
c = new GridBagConstraints();
c.gridx=0;
c.gridy=2;
c.gridwidth=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("B"), c);
c = new GridBagConstraints();
c.gridx=1;
c.gridy=0;
c.gridwidth=2;
c.gridheight=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 2;
c.weighty = 2;
test.add(new JButton("C"), c);
test.setVisible(true);

I know how you feel... It seems GridBagLayout reduces the width of columns to 0 when they are not filled by 1x1 component. I don't know if the following is the most efficient solution, but it works (by adding two dummy JPanel to "inflate" columns 1 and 2):
JFrame test = new JFrame();
test.setSize(800,800);
test.setLayout(new GridBagLayout());
test.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
GridBagConstraints c;
c = new GridBagConstraints();
c.gridx=0;
c.gridy=0;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("A"), c);
c = new GridBagConstraints();
c.gridx=0;
c.gridy=1;
c.gridwidth=2;
c.fill = GridBagConstraints.BOTH;
c.weightx = 1;
c.weighty = 1;
test.add(new JButton("B"), c);
c = new GridBagConstraints();
c.gridx=1;
c.gridy=0;
c.gridwidth=2;
c.gridheight=1;
c.fill = GridBagConstraints.BOTH;
c.weightx = 2;
c.weighty = 2;
test.add(new JButton("C"), c);
c = new GridBagConstraints();
c.gridx=1;
c.gridy=2;
c.gridwidth=1;
c.gridheight=0;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.weighty = 0;
test.add(new JPanel(), c);
c = new GridBagConstraints();
c.gridx=2;
c.gridy=3;
c.gridwidth=1;
c.gridheight=0;
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1;
c.weighty = 0;
test.add(new JPanel(), c);
test.setVisible(true);

TL;DR
Either add a dummy first row of a small-sized single-cell JPanels or JSeparators that can serve as padding and overcome this buggy-feature. Another alternative is to set GridBagLayout.columnWeights to something reasonable if you know your weights in advance (for exapmle, if you just want all your columns to have equal size), but while this workaround fixes the weight problem, it may still lead to wrong column widths.
This answer describes the problem with column widths, but exactly the same algorithms are applied to row heights, so if you're having trouble with multi-row components, all of the following still holds (just change x to y and widths to heights).
The docs
The reason behind this behavior is one real weird property of an otherwise quite flexible and useful layout manager. I don't even know whether it's a bug or a feature, because widths of columns (and, consequently, components) are determined by weights, and all the docs say about weights is
The grid bag layout manager calculates the weight of a column to be
the maximum weightx of all the components in a column. If the
resulting layout is smaller horizontally than the area it needs to
fill, the extra space is distributed to each column in proportion to
its weight. A column that has a weight of zero receives no extra
space.
This isn't helpful at all because it says nothing about the case when a component occupies several cells. The tutorial is a bit more “helpful”:
Generally weights are specified with 0.0 and 1.0 as the extremes: the
numbers in between are used as necessary. Larger numbers indicate that
the component's row or column should get more space. For each column,
the weight is related to the highest weightx specified for a component
within that column, with each multicolumn component's weight being
split somehow between the columns the component is in. Similarly, each
row's weight is related to the highest weighty specified for a
component within that row. Extra space tends to go toward the
rightmost column and bottom row.
(Emphasis mine.)
Really? Split somehow? Now, that is really helpful.
The actual algorithm
The actual algorithm behind this split is really unintuitive. It iterates over all components several times. During the first pass, it only works with components that have the smallest size (in terms of cells occupied). Then it takes the next size and iterates again, until all sizes are extinguished. In your case, the component A has size 1, and the components B and C have size 2, so the order of iteration will be A, C, B. If A had size 3, then the order would be C, B, A.
During each iteration, the algorithm calculates weights for columns that the component occupies, using the following procedure.
Take the component weight and subtract weights of all columns it occupies, as they are calculated by this moment (including previous iterations for smaller sizes!). The resulting number is the difference between the current total weight of the columns and the weight of the component.
If the resulting number is zero or less, do nothing.
Otherwise, distribute this difference between the columns, proportional to their current weights!
If anything left of the weight difference, add these “leftovers” to the rightmost cell occupied by the component.
The actual code, for the intrigued:
px = constraints.tempX + constraints.tempWidth; // tempX is gridx, tempWidth is gridwidth
weight_diff = constraints.weightx; // the weight of the component
for (k = constraints.tempX; k < px; k++) // iteration over the cells
weight_diff -= r.weightX[k]; // r.weightX holds the current column weights
if (weight_diff > 0.0) {
weight = 0.0;
for (k = constraints.tempX; k < px; k++)
weight += r.weightX[k]; // weight is the total weight of the cells
for (k = constraints.tempX; weight > 0.0 && k < px; k++) {
double wt = r.weightX[k];
double dx = (wt * weight_diff) / weight; // a part of the weight
r.weightX[k] += dx;
weight_diff -= dx;
weight -= wt;
}
/* Assign the remainder to the rightmost cell */
r.weightX[px-1] += weight_diff;
}
(Some of the comments mine.)
This algorithm sounds more or less reasonable, but it has a few drawbacks.
If one of the columns has its current weight equal to zero, then it will get no weight addition at all. If all the columns have zero weight, then the loop won't even start and all the weight will be considered “leftovers” and go into the rightmost cell!
That's why your argument
I don't think there can be a vanishingly small column between A,B and
C because C starts in column 1.
doesn't really work because it's the column that C ends (not starts!) at that matters here.
Examples
What does this mean in your case? I assume that gridy=2 is a typo, and BB should have gridy=1 (but that doesn't really change anything because all you did was add an invisible row 1 between 0 and 2).
During the first pass, only A is visited. It has weightx of 1, and it all goes to column 0. No surprise here. But columns 1 and 2 still have zero weights.
During the second pass, CC is visited first. It has weightx of 2. It occupies columns 1 and 2, but since they both have zero weights, all weight goes into column 2 that now has weight 2.
Then BB is visited. It has weightx of 1. It is split between columns 0 and 1. But column 1 still has zero weight, so all of it goes into column 0. But it already has weight 1, so the difference between its weight and the weight of the component is zero, and nothing happens.
The resulting weights are 1-0-2, and the layout is
A||CC
B|| // B actually goes into the second column, but it has zero width
But what if BB had weightx of 2? Then everything would be the same up to step 3, but then the difference between the weight of the component (2) and the current weight of column 0 (1) would be 1, so that 1 would be added to the weight of column 0, and its weight would become 2. The weight of column 1 would still be zero, and the resulting layout would be
AA||CC
BB|| // B actually goes into the second column, but it has zero width
All three components would have the same width, but it still wouldn't look like we expect.
The solution
One obvious solution, mentioned in the other answer, is to add some dummy single-cell components into the first row. It will cause columns to have non-zero weights during all passes, as soon as the algorithm is done with the first row. JSeparator or JPanel works fine for this purpose. By playing around with insets and component's heights it is easily possible to get a nice look.
Another possible solution
If you know your column weights in advance, you can simply override the whole algorithm and simply provide the right weights by
GridBagLayout layout = new GridBagLayout();
layout.columnWeights = new double[] { 1.0, 1.0, 1.0 };
test.setLayout(layout);
Adjust the weights accordingly if you need your columns to have different weights. The problem with this approach, however, that a somewhat similar algorithm is applied to calculate the column widths (as opposed to weights). In particular, if, at some moment, a component is found to require more width than the total width of the column the component occupies, then the width difference is spread among all columns.
This means that if, for example, a component that needs 150 pixels wants to occupy two columns that have current widths of 0 and 50, then the difference (100) is split between the columns evenly, resulting in a 50/100 split instead of 75/75 as one would expect. The solution is to either switch to the fake first row solution or to set column widths directly in the same manner as columnWeights (by setting GridBagLayout.columnWidths). This, of course, requires a bit more complicated calculations than widths.

Related

Java - Resize buttons in GridBagLayout

how can I make the buttons height a bit bigger and the width of right and left columns on the same size. I have tried weightx weighty heightx heighty but it didn't work. Thanks in advance. Here's my code:
Container contentPane = getContentPane();
contentPane.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
c.insets = new Insets(20,20,20,20);
c.fill = GridBagConstraints.HORIZONTAL;
c.weightx = 1.0;
JButton back = new JButton("Back to previous menu");
c.gridx = 1;
c.gridy = 0;
contentPane.add(back,c);
JLabel welcome = new JLabel("Hi secretary, please select an action");
c.gridx = 0;
c.gridy = 0;
contentPane.add(welcome,c);
There's more code of buttons definition but it's just positioning themselves on the layout.
Do not set the preferred size of your buttons. This will cause serious visual issues for users who have desktop fonts which are different from yours.
You should let the button define its own preferred size. The best way to augment the preferred height is with GridBagConstraints.ipady:
c.ipady = 24;
Another option is to use JButton.setMargin to give each button an additional margin:
Insets margin = new Insets(12, 0, 12, 0);
back.setMargin(margin);
welcome.setMargin(margin);
Forcing the left and right sides to have equal widths is more difficult. GridBagLayout is not capable of doing that. The weightx field only determines the distribution of extra space, when the window is wider than it needs to be in order to accommodate all child components’ preferred sizes. If the columns have different widths to begin with, distributing the extra space with weightx will not make their widths equal.
Personally, I don’t think forcing the two sides to have equal widths will add any value. If you insist on the left and right sides being exactly equal width, you will have to abandon the use of GridBagLayout. SpringLayout can do it, but it’s difficult to use. If you’re willing to introduce a third-party dependency, something like MiG Layout may be able to do it.

Multiple GridBagConstraints?

I'm a bit confused, is it possible to have multiple GridBagConstraints?
I have two panels using GridBagLayout, both affected by the same constraints. This gives me an issue when it comes to putting smaller components next to larger ones as illustrated below. The size of the panel on the left, means the cell on that row is very large, centering the panel on the right and it's components. I've tried using separate constraints for each panel but I haven't seen any differences, I think I'm doing something wrong.
How can I achieve two panels positioned next to each other, however have the components of each influenced by separate constraints?
Here's what I currently have:
Created with:
// layout
GridBagConstraints c = new GridBagConstraints();
// components
JPanel supplier = new JPanel(new GridBagLayout());
// grab the suppliers
Suppliers.Supplier[] suppliers = new Suppliers.Supplier[Suppliers.supplier.size()];
for (int i = 0; i < Suppliers.supplier.size(); i++) {
suppliers[i] = Suppliers.supplier.get(i);
}
JPanel resultsPanel = new JPanel();
JScrollPane scrollpane = new JScrollPane(resultsPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
scrollpane.setPreferredSize(new Dimension(120, 300));
// style properties
c.gridx = 0;
c.gridy = 0;
c.insets = new Insets(10, 10, 10, 10);
supplier.add(scrollpane, c);
int position = 0;
for (int i = 0; i < Suppliers.supplier.size(); i++) {
b_supplierSuppliers.add(new JButton(suppliers[i].getName()));
b_supplierSuppliers.get(i).setActionCommand(suppliers[i].getId());
b_supplierSuppliers.get(i).addActionListener(this);
// style properties
c.gridx = 0;
c.gridy = position;
c.insets = new Insets(10, 10, 10, 10);
c.gridwidth = 1;
resultsPanel.add(b_supplierSuppliers.get(i), c);
position++;
}
JPanel resultsPaneltwo = new JPanel(new GridBagLayout());
// style properties
c.gridx = 1;
c.gridy = 0;
supplier.add(resultsPaneltwo, c);
// code label
l_supplierCode = new JLabel("Supplier Code");
// style properties
c.gridx = 1;
c.gridy = 0;
c.insets = new Insets(0, 10, 10, 10);
c.gridwidth = 4;
c.fill = 4;
resultsPaneltwo.add(l_supplierCode, c);
However I'm after:
Any help is much appreciated.
First, to answer your initial question. Yes, you should use different GridBagConstraints objects for each component. It's worth noting however that it is not essential to create new ones for each component, and changes to the constraint after it has been assigned to another won't affect the earlier component's position. So, for the sake of readability and later debugging it is worth declaring multiple constraints but it's not essential.
Also, it looks as though you need to specify a weighty for your constraint on your right cell. By default cells are centered in any spare space. In order to specify where that extra space is placed (vertically for y, and horizontally for x), you need to give the constraint a weight value;
c.weighty = 1;
This should make it so all extra vertical space is allocated to that cell's constraint, and as such push the cell to the top. If you create a second constraint and do the following;
c2.weighty = 1;
The extra space would be spread evenly between those two cell's constraints.I hope this helps. Let me know how you get on.
The official Oracle GridBagLayout guide says the following on the weight attributes;
weightx, weighty
Specifying weights is an art that can have a
significant impact on the appearance of the components a GridBagLayout
controls. Weights are used to determine how to distribute space among
columns (weightx) and among rows (weighty); this is important for
specifying resizing behavior. Unless you specify at least one non-zero
value for weightx or weighty, all the components clump together in the
center of their container. This is because when the weight is 0.0 (the
default), the GridBagLayout puts any extra space between its grid of
cells and the edges of the container.
Generally weights are specified with 0.0 and 1.0 as the extremes: the
numbers in between are used as necessary. Larger numbers indicate that
the component's row or column should get more space. For each column,
the weight is related to the highest weightx specified for a component
within that column, with each multicolumn component's weight being
split somehow between the columns the component is in. Similarly, each
row's weight is related to the highest weighty specified for a
component within that row. Extra space tends to go toward the
rightmost column and bottom row.

GridBagLayout: how to fill all empty spaces

I've have a JFrame contains some JPanels using a gridBagLayout (3 rows, one column). That's my code:
Container main_container = getContentPane();
GridBagLayout layout = new GridBagLayout();
main_container.setLayout(layout);
GridBagConstraints c = new GridBagConstraints();
StatoMagazzini jpanel_stato_magazzini = new StatoMagazzini();
c.gridx = 1;
c.gridy = 2;
c.fill = GridBagConstraints.BOTH;
layout.setConstraints(jpanel_stato_magazzini, c);
AcquistoLotto jpanel_acquisto = new AcquistoLotto(i, jpanel_stato_magazzini);
c.gridx = 1;
c.gridy=1;
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.FIRST_LINE_START;
layout.setConstraints(jpanel_acquisto, c);
ButtonPanel jpanel_button_panel = new ButtonPanel(i);
c.gridx=1;
c.gridy=3;
c.anchor = GridBagConstraints.CENTER;
layout.setConstraints(jpanel_button_panel, c);
main_container.add(jpanel_acquisto);
main_container.add(jpanel_stato_magazzini);
main_container.add(jpanel_button_panel);
pack();
and that's the result (a ugly result): https://docs.google.com/file/d/0Bxi2arJ2Dv9xbEo0Smd5QUN4UGc/edit?usp=sharing
i would eliminate that empty spaces on top and extend the second component (that is a scrollable JTable). How i should modify code?
When a GridBagLayout has more space than it needs, it distributes that extra space using the weightx and weighty properties of each cell. If no cells have a non-zero weight property, the extra space is not allocated to any cell, and instead all cells are centered, and sized to their preferred width/height.
If you do c.weighty = 1 for the constraints used by the component containing the JTable, that component will be allocated all extra vertical space. You may also want to do c.weightx = 1 so the table will fill all horizontal space.
you could set the layout of the container to BorderLayout (or something equivalent) and create an extra JPanel with the GridBagLayout and add it to the container
because of Borderlayout, the JPanel will take as much space as possible

How do you stop a JLabel changing its size when its text changes?

I'm generating some JComponents in code and using the GridBag layout to arrange them. My layout consists of 12 rows and 3 columns, with each row consisting of a JSlider, a JCheckBox and a JLabel. Here's the code I'm using to generate the UI:
final int NUM_MOTORS = 12;
// This is the panel I'm adding the components to.
pnlMotorSliders.setLayout(new GridBagLayout());
GridBagConstraints c = new GridBagConstraints();
for (int i = 0; i < NUM_MOTORS; ++i) {
c.gridy = i;
// Create the slider
JSlider slider = new JSlider(SwingConstants.HORIZONTAL, 10, 4085, 10);
c.fill = GridBagConstraints.HORIZONTAL;
c.gridx = 0;
c.weightx = 0.9;
pnlMotorSliders.add(slider, c);
// Create the checkbox
JCheckBox checkBox = new JCheckBox();
checkBox.setOpaque(true);
checkBox.setBackground(Color.blue);
c.fill = GridBagConstraints.NONE;
c.gridx = 1;
c.weightx = 0.1;
pnlMotorSliders.add(checkBox, c);
// Create the current label
JLabel label = new JLabel("0");
label.setBorder(BorderFactory.createLineBorder(Color.red));
c.fill = GridBagConstraints.HORIZONTAL;
c.gridx = 2;
c.weightx = 0.2;
pnlMotorSliders.add(label, c);
}
The problem I'm having is that when I set the text in any of the JLabels, they change their width and affect the rest of the layout, even if the width of the text that I'm setting appears to be much smaller than the width of the JLabel. The following two screenshots demonstrate what I mean (the red and blue borders were for debugging purposes):
I've set the text on the bottom JLabel to "-12". Even though the JLabel appears to be much wider than the text, it has changed its size, affecting the rest of the layout.
Why is this happening and what can I do to prevent it?
You can fix the size of the labels by setting the minimum, prefered and maximum size:
label.setMinimumSize(width, height);
label.setPreferedSize(width, height);
label.setMaximumSize(width, height);
Also make sure to set the GridBagConstraints#fill to NONE, although I am not sure if that is still neccessary (I think it is).
EDIT: btw, to get rid of those nasty dashed lines around the focused Component, you can just set it to be not focusable:
slider.setFocusable(false);
The set-the-preferred-size solution works only if you don't have the components horizontally fill their bag in the GridBagLayout.
Another solution is to remove the weight you have placed on components in that column of your GridBagLayout. You can then control the column width manually. An easy way to do so (at design time) is to place a JLabel in the column with zero height and the specific width you desire.
Why is this? You need to dig into how GridBagLayout works:
The GridBagLayout sizes its columns based on the space required, and then uses the weights to allocate the "left over" space so that the resulting column widths then add up to the total width of the panel. The space required for each column is computed by finding the widest of the components in that column.
The space required for each component is determined by asking it what width it would prefer. In your case, a JLabel with "0" is smaller than a JLabel with "-12" and so the column is changing size.
The left over space is allocated based on the horizontal weights assigned to components in each column. The weights are totaled and percentages for each column are determined based on that column's percent of the total.
The column size is determined based on the space required PLUS the left over space. So, if all your components in the column have no weight then you'll not get any left over space, and hence not get dynamic changes.
A third solution is to just not use GridBagLayout.
Explicitely set the preferred size of your labels using JLabel#setPreferredSize(Dimension).
This way, you lock the size of the component, and tell the layout manager not to resize it.
label.setPreferredSize(new Dimension(width, height));
This was only way i could find to make it work. Setting a Minimum and/or a Maximum didn't do anything for me. Also, for me on java 8 i needed to use a Dimension, or there was no change to the previous, inappropriate sizes.

Java(Swing): influence height of JList in GridBagLayout

I want to build a dialog in Java with a List and a couple of buttons underneath it.
The list ends up with the same height as the buttons (about one line) and the whole dialog is about two lines of height.
However, I'd like the dialog to be taller (maybe 10 lines) and the JList to take up most of the space .. I've played around with the parameters, but for the life of it can't get it to work. Any ideas?
Here's my current code:
//layout
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
int y = 0;
//List
gbc.gridx = 0;
gbc.gridy = y;
gbc.weighty = 3;
gbc.weightx = 1;
gbc.gridwidth= 3;
add(new JScrollPane(_myList), gbc);
_myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
// Buttons
gbc.gridx = 1;
gbc.gridy = ++y;
gbc.gridwidth = 1;
gbc.weighty = 0;
add(_Save, gbc);
gbc.gridx = 2;
add(_Cancel, gbc);
For the list set weightY=1 instead of 3. The setting of 3 will make the space for the list larger than the list itself. 99.9% of the time GridBagLayout is used the weightX/Y values should always be either 0 or 1. Also the gridWidth should probably be 2 instead of 3.
You might as well consider calling _myList.setVisibleRowCount(n) to force a preferred size (in number of visible rows) for your list.
I've done some more poling around, and apparently the behaviour is caused by the number of items in the ListModel of _myList. When I populate it with a larger number of items than the one or two it has in my current usage, then the list is properly displayed. Hopefully that helps to pin down the problem and find a solution ..
Found the problem .. and it has nothing to do with the layout code.
I was adding a null to ListModel, and that seemed to confuse the LayoutManager. Would close the question, but not yet enough mojo ...

Categories