Uno slider multi thumb per Java Swing
Gennaio 3, 2007
Anno nuovo, vita nuova… Ho abbandonato JBoss e sono passato su qualcosa che ho sempre sfiorato ma che, vuoi per un motivo vuoi per un altro, ho poi sempre schivato con destrezza. Questa volta però un pò me la sono cercata (in fondo gli ostacoli vanno affrontati, no?) e quindi eccomi qui a combattere con Java Swing!
Non voglio certo annoiarvi sugli aspetti positivi (e ahimè negativi) che un programmatore incontra nel costruire una piacevole (ma soprattutto intuitiva) GUI in Java. Mi soffermerò, invece, su un particolare componente e su un suo possibile utilizzo.
Lo slider è un componente che permette facilmente di selezionare un valore compreso in un certo range. E’ implementato dalla classe JSlider e, come potete notare nella figura sotto, rispecchia le nostre richieste: è piacevole e intuitivo.

Il problema è che, per l’applicazione che sto sviluppando, avrei bisogno di uno slider con più cursori (thumb), per la precisione uno slider con tre cursori. Come è mio solito fare, ho interrogato l’oracolo Google, ma non ho trovato qualcosa di utilizzabile, e quindi olio di gomito…
I requisiti: ho bisogno di 3 cursori, su ogni cursore deve essere mostrato il suo valore e, infine, mi piacerebbe che il componente s’integrasse con il Look&Feel scelto… chiedo troppo?
Ah! Dimenticavo: non amo farmi del male… ma mentre sviluppavo ho aggiunto la parametrizzazione del numero dei cursori come ulteriore requisito… non si sa mai, questo componente potrebbe tornarmi utile un domani.
La classe completa che implementa lo SliderMultiThumb (il nome scelto mi sembra più che appropriato no?) la trovate qui. Vediamo, a grandi linee, come ci sono arrivato. L’idea è quella di gestire l’intera barra come valore unitario (di conseguenza valore minimo 0 e valore massimo 1) e di avere tutti i valori dei cursori in centesimi.
private static final double DEFAULT_VALUE_MAX = 1; private static final double DEFAULT_VALUE_MIN = 0; // number of thumbs private int thumbs; // thumb values (centesimal) private double thumbValuesCentesimal[]; // thumb max & min values private double maxValue = DEFAULT_VALUE_MAX; private double minValue = DEFAULT_VALUE_MIN;
Il costruttore dello SliderMultiThumb richiede come argomento l’intero, che definisce il numero di cursori richiesti.
/**
* Constructs and initializes the slider.
*
* @param thumbs thumbs' number
*/
public SliderMultiThumb(int thumbs){
[...]
}
Una volta costruito il componente, se non passo i valori dei cursori, utilizzando il metodo setValueThumbs(double[] valueThumbs), gli stessi dovranno posizionarsi in modo equidistante tra di loro. Pertanto nel costruttore:
// setting value for each thumbs... i want to put each thumb at the same distance with other ones
double[] valueThumbs = new double[thumbs];
final double valueAverage = (maxValue - minValue) / (thumbs + 1); // thumbs + 1 = the spaces between thumbs
for (int i=0; i < thumbs; i++){
valueThumbs[i] = minValue + valueAverage * (i + 1);
}
setValidValueThumbs(valueThumbs, false);
Ad ogni cursore devo associare un valore non solo relativo all’effettiva entità tra il minimo e il massimo, ma anche alla posizione (in senso grafico) sull’asse x (ho previsto per SliderMultiThumb solo l’implementazione orizzontale). Il metodo setValidValueThumbs, quindi, calcola lo spazio che sull’asse delle x rimane dopo aver eliminato quello utilizzato per disegnare i cursori (l’ampiezza del cursore è definita da una costante) e lo distribuisce ai vari cursori, secondo la percentuale del proprio valore.
// total is the available space without the thumbs' paint
double total = (double)( componentWidth - (THUMB_WIDTH * thumbs * 2) );
for (int i=0; i < thumbValuesCentesimal.length; i++){
// get centesimal value
if (!isCentesimal){
thumbValuesCentesimal[i] = (valueThumbs[i] - minValue) / (maxValue - minValue);
}
// ...and then new position is the percentual value of total and previous thumbs' with additionally one (for this)
pixPosition[i] = (int)((thumbValuesCentesimal[i] * total) + THUMB_WIDTH * (2*i + 1) );
}
Il metodo paint disegna l’intero componente, dividendolo in 2 regioni in cui mostrare rispettivamente i valori dei cursori e la barra con i cursori.
int width = getSize().width; int height = getSize().height; // text region g.setColor( getBackgroundColor() ); g.fillRect( 0, 0, width, TEXT_HEIGHT ); // region where thumbs run along g.setColor( getBackgroundColor() ); g.fillRect( 0, TEXT_HEIGHT, width, height - TEXT_HEIGHT); // nice line in the middle of the bar with light shape g.setColor( Color.DARK_GRAY ); g.fillRect( 0, TEXT_HEIGHT + (height - TEXT_HEIGHT) / 2, width, 1); g.setColor( Color.lightGray ); g.fillRect( 0, TEXT_HEIGHT + (height - TEXT_HEIGHT) / 2 - 1, width, 1);
Il disegno dei cursori viene fatto ciclando sull’array pixPosition, che come visto precedentemente, mi dà l’esatta posizione del cursore. Avendo il disegno del cursore con sopra il rispettivo valore (renderizzato in base al vero minimo e massimo settato), può capitare che, avvicinando due cursori al massimo (ovvero facendo in modo che abbiano lo stesso valore), le scritte in alto collidano (ma non perfettamente). Pertanto, in caso di stessi valori tra più cursori, la scritta viene disegnata solo una volta, centrandola rispetto ai cursori.
// now print thumb value... if other thumbs have same value go on... (avoid to print same value up each thumb)
if (i == pixPosition.length - 1 || thumbValuesCentesimal[i] != thumbValuesCentesimal[i+1]){
// check the first thumb with the same value
int firstThumbWithSameValue = i;
for (int j=i; j >= 0; j--){
if (thumbValuesCentesimal[j] != thumbValuesCentesimal[i]){
break;
}
firstThumbWithSameValue = j;
}
final String value = render( getValue(i) ).trim();
final int spaceText = getFontMetrics(getFont()).stringWidth(value); // space used by text (in pixels)
int xTextPosition = pixPosition[firstThumbWithSameValue] + (pixPosition[i] - pixPosition[firstThumbWithSameValue]) / 2 - spaceText / 2;
// this check is to avoid that thumb taken to the max value (or min), have their value written outside the component
if (xTextPosition + spaceText > width){
xTextPosition = width - spaceText;
} else if (xTextPosition < 0){
xTextPosition = 0;
}
g.setFont(getFont());
g.drawString(value, xTextPosition, TEXT_HEIGHT - TEXT_BUFFER);
Adesso occorre far sì che i cursori si muovano… è necessario aggiungere un MouseAdapter per la pressione del mouse e un MouseMotionAdapter per lo spostamento con pressione dello stesso. Entrambi gli eventi passano la posizione del puntatore sull’asse delle x ad handleMouse, che trova il cursore più vicino:
// check thumb closer
int thumbCloser = 0;
int xDistance = Integer.MAX_VALUE;
for (int i=0; i < pixPosition.length; i++){
int dist = Math.abs(pixPosition[i] - x);
if (dist < xDistance){
thumbCloser = i;
xDistance = dist;
}
}
Successivamente devo trovare il limite alla mia sinistra (che potrebbe essere il valore minimo nel caso in cui sto muovendo il primo cursore) e il limite alla mia destra (che potrebbe essere il valore massimo nel caso in cui sto muovendo l’ultimo cursore). Ciò che devo fare è aggiornare la posizione del cursore selezionato (thumbCloser) alla posizione passatami dai listeners, facendo attenzione a non superare i limiti.
// now check where is my limit on the left
int left = 0; // if i'm moving the first thumb then my limit on the left is 0
int xMinLeft = THUMB_WIDTH;
if (thumbCloser != 0){ // otherwise will be the thumb on my left
left = pixPosition[thumbCloser - 1];
xMinLeft = THUMB_WIDTH * 2;
}
// now check where is my limit on the right
int right = componentWidth; // if i'm moving the lst thumb then my limit on the left is component's width
int xMinRight = THUMB_WIDTH;
if (thumbCloser != thumbs - 1){ // otherwise will be the thumb on my right
right = pixPosition[thumbCloser + 1];
xMinRight = THUMB_WIDTH * 2;
}
[...]
// if exceeded limit...
if (x < right - xMinRight){
x = right - xMinRight;
}
Ecco come si presenta lo SliderMultiThumb con 4 cursori:

Ho sorvolato su alcune banalità, tipo la possibilità di visualizzare o meno i valori decimali o il colore di background… Per esempio, per avere uno slider di colore arancio, con 4 cursori che spaziano su un range che va da -100 a 400, utilizzando solo valori interi e con una situazione iniziale che prevede il primo cursore a -80, il secondo e terzo appaiati a 0 e l’ultimo sul valore 230, scriveremo:
JPanel p = new JPanel();
SliderMultiThumb smt = new SliderMultiThumb(4);
smt.setRange(-100, 400);
smt.setShowDecimalValues(false);
smt.setBackgroundColor( Color.orange );
smt.setValueThumbs(new double[]{-80, 0, 0, 230});
p.add(smt);
…e questo è l’esito grafico:

Manca solo la rendizzazione con il Look&Feel selezionato… ma per ora va bene così, se un giorno mi ritroverò a girare i pollici (beata gioventù!) probabilmente scriverò “Uno slider multi thumb per Java Swing 2″. Alla prossima
1 Comment Add your own
Leave a Comment
Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>
Trackback this post | Subscribe to the comments via RSS Feed
1.
Gianpaolo | Gennaio 9, 2007 at 3:56 pm
Ti stimo e ti apprezzo…