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

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

« Older   Newer »
  Share  
view post Posted on 31/3/2019, 12:09
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,223

Status:


I miei saluti.

Premessa



Questo post è il rimaneggiamento di un recedente appunto postato ad uso di un "collega", ossia di una persona che, come me, affronta in modalità amatoriale problematiche inerenti la programmazione, e che aveva posto :
CITAZIONE
Per i dati da esporre, pensavo di partire con una lista composta da righe con [file icona, testo]
il file serve per leggere l'icona da visualizzare, il testo (non un nome file ma una dicitura) è quello da indicare accanto
Ovviamente comunque il testo potrebbe anche essere un nome file.

Il contesto del discorzo intrattenuto comportava, in merito alle "icone", la miniaturazzazione di files di immagini, nell'ordine delle centinaia, con testo un po' a piacere collegato, la qual cosa mi fece pensare ad una specie di "diario fotografico" utilizzante un oggetto "specializzato" alla esposizione delle immagini miniaturizzate con didascalia correlata.

Sviluppata rapidamente una prima bozza di detto oggetto, ed una serie di test da utilizzare in fase di sviluppo per verificare la "resa grafica" in fase di codifica ho, brutalmente, presentato al collega proponente quanto fatto, detta prima bozza sarà trattata nel seguito di questo post, ma con riserva di ulteriore "elaborazione" per eventuali accorgimenti un merito alla presentazione grafica dell'oggetto realizzato.

Gli "accorgimenti" riguardano tanto il sistema di navigazione tra le immagini quanto l'aspetto del controllo, che è in "divenire", dato che devo studiarmi tkinter durante la realizzazione, questo 3d mira alla annotazione di quanto capisco e delle relative problematiche, questo sarà, quindi, un post con frequenti mutazioni.

In merito ai "sistemi di navigazione" sperimentati, sono tre :

  • una metodologia basata su bottoni di comando per spostamenti singoli e di pagina (numero di righe visualizzate), oltre una casella di input per grossi spostamenti

  • una metodologia basata su di un widget "Scrollbar" slegato dalla wiev di un controllo, funzionale ma che trovo insoddisfacente

  • una metodologia basata su di un widget "Scale" (al momento la preferita)


In seguito si tratterà le tre metodologie di navigazione affrontate, con particolare enfasi sulla seconda, per la "insoddisfazione" provata e non risolta, per la quale intendo chiedere l'aiuto ad user molto più avanzati di me (uno dei motivi che mi hanno spinto a scrivere questo post).

Il tema "Aspetto" sarà affrontato solo per la variante che sceglierò di utilizzare per definire la applicazione "FotoDiario" immaginata, che sarà trattata nei post finali di questo 3d.

Edited by nuzzopippo - 12/4/2019, 16:03
 
Web  Top
view post Posted on 12/4/2019, 15:10
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,223

Status:


La prima bozza


La prima bozza di seguito esposta è un visualizzatore a "righe" di immagini con didascalia correlata e navigazione tramite pulsanti di comando più casella di input dell'indice di immagine iniziale da visualizzare.

Tale sistema di navigazione è stato inizialmente scelto per la sua facilità di realizzazione.

Considerazioni preliminari


La richiesta possibilità di "iconizzare" centinaia di immagini mi ha portato a considerare che i tempi di elaborazione necessari potessero essere lunghi, così come elevata la quantità di memoria occorrente.
Si potrebbe, in vero, miniaturizzare le immagini preliminalmente ma si avrebbe comunque un oneroso carico in memoria. Inoltre, quando si trattasse di immagini fotografiche ad alta risoluzione, miniature eccessivamente ridotte potrebbero risultare "sgradevoli" alla vista.

Quanto sopra, mi ha fatto ritenere di realizzare un oggetto ("controllo" da qui in poi) capace di miniaturizzare da se le immagini da visualizzare ad una dimensione indicata, con un numero fisso* di righe/immagine da visualizzare, sempre indicato.

* - N.B : ho provato a gestire un adeguamento dinamico delle righe al variare della dimensione del widget contentore, tkinter va in palla, molto probabilmente per l'elevata frequenda del refresh e delle elaborazioni che vengono scatenate. Al momento non ho trovato modo di effettuare l'aggiornamento di una finestra solo a ridimensionamento stabilizzato.

detto controllo è un elemento composito, costituito da due classi, di cui una definisce una singola "riga" del controllo, l'altra l'insieme e la navigazione, le classi:

  • FRMTumbnail : è una singola riga dati costituita da una immagine, di dimensione data, e relativa didascalia

  • FRMTmbCollection : definisce una data quantità di righe dati e provvede alla navigazione nell'insieme dei dati di riferimento


questo controllo richiede la presenza nel sistema di due librerie :

  • PIL (od anche pillow), dalla quale importa la classe Image, per la maggiore tipologia di files grafici supportata;

  • ImageTk, estensione a PIL per la conversione delle immagini in formati rappresentabili con tkinter.

Qualora voleste utilizzare il codice esposto (scritto per python 3) verificate la presenza di dette librerie nel vosto environment.

FRMTumbnail


Classe derivata da Frame, definisce un pannello per una singola miniatura, si aspetta di ricevere un riferimento alla finestra/Frame madre e la dimensione di rappresentazione dell'immagine da visualizzare.

Costruttore:
CODICE
def __init__(self, master, mater, imgdim=60)

ove:

  • master : riferimento all'oggetto padre correlato;

  • mater : riferimento all'oggetto principale di appartenenza, non necessariamente il master, mater dovrà esporre il metodo pubblico "set_selezione(indice);

  • imgdim : dimensione (quadrata) della casella immagine espressa in pixel, se il parametro viene omesso assumerà un valore di 60 pixel.


Si è previsto che i dati di immagine non vengano alla istanza della classe ma definiti tramite gli appositi metodi "set_fileimg" e "set_dida", ciò in considerazione che non necessariamente esistano inizialmente.
Oltre ai metodi per la impostazione dei dati, la classe espone diversi metodi per la impostazione di varie caratteristiche e variabili, di norma denominati come "set_qualcosa", essi sono:
set_fileimg(self, filename), set_dida(self, testo), set_indice(self, indice), clear(self), set_default_col(self), set_selected_col(self)
ed un metodo per restituzione di dati :
get_indice(self)
Lo scopo di tali metodi è riassunto nella stringa di documentazione relativa.

Per altro, la classe dispone di un metodo "privato", _display(self), che provvede a ridimensionare e visualizzare miniaturizzato il file immgine ricevuto e di un metodo di callback che intercetta la pressione del tasto sinistro del mouse sulla didascalia ed invoca* il metodo "set_selezione(indice)" già nominato in precedenza.

* - N.B : in fase iniziale, ho provato a gestire gli eventi di tastiera per la navigazione tra i dati, provando la implementazione di metodi bind_class e bind_all nella classe "FRMTmbCollection" e bintags e bind nella classe che si sta trattando, bind_all funziona ma a livello globale (qualunque fosse il focus nella finestra), qualsiasi altro metodo di intercettazzione della tastiera non ha effetto a nessun livello, suppongo per le caratteristiche dei widget usati riguardo al focus.

La classe in esame utilizza il gestore di layout "pack", per la versione del controllo utilizzante il widget "Scale" (al momento preferita) ho definito due varianti, una con posizionamento "pack()" ed una con posizionamento "grid()", ne parlerò nel post relativo.

Il codice della classe in esame al momento (~90 righe):

CODICE
class FRMTumbnail(tk.Frame):
   '''Pannello per singola miniatura (tumbnail) con descrizione.'''
   def __init__(self, master, mater, imgdim=60):
       self.mater = mater
       self.f_img = ''
       super().__init__(master)
       self.dim_tmb = imgdim
       self._curr_img = None
       self.indice = None
       self.default_bc = self.cget('bg')
       self.configure(relief='sunken', border=2, padx=2, pady=2)
       self.cnv_tmb = tk.Canvas(self,
                                width=self.dim_tmb,
                                height=self.dim_tmb,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                             )
       self.cnv_tmb.pack(side='left')
       self.cnv_tmb.bind('<Button-1>', self._scelto)
       f = tk.Frame(self, height=self.dim_tmb+4)
       f.pack(side='left', expand=True, fill='x')
       f.pack_propagate(0)
       self.lbl_dida = tk.Label(f,
                               text='...',
                               relief='raised',
                               justify='left',
                               border=2
                               )
       self.lbl_dida.pack(expand=True, fill='both')
       self.lbl_dida.bind('<Button-1>', self._scelto)
       
   def set_fileimg(self, filename):
       ''' Impostazione diretta del file immagine da miniaturizzare. '''
       self.f_img = filename
       self._display()
   
   def _display(self):
       ''' Ridimensiona e visualizza l'immagine da mostrare. '''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(self.f_img)
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       if img_x >= img_y:
           f_scala = self.cnv_tmb.winfo_width() / img_x
       else:
           f_scala = self.cnv_tmb.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self._curr_img = ImageTk.PhotoImage(img)
       x = self.cnv_tmb.winfo_width() // 2
       y = self.cnv_tmb.winfo_height() // 2
       self.cnv_tmb.create_image(x, y, image=self._curr_img, anchor='center')
   
   def set_dida(self, testo):
       '''Impostazione della didascalia da mostrare a fianco dell'immagine.'''
       self.lbl_dida.configure(text=testo)
   
   def set_indice(self, indice):
       '''Impostazione dell'indice assegnato alla riga.'''
       self.indice = indice
   
   def get_indice(self):
       '''Restituisce l'indice assegnato alla riga.'''
       return self.indice
       
   def clear(self):
       '''Ripulisce la riga da eventuali dati presenti.'''
       self.cnv_tmb.delete('all')
       self.lbl_dida.configure(text='...')
       self.set_default_col()
   
   def _scelto(self, evt):
       '''Imposta la riga come immagine scelta.'''
       if not self.mater: return
       self.set_selected_col()
       self.mater.set_selezione(self.indice)
       return 'breack'
   
   def set_default_col(self):
       '''Imposta il colore del controllo ai valori di default.'''
       self.configure(bg=self.default_bc)
       self.lbl_dida.configure(bg=self.default_bc)
   
   def set_selected_col(self):
       '''Imposta il colore del controllo in modalità "selezionato".'''
       self.configure(bg='#ffffc0')
       self.lbl_dida.configure(bg='#ffffc0')


FRMTmbCollection


Classe derivata da Frame, definisce un insieme di oggetti FRMTumbnail, inizialmente vuoti, utilizzati per rappresentare un insieme di immagini, da file, ridimensionate, con una didascalia associta; fornisce la capacità di "navigazione" tra i dati da rappresentare.
La classe si aspetta di ricevere il numero di "righe" da rappresentare, di default 2, e la dimensione dell'area per la rappresentazione miniaturizzata, di default 60 pixel, la classe si aspetta, altresì, di ricevere un riferimento alla finestra chiamante, parametro "mater", di default non valorizzato, non necessariamente corrispondente al widget master associato.
Questa classe è tanto la "mater" quanto il master degli oggetti FRMTumbnail associati.

Costruttore:
CODICE
def __init__(self, master, righe=2, dim=60, mater=None)

ove:

  • master : riferimento all'oggetto padre correlato;

  • righe : il numero di righe, ossia oggetti FRMTumbnail da definire nel controllo, alla inizializzazione, la classe richiamerà il metodo interno "_crea_righe(self)" che provvederà ad istanziare e memorizzare gli elementi necessari;

  • dim : dimensione (quadrata) della casella immagine espressa in pixel, se il parametro viene omesso assumerà un valore di 60 pixel;

  • mater : riferimento all'oggetto principale di appartenenza, non necessariamente il master, mater dovrà esporre alcuni metodi pubblici per la comunicazione


Formato dei dati da rappresentare


Si è optato, per la semplicità della gestione, di stabilire che i dati siano una semplice lista di dizionari.
Un singolo "dizionario" rappresenta una riga di dati da visualizzare e dovrà necessariamente possedere le chiavi:

  • img : con associato il path, relativo o assoluto, del file di immagine da visualizzare, ovviamente, a livello operativo sarà bene utilizzare solo path assoluti;

  • dida : una (breve) stringa di testo che si vuole associata all'immagine.
Il processo chiamante dovrà provvedere alla definizione della lista di dizionari necessaria, eventuali chiavi "altre" presenti nei dizionari saranno ignorate dal controllo.

I dati potranno essere caricati nel controllo tramite il metodo pubblico "set_data(self, dati)" di questa classe che provvederà a ripulire le righe di eventuai dati già presenti ed a richiamare il metodo interno "_refresh_rows(self)" che aggiornerà la visualizzazione in tutte le righe definite.

Sistema di navigazione


Le tre "varianti"* del controllo indicate nel primo post hanno un "sistema" di navigazione diversificato, quanto segue è relativo alla sola prima variante implementa, di cui si tratta al momento.

* - N.B : non si dimentichi che questo post è nato a supporto delle esigenze di un collega, l'esame di diverse "possibilità" è motivato dalle indicazioni espresse dallo stesso.

Nella variante in esame, il sistema di navigazione riferisce ad un "pivot", cioè un segnaposto che rappresenta l'indice nella lista dati corrispondente alla prima riga di rappresentazione, tale indice è memorizzato nella variabile di istanza "self.pivot" che è alla base di tutte assegnazioni dati alle righe.

Il valore di tale pivot viene controllatto tramite i quattro pulsanti in figura sotto
png
che provvedono ad incrementare o decrementare l'indice di riferimento per un valore unitario o pari al numero di righe nel controllo, invocando i metodi di clallback :
_incr_uno(self, evt=None), _incr_rows(self, evt=None), _decr_uno(self, evt=None) e _decr_rows(self, evt=None)
che provvedono ai controlli ed aggiornamenti del caso.
Ovviamente, tale metodologia è insufficiente in caso vi siano numerosi dati, può, quindi, essere indicato un indice voluto direttamente nella Entry intermedia tra i pulsanti e premendo "Invio", alla pressione di tale tasto sarà invocato il metodo "_reset_pivot(self, evt)" che provvederà a valutare la validità dell'immissione e procederà :
    <li>azzerando il contenuto del vidget se non valido;
  • azzerando il contenuto del vidget e riposizionando il pivot al valore indicato se ricompreso nell'intervallo esistente;
  • non facendo nulla se il valore è valido ma al di fuori dei dati esistenti.

Ora il codice specifico di FRMTmbCollection (~150 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(expand=True, fill='both')
       self._crea_righe()
       self.sf_command = tk.Frame(self)
       self.sf_command.pack(expand=True, fill='x')
       # memorizzazione immagini icone per pulsanti
       self.ico_scendigiu = tk.PhotoImage(data=icoscendi_giu)
       self.ico_scendiuno = tk.PhotoImage(data=icoscendi_uno)
       self.ico_saliuno = tk.PhotoImage(data=icosali_uno)
       self.ico_salisu = tk.PhotoImage(data=icosali_su)
       # definizione comandi
       self.bt_scendigiu = tk.Button(self.sf_command,
                                     image=self.ico_scendigiu,
                                     padx=4,
                                     pady=4,
                                     command=self._incr_rows
                                     )
       self.bt_scendigiu.pack(side='left')
       self.bt_scendiuno = tk.Button(self.sf_command,
                                     image=self.ico_scendiuno,
                                     padx=4,
                                     pady=4,
                                     command=self._incr_uno
                                     )
       self.bt_scendiuno.pack(side='left')
       sep1 = tk.Frame(self.sf_command, width=20)
       sep1.pack(side='left', expand=True, fill='x')
       self.indSet = tk.Entry(self.sf_command,
                              width=5,
                              relief='sunken',
                              border=2,
                              )
       self.indSet.pack(side='left')
       self.indSet.bind('<Return>', self._reset_pivot)
       self.indSet.bind('<KP_Enter>', self._reset_pivot)
       sep2 = tk.Frame(self.sf_command, width=20)
       sep2.pack(side='left',expand=True, fill='x')
       self.bt_saliuno = tk.Button(self.sf_command,
                                   image=self.ico_saliuno,
                                   padx=4,
                                   pady=4,
                                   command=self._decr_uno
                                   )
       self.bt_saliuno.pack(side='left')
       self.bt_salisu = tk.Button(self.sf_command,
                                  image=self.ico_salisu,
                                  padx=4,
                                  pady=4,
                                  command=self._decr_rows
                                  )
       self.bt_salisu.pack(side='left')
       
   
   def _crea_righe(self):
       '''Crea le righe per la visualizzazione dati.'''
       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 _reset_pivot(self, evt):
       '''Reimposta l'indice del primo elemento visualizzato.'''
       try :
           val = int(self.indSet.get()) - 1
           if val > 0 and val < len(self.dati):
               self.pivot = val
               self.indSet.delete(0, tk.END)
               self._refresh_rows()
       except ValueError:
           self.indSet.delete(0, tk.END)
   
   def set_data(self, dati):
       '''Imposta la lista dei dati da visualizzare.'''
       self.dati = dati
       self.pivot = 0
       for riga in self.rows:
           riga.clear()
       self._refresh_rows()
   
   def set_selezione(self, indice):
       '''Imposta l'indice della lista dati correntemente selezionato.'''
       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.pivot + indice)
   
   def _refresh_rows(self):
       '''Aggiorna i dati visualizzati nelle righe.'''
       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
       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()
   
   def _incr_uno(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
incrementando di uno.'''
       if self.pivot + 1 < len(self.dati):
           self.pivot += 1
           self._refresh_rows()
   
   def _incr_rows(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
incrementando di una "pagina".'''
       if self.pivot + self.righe < len(self.dati):
           self.pivot += self.righe
       else :
           self.pivot = len(self.dati) - self.righe
       self._refresh_rows()
   
   def _decr_uno(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
decrementando di uno.'''
       if self.pivot - 1 >= 0:
           self.pivot -= 1
       else:
           self.pivot = 0
       self._refresh_rows()
   
   def _decr_rows(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
decrementando di una "pagina".'''
       if self.pivot - self.righe >= 0:
           self.pivot -= self.righe
       else:
           self.pivot = 0
       self._refresh_rows()


Da tener presente : questa (e solo questa) variante del controllo adotta dei bottoni di comando per la naviazione tra i dati, tali bottoni, nella loro costruzione, espongono delle icone memorizzate nel costruttore, istruzioni :
CODICE
# memorizzazione immagini icone per pulsanti
       self.ico_scendigiu = tk.PhotoImage(data=icoscendi_giu)
       self.ico_scendiuno = tk.PhotoImage(data=icoscendi_uno)
       self.ico_saliuno = tk.PhotoImage(data=icosali_uno)
       self.ico_salisu = tk.PhotoImage(data=icosali_su)

le immagini sono contenute, codificate in base64, in stringe di testo assegnate a variabi globali del modulo.
Volendo sostituirne una si può convertire la nuova immagine in base64 e sostituirne il valore oppure utilizzare il caricamento diretto del file con una istruzione tipo "tk.PhotoImage(file='path_file')".

Troverete tali immagini nel codice complessivo del modulo in calce a questo post.

Comunicazione interna tra le classi


A parte l'ovvia impostazione dei dati, effettuata da FRMTmbCollection (o da altro, volendo) utilizzando i metodi pubblici "set_qualcosa" implementati in FRMTumbnail, quest'ultima comunica alla classe "madre", invocazione "self.mater.set_selezione(self.indice)" della funzione di callback "self._scelto()", solo e soltanto, il caso sia stata cliccata con il tasto sinistro del mouse, assumento la condizione di "selezionata", comunicando alla stessa il proprio indice ricevuto in fase di definizione e ponendo il colore di background dei widget ad un giallo chiaro. FRMTmbCollection provvederà a calcolare il corrispondente indice nella lista dati in visualizzazione che memorizzerà (self.pivot + indice_ricevuto).
Nella esecuzione degli eventi di "navigazione" tra i dati prima descritti FRMTmbCollection provvederà ad richiedere agli oggetti FRMTumbnail memorizzati, tramite il metodo "set_default_col()", di porsi alla colorazione ordinaria (condizione "non selezionato") ovvero, se l'indice generale dati corrisponde, si assumere i colori di selezione, invocando "set_selected_col()".

N.B. : L'indice generale calcolato rimane "fisso" sino alla successiva selezione di una immagine, navigando tra i dati non apparirà una riga selezionata se non vengono esposti i corrispondenti dati.

Comunicazioni con l'oggetto "madre" del controllo


FRMTmbCollection si fa carico delle comunicazioni con l'oggetto indicato quale "madre" del controllo composito rappresentato.
Banalmente, allo stato ancora grezzo del controllo (si sta ora esponendo la sola ossatura funzionale), esso consiste nella sola notifica dell'indice generale dati corrente in corrispondenza del suo variare, per eventuali azioni, effettuato tramite l'invocazione del metodo "mater.set_selezione(self.pivot + indice)" che "mater" DEVE possedere.
Chiaramente non si citano le comunicazioni inerenti al caricamento dei dati, già descritte per FRMTmbCollection.

Ulteriori forme di "comunicazione" tra FRMTmbCollection ed il suo eventuale oggetto saranno implementate ed esposte trattando la terza delle varianti indicate nel primo post (che ho deciso di adottare per mio uso).

Chiusura del discorso


Come indicato nel primo post, questi sono solo appunti di studio di tkinter, che non conosco, fatti a memoria mia e del collega che mi ha solleticato l'idea.
Nel corso dello sviluppo sin ora fatto ho avuto la necessità di verificare la resa del controllo stesso in tutte le sue varianti ed ho, quindi, creato un file di test che sarà trattato nel prossimo post.
Tale file di test è una applicazione valida per tutte le varianti implementate ed è da avviare da shell indicado il progressivo di test da effettuare, il nome del file od il numero di righe dati da visualizzare (a seconda del test) e la dimensione delle immagini.
Seguono gli esempi relativi al prodotto del controllo esaminato con la indicazione del relativo comando di lancio, sul mio sistema Linux :

comando : python3 test.py 1 risorse/01.jpg 120
effetto : visualizza in una finestra una singola riga di classe FRMTumbnail con dimenione immagine 120 pixel, ed un bottone per il caricamento dell'immagine.

png



comando : python3 test.py 2 5 60
effetto : visualizza solo un "controllo" FRMTmbCollection contenente 5 oggetti FRMTumbnail, con area immagine di 90 pixel, ed un bottone per il caricamento delle immagini.

png


La terza "riga" è selezionata.

comando : python3 test.py 3 5 90
effetto : visualizza una finestra con un'area testo sul lato sinistro, un'area per l'ingrandimento della immagine corrente in alto, un "controllo" FRMTmbCollection contenente 5 oggetti FRMTumbnail, con area immagine di 60 pixel, in collocazione "orizzontale" (si ridimensiona orizzontalmente) ed un bottone per il caricamento delle immagini in basso.

png


La terza "riga" è selezionata.

comando : python3 test.py 4 5 90
effetto : visualizza una finestra con un "controllo" FRMTmbCollection contenente 5 oggetti FRMTumbnail, con area immagine di 60 pixel, a sinistra in collocazione "verticale" (NON si ridimensiona), un'area per l'ingrandimento della immagine corrente in alto, un'area testo immediataente sotto, ed un bottone per il caricamento delle immagini in basso.

png


la sesta riga è selezionata.

Ed ora, in fine, il codice completo della prima versione del controllo (file my_object.py, 336 righe) _

CODICE
#-*- coding: utf-8 -*-

import tkinter as tk
from PIL import Image
from PIL import ImageTk

# icone png 22x22 codificate in base64
icoscendi_giu = '''
iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXM
AAAsTAAALEwEAmpwYAAAAB3RJTUUH4wMeDAoYS/sQSAAAA3ZJREFUOMulVG1oXEUUPfN2MalKKeiPJ1
EqVOEJ0kpJaalIWDVNkFCTGLs2GkGh+BlbkVhNqNuUukuCW5S80u4qGIylxso2L9m0kVpfgwpSs6bUq
otW7Q/XfZoKGmKSfW/mXn90EzcfJiE9MHDmDnPm3MudK5gZC2HgzMCtPkKblLJKCE5OKrW7urL6EhYD
My+4Bk71dyMDnlpWMtG92B1mhh8ATpw4pntKNBNzLTH3CCUP1NbW/wwA0vOqCo14rju9j8c71njE+5i
oTJHqczVqbXq2yQEADQA8KUKl6zc21pZuK6nbEHxOkvji6NFOAwCIKTl8OUUA8NmlM4pBSQAw46YhCZ
8/v3VnfWP1iyU1W+ue1iapeepRwcz46PgHvz68cXtJobP3Bt91VM4NFK9aOVkErc2VsoohkpMyt3tsd
KwYiuzGB3fqhXciR/ZnXmlquXnasSI1OLv2j5c9obPQ7NE/RotrqrcFg3X11z1Stz04dvnPYvbUHNHh
kRRIqp6pvQYAkPK1rsFO5xf3pxniT5bv0NknbTNuGgBgmlGDoNkv1OzSAWA1VgMAvv37AvpP9jueywd
mlAIAOjvjBmuaXXZPQF9TdNuMBzqstxypvGcEi0O7HnpJB4AG7TF00fu48Nd5HLcSjnJzgVAonJ633W
KxmHH4nYPZixd/4MIWQwb8Zkd0mg9kTzIy4PPfnON9+0PZvXtfNWa3m5j9Qcy4aUCRXX7fFt24/g68r
cWwg56a0//nRobR05twciQD4UKns0tRiKgZNQSxXXF/hX7nqrVzzr8eGYJlWY7L84v+rzAARKIRwwdh
V5ZX6OtuXD8dH3LOwuq1HOlSIByeX3RBYQCIRFoNEsKu3PKAXqpvwNnsl+jrtRzpuYFw+I30QqNCLDa
EWlubDdK0T9etveumVCqVZendu5jokoQBYM+el2/3FHeRFA3t7e0/YglYkvBy4AeAROLDu+ET7URqs1
IEIgVSBEVX+JUYgZTKxwq4IihSIKK0cmVDS0toCAD8Qgh/97Ejrwc3Pbr5ahymfv/KsPqsw0KITcwsN
QAr/pmYuOFqU/8tm8H4+IQEsEIIIfwAfKc+Pt12KHcwfI3ff8ucVOdP+z+eL9MnY6e/S6e/jwHwARAC
QBGAlQCuzQeXCwVgHMAoAFcAEPnxqeX5csEACAAxM/8LnihKT638MhEAAAAASUVORK5CYII=
'''

icoscendi_uno = '''
iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXM
AAAsTAAALEwEAmpwYAAAAB3RJTUUH4wMeDA0pVWSGtQAAA0xJREFUOMvFlF9MFFcYxc+d2Q0oPvi4Bh
9MCGZeTKNJm2atUZqNiWZeNJsgfaxxYddFbFdZV6WOi64SkFCJJpI02z601tgUgTE0aWE1iDUkKiJEI
+KfhXE38Q9ZVNiZuXOvD+DqhuISbOJJ7sOdfN9vZu495yPIoc7O1hUQ7XWUmjLnUC0jHdyypewRPlYX
/2o/99XYWgYNvGysjP3Zev7cfPqEXAWmachXSC8BgLPkLDGoLv8vYM65eufVEAOASyPdFmNcnQ/Ylqs
g/fpVcOD2LfxKf5Gf8FFVT08G8SlFchVEo9EVNjuvMymVAaLqUzTo9XofffRREBuvczrXuYvyioXh9D
13V3cXAJTOCxyNnnJQJu5njG3ljF1gltno8333AABMQ5eL8ooFACjOXyk0m00ZV9TWHiyyOAlbjK1nn
HcYxDxcr9QnM64wGTn0dYmrsnyzr7BC9u9kROxtbm6QAIAD6o2n1xkAqP1tFp9xhaIoEoNwpWv7pW9q
dxwtPOqJVAg62591xqfPNI/55MrC93/lxB8NSW4aJWLBorQdQh2lVCaEqJOmHtRTE/kQ7DFle9jxfk+
gabd2or5peeaLLcu6PKIPT0cY0zYNuPc4GBFiU+OpfL+3qnR3ZaCgyv99qZ6ayGcQZ0G7h/+BaVkXsg
LCqfFDR0dbcnD8FjbhXbCqt+1zQBBiiqJIABBSQhLjQiy840gW9OrjHnS0tyVNThvfPhOnB83f4y7Xh
ov3Ru67J1a+XKKJWqap67PYkvX969zOtV+OiBDO15ZHMlCVtuE34XcU/FSQtAyj5PTJM8P/aZFIRJEi
x8KJ40MRDg1Z60DNvqx9dXwvhwZeWeVLePweKWdiFCUkHQrXJKCBD8T7Z70AGrhz1Mmhge/c5Z0f9K1
CoZB0oCaYgAZe/XjvLPCyh8u4z1+e8Hjmhs4Z6UAoIBHGYw27GrMuas3r1fjix8+TzDRKWlp+vrugWe
EP+CWR81hPxVWHK8+F5y+eQ2wRkrDoB6EZV8yhvL5/+2yckL41D1dvXKWvWjzWGn928+bAt2p7ZxyAA
cBaCHgpgKXaqCZMpFKDhqFLvT3XjgwNDsUB0Jk1tdCxaQOwCMBiAHYAJoDJGSD9UOMbgQOrMZhndNAA
AAAASUVORK5CYII=
'''

icosali_uno = '''
iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXM
AAAsTAAALEwEAmpwYAAAAB3RJTUUH4wMeDBERmxFjdgAAAy1JREFUOMvF1F9IVFkcB/DvPSpWtBARdC
GQaAmuPVQPQUGlXLJiYSCLgdpa2yz7Q2Sh3TCkYNqNSUn6p4sJS0NENybGcnX8U1a3kR4qe5D+6PXPL
gVJ90FtCp2Z8+feuy+bNFg6mw/94Lyc8/t+OJzDORJSqFDo+mLHda9xSSrY4d3Rn0qGTI3qiu26kRUr
Vq10uYgE9ICC6ZYe0pUbwWvvMAj30whc/fNdXaBO+eYd6yFdgRDGz2sK5M/nC/OKZNi2UVdXrfxvWNc
DisOYsX3tTvlL6/t/Oihz2zGqq6uUlGFdDygCMH7J3SVPtqNDm47I3JGMqiq/MiUc0AMKdxxjZ25hEj
oQ70NtuAb9sd6kcKn3qOxKxPD7J+LkcxTCNn5V9ySh/fE+tN5ttRIssbm5NWz1jpkAgFySAwA4tu24D
GIbPr9PmQBfuX5lkcuFUbiuaCJ6p8WiNldLirUGwaja2NRgdX94iYjTAZN0AwDKC07KEI5x4sSxH5Ng
m7HSU+tPJ6F9YyZaWpss5ghVK9ZMANC0cpNTR/2r8bb1fKQL+3BgvH9r0TbZdvBbMizs/Dfum/Emc7Q
HLW3NFqdUTUtPS9Rergleqjk/dqnmfFCaKSVowlZv3a639g/tG89ku0sgHDs3CRZc3HoVfYEcKQcvRr
rQ3Ba2BKOqppWbGSCVqrrOe3hzyaz1eRu9GSCVPp/PpI5Qe26+tCL/GACAhmf1EFw0JcGSS/3t9+5c3
nTTM9jW3qbbNLFG08pNAOCMeZTZ2QQAsn9YQhhlHgA44ztjCk5XNzY2/VFy7sjgw4hRTePiVMrP+tzF
s8EnTx/bWW+z3FB9UPgrfg+mkkufqoHGxsqMyH3s5bs9vaQ3zBOxMnzPkqZq8PmOLyRpmZWMM48kSWF
OY2UVFRdeT/soQDIq8/O3eJfOXUa6hrq8N4I6AGyd9kdPKfMsnbuMAMDyecsJYwnPdC8vE8C8WDze8X
DgwYbuRd0ks32G8yzW2QFgAYAhAPRr4bRJ4DkA5ox+HB2MRkcWRDvfzx/4e+BJR+RR7fDwcBSAABD/1
stLBzATwCwAGQA4gNh/oJgs+C/j2orzLro7qAAAAABJRU5ErkJggg==
'''

icosali_su = '''
iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXM
AAAsTAAALEwEAmpwYAAAAB3RJTUUH4wMeDBAA6LpyxQAAA3xJREFUOMu1lG1om1UUx3/Pk4TQ0iETPw
T2SUV4BFkFUaEKEoSpWN0K+6BDP1indW/dcHUzWJfUhXTtMqwmq0ulc1NXbRldadZu80PTdlltbWvpQ
Cj4weF8TByTvWSteXvu8YNrMXNNK2MH/lzOvef/55xz7z2aiPDdQF+Fhm2/iKpQSmFZCqUUov5ZlVJY
t/i3wQxKXtu4cdMkgB0A0fauefiFCu7ALuR/Mfr6o58BTwDoAJayVvzGxTvR5drVK1iWlZr37QAqa22
a+HHs627VZRSWJ8VKL0BMYiOWxYfzwpqIcDfMvpygyJHIQ5qlvpJ87vWamtqfl8PRlwpoa2szbDC05t
nnnhTNPhQOHzCWlbKILIpIJGRE2lsTmIhSSjCRT8MtiWAwYBTjicjiGbe1hQ2x6bGa5ze7Lt13CT2hM
7pylNqqHS7Nbo81NjYa/zvjUCRktEZCCUxkdG5UMFlAR6pDMJHm/Y0JX8C3aOa3FT146KbobKHoPPxX
/IKJ7A34Eh6fx1hSOBQKGuHWTxKYSMf1jgWh+E9nZV+zX6bPTy3sVV+uFkzE66tPeDw7jUV7HA4fMND
tsa1rt7v8pX42pDYAMP3nFMPRWDKdyVf19J5ITv4xDsDhzGHcdjcNb/ldottidXXbjP88t5aWlgcE2+
C2dTtcbzqrqZ+rB2Dq8iTRvt5kNq3c3npvTzqfc/f0dCd/+H0UgEdT5QDs29zs0m2OwdramgcLLi/4c
XP43+VhIpNTE/JRwJvw3dJDj2ensctTlxj5Pi6YyPT5yQXOO1vePjYfZwfI5/PrLJeFLWkDYCI5zsn+
3qRYOTdOZ7qpOdCZzWUrNU07KU7Hbit1w338eFdsuGrQVX7/Y3yb+oZXVryKstQzBa2wrFx33YU6AEZ
+PUu0rzeZtnJurzcw49D0psoXX1q/542G0rUvV613iN4UDIZmspm0u7OrM9k3HWXMMcbR2BGy+Vy0QD
iXlsA9x8oO7fn8A7P/dH+HlU0/HfAGZgCymWzlIytX6wCr7y3Xs9lcJUAoFJlRucxTJ3pOHLzWeNWMD
QyEyGsNS35pwAmsem/Xu6eG40PW+xd3y9Evv7Bqt285BawCnMW+dLHpVgaUnTl9pv3G7GxZSWnJ4+eu
x8fPxePtN8/SQGYxctF5rGmaHSgBSgEHkAPmgL9EJF9sVNy1Qf839GXHQ0pkWz0AAAAASUVORK5CYII
=
'''


# *** CLASSI ***



class FRMTumbnail(tk.Frame):
   '''Pannello per singola miniatura (tumbnail) con descrizione.'''
   def __init__(self, master, mater, imgdim=60):
       self.mater = mater
       self.f_img = ''
       super().__init__(master)
       self.dim_tmb = imgdim
       self._curr_img = None
       self.indice = None
       self.default_bc = self.cget('bg')
       self.configure(relief='sunken', border=2, padx=2, pady=2)
       self.cnv_tmb = tk.Canvas(self,
                                width=self.dim_tmb,
                                height=self.dim_tmb,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                             )
       self.cnv_tmb.pack(side='left')
       self.cnv_tmb.bind('<Button-1>', self._scelto)
       f = tk.Frame(self, height=self.dim_tmb+4)
       f.pack(side='left', expand=True, fill='x')
       f.pack_propagate(0)
       self.lbl_dida = tk.Label(f,
                               text='...',
                               relief='raised',
                               justify='left',
                               border=2
                               )
       self.lbl_dida.pack(expand=True, fill='both')
       self.lbl_dida.bind('<Button-1>', self._scelto)
       
   def set_fileimg(self, filename):
       ''' Impostazione diretta del file immagine da miniaturizzare. '''
       self.f_img = filename
       self._display()
   
   def _display(self):
       ''' Ridimensiona e visualizza l'immagine da mostrare. '''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(self.f_img)
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       if img_x >= img_y:
           f_scala = self.cnv_tmb.winfo_width() / img_x
       else:
           f_scala = self.cnv_tmb.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self._curr_img = ImageTk.PhotoImage(img)
       x = self.cnv_tmb.winfo_width() // 2
       y = self.cnv_tmb.winfo_height() // 2
       self.cnv_tmb.create_image(x, y, image=self._curr_img, anchor='center')
   
   def set_dida(self, testo):
       '''Impostazione della didascalia da mostrare a fianco dell'immagine.'''
       self.lbl_dida.configure(text=testo)
   
   def set_indice(self, indice):
       '''Impostazione dell'indice assegnato alla riga.'''
       self.indice = indice
   
   def get_indice(self):
       '''Restituisce l'indice assegnato alla riga.'''
       return self.indice
       
   def clear(self):
       '''Ripulisce la riga da eventuali dati presenti.'''
       self.cnv_tmb.delete('all')
       self.lbl_dida.configure(text='...')
       self.set_default_col()
   
   def _scelto(self, evt):
       '''Imposta la riga come immagine scelta.'''
       if not self.mater: return
       self.set_selected_col()
       self.mater.set_selezione(self.indice)
       return 'breack'
   
   def set_default_col(self):
       '''Imposta il colore del controllo ai valori di default.'''
       self.configure(bg=self.default_bc)
       self.lbl_dida.configure(bg=self.default_bc)
   
   def set_selected_col(self):
       '''Imposta il colore del controllo in modalità "selezionato".'''
       self.configure(bg='#ffffc0')
       self.lbl_dida.configure(bg='#ffffc0')



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(expand=True, fill='both')
       self._crea_righe()
       self.sf_command = tk.Frame(self)
       self.sf_command.pack(expand=True, fill='x')
       # memorizzazione immagini icone per pulsanti
       self.ico_scendigiu = tk.PhotoImage(data=icoscendi_giu)
       self.ico_scendiuno = tk.PhotoImage(data=icoscendi_uno)
       self.ico_saliuno = tk.PhotoImage(data=icosali_uno)
       self.ico_salisu = tk.PhotoImage(data=icosali_su)
       # definizione comandi
       self.bt_scendigiu = tk.Button(self.sf_command,
                                     image=self.ico_scendigiu,
                                     padx=4,
                                     pady=4,
                                     command=self._incr_rows
                                     )
       self.bt_scendigiu.pack(side='left')
       self.bt_scendiuno = tk.Button(self.sf_command,
                                     image=self.ico_scendiuno,
                                     padx=4,
                                     pady=4,
                                     command=self._incr_uno
                                     )
       self.bt_scendiuno.pack(side='left')
       sep1 = tk.Frame(self.sf_command, width=20)
       sep1.pack(side='left', expand=True, fill='x')
       self.indSet = tk.Entry(self.sf_command,
                              width=5,
                              relief='sunken',
                              border=2,
                              )
       self.indSet.pack(side='left')
       self.indSet.bind('<Return>', self._reset_pivot)
       self.indSet.bind('<KP_Enter>', self._reset_pivot)
       sep2 = tk.Frame(self.sf_command, width=20)
       sep2.pack(side='left',expand=True, fill='x')
       self.bt_saliuno = tk.Button(self.sf_command,
                                   image=self.ico_saliuno,
                                   padx=4,
                                   pady=4,
                                   command=self._decr_uno
                                   )
       self.bt_saliuno.pack(side='left')
       self.bt_salisu = tk.Button(self.sf_command,
                                  image=self.ico_salisu,
                                  padx=4,
                                  pady=4,
                                  command=self._decr_rows
                                  )
       self.bt_salisu.pack(side='left')
       
   
   def _crea_righe(self):
       '''Crea le righe per la visualizzazione dati.'''
       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 _reset_pivot(self, evt):
       '''Reimposta l'indice del primo elemento visualizzato.'''
       try :
           val = int(self.indSet.get()) - 1
           if val > 0 and val < len(self.dati):
               self.pivot = val
               self.indSet.delete(0, tk.END)
               self._refresh_rows()
       except ValueError:
           self.indSet.delete(0, tk.END)
   
   def set_data(self, dati):
       '''Imposta la lista dei dati da visualizzare.'''
       self.dati = dati
       self.pivot = 0
       for riga in self.rows:
           riga.clear()
       self._refresh_rows()
   
   def set_selezione(self, indice):
       '''Imposta l'indice della lista dati correntemente selezionato.'''
       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.pivot + indice)
   
   def _refresh_rows(self):
       '''Aggiorna i dati visualizzati nelle righe.'''
       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
       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()
   
   def _incr_uno(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
incrementando di uno.'''
       if self.pivot + 1 < len(self.dati):
           self.pivot += 1
           self._refresh_rows()
   
   def _incr_rows(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
incrementando di una "pagina".'''
       if self.pivot + self.righe < len(self.dati):
           self.pivot += self.righe
       else :
           self.pivot = len(self.dati) - self.righe
       self._refresh_rows()
   
   def _decr_uno(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
decrementando di uno.'''
       if self.pivot - 1 >= 0:
           self.pivot -= 1
       else:
           self.pivot = 0
       self._refresh_rows()
   
   def _decr_rows(self, evt=None):
       '''Reimposta l'indice del primo elemento visualizzato,
decrementando di una "pagina".'''
       if self.pivot - self.righe >= 0:
           self.pivot -= self.righe
       else:
           self.pivot = 0
       self._refresh_rows()


Edited by nuzzopippo - 24/4/2019, 07:49
 
Web  Top
view post Posted on 26/4/2019, 08:56
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,223

Status:


Testare il "controllo"



Conte anticipato nei post precedenti, procedendo nella fase di realizzazione del controllo ho sentito la necessità di testare la resa dello stesso, vuoi per sperimentare la miriadi di opzioni di configurazione disponibili per i vari widget, vuoi per verificare la resa grafica delle impostazioni adottate.
Verifiche che mi hanno portato ad esaminare numerose ipotesi prima di decidere l'iter di sviluppo da seguire.

Preparazione dell'ambiente di controllo


Data la natura del controllo da realizzare ho sbrigativamente collezzionato una dozzina di immagini, per lo più sfondi di desktop, di varia risoluzione, dal 500x500 pixel al Full HD, e mediamente "leggere" come peso, tra i 100 ed i 500 Kb, rinominandole dal 01.* al 12.* e stipandole in una sotto-directory della direttrice con il codice in sviluppo (risorse).

Tale accorgimento scaturisce dalla decisione di adottare un approccio a linea di comando, un paio dei test vertono sul colo controllo "FRMTumbnail" e prevedono l'inserimento del nome del file da miniaturizzare tra i parametri, inserire un percorso relativo seguito da uno "01.jpg" è molto più sbrigativo del path completo di un generico file.

Il test (file test.py)


Come già detto in precedenza, il controllo prevede l'utilizzo di dati con una singola riga strutturamente rappresentata da un dizionario contenente obbligatoriamente le chiavi "img", il cui valore indica il path di un file, e "dida", il cui valore è una stringa di testo rappresentante una didascalia associata all'immagine.
Data l'idea finalizzante il controllo in esame, cioè la creazione di un "Foto-Diario", un diario fotografico, nel test si è preparata una lista di dizionari (img_coll), uno per ogni immagine selezionata, contenenti oltre le chiavi prima dette una ulteriore chiave "testo" destinata ad una striga di testo più estesa atta a rappresentare commenti più estesi ... nel codice, dozzinali versi venuti d'impulso nell'associare le immagini, li lascio alla vostra ilarità.
Detta lista "img_coll" simula i dati applicativi di una eventuale applicazione utilizzante il controllo.

Oltre alla simulazione dei dati, nel codice di test vengono definite 8 classi, tutte estendenti direttamente la classe Tk di tkinter, in qualità di finestra principale applicativa, di cui quattro, denominate "GUI_num", ove num varia da 01 a 04, sono dedicate all'utilizzo del gestore di geometria "pack()", e quattro, denominate "GUI_num_G", ove num varia da 01 a 04, sono dedicate all'utilizzo del gestore di geometria "grid()".
Non si è voluto realizzare una variante adottante il gestore di geometria "place()" dato l'alto numero di "conticini" necessari al ridimensionamento della finestra, passate esperienze in Visual Basic, versioni pre-.NET, che hanno solo un simile piazzamento, mi rendono convinto non valga la pena, salvo casi estremi, adottare una simile geometria, stante i buoni effetti ottenibili con gli altri due gestori.

Le varianti con gestore di geometria "pack()", per la versione originaria del controllo, le avete già viste in calce al precedente post, ordinate secondo la successione GUI_01 - GUI_04, il funzionamento delle finestre è uniforme per tutte e tre le versioni, è la diversa "composizione" del controllo a far vedere delle differenze.
Anche se non è cosa buona e giusta mischiare più gestori di geometria nella stessa finestra, una finestra utilizzante il gestore grid può visualizzare decentemente il controllo che utilizza il gestore pack, di seguito esempio della rappresentazione della versione iniziale del controllo con finestre adottanti il gestore grid:

png



png



png



png



Come potete vedere dalle immagini, per questa versione del controllo la differenza visibile è quasi impercettibile, praticamente il solo bottone "mostra immagine/i", trattando la variante "con scrollbar" del controllo mostrerò e motiverò la decisione di testare con differenti gestori di geometria quanto in sviluppo.
Per altro, solo per la terza versione del controllo, quella che ho deciso di adottare per mio uso, ho deciso di fare una doppia versione dello stesso, che motiverò, adottanti specificatamente una il gestore pack e l'altra il gestore grid.

Di per se, le classi implementate sono semplicissime, limitandosi a costruire la finestra ed implementando, le versioni 01 e 02, soltanto una funzione "mostra()" che si limita ad inviare nome file e dimensione a FRMTumbnail la 01 e la lista dati "img_coll" a FRMTmbCollection la 02.

Leggermente più articolate le versioni 03 e 04, ove sono implementati il metodo pubblico* "set_selezione(self, indice)", che riceve l'indice di lista corrispondente ad una immagine/didascalia selezionata in FRMTmbCollection, cliccando con il tasto sinistro del mouse, e provvede a visualizzare, adattandola all'area disponibile, la corrispondente immagine tramite il metodo privato* "_mostra_img(self, env=None)".

* - N.B : Ovviamente, per quel che vale la distinzione tra metodo "pubblico" e metodo "privato" in python, si veda a tal proposito la "Pep 8 -- Style Guide for Python Code".

Il ccodice ora (file test.py : 631 righe)

CODICE
# -*- coding: utf-8 -*-

import sys
import tkinter as tk
from PIL import Image
from PIL import ImageTk
import my_object as fd

img_coll = [{'img'   : 'risorse/01.jpg',
            'dida'  : 'La Fonte',
            'testo' : ('Ciò che per primo\n' +
                       'placò la mia sete,\n' +
                       'nutrì anima\n' +
                       'e corpo,\n' +
                       'strumento di madre,\n' +
                       'orgoglio di donna,\n' +
                       'a me\n' +
                       'da sempre caro.'
                       )
            },
           {'img'   : 'risorse/02.jpg',
            'dida'  : 'Pensiero',
            'testo' : ('Lontano è volto il tuo sguardo\n'+
                       'conturbante donzella,\n' +
                       'di me ignara,\n' +
                       'ai palpiti sorda.\n' +
                       'Cosa mai\n' +
                       "al di la dell'orizzonte,\n" +
                       'te attrae?\n' +
                       'Non ti accorgi?\n' +
                       'Dietro te\n' +
                       'ogni cosa svanisce.'
                       )
            },
           {'img'   : 'risorse/03.png',
            'dida'  : 'La gatta',
            'testo' : ('Facesti le fusa,\n'+
                       'enigmatica guardavi\n' +
                       'me.\n' +
                       'Ti accolsi\n' +
                       'e con i tuoi artigli\n' +
                       'lacerasti\n' +
                       '... ma solo la mia pelle.'
                       )
            },
           {'img'   : 'risorse/04.jpg',
            'dida'  : 'Mi tocca?',
            'testo' : ('Erculea fatica\n'+
                       'ogni giorno me aspetta.\n' +
                       'E scrutare il nascosto\n' +
                       'che errore mio attende!\n' +
                       'Stanco oramai\n' +
                       'altrove mi volgo,\n' +
                       'non sarà mai\n' +
                       'un dì del ritorno.'
                       )
            },
           {'img'   : 'risorse/05.jpg',
            'dida'  : 'Eleanor',
            'testo' : ('Una fonte\n'+
                       'per Te creai,\n' +
                       'la illuminò mai\n' +
                       'un tuo sorriso?\n' +
                       'Ancor secca in me zampilla,\n' +
                       'vi è una carezza\n' +
                       'per Eleanor la bella.'
                       )
            },
           {'img'   : 'risorse/06.jpg',
            'dida'  : 'Afflizione',
            'testo' : ('Apristi le braccia\n'+
                       'e me cullasti.\n' +
                       'Il tuo calore disperse\n' +
                       'il decennio oscuro.\n' +
                       'Ti amero mai abbastanza\n' +
                       "nell'eterno futuro?"
                       )
            },
           {'img'   : 'risorse/07.jpg',
            'dida'  : 'Corazza',
            'testo' : ('Grazioso è il nasino che arricci,\n'+
                       'in tono con le tue grazie,\n' +
                       'altera bellezza\n' +
                       'che nascondi dietro il tuo dispetto?\n' +
                       "Forse, l'animo vuoto che non sa dire?\n" +
                       "Vattene! È un'altra la mia strada."
                       )
            },
           {'img'   : 'risorse/08.jpg',
            'dida'  : 'Speranza',
            'testo' : ("Per quanto la notte\n"+
                       "possa essere cupa,\n" +
                       "un'alba verrà ancora\n" +
                       "se non mia\n" +
                       "sia tua."
                       )
            },
           {'img'   : 'risorse/09.jpg',
            'dida'  : 'Addolorata',
            'testo' : ("Sei sola, amica mia,\n"+
                       "il tempo\n" +
                       "ha corroso il tuo amore,\n" +
                       "la noia\n" +
                       "il suo epitafio.\n" +
                       "Che importa s'egli\n" +
                       "ama ancora?\n" +
                       "Dentro te, sei persa."
                       )
            },
           {'img'   : 'risorse/10.jpg',
            'dida'  : 'Pater',
            'testo' : ("Da Te, padre,\n"+
                       "deriva la mia essenza.\n" +
                       "Come il Tuo\n" +
                       "è al mio sguardo,\n" +
                       "orizzonte il tetto."
                       )
            },
           {'img'   : 'risorse/11.jpg',
            'dida'  : 'Emotico',
            'testo' : ("A chi rivolge\n"+
                       "la tua ira?\n" +
                       "Chi ti è accanto\n" +
                       "ha maggior peso\n" +
                       "della tua colpa?"
                       )
            },
           {'img'   : 'risorse/12.jpg',
            'dida'  : 'Vindicta',
            'testo' : ("Dove andrai mai\n"+
                       "domine?\n" +
                       "Le leggi creasti\n" +
                       "per me spolpare,\n" +
                       "te credi più forte.\n" +
                       "\nDimentichi :\n" +
                       "sei solo carne!\n" +
                       "Avrò fame,\n"  +
                       "ti divorerò\n" +
                       "ancora palpitante."
                       )
            }
           ]



class GUI_01(tk.Tk):
   ''' Finestra principale test 01 : singola riga. '''

   def __init__(self, fnome='', dim=60):
       super().__init__()
       self.f_nome = fnome
       self.title('Test 01')
       sfondo = tk.Frame()
       sfondo.pack(fill='both')
       self.f = fd.FRMTumbnail(sfondo, None, dim)
       self.f.pack(fill='x')
       f2 = tk.Frame(sfondo, relief='sunken', padx=2, pady=2)
       f2.pack(fill='x')
       btn = tk.Button(f2, text='Mostra immagine', command=self.mostra)
       btn.pack()
       self.minsize(300, self.winfo_reqheight())
       # self.update()
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_fileimg(self.f_nome)
       self.f.set_dida("un po' di testo a caso")


class GUI_01_G(tk.Tk):
   ''' Finestra principale test 05 - variante layout grid : singola riga. '''

   def __init__(self, fnome='', dim=60):
       super().__init__()
       self.f_nome = fnome
       self.title('Test 05-Grid')
       sfondo = tk.Frame().grid(sticky='nsew')
       self.f = fd.GriFRMTmb(sfondo, None, dim)
       btn = tk.Button(sfondo,
                       text='Mostra immagine',
                       command=self.mostra)
       self.f.grid(row=0, padx=2, pady=2, sticky='ew')
       btn.grid(row=1, padx=2, pady=2, sticky='nsew')
       self.columnconfigure(0, minsize=300, weight=1)
       self.update()
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_fileimg(self.f_nome)
       self.f.set_dida("un po' di testo a caso")


class GUI_02(tk.Tk):
   '''Finestra principale test 02 : solo contenitore miniature.'''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 02')
       sfondo = tk.Frame()
       sfondo.pack(fill='both')
       self.f = fd.FRMTmbCollection(sfondo, righe, dim)
       self.f.pack(fill='x')
       f2 = tk.Frame(sfondo, relief='sunken', padx=2, pady=2)
       f2.pack(fill='x')
       btn = tk.Button(f2, text='Mostra immagini', command=self.mostra)
       btn.pack()
       self.update()
       self.minsize(400, self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)
   
       
class GUI_02_G(tk.Tk):
   ''' Finestra principale test 05 - variante layout grid : solo miniature. '''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 06-Grid')
       self.f = fd.GriFRMTmbColl(self, righe, dim)
       btn = tk.Button(self, text='Mostra immagini', command=self.mostra)
       self.f.grid(row=0, column=0, sticky='ew')
       btn.grid(row=1, column=0, sticky='nsew')
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(1, weight=1)
       self.update()
       self.minsize(400, self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)



class GUI_03(tk.Tk):
   '''Finestra principale test 03, componenti :
- contenitore miniature;
- area di testo descrittivo;
- area visualizzazione immagine.

Disposizione tumbnail orizzontale

Implementa metodo pubblico per invocazione aggiornamento dati.
'''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 03')
       sfondo = tk.Frame(self)
       sfondo.pack(fill='both')
       self.img = None
       self.indice = None
       self.in_vis = False
       # area del testo
       txt_sf = tk.Frame(self)
       txt_sf.pack(side='left', ipadx=1, ipady=1, fill='y')
       self.testo =tk.Text(txt_sf, width=40)
       self.testo.pack(side='left', expand=True, fill='both')
       sy = tk.Scrollbar(txt_sf)
       sy.pack(side='right', expand=True, fill='y')
       sy.config(command=self.testo.yview)
       self.testo.config(yscrollcommand=sy.set)
       # contenitore del resto
       s_sf = tk.Frame(self)
       s_sf.pack(side='left', ipadx=1, ipady=1, expand=True, fill='both')
       # immagine        
       self.cnv_img = tk.Canvas(s_sf,
                                width=300,
                                height=300,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                                )
       self.cnv_img.pack(expand=True, fill='both')
       self.cnv_img.bind("<Configure>", self._mostra_img)
       self.f = fd.FRMTmbCollection(s_sf, righe, dim, self)
       self.f.pack(fill='x')
       f2 = tk.Frame(s_sf, relief='sunken', padx=2, pady=2)
       f2.pack(fill='x')
       btn = tk.Button(f2, text='Mostra immagini', command=self.mostra)
       btn.pack()
       self.update()
       self.minsize(self.winfo_reqwidth(), self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)
   
   def set_selezione(self, indice):
       self.in_vis = True
       self.testo.delete('1.0', tk.END)
       self.cnv_img.delete('all')
       if indice >= len(img_coll): return
       self.indice = indice
       self.testo.insert(tk.END, img_coll[indice]['testo'])
       self._mostra_img(None)
   
   def _mostra_img(self, env=None):
       #if not self.img: return
       if not self.in_vis: return
       ''' Riscala e mostra l'immagine.'''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(img_coll[self.indice]['img'])
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       # calcola i rapporti di scala
       r_img = img_x / img_y
       r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
       if r_cnv <= r_img:
           f_scala = self.cnv_img.winfo_width() / img_x
       else:
           f_scala = self.cnv_img.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self.img = ImageTk.PhotoImage(img)
       x = self.cnv_img.winfo_width() // 2
       y = self.cnv_img.winfo_height() // 2
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')
       
       
class GUI_03_G(tk.Tk):        
   '''Finestra principale test 07-grid, componenti :
- contenitore miniature;
- area di testo descrittivo;
- area visualizzazione immagine.

Disposizione tumbnail orizzontale

Implementa metodo pubblico per invocazione aggiornamento dati.
'''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 07-grid')
       self.img = None
       self.indice = None
       self.in_vis = False
       self.testo = tk.Text(self, width=40)
       self.testo.grid(row=0, column=0, rowspan=2, sticky='ns')
       sy = tk.Scrollbar(self)
       sy.grid(row=0, column=1, rowspan=2, sticky='ns')
       sy.config(command=self.testo.yview)
       self.testo.config(yscrollcommand=sy.set)
       self.cnv_img = tk.Canvas(self,
                                width=300,
                                height=300,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                                )
       self.cnv_img.grid(row=0, column=2, sticky='nsew')
       self.cnv_img.bind("<Configure>", self._mostra_img)
       self.f = fd.GriFRMTmbColl(self, righe, dim, self)
       self.f.grid(row=1, column=2, sticky='ew')
       btn = tk.Button(self, text='Mostra immagini', command=self.mostra)
       btn.grid(row=3, column=0, columnspan=3, sticky='ew')
       self.grid_columnconfigure(2, weight=1)
       self.grid_rowconfigure(0, weight=1)
       self.update()
       self.minsize(self.winfo_reqwidth(), self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)
   
   def set_selezione(self, indice):
       self.in_vis = True
       self.testo.delete('1.0', tk.END)
       self.cnv_img.delete('all')
       if indice >= len(img_coll): return
       self.indice = indice
       self.testo.insert(tk.END, img_coll[indice]['testo'])
       self._mostra_img(None)
   
   def _mostra_img(self, env=None):
       #if not self.img: return
       if not self.in_vis: return
       ''' Riscala e mostra l'immagine.'''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(img_coll[self.indice]['img'])
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       # calcola i rapporti di scala
       r_img = img_x / img_y
       r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
       if r_cnv <= r_img:
           f_scala = self.cnv_img.winfo_width() / img_x
       else:
           f_scala = self.cnv_img.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self.img = ImageTk.PhotoImage(img)
       x = self.cnv_img.winfo_width() // 2
       y = self.cnv_img.winfo_height() // 2
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')
   
       


class GUI_04(tk.Tk):
   '''Finestra principale test 04, componenti :
- contenitore miniature;
- area di testo descrittivo;
- area visualizzazione immagine.

Disposizione tumbnail verticale

Implementa metodo pubblico per invocazione aggiornamento dati.
'''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 04')
       sfondo = tk.Frame(self)
       sfondo.pack(fill='both')
       self.img = None
       self.indice = None
       self.in_vis = False
       sftmb = tk.Frame(self, width=200)
       sftmb.pack(side='left', ipadx=1, ipady=1, fill='y')
       self.f = fd.FRMTmbCollection(sftmb, righe, dim, self)
       self.f.pack(side='left', fill='y')
       sftmb.configure(width=4*dim)
       s_sf = tk.Frame(self)
       s_sf.pack(side='left', ipadx=1, ipady=1, expand=True, fill='both')
       # immagine        
       self.cnv_img = tk.Canvas(s_sf,
                                width=300,
                                height=300,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                                )
       self.cnv_img.pack(expand=True, fill='both')
       self.cnv_img.bind("<Configure>", self._mostra_img)
       # area del testo
       f2 = tk.Frame(s_sf, relief='sunken', padx=2, pady=2)
       f2.pack(fill='x')
       txt_sf = tk.Frame(f2)
       txt_sf.pack(expand=True, fill='x')
       self.testo =tk.Text(txt_sf, height=10)
       self.testo.pack(side='left', expand=True, fill='x')
       sy = tk.Scrollbar(txt_sf)
       sy.pack(side='left', fill='y')
       sy.config(command=self.testo.yview)
       self.testo.config(yscrollcommand=sy.set)
       btn = tk.Button(f2, text='Mostra immagini', command=self.mostra)
       btn.pack()
       self.update()
       self.minsize(self.winfo_reqwidth(), self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)
   
   def set_selezione(self, indice):
       self.in_vis = True
       self.testo.delete('1.0', tk.END)
       self.cnv_img.delete('all')
       if indice >= len(img_coll): return
       self.indice = indice
       self.testo.insert(tk.END, img_coll[indice]['testo'])
       self._mostra_img(None)
   
   def _mostra_img(self, env=None):
       if not self.in_vis: return
       ''' Riscala e mostra l'immagine.'''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(img_coll[self.indice]['img'])
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       # calcola i rapporti di scala
       r_img = img_x / img_y
       r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
       if r_cnv <= r_img:
           f_scala = self.cnv_img.winfo_width() / img_x
       else:
           f_scala = self.cnv_img.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self.img = ImageTk.PhotoImage(img)
       x = self.cnv_img.winfo_width() // 2
       y = self.cnv_img.winfo_height() // 2
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')


class GUI_04_G(tk.Tk):
   '''Finestra principale test 08 - versione grid, componenti :
- contenitore miniature;
- area di testo descrittivo;
- area visualizzazione immagine.

Disposizione tumbnail verticale

Implementa metodo pubblico per invocazione aggiornamento dati.
'''
   
   def __init__(self, righe=2, dim=60):
       super().__init__()
       self.title('Test 08-grid')
       self.img = None
       self.indice = None
       self.in_vis = False
       #self.f = fd.GriFRMTmbColl(self, righe, dim, self)
       self.f = fd.FRMTmbCollection(self, righe, dim, self)
       self.f.grid(row=0, column=0, rowspan=2, sticky='nsew')
       # immagine        
       self.cnv_img = tk.Canvas(self,
                                width=300,
                                height=300,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                                )
       self.cnv_img.grid(row=0, column=1, columnspan=2, sticky='nsew')
       self.cnv_img.bind("<Configure>", self._mostra_img)
       # area del testo
       self.testo =tk.Text(self, height=10)
       self.testo.grid(row=1, column=1, sticky='ew')
       sy = tk.Scrollbar(self)
       sy.grid(row=1, column=2, sticky='ns')
       sy.config(command=self.testo.yview)
       self.testo.config(yscrollcommand=sy.set)
       btn = tk.Button(self, text='Mostra immagini', command=self.mostra)
       btn.grid(row=3, column=0, columnspan=3, sticky='ew')
       self.grid_columnconfigure(0, minsize=300)
       self.grid_columnconfigure(1, weight=1)
       self.grid_rowconfigure(0, weight=1)
       self.update()
       self.minsize(self.winfo_reqwidth(), self.winfo_reqheight())
       centraFinestra(self)
   
   def mostra(self):
       self.f.set_data(img_coll)
   
   def set_selezione(self, indice):
       self.in_vis = True
       self.testo.delete('1.0', tk.END)
       self.cnv_img.delete('all')
       if indice >= len(img_coll): return
       self.indice = indice
       self.testo.insert(tk.END, img_coll[indice]['testo'])
       self._mostra_img(None)
   
   def _mostra_img(self, env=None):
       if not self.in_vis: return
       ''' Riscala e mostra l'immagine.'''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(img_coll[self.indice]['img'])
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       # calcola i rapporti di scala
       r_img = img_x / img_y
       r_cnv = self.cnv_img.winfo_width() / self.cnv_img.winfo_height()
       if r_cnv <= r_img:
           f_scala = self.cnv_img.winfo_width() / img_x
       else:
           f_scala = self.cnv_img.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self.img = ImageTk.PhotoImage(img)
       x = self.cnv_img.winfo_width() // 2
       y = self.cnv_img.winfo_height() // 2
       self.cnv_img.create_image(x, y, image=self.img, anchor='center')




def centraFinestra(gui):
   ''' Centra una finestra, passata come parametro, sullo schermo '''
   l = gui.winfo_screenwidth()
   a = gui.winfo_screenheight()
   wx = gui.winfo_reqwidth()
   wy = gui.winfo_reqheight()
   gui.geometry('{}x{}+{}+{}'.format(wx, wy, (l-wx)//2, (a-wy)//2))

   

if __name__ == '__main__':
   if len(sys.argv) < 3:
       print('Numero argomenti errato')
       sys.exit('Numero argomenti errato')

   step = int(sys.argv[1])
   
   if step == 1:                                # test x layout pack
       # verifica singola riga di miniatura
       f_nome = sys.argv[2]
       dim = int(sys.argv[3])
       win = GUI_01(f_nome, dim)
   elif step == 2:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_02(righe, dim)
   elif step == 3:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_03(righe, dim)
   elif step == 4:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_04(righe, dim)
   if step == 5:                                # test x layout grid
       # verifica singola riga di miniatura
       f_nome = sys.argv[2]
       dim = int(sys.argv[3])
       win = GUI_01_G(f_nome, dim)
   if step == 6:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_02_G(righe, dim)
   if step == 7:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_03_G(righe, dim)
   if step == 8:
       righe = int(sys.argv[2])
       dim = int(sys.argv[3])
       win = GUI_04_G(righe, dim)
       

   win.mainloop()


Questo codice è la versione finale del test, specifica per la terza versione del controllo, le classi "GUI_num_G" richiamano esplicitamente le versioni "grid" del controllo, qualora si vogliano visualizzare le versioni "pack" dei controlli, si sostituiscano le chiamate "self.f = fd.GriFRMTmb(sfondo, None, dim)" con "self.f = fd.FRMTumbnail(sfondo, None, dim)" e "self.f = fd.GriFRMTmbColl(self, righe, dim)" con "self.f = fd.FRMTmbCollection(sfondo, righe, dim)".

Il test si avvia da shell, comando :
CODICE
python3 test.py numtest opzione1 opzione2

ove
  • numtest è un numero intero da 1 ad 8 che definisce quale classe "GUI..." utilizzare;
  • opzione1 è un nome di file per i test 1 e 5, il numero di righe di miniatura da visualizzare per gli altri test;
  • opzione2 la dimensione, in pixel, dell'aria per la miniatura.

Buon divertimento.

Il prossimo post riguarderà la versione del controllo con scrollbar ed i "perché" si è deciso di trovare i costrutti del gestore di geometria grid.
 
Web  Top
view post Posted on 4/5/2019, 11:00
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,223

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
view post Posted on 4/2/2021, 11:21
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,223

Status:


Ogni tanto mi capita di venire qui e trovare qualcuno che legge questo post ... evento che, invariabilmente, mi fa sentire in colpa dato che avrei potuto completarlo ma non lo ho fatto per problemi di tempo in primo luogo, poi anche perché, avendolo risolto, altre problematiche hanno deviato la mia attenzione ed, infine, perché acquisendo ulteriori conoscenze ho intravisto alcune pecche nel design del controllo, che non contempla i pattern MVC ed observer, a me sconosciuti all'epoca.

Chiedo scusa ad eventuali (ed improbabili) interessati. Cercherò di rimediare ora.

È giunto, quindi, il momento di parlare della

Versione con Scale



Come già descritto nei post precedenti, tre sono le esigenze che ho ritenuto fondamentali per la implementazione del controllo di che trattasi :
  1. limitare il numero di immagini da elaborare per singolo processo di refresh;
  2. fornire un efficace sistema di navigazione;
  3. identificare correttamente i dati selezionati.

Come già detto nel post sulla "Prima Bozza" i dati altro non sono che una lista di dizionari ove il singolo "dizionario" costituente elemento della lista contiene un riferimento al file immagine da visualizzare, oltre ad una eventuale associazione di una didascalia e di un testo.
Le due varianti di controllo precedentemente esposte altro non sono che delle metodologie per tener traccia dell'indice che i singoli dati-immagine occupano nella lista.
I primi due tentativi sopra (non dimentichiamo che il post è stato un work-in-progress) pur se funzionanti (a modo loro nel caso della versione con scroll) mi lasciavano insoddisfatto, cercando "alternative" trovai il widget "Scale"

Tale widget non è altro che uno slider, un cursore su di una scala graduata in sostanza, che può essere esposto, a scelta, tanto in modalità orizzontale quanto in modalità verticale.
Tra le proprietà impostabili di questo widget ve ne sono alcune particolarmente interessanti :
  • from_ - permette di definire un numero intero inizio della scala;
  • to - permette di definire un numero intero termine della scala;
  • tickinterval - permette di definire un intervallo di graduazione della scala.

Per altro, il widget scale espone il metodo "get()" che restituisce un numero intero che è il valore corrente del cursore nella scala di valori impostata e scatena un evento al riposizionamento del cursore.
Queste due ultime caratteristiche sono esattamente ciò che serve per tracciare in modo semplice ed efficace la posizione di un indice nella lista dati, potendo intercettare l'evento di riposizionamento del cursore in una funzione/metodo di callback che provvederà a leggere il valore del cursore ed agire di conseguenza, potremo, dunque, implementare il widget in questione nella classe che espone la nostra "collezione" di immagini definendo direttamente anche il callback correlato, con questo stralcio di codice :

CODICE
self.pivot_scale = tk.Scale(self,
                                   from_=0,
                                   to=0,
                                   orient=tk.VERTICAL,
                                   command=self._scale_event)

ovviamente, "self.pivot_scale" è il nostro widget mentre "self._scale_event" è il metodo di callback che gestisce il posizionamento del cursore, assegnato al widget scale tramite la proprietà "command".
È banale osservare che in fase di inizializzazione le proprietà "from_" e "to" sono inizializzate a valori nulli, dato che il controllo non possiede nativamente una lista dati. Detta lista sarà fornita dall'esterno tramite il metodo "set_data(self, dati)" della classe per la collezione di immagini, è alla ricezione di detti dati (ovviamente di formato ben specifico) che saranno impostati i valori necessari alla navigazione tra i dati :

CODICE
def set_data(self, dati):
       self.dati = dati
       self.pivot = 0
       for riga in self.rows:
           riga.clear()
       self.pivot_scale.configure(from_=1,
                                  to=len(self.dati),
                                  tickinterval=len(self.dati)//5
                                  )
       self._refresh_rows()

(scusate la parziale mancata aderenza alla Pep 8)

Vi è da notare un particolare : "from_" è inizializzato ad "1" mentre "to" è pari al numero dei dati, ciò contrasta con l'indicizzazione di una lista che va da zero al numero dei dati meno uno, cosa di cui si dovrà, necessariamente, tenere conto nella "navigazione" tra i dati, semplicemente diminuendo di "1" il valore corrente del controllo scale.
da notarsi l'impostazione del parametro "tickinterval", esso controlla la rappresentazione delle etichette di valore nella scala graduata del controllo Scale, impostata ad ogni quinto del numero complessivo dei dati.

In merito alla "navigazione", tra i dati, essa è gestita leggendo il valore corrente di Scale tramite il suo metodo ".get()", decrementandolo ed assegnando il valore calcolato alla variabile di istanza "pivot", nella funzione di callback "_scale_event" collegata al controllo in fase di inizializzazione, per quindi richiamare il metodo di classe "_refresh_rows()", per il resto funziona in perfetta analogia a quanto esposto per le altre varianti.

Questo è l'aspetto del controllo riveniente (con grid)

png



Un piccolo accorgimento...


Come detto nei precedenti post, per questa versione del controllo ho voluto definire due varianti, una utilizzante il gestore di geometria "pack", l'altra utilizzante il gestore di geometria "grid".
Tale scelta è giusto un vezzo, si riesce tranquillamente a far coesistere oggetti utilizzanti i due diversi gestori, magari con qualche piccola attenzione, per implementare la "differenziazione" ho semplicemente definito in "FRMTumbnail" ed in "FRMTmbCollection" un metodo "_popola(self)" che provvede ad istanziare i widget necessari e definirne le caratteristiche, ho quindi sub-classato le due classi riscrivento i metodi utilizzando il gestore di geometria "grid" invece di "pack" ... in FRMTmbCollection ho riscritto anche il metodo "_crea_righe(self)" che provvede ad una singola riga dati.


Altre cosette ...


In linea di massima, le funzionalità del controllo sono analoghe nelle tre varianti, la differenza fondamentale consiste nella modalità di controllo del "pivot".
Un piccolo particolare di tale differenza è il controllo che il pivot non possa essere posizionato in maniera tale da far presentare righe dati "vuote", effetto giudicato sgradevole in fase di uso operativo del controllo (si, ho realizzato "fotodiario", magari, se ho tempo, posto anche quello). Banalmente, ho utilizzato direttamente il metodo di callback di scale per implementare la verifica

CODICE
def _scale_event(self, evt):
       '''Gestisce lo scroll del widget scale associato.'''
       if not self.dati: return
       value = int(self.pivot_scale.get()) - 1
       if value + self.righe >= len(self.dati):
           self.pivot  = len(self.dati) - self.righe
       else:
           self.pivot = value
       self._refresh_rows()

che si limita a controllare che il valore restituito da controllo scale, incrementato dal numero di righe dati rappresentate non superi i dati memorizzati, nel caso si calcola il massimo pivot "possibile" e lo assegna, altrimenti assegna direttamente il valore restituito.

Altra faccenda, trovata "sgradevole" è lo spazio vuoto che può venire a formarsi ridimensionando la finestra, ciò nasce dalla scelta di NON intercettare gli eventi di ridimensionamento del frame, e quindi ricalcolare il controllo e ridisegnare ogni sua parte, immagini comprese, dato che l'elevatissimo numero di eventi che sarebbero intercettati porterebbe tkinter al crash ... Operativamente ho "rimediato" facendo si che l'adattamento fosse una scelta dell'utente, creando un metodo pubblico (non compreso nel codice corrente del controllo) che può essere invocato e provvede a calcolare il numero di righe rappresentabili e ridefinire il controllo, la espongo qui, nel caso interessi qualcuno

CODICE
def adegua_righe(self):
       '''
       Ricalcola, sulla base della prima riga, quante sono le righe dati
       rappresentabili con le dimensioni correnti e ricalibra il controllo.
       '''
       h_rif = self.rows[0].winfo_height()
       h_disp = self.winfo_height()
       new_righe = h_disp // h_rif
       # se il numero di righe disponibili è diverso dal numero di righe
       # corrente distrugge i controlli presenti, ridefinisce il numero di
       # righe, ricrea un numero di controlli adeguato e visualizza
       if new_righe != self.righe:
           for riga in reversed(self.rows):
               riga.rimuovi()
               riga.destroy()
               self.rows.pop()
           self.pivot_scale.destroy()
           self.righe = new_righe
           self._popola()
           if self.dati:
               self.pivot_scale.configure(from_=1,
                                          to=len(self.dati),
                                          tickinterval=len(self.dati)//5
                                          )
           self.update()


Conclusione


Per concludere, il sorgente completo della terza versione del "controllo personalizzato" a suo tempo realizzato, 228 righe

CODICE
#-*- coding: utf-8 -*-

import tkinter as tk
from PIL import Image
from PIL import ImageTk



# *** CLASSI ***


class FRMTumbnail(tk.Frame):
   '''Pannello per singola miniatura (tumbnail) con descrizione.'''
   def __init__(self, master, mater, imgdim=60):
       self.mater = mater
       self.f_img = ''
       super().__init__(master)
       self.dim_tmb = imgdim
       self._curr_img = None
       self.indice = None
       self._popola()
   
   def _popola(self):
       self.default_bc = self.cget('bg')
       self.configure(relief='sunken', border=2, padx=2, pady=2)
       self.cnv_tmb = tk.Canvas(self,
                                width=self.dim_tmb,
                                height=self.dim_tmb,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                             )
       self.cnv_tmb.pack(side='left')
       self.cnv_tmb.bind('<Button-1>', self._scelto)
       f = tk.Frame(self, height=self.dim_tmb+4)
       f.pack(side='left', expand=True, fill='x')
       self.lbl_dida = tk.Label(f,
                               text='...',
                               justify='left',
                               )
       self.lbl_dida.pack(expand=True, fill='both')
       self.lbl_dida.bind('<Button-1>', self._scelto)
       
       
   def set_fileimg(self, filename):
       ''' Impostazione diretta del file immagine da miniaturizzare. '''
       self.f_img = filename
       self._display()
   
   def _display(self):
       ''' Ridimensiona e visualizza l'immagine da mostrare. '''
       # Import l'immagine tramite PIL (per i file *.jpg)
       img = Image.open(self.f_img)
       # proporzionamento immagine all'area disponibile
       img_x, img_y = img.size
       if img_x >= img_y:
           f_scala = self.cnv_tmb.winfo_width() / img_x
       else:
           f_scala = self.cnv_tmb.winfo_height() / img_y
       fx = int(img_x * f_scala)
       fy = int(img_y * f_scala)
       img = img.resize((fx, fy), Image.ANTIALIAS)
       # converto per il canvas
       self._curr_img = ImageTk.PhotoImage(img)
       x = self.cnv_tmb.winfo_width() // 2
       y = self.cnv_tmb.winfo_height() // 2
       self.cnv_tmb.create_image(x, y, image=self._curr_img, anchor='center')
   
   def set_dida(self, testo):
       self.lbl_dida.configure(text=testo)
   
   def set_indice(self, indice):
       self.indice = indice
   
   def get_indice(self):
       return self.indice
       
   def clear(self):
       self.cnv_tmb.delete('all')
       self.lbl_dida.configure(text='...')
       self.set_default_col()
   
   def _scelto(self, evt):
       if not self.mater: return
       self.set_selected_col()
       self.mater.set_selezione(self.indice)
       return 'breack'
   
   def set_default_col(self):
       self.configure(bg=self.default_bc)
       self.lbl_dida.configure(bg=self.default_bc)
   
   def set_selected_col(self):
       self.configure(bg='#ffffc0')
       self.lbl_dida.configure(bg='#ffffc0')



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._popola()
   
   def _popola(self):
       self.sf_tmb = tk.Frame(self)
       self.sf_tmb.pack(side='left', expand=True, fill='both')
       self._crea_righe()
       self.pivot_scale = tk.Scale(self,
                                   from_=0,
                                   to=0,
                                   orient=tk.VERTICAL,
                                   command=self._scale_event)
       self.pivot_scale.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()
       self.pivot_scale.configure(from_=1,
                                  to=len(self.dati),
                                  tickinterval=len(self.dati)//5
                                  )
       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
       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()
   
   def _scale_event(self, evt):
       '''Gestisce lo scroll del widget scale associato.'''
       if not self.dati: return
       value = int(self.pivot_scale.get()) - 1
       if value + self.righe >= len(self.dati):
           self.pivot  = len(self.dati) - self.righe
       else:
           self.pivot = value
       self._refresh_rows()


# *** VERSIONE COSTRUITA CON GRID ***

class GriFRMTmb(FRMTumbnail):
   '''Pannello per singola miniatura (tumbnail) con descrizione.'''
   def __init__(self, master, mater, imgdim=60):
       super().__init__(master, mater, imgdim)
   
   def _popola(self):
       self.default_bc = self.cget('bg')
       self.configure(relief='sunken', border=2, padx=2, pady=2)
       self.cnv_tmb = tk.Canvas(self,
                                width=self.dim_tmb,
                                height=self.dim_tmb,
                                bg='#ffffc0',
                                relief='raised',
                                border=2
                                )
       self.cnv_tmb.bind('<Button-1>', self._scelto)
       self.lbl_dida = tk.Label(self,
                               text='...',
                               justify='left',
                               )
       self.lbl_dida.bind('<Button-1>', self._scelto)
       self.cnv_tmb.grid(row=0, column=0, sticky='w')
       self.lbl_dida.grid(row=0, column=1, sticky='ew')
       self.columnconfigure(1, minsize=3*self.dim_tmb, weight=1)
       



class GriFRMTmbColl(FRMTmbCollection):
   '''Contenitore/gestore di un insieme di oggetti GriFRMTmb.'''
   def __init__(self, master, righe=2, dim=60, mater=None):
       super().__init__(master, righe, dim, mater)
   
   def _popola(self):
       self._crea_righe()
       self.pivot_scale = tk.Scale(self,
                                   from_=0,
                                   to=0,
                                   orient=tk.VERTICAL,
                                   command=self._scale_event)
       self.pivot_scale.grid(row=0, column=1, rowspan=self.righe, sticky='ns')
       self.columnconfigure(0, weight=1)
       self.columnconfigure(1, weight=0)      
   
   def _crea_righe(self):
       for i in range(self.righe):
           riga = FRMTumbnail(self, self, self.dim_tmb)
           riga.grid(row=i, column=0, sticky='ew')
           riga.set_indice(i)
           self.rows.append(riga)


ed un esempio di "come viene" in sede di test

png



... che dire, è stato il mio primo "esperimento" circa fare qualcosa "ad interfaccia grafica" con python, probabilmente ora lo farei un po' diverso, o forse non lo rifarei proprio, dato che sto cercando di abbandonare tkinter a favore di wxPython, comunque ogni tanto qualcuno legge questo post, è bene, quindi, completarlo seppur così in ritardo, magari a quel qualcuno potrebbe essere utile.

Per altro, nei forum, vedo che molti iniziandi si interessano a tkinter ... qualche esempietto e qualche piccola applicazione "carina" la ho realizzata con tale framework, se mi riesce magari ne posto qualcuna, anche se nessuna di esse adotta i patterns MVC/Observer (basilari per un buon design di interfacce grafiche)

Edited by nuzzopippo - 13/2/2021, 10:10
 
Web  Top
4 replies since 31/3/2019, 12:09   1053 views
  Share