Αντρω δι Νεξυνω

[Python] TK : un controllo "personalizzato", Pensieri e riscontri nella creazione di un oggetto

« Older   Newer »
  Share  
nuzzopippo
view post Posted on 4/5/2019, 11:00 by: nuzzopippo
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Mi son fatto attendere, temo, se qualcuno mai abbia re-guardato questo post :D

È venuto il tempo di parlare della

Versione con scrollbarr



Per altro, questo post tratta varianti alla sola classe "FRMTmbCollection", depurata dalla definizione dei pulsanti di comando (ed entry relazionata) e relativi callback e metodi di navigazione tra i dati, fermo il resto.

Preciso subito che il widget "scrollbar", qui utilizzato, NON è stato creato per una navigazione tra dati, obiettivo del controllo in costruzione, bensì per navigare in un'area di visualizzazione nel senso verticale od orizzontale a seconda dell'utilizzo che si voglia fare. Per far ciò, di norma, lo si collega alla xview/yview del widget collegato affibiando al "command" della scrollbar il dialogo ... che consiste essenzialmente in un discorso a base di pixel ;)

Date le impostazioni del controllo, non si dispone di una "Area di visualizzazione", tanto meno di un widget da associare, nel nostro caso occorrerebbe che la scrollbar "navighi" tra i dati che verranno associati dall'esterno, ciò può essere fatto sfruttando il metodo "set(first, last)" di scrollbar che determina un intervallo di valori che verranno dalla stessa restituiti.
Certo, si tratta di uno "sporco trucco", credo che lo scopo del metodo sia in realtà quello di limitare ad un settore dell'area di visualizzazione di un generico widget ma dato che tratta interi e che NON vogliamo associare un widget alla scrollbar a noi potrebbe anche andare bene, definito una scrollbar nel nostro controllo
CODICE
self.pivot_scroll = tk.Scrollbar(self,
                                        orient=tk.VERTICAL,
                                        jump=1,
                                        command=self._scrollEvent)
       self.pivot_scroll.pack(side='right', fill='y')

N.B : si noti l'impostazione "jump=1" della scrollbar, di defaul è 0 (zero); nella condizione di default la scrollbar genera un evento di aggiornamento ogni pochi millisecondi e scatenerebbe il refresh del controllo, con nuovo caricamento delle immagini da file e calcoli per il ridimensionamento delle immagini ed aggiornamento delle didascalie, elaborazioni pesanti che potrebbero anche mandare in tilt tkinter. Ponendo jump ad 1 (uno) l'evento di aggiornamento si scatena solo al rilascio del tasto del mouse, con minore carico al sistema.

possiamo assegnargli un intervallo "di risposta" al caricamento dei dati da visualizzare
CODICE
def set_data(self, dati):
       self.dati = dati
       self.pivot = 0
       for riga in self.rows:
           riga.clear()
       # configurazione scrollbarr
       self.pivot_scroll.set(0, len(self.dati) - 1 - self.righe)
       # fine per scroll
       self._refresh_rows()


Da notarsi, che il metodo "set(num, num)", di scrollbar, in realtà "servirebbe" per collegare il suo slider della scrolbar ad un'area di visualizzazione di un altro widget, ovvero ad eseguire un posizionamento "manuale" dello stesso slider, sempre riferito a detta area.
Come è ovvio dal nome stesso dato alla scrollbar, ciò che in realtà noi dobbiamo "scrollare" è il riferimento dell'indice iniziale della prima riga dati visualizzata nella nostra classe ("self.pivot" nel codice), non lo colleghiamo ad alcun widget.
Confesso che ho avuto un po' di difficoltà a capire come continuare, TUTTI gli esempi reperiti usano l'associazione con un widget (cosa che non volevo), anche questo qui che, però, esemplifica come scrollare una entry "oltre" la sua area di visualizzazione :D

L'esempio su in link mostra che una scrollbar invia una lista di dati alla funzione di callback collegata, inserito una veloce verifica di cosa viene inviato
CODICE
for elem in L: print(elem, end=' - ')
       print()

dall'output della applicazione
CODICE
NzP:~$ python3 test.py 4 5 60
scroll - 1 - units -
moveto - 0.3719512195121951 -
moveto - 0.04065040650406504 -
scroll - 1 - pages -
NzP:~$

e dalla documentazione del widget si desume che :

  • in caso di "trascinamento" la funzione di callback riceve due parametri, il primo descrive il tipo di evento ed è una stringa valorizzata a "moveto" mentre il secondo è un numero in virgola mobile tra 0 ed 1;
  • in caso di click, su di una freccia o su di uno spazio limitrofo allo slider della scrollbar, avremo tre parametri, il primo continua a descrivere il tipo di evento ed è sempre una stringa, ora valorizzata a "scroll", il secondo è un valore unitario di spostamento mentre il terzo è una stringa che descrive a "cosa" si applica il valore, e può essere "units" o "pages" a seconda se il click sia stato effettuato sulle frecce terminali o sugli spazi limitrofi allo slider;
  • i valori possono essere positivi o negativi a seconda il senso trascinamento od il triangolo terminale premuto.
Ciò consente di gestire direttamente l'evento, creando un handler che provvede a "convertire" i dati ricevuti, nel senso voluto.
Sfruttando la faccenda in questo modo

CODICE
def _scrollEvent(self, *L):
       """Gestisce lo scroll della vertical barr associata."""
       if not self.dati: return
       op, valore = L[0], L[1]
       if op == 'scroll':
           modo = L[2]
           if modo == 'pages':
               val = int(valore) * self.righe
           else:
               val = int(valore)
           if (self.pivot + val) < 0:
               self.pivot = 0
           elif (self.pivot + val) > (len(self.dati) - self.righe):
               self.pivot = len(self.dati) - self.righe
           else:
               self.pivot += val
       elif op == 'moveto':
           self.pivot += int(len(self.dati) * float(valore)) - 1
           if self.pivot < 0:
               self.pivot = 0
           elif self.pivot > len(self.dati) - self.righe:
               self.pivot = len(self.dati) - self.righe
       self._refresh_rows()
       if self.sel_index:
           der_index = self.sel_index - self.pivot
           if der_index >= 0 and der_index < self.righe:
               self.rows[der_index].set_selected_col()


Si può provvedere a ridefinire il nostro "pivot".
Da notare che, in questa versione, ho fatto in modo che non si possa procedere a visualizzare oltre l'ultimo elemento dei dati, facendo comparire righe prive di dati.

A questo punto, si dispone di una versione del nostro "controllo" utilizzante una scrollnar per la navigazione e funzionante ... a modo suo ;)
di seguito il codice base della nuova versione della nostra FRMTmbCollection (~90 righe)

CODICE
class FRMTmbCollection(tk.Frame):
   '''Contenitore/gestore di un insieme di oggetti FRMTumbnail.'''
   def __init__(self, master, righe=2, dim=60, mater=None):
       super().__init__(master)
       self.master = master
       self.mater = mater
       self.righe = righe
       self.dim_tmb = dim
       self.pivot = 0
       self.rows = []
       self.dati = None
       self.sel_index = None
       self.sf_tmb = tk.Frame(self)
       self.sf_tmb.pack(side='left', expand=True, fill='both')
       self._crea_righe()
       self.pivot_scroll = tk.Scrollbar(self,
                                        orient=tk.VERTICAL,
                                        jump=1,
                                        command=self._scrollEvent)
       self.pivot_scroll.pack(side='right', fill='y')
   
   def _crea_righe(self):
       for i in range(self.righe):
           riga = FRMTumbnail(self.sf_tmb, self, self.dim_tmb)
           riga.pack(fill='x')
           riga.set_indice(i)
           self.rows.append(riga)
   
   def set_data(self, dati):
       self.dati = dati
       self.pivot = 0
       for riga in self.rows:
           riga.clear()
       # configurazione scrollbarr
       self.pivot_scroll.set(0, len(self.dati) - self.righe)
       # fine per scroll
       self._refresh_rows()
   
   def set_selezione(self, indice):
       if not self.dati: return
       for riga in self.rows:
           if riga.get_indice() != indice:
               riga.set_default_col()
       self.sel_index = self.pivot + indice
       if self.mater:
           self.mater.set_selezione(self.sel_index)
   
   def _refresh_rows(self):
       if len(self.dati) == 0 : return
       i = 0
       for riga in self.rows:
           riga.clear()
           dati_ind = self.pivot + i
           if dati_ind <= len(self.dati) - 1:
               riga.set_fileimg(self.dati[dati_ind]['img'])
               riga.set_dida(self.dati[dati_ind]['dida'])
           i += 1
   
   def _scrollEvent(self, *L):
       """Gestisce lo scroll della vertical barr associata."""
       if not self.dati: return
       for elem in L: print(elem, end=' - ')
       print()
       op, valore = L[0], L[1]
       if op == 'scroll':
           modo = L[2]
           if modo == 'pages':
               val = int(valore) * self.righe
           else:
               val = int(valore)
           if (self.pivot + val) < 0:
               self.pivot = 0
           elif (self.pivot + val) > (len(self.dati) - self.righe):
               self.pivot = len(self.dati) - self.righe
           else:
               self.pivot += val
       elif op == 'moveto':
           self.pivot += int(len(self.dati) * float(valore)) - 1
           if self.pivot < 0:
               self.pivot = 0
           elif self.pivot > len(self.dati) - self.righe:
               self.pivot = len(self.dati) - self.righe
       self._refresh_rows()
       if self.sel_index:
           der_index = self.sel_index - self.pivot
           if der_index >= 0 and der_index < self.righe:
               self.rows[der_index].set_selected_col()


Per funzionare a modo suo funziona, seguono le schermate della nuova versione del controllo nei test 4 (pack) ed 8 (grid)

SCREEN CON GESTORE DI GEOMETRIA PACK
png



SCREEN CON GESTORE DI GEOMETRIA GRID
png




Cosa vuol dire "a modo suo"?



Da quel che ho capito e già detto, il widget scrollbar è ideato per riferirsi al "posizionameto", verticale od orizzontale, relativo ad una area di visualizzazione di un secondo widget ... faccenda allegramente ignorata nella costruzione di questa versione del controllo : non vi è un'area disponibile e le ipotesi progettuali rendono improponibile crearla dato che potrebbe implicare l'elaborazione di un numero indefinitamente alto di immagini.

l'istruzione
CODICE
self.pivot_scroll.set(0, len(self.dati) - 1 - self.righe)

definita nel processo di caricamento dati e necessaria per il funzionamento della scrollbar (in assenza da "out of range" ad ogni evento), ha quale effetto il dimensionamento dello slider della scrollbar a tuttà l'altezza disponibile nella stessa ... evento logico e. tutto sommato, scontato, non essendo disponibile alcun riferimento per un proporzionamento relativo.
L'immagine sottostante è relativa alla condizione del controllo non appena avvenuto il caricamento :

png



Un secondo, rilevante, "particolare" che non mi è riuscito di risolvere (motivo per cui ho tardato tanto, ho cercato e letto a lungo) è che non essendoci un'area di riferimento non esiste nemmeno un posizionamento relativo, faccenda che mi ha costretto ad interpretare gli scorrimenti dimensionandoli allo spostamento rispetto al "pivot" ma senza podizionamento dello slider stesso.

Uno strano effetto si ottiene, poi, scorrendo lo slider dagli estremi superiore od inferiore : la dimensione dello slider viene ridotta fino al limite del trascinamento, vedi giù

png



A questo punto lo slider assume la "nuova dimensione" ed è possibile "acchiapparlo e scorrere con esso

png



L'insieme di questi effetti : è possibile, si, scorrere l'intero intervallo dei dati disponibili ma, visualmente, la precisione lascia decisamente a desiderare ... riesce a confondere me che l'ho costruito, si immagina un utente qualunque?

Confesso che questo è uno dei motivi per cui ho intrapreso la scrittura di questi lunghi post, oltre che per rispondere all'amico che ha posto il "problema" e fatto nascere l'idea : creare un articolato di supporto ad una richiesta di "aiuto" da proporre in un forum tematico.
In effetti, la metodologia "scrollbar" sarebbe la più logica se solo si riuscisse ad astrarla dalla mera grafica ed utilizzarla coerentemente su soli intervalli di indici ... cosa effettuata nella terza versione del controllo.


L'avvento del gestore "grid"


La "scomparsa" dei pulsanti di navigazione (e l'utilizzo della scrollbar) ha evidenziato le proprietà "elastiche" del gestore di geometria pack. non tanto evidente nel caso il nostro controllo sia l'unico componente della finestra, adeguandosi alla dimensione della stessa, salta agli occhi nel suo utilizzo "in verticale" accanto ad altri widget, in tal caso all'avvio assume le minime dimensioni necessarie

png



salvo poi variare dinamicamente dimensione al caricamento dei dati, adeguandosi alla dimensione necessaria per la più lunga delle didascalie, come può vedersi nello scrrn del gestore pack prima visualizzato.
L'effetto è sconcertante e per nulla piacevole ma svanisce se la finestra che invoca il controllo utilizza il gestore di geometria grid e configura la colonna contenente il controllo con una dimensione minima (codice "self.grid_columnconfigure(0, minsize=300)" nello "__init__" di GUI_04_G del test), cosa che non riesce facile con pack.

Edited by nuzzopippo - 5/5/2019, 11:52
 
Web  Top
4 replies since 31/3/2019, 12:09   1066 views
  Share