Uno slider multi thumb per Java Swing

gennaio 3, 2007 at 5:34 PM 4 commenti

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.

JSlider Component

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:

SliderMultiThumb

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:

SliderMultiThumb in Azione

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 😉

Entry filed under: slider, swing.

Servizi JMS transazionali in JBoss AS 4.x Vector VS ArrayList

4 commenti Add your own

  • 1. Gianpaolo  |  gennaio 9, 2007 alle 3:56 PM

    Ti stimo e ti apprezzo…

    Rispondi
  • 2. flip  |  Maggio 20, 2009 alle 9:18 am

    E’ proprio quello che mi serve.. 🙂 potresti mettere a disposizione il file dell intera classe? grazie

    Rispondi
  • 3. Musikele  |  novembre 5, 2011 alle 4:55 PM

    Ciao, spero che questo blog sia ancora attivo e che tu mi possa rispondere!
    Devo implementare un progetto audio e mi serve uno slider con 3 thumb, uno che rappresenta la linea temporale del brano musicale e altri due che devono fungere da “start” e “stop” per una parte del brano soltanto.
    Il tuo multithumb slider è cosa buona e giusta, ma devo chiederti alcune domande:
    1) è accessibile? (il progetto è per ciechi)
    2) posso personalizzare i cursori dello slider? (i due start e stop devono essere diversi per non confondersi)
    3) è possibile fare in modo che stop non vada a finire mai dietro strart?
    spero che hai capito il mio problema. Mi serve per la tesi di laurea, e o me lo scrivo da me (con il rischio di perdere ore) oppure ne trovo una già fatta. tentar non nuocere… chiedere non offende…
    Grazie dell’articolo che comunque è un punto di partenza “in caso di”.

    Rispondi
  • 4. Gain Muscle  |  settembre 26, 2013 alle 4:39 PM

    I read this article completely about the difference of
    most up-to-date and earlier technologies, it’s amazing
    article.

    Rispondi

Lascia un commento

Trackback this post  |  Subscribe to the comments via RSS Feed


Calendario

gennaio: 2007
L M M G V S D
1234567
891011121314
15161718192021
22232425262728
293031  

Most Recent Posts