mutterings of a cynic

Sunday, June 24, 2007

rounding errors in swing

I quite enjoyed blogging about layout managers the other day so I thought I'd follow up with another easy mistake to make in swing - positioning your components incorrectly due to rounding errors.

I recently wanted the equivalent of a GridLayout, but I wanted to be able to set the ratios of the grid sizes myself rather than have them all equal.

The required result is pictured below using the LayoutManager configured to lay the components out vertically using the ratios specified in their names.



As you can see here, the buttons are laid out such that all the available space (within the red border) is completely used up.

Naively, you would think that the code to produce that result looks like this (stripped down for easier reading):


float[] ratios = ...;
Component[] cc = parent.getComponents();

int x = insets.left;
int y = insets.top;
int tw = [avail width minus insets]
int th = [avail height minus insets]

for (int i = 0; i < cc.length; i++) {
  cc[i].setLocation(x, y);
  int h = (int) (th * ratios[i]);
  cc[i].setSize(tw, h);
  y += h;
}


However if you do that, the result looks like this:



Note that the bottom button is a good 3 pixels away from the border. Problem here is that we have a cumulative rounding error. Don't think that the solution is to round the calculation of each unit height either, if you do that, you'll end up using more vertical space than available.



So the solution is to not store the individual ratios at all, but rather store the cumulative ratios in an array that is 1 longer than the component array. For instance, rather than have an array like this [0.25, 0.5, 0.25] to lay out 3 components, use an array like this [0, 0.25, 0.75, 1] (make sure you manually clamp the last value to 1 to avoid another possible rounding error).

Here's some code:


float[] cumulativeRatios = ...;
int[] ys = new int[cumulativeRatios.length];

int tw = [avail width minus insets]
int th = [avail height minus insets]

for (int i = 0; i < cumulativeRatios.length; i++) {
  ys[i] = Math.round(th * cumulativeRatios[i]);
}

Component[] cc = parent.getComponents();
for (int i = 0; i < cc.length; i++) {
  cc[i].setLocation(insets.left, insets.top + ys[i]);
  cc[i].setSize(tw, ys[i + 1] - ys[i]);
}


Now you've spread the rounding errors over the available space in the parent and your components are laid out edge to edge - much nicer!

Incidentally, GridLayout doesn't use this mechanism of avoiding rounding errors. Here's a picture of the same test code using GridLayout with 5 rows and 1 column. This isn't necessarily wrong, it just might not be ideal in all circumstances.



I won't post the full code to the layout manager (unless someone asks for it), but rather leave it as an exercise for the reader.

As before, questions, comments and critique welcome.

Labels: , ,


1 Comments:

  • I just wanted to qualify an aspect of the chosen coding style here.

    Clearly you only need your ratios array to be 1 shorter than the component array since you can omit first and last values of the array since they will always be 0 and 1. I don't do this because it's laughable optimisation that would make the subsequent code messier.

    Don't try to be clever and be tempted to "optimise" something just because you can. You'll be more likely to introduce a bug, and even if you don't it'll be more likely that someone else will add a bug later because the code isn't clear enough.

    By Blogger nj, at 11:45 am  

Post a Comment

<< Home