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

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

« Older   Newer »
  Share  
nuzzopippo
view post Posted on 12/4/2019, 15:10 by: nuzzopippo
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

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
4 replies since 31/3/2019, 12:09   1066 views
  Share