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

[Python] Giochicchiando con matplotlib e tkinter, Un test per vedere

« Older   Newer »
  Share  
view post Posted on 15/5/2022, 08:50
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Salve agli improbabili lettori, ogni tanto mi prende l'impulso di venire qui e lasciare un appunto ... a dire il vero, nel tempo si è accumulata un sacco di roba che vorrei scrivere qui ma è talmente tanta e così abbondante che non saprei neanche da dove iniziare ... peccato.


Comunque, in questo caso, l'impulso mi è venuto da questo post nel Forum di python.it ove l'utente @Domedron chiedeva:
"Quesito: Se creo uno spazio di lavoro con tknter, grazie all'uso dei frame so come collocare gli oggetti: pulsanti, Entry, .... Ma se in un frame volessi mettere un grafico di matplotlib in uno di quei frame come dovrei fare?"

Tanto per cambiare, non conoscevo la liberia "matplotlib" ma una veloce guardata sulla docs e su stackoverflow mi ha fatto capire "come" poter utilizzare alcuni oggetti forniti da matplotlib nell'ambito di una applicazione grafica tkinter, cosa di cui ho comunicato l'essenziale nel post sopra citato.

Per altro l'argomento intriga anche me, dato che ho visto degli esempi di utilizzo delle trasformate di Fourier e matplotlib per definire degli spettri per campionare segnali audio, cosa che si sposerebbe meravigliosamente (ammesso mi riesca di capire Fourier) con una applicazione di archiviazione e classificazione di file multimediali audio/video che ho realizzato per gioco.

In attesa di decifrare l'astrusa matematica Foureriana (o almeno capire come utilizzarla) ho sperimentato come far definire e salvare ad un generico utente un minimale grafico a barre, permettendogli di scegliere il colore delle varie barre oltre che eventuali titolo ed etichetta verticale.
Devo dire che è stato sorprendentemente facile ottenere il risultato sotto in figura

png


matplotlib è uno strumento straordinario, molto potente, maturo ed amichevole, rimpiango di non averlo conosciuto quando ancora lavoravo, purtroppo doversi "sbrigare" non permette molto di cazzeggiare (e quindi imparare).
Comunque è pure una libreria molto estesa, che rende necessario un approfondito studio, oltre che una certa familiarità con metodi matematico-statistici, per poterla padroneggiare.
Come accennato, matplotlib fornisce dei particolari "strumenti" per permettere l'utilizzo della libreria in alcuni framework grafici, tra cui tlinter, tali strumenti sono contenuti nel modulo "matplotlib.backends", in particolare "backend_tkagg" fornisce la classe "FigureCanvasTkAgg" che istanziata restituisce un oggetto gestibile nell'ambito di una GUI tkinter.

Di per se FigureCanvasTkAgg non fornisce direttamente molti metodi, il più interessante è "drav()" che ridisegna il widget ... però eredita una possente API dalla sua classe-madre "FigureCanvasAgg", tutta da studiarsela ma che per gli scopi posti è stato sufficiente solo sfiorare superficialmente.

Utilizzo di matplotlib nella applicazione


Premesso che il metodo adottato è certo un po' rozzo, dato che si tratta del mio primissimo esperimento con matplotlib, osservando il costruttore riportato nella documantazione per la classe FigureCanvasTkAgg
CODICE
class matplotlib.backends.backend_tkagg.FigureCanvasTkAgg(figure=None, master=None, resize_callback=<deprecated parameter>)

si nota la presenza di due parametri, "figure" e "master", oltre ad un deprecato riferimento ad una funzione di callback.
Il parametro "master", banalmente, è la finestra che istanzia ed espone il grafico da realizzarsi, discorso diverso è invece il parametro "figure", la lettura della docs e di alcuni esempi forniti dalla stessa documentazione mi hanno fatto ritenere sia un oggetto di classe "matplotlib.figure.Figure", oggetto terribilmente complesso già a partire dal suo costruttore riportato nella documentazione che, tra le altre cose, permette di definire le dimensioni e la risoluzione del grafico e di definire degli "assi", in realtà "axes", un oggetto che, se già Figure è complesso, questo è da mettersi a piangere!
Pasticciato un pochino con Figure ed Axes, ottenendo risultati sconfortanti (gli assi NON si vedevano) mi è casulamente caduto l'occhio sul metodo "add_subplot()" di Figure, che tra l'altro, restituisce un oggetto "axes.SubplotBase", una prova di utilizzo con i valori di default è stato il punto di svolta che mi ha rivelato quanto in realtà sia amichevole matplotlib!
Nel corso delle ricerche varie effettuate mi ero reso conto di quanto lavoro si facesse con gli oggetti Axes, pertanto ho definito una funzione da utilizzare solo in fase di inizializzazione che costruisse e restituisse un oggetto "Figure" con valori di default, eseguendo sullo stesso un "add_subplot()" minimale memorizzando quale variabile di istanza l'oggetto axes.SubplotBase restituito
CODICE
def _make_fig(self):
       fig = Figure()
       self.ax = fig.add_subplot(111)
       labels = [x[0] for x in self.bars]
       values = [x[1] for x in self.bars]
       colors = [x[2] for x in self.bars]
       self.ax.bar(labels, values, color=colors)
       return fig

L'oggetto Figure restituito da self._make_fig, viene dato in pasto alla istanza di FigureCanvasTkAgg in fase della GUI che lo espone
CODICE
def _populate(self):
       fig = self._make_fig()
       self.canvas = FigureCanvasTkAgg(fig, self)
       self.canvas.draw()
       self.canvas.get_tk_widget().grid(row=0, column=0,
                                        padx=4, pady=4, sticky='nsew')
       ...

ovviamente, _populate() è il processo di definizione dei vari widget costituenti l'interfaccia grafica utente.

Da notare che l'oggetto di classe Figure instanziato, malgrado le sue numerose proprietà e metodi, non è stato neanche memorizzato come variabile di istanza nella gui, in effetti per i semplici scopi posti per questa prova sperimentale, ossia generare un grafico a barre a discrezione dell'user, è sufficiente agire tramite gli oggetti:
  • self.ax, oggetto di classe axes.SubplotBase restituito dall'oggetto "Figure" nel metodo "_make_fig(self)" della GUI (sarebbe a dire MyApp), che provvede alla definizione di titolo ed etichetta verticale, oltre che alla definizione delle barre-dati;
  • self.canvas, oggetto di classe FigureCanvasTkAgg instanziato nel metodo di inizializzazione "_populate(self)" della GUI, che provvede al ridisegno del grafico ed al suo salvataggio.
Anche la parte di codice specifica per la realizzazione (dinamica) del grafico è estremamente ridotta, una quindicina di righe, la maggior parte di recupero dati, sull'insieme di 325 costituenti l'applicazione.
Il grafico viene realizzato dal metodo "_plot(self)" della GUI
CODICE
def _plot(self):
       labels = [x[0] for x in self.bars]
       values = [x[1] for x in self.bars]
       colors = [x[2] for x in self.bars]
       self.ax.clear()
       if self.bar_title:
           self.ax.set_title(self.bar_title)
       if self.bar_y_label:
           self.ax.set_ylabel(self.bar_y_label)
       self.ax.bar(labels, values, color=colors)
       self.canvas.draw()

ove l'oggetto self.ax provvede ad azzerare il grafico esistente, tramite il suo metodo clear(), e quindi a definire, se stabiliti dall'user, il titolo tramite il suo metodo .set_title(stringa), etichetta verticale con il suo metodo set_ylabel(stringa), e le varie barre dati con il suo metodo bar(iterable, iterable, color=iterable); quindi l'oggetto self.canvas ri-disegna il tutto con il suo metodo draw().
Dal codice soprastante è evidente che la definizione delle barre dati viene memorizzata nella variabile di istanza self.bars della GUI che è una lista di tuple i cui vari elementi costituenti le singole tuple vengono estratti separatamente per costruire delle liste da dare in pasto a self.ax.bar(...), mentre titolo ed etichetta verticale hanno specifiche variabili di istanza nella GUI.

Anche il salvataggio del grafico in immagine, sbrigativamente impostata esclusivamente di tipo PNG, è estremamente ridotto
CODICE
def _on_save(self):
       f_name = fdg.asksaveasfilename(title='Salva grafico',
                                      initialfile='grafico',
                                      defaultextension='.png',
                                      filetypes=[('immagine png', '*.png')])
       if not f_name: return
       self.canvas.print_png(f_name)

in sostanza a parte presentare la sottostante finestra per ottenere il nome-file di destinazione

png


viene utilizzato da self.canvas il metodo print_png(nome_del_file) ereditato dalla classe-madre della sua istanza.
Come impostato (cioè SENZA alcuna gestione degli errori) il "salvataggio" potrebbe dare alcuni problemi, essendo oltre tutto richiamabile a discrezione dell'utente più volte in una sessione, ma una definizione puntigliosa della GUI esula dal suo essere "prova".


I colori delle "barre"


Per non perdersi nei meandri della definizione dei colori da parte dell'user, ho deciso di ricorrere ai color-names supportati da tkinter ... e qui ho avuto una piccola sorpresa perché matplotlib NON supporta l'intero insieme dei color-names supportati da tkinter.
Faccenda, questa, che mi ha costretto a ricorrere alla, veramente ottima, documentazione di matplotlib trovando soluzione nel modulo matplotlib.colors che definisce ben tre liste di colori supportati : BASE_COLORS, TABLEAU_COLORS e CSS4_COLORS

Ho approfittato di dette definizioni per stabilire un insieme di colori da esporre all'user definendo un canvas tkinter "ordinario" popolato (con un procedimento un pochino articolato a dire il vero) da un insieme di rettangoli colorati con il colore al momento in lista e scrivendoci dentro il color-name corrispondente con il colore in negativo ed effettuando il binding del tasto sinistro del mouse in modo tale che il colore del rettangolo selezionato vada a costituire il colore di sfondo di una etichetta di esempio ed il color-name valorizza la variabile di istanza self.color.

La definizione dei colori utilizzabili completa l'insieme delle iterazioni GUI-matplotlib, tutto il resto del codice riguarda la funzionalità della GUI e l'interazione con l'user.

... riguardo al metodo di "esposizione", come detto è un po' complesso, sarebbe lungo e fuori argomento esaminarlo, si rimanda al codice dei metodi della GUI _populate_colors(self, pan) e __negative_color(self, color, cnv) oltre che all callback __rec_or_txt_select(self, evt)

Esamineremo sommariamente, invece, il funzionamento della GUI

L'interfaccia utente


Come già anticipato all'inizio, l'applicazione è mirata alla realizzazione, da parte di un generico user, di un semplice grafico a barre realizzato con definizione a piacere dell'utente di vari elementi costituenti il grafico, limitatamente ai soli titolo, etichetta verticale e barre dati.
In sostanza è un semplice "studio di fattibilità" ridotto all'osso dell'utilizzo di matplotlib in una applicazione tkinter. Come tale, è carente di numerose caratteristiche che potrebbero occorrere in una applicazione "da produzione", tipo verifica permessi di scrittura, maggiori elementi costituenti il grafico, etc.
La "costruzione" del grafico avviene con una certa dinamicità, ogni volta che l'utente definisce un elemento che lo costituisce viene involato il metodo "_plot() della GUI ed il grafico ridisegnato.

png


La soprastante immagine mostra una sessione in fase iniziale, in cui l'user ha solo impostato il titolo e l'etichetta verticale premendo il tasto "Imposta" dopo aver compilato i campi relativi, il callback del tasto Imposta ("_on_title(self)") effettuerà l'aggiornamento dati ed il ridisegno, si rimanda al codice per dettagli.
La definizione di titolo ed etichetta verticale può essere effettuata più volte a discrezione dell'user nel corso della sessione, fatto salvo non si sia in condizione di inserimento dei valori per una barra di dati.
La definizione di una barra avviene in due fasi, entrambe finalizzate dal pulsante "Aggiungi barra" il cui callback ("_add_bar(self)") imposta una variabile di istanza della GUI ("self.state") in modalità di aggiunta dati alla prima pressione mentre alla seconda pressione verifica se siano stati valorizzati i campi "Etichetta", "Valore" e "Colore" e la loro congruenza con le tipologie previste, se tutto è in ordine aggiunge un record al dizionario dati, ridisegna il grafico ed aggiorna l'elenco delle barre definite.

png


Da tener presente che mentre i primi due campi dati sono valorizzati tramite digitazione nelle entry preposte, la valorizzazione del campo "Colore" avviene cliccando su di un rettangolo colorato nel canvas dei color-names di cui si è parlato in precedenza, cosa che può essere fatta in qualsiasi momento, nulla vieta che si utilizzi la stessa colorazione per turre le barre.

C'è un bug particolare che afflige il canvas dei colori, che mi ha fatto sorridere e pensare chisenefrega : il binding del canvas dei colori è stabilito sull'intero canvas che intercetta il controllo su cui è stato fatto click, ne estrae il colore associato e fornisce il color-name alla GUI ... ma in un "rettangolo colore" vi sono DUE item, il rettangolo stesso ed il name-color associato scrittoci nel centro colorato in negativo. Qualora venga cliccato il testo interno al rettangolo verrà fornito il codice esadecimale del colore del testo, che per matplotlib andrà bene ugualmente ... ed anche per l'applicazione se l'utente decide di lasciarlo li ;)

Per altro, comparando l'immagine sopra con quella precedente, si può osservare come i valori di riferimento di ascisse ed ordinate siano cambiati : ci ha provveduto matplotlib da se!

Un particolare che potrebbe interessare


Come accennato prima, una volta definita una barra-dati essa viene visualizzata nell'elenco delle barre definite, presente nella parte bassa della finestra, che riporta, per ogni singola barra, l'etichetta, il valore ed il colore assegnati in una visualizzazione di tipo tabellare.
Detta visualizzazione è realizzata utilizzando una classe ("DataPanel") da me "realizzata" specificatamente per la visualizzazione statica, intesa quale non modificabile, di dati tabellari e riposta nel mio "cassetto degli attrezzi" per tkinter ed il cui "costruttore"
CODICE
class DataPanel(tk.Frame):
   def __init__(self, parent, headings, func=None, invert=False, no_evidence=False, *args, **kwargs):
       self...

definisce delle particolari caratteristiche di instanza e funzionamento dipendenti da alcuni parametri definiti :

il parametro "headings", obbligatorio, è dedicato alla costruzione della riga di intestazione delle colonne di dati e richiede una lista di dizionari nella quale ogni dizionario definisce l'intazione di una singola colonna e contiene cle chiavi
  • "name" : è il nome interno della colonna;
  • "label : l'etichetta da scrivere per la colonna;
  • "measure" : una stringa che sarà valutata per definire la dimensione della colonna, se assente (valore None) la colonna avrà dimensione variabile ed adattativa allo spazio disponibile altrimenti sarà della giusta dimensione fissa.
In sostanza, una fase occorrente in fase di instanziamento di un data panel è la creazione di un tale dizionario, nel nostro caso avviene in fase di inizializzazione, verso la fine del metodo "_populate()"
CODICE
heads = [{'name': 'etichetta', 'label': 'Etichetta', 'measure': 'X'*12},
                {'name': 'valore', 'label': 'Valore', 'measure': ' 00000 '},
                {'name': 'colore', 'label': 'Colore', 'measure': None}]
       self.data_p = DataPanel(par_pnl, heads, func=self.selected_bar, no_evidence=True)

In questo test è fissa, definita una singola volta ma in contesti di utilizzo uso pre-definire una serie di righe di intestazione da utilizzare dinamicamente nella stessa finestra distruggendo e ricreando dei DataPanel per diverse rappresentazioni di dati eterogenei.

Il parametro "func" permette di collegare una funzione della classe chiamante (il "parent") cui trasmettere un elemento dati eventualmente selezionato da trattare.

DataPanel prevede colorazione diversificata delle singole righe dati in base al valore (booleano) dell'ultimo elemento della singola riga dati, tale colorazione può essere invertita ponendo a True il parametro "invert", non utilizzato nello instanziamento di questa applicazione, oppure soppressa, lasciando la colorazione di default per tutte le righe di dati, ponendo a Ture il parametr "no_evidence", come fatto nel caso in specie.

L'aggiornamento dati avviene invocando il metodo "_update_data(self, message)" di DataPanel, il parametro "message", così denominato perché il contesto d'uso per cui ho pensato DataPanel è di tipo "Publisher/Subscriber", si aspetta di ricevere una lista di liste o di tuple che, in ogni caso, sarà indicizzata internamente.

Per la rappresentazione dei dati viene utilizzato un ttk.Treeview.

Nel nostro caso, abbiamo assegnato al DataPanel il metodo "selected_bar" di MyApp quale funzione per il trattamento di un elemento (barra dati) selezionato in DataPanel, tale funzione provvederà, se NON si è in fase di definizione di una nuova barra, a rendere corrente l'elemento selezionato, che potrà, nel caso essere cancellato

png


l'immagine soprastante riporta una esemplificazione, ho assegnto alla lattuga lo stesso colore che intendo utilizzare per le carote, non va, quindi seleziono la lattuga nell'elenco, essa diventerà l'elemento corrente, e premo il pulsante "Rimuovi barra" che, obbediente, la eliminirà senza chiedere nulla.

Per ragioni di brevità (essendo solo un test di massima) non ho voluto implementare la logica per la modifica dei dati, in ogni caso ci vuole poco ad inserli nuovamente ed una volta raggiunta una situazione soddisfacente, tipo questa

png


premere il pulsante "Salva" permetterà di produrre una immagine PNG da utilizzare poi per i propri scopi.

È tutto, anche se l'utente @Domedron non si è dimostrato interessato ad approfondire il discorso mi ha intrigato vedere un po' di contesto e toccare con mano quanto sia semplice un utilizzo minimale delle enormi potenzialità di matplotlib in una applicazione tkinter, le librerie richieste sono PIL e matplotlib, oltre, naturalmente, tkinter, ttk e qualcosa della libreria standard, questo post è a mia promemoria nel caso mi serva in futuro.
Se capitasse un qualche improbabile interessato lettore che volesse vedere 'sta cosa, sotto il codice sorgente completo del test

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

from tkinter import font as fnt
import tkinter as tk
from tkinter import messagebox as msgb
from tkinter import ttk
from tkinter import filedialog as fdg
from PIL import Image
import io

import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import matplotlib.colors as mcolors


class DataPanel(tk.Frame):
   def __init__(self, parent, headings, func=None, invert=False, no_evidence=False, *args, **kwargs):
       self.parent = parent
       self.parent_function = func
       self.invert = invert
       self.no_evidence = no_evidence
       self.data_list = []
       self.state = ''
       super().__init__(self.parent, *args, **kwargs)
       cols = [x['name'] for x in headings]
       labels = [x['label'] for x in headings]
       measures = [x['measure'] for x in headings]
       self.data_view = ttk.Treeview(self, columns=cols, show='headings', selectmode='browse')
       for i in range(len(cols)):
           self.data_view.heading(cols[i], text=labels[i])
           if measures[i]:
               col_w = fnt.Font().measure(measures[i])
               self.data_view.column(cols[i], width=col_w, stretch=False)
           else:
               self.data_view.column(cols[i], stretch=True)
       if not self.invert:
           self.data_view.tag_configure('active', background='white')
           self.data_view.tag_configure('noactive', background='#fffdcd')
       else:
           self.data_view.tag_configure('active', background='#fffdcd')
           self.data_view.tag_configure('noactive', background='white')
       self.data_view.grid(row=0, column=0, sticky='nsew')
       v_scroll = ttk.Scrollbar(self, orient=tk.VERTICAL, command=self.data_view.yview)
       v_scroll.grid(row=0, column=1, sticky='ns')
       h_scroll = ttk.Scrollbar(self, orient=tk.HORIZONTAL, command=self.data_view.xview)
       h_scroll.grid(row=1, column=0, sticky='ew')
       self.data_view.configure(yscrollcommand=v_scroll.set)
       self.data_view.configure(xscrollcommand=h_scroll.set)
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(0, weight=1)
       self.data_view.bind('<<TreeviewSelect>>', self._on_select)        
   
   def _on_select(self, evt=None):
       if self.parent_function is None: return
       selection = self.data_view.selection()
       if not selection: return
       curr_iid = selection[0]
       curr_item = None
       for el in self.data_list:
           if el[-1] == curr_iid:
               curr_item = el
               break
       if curr_item is None: return
       self.parent_function(curr_item[:-1])

   def _update_data(self, message):
       '''Aggiorna l'elenco delle scadenze trovate.'''
       # Azzeramento dei dati presenti nella treeview ed in self.data_list
       self.data_list.clear()
       childrens = None
       childrens = self.data_view.get_children('')
       if childrens:
           for elem in childrens:
               self.data_view.delete(elem)
       if len(message) == 0: return
       # caricamento nuovi dati
       for row in message:
           if not self.no_evidence:
               if row[-1]:
                   node_id = self.data_view.insert('', 'end', values=row[:-1], tags=('active',))
               else:
                   node_id = self.data_view.insert('', 'end', values=row[:-1], tags=('noactive',))
           else:
               node_id = self.data_view.insert('', 'end', values=row, tags=('active',))
           l_row = list(row)
           l_row.append(node_id)
           self.data_list.append(l_row)

class MyApp(tk.Tk):
   def __init__(self):
       super().__init__()
       self.title('Prova Domedron 2')
       self.color_names = []
       self.bars = []
       self.bar_title = ''
       self.bar_y_label = ''
       self.state = ''
       self.sel_index = None
       self.color = None
       self._populate()

   def _populate(self):
       fig = self._make_fig()
       self.canvas = FigureCanvasTkAgg(fig, self)
       self.canvas.draw()
       self.canvas.get_tk_widget().grid(row=0, column=0,
                                        padx=4, pady=4, sticky='nsew')
       # blocco impostazione titoli grafico
       title_pnl = tk.Frame(self)
       title_pnl.grid(row=1, column=0, padx=4, pady=4, sticky='nsew')
       lbl = tk.Label(title_pnl, text='Titolo :')
       lbl.grid(row=0, column=0, padx=4, pady=4, sticky='w')
       self.e_title = tk.Entry(title_pnl, width=20)
       self.e_title.grid(row=0, column=1, padx=4, pady=4, sticky='ew')
       lbl = tk.Label(title_pnl, text='Etich. vert. :')
       lbl.grid(row=0, column=2, padx=4, pady=4, sticky='w')
       self.e_ylabel = tk.Entry(title_pnl, width=20)
       self.e_ylabel.grid(row=0, column=3, padx=4, pady=4, sticky='ew')
       btn = tk.Button(title_pnl, text='Imposta', command=self._on_title)
       btn.grid(row=0, column=4, padx=4, pady=4, sticky='ew')
       title_pnl.grid_columnconfigure(1, weight=1)
       title_pnl.grid_columnconfigure(3, weight=1)
       # blocco impostazione barre
       par_pnl = tk.Frame(self)
       par_pnl.grid(row=2, column=0, padx=4, pady=4, sticky='nsew')
       lbl = tk.Label(par_pnl, text='Etichetta :')
       lbl.grid(row=0, column=0, padx=4, pady=4, sticky='w')
       self.e_label = tk.Entry(par_pnl, width=10)
       self.e_label.grid(row=0, column=1, padx=4, pady=4, sticky='w')
       lbl = tk.Label(par_pnl, text='Valore :')
       lbl.grid(row=1, column=0, padx=4, pady=4, sticky='w')
       self.e_value = tk.Entry(par_pnl, width=6)
       self.e_value.grid(row=1, column=1, padx=4, pady=4, sticky='w')
       lbl = tk.Label(par_pnl, text='Colore :')
       lbl.grid(row=2, column=0, padx=4, pady=4, sticky='w')
       self.l_color = tk.Label(par_pnl, text='Colore Barra')
       self.l_color.grid(row=2, column=1, padx=4, pady=4, sticky='nsew')
       # blocco pannello dei colori per le barre
       c_colors = tk.Canvas(par_pnl, width=200, height=100)
       c_colors.grid(row=0, column=2, rowspan=3, stick='nsew')
       v_scroll = tk.Scrollbar(par_pnl)
       v_scroll.grid(row=0, column=3, rowspan=3, sticky='ns')
       v_scroll['command'] = c_colors.yview
       c_colors['yscrollcommand'] = v_scroll.set
       c_colors.bind('<Button-1>', self.__rec_or_txt_select)
       # comandi per barre
       bar_cmd_pnl = tk.Frame(par_pnl)
       bar_cmd_pnl.grid(row=3, column=0, columnspan=4,
                        padx=4, pady=4, sticky='ew')
       bt = tk.Button(bar_cmd_pnl, text='Aggiungi barra', command=self._add_bar)
       bt.grid(row=0, column=0, padx=4, pady=4, sticky='ew')
       bt = tk.Button(bar_cmd_pnl, text='Rimuovi barra', command=self._del_bar)
       bt.grid(row=0, column=1, padx=4, pady=4, sticky='ew')
       bt = tk.Button(bar_cmd_pnl, text='Salva', command=self._on_save)
       bt.grid(row=0, column=2, padx=4, pady=4, sticky='ew')
       bt = tk.Button(bar_cmd_pnl, text='Chiudi', command=self.destroy)
       bt.grid(row=0, column=3, padx=4, pady=4, sticky='ew')
       bar_cmd_pnl.grid_columnconfigure(0, weight=1, uniform='cmd')
       bar_cmd_pnl.grid_columnconfigure(1, weight=1, uniform='cmd')
       bar_cmd_pnl.grid_columnconfigure(2, weight=1, uniform='cmd')
       bar_cmd_pnl.grid_columnconfigure(3, weight=1, uniform='cmd')
       heads = [{'name': 'etichetta', 'label': 'Etichetta', 'measure': 'X'*12},
                {'name': 'valore', 'label': 'Valore', 'measure': ' 00000 '},
                {'name': 'colore', 'label': 'Colore', 'measure': None}]
       self.data_p = DataPanel(par_pnl, heads, func=self.selected_bar, no_evidence=True)
       self.data_p.grid(row=4, column=0, columnspan=4,
                        padx=4, pady=4, sticky='nsew')
       par_pnl.grid_columnconfigure(1, weight=1)
       
       self.update()
       self._populate_colors(c_colors)
       
       self.grid_rowconfigure(0, weight=1)
       self.grid_columnconfigure(0, weight=1)

   def _make_fig(self):
       fig = Figure()
       self.ax = fig.add_subplot(111)
       labels = [x[0] for x in self.bars]
       values = [x[1] for x in self.bars]
       colors = [x[2] for x in self.bars]
       self.ax.bar(labels, values, color=colors)
       return fig

   def _plot(self):
       labels = [x[0] for x in self.bars]
       values = [x[1] for x in self.bars]
       colors = [x[2] for x in self.bars]
       self.ax.clear()
       if self.bar_title:
           self.ax.set_title(self.bar_title)
       if self.bar_y_label:
           self.ax.set_ylabel(self.bar_y_label)
       self.ax.bar(labels, values, color=colors)
       self.canvas.draw()
   
   def _populate_colors(self, pan):
       colors = list(mcolors.BASE_COLORS)
       colors += list(mcolors.TABLEAU_COLORS)
       colors += list(mcolors.CSS4_COLORS)
       if not colors:
           return
       row = 0
       posy = 0
       dim_x = pan.winfo_width()
       my_font = fnt.Font(size=8, family='TkDefaultFont')
       dim_ty = my_font.metrics('linespace')
       incr = int(dim_ty * 2)
       for color in colors:
           try:
               rid = pan.create_rectangle((0, posy, dim_x, posy+incr), fill=color)
               n_c = self.__negative_color(color, pan)
               dim_tx = my_font.measure(color)
               x = dim_x // 2
               y = posy + incr//2
               pan.create_text(x, y, text=color, fill=n_c)
               pan.tag_lower(rid)
               posy += incr
               # i bind dopo i test
               #pan.tag_bind(rid, '<Button1>', self.__rec_select)
               row += 1
           except:
               pass
       pan.configure(scrollregion=(0, 0, dim_x, posy))

   def __negative_color(self, color, cnv):
       '''
       restituisce il negativo di un named-color

       parametri : color : il color-named da trattare
                   cnv   : il canvas in cui verrà applicato, può essere, comunque
                           un generico widget con che dispone del metodo winfo_rgb
       '''
       c_rgb = cnv.winfo_rgb(color)
       n_rgb = (255-c_rgb[0]//256, 255-c_rgb[1]//256, 255-c_rgb[2]//256)
       return '#%02x%02x%02x' % (n_rgb)

   def __rec_or_txt_select(self, evt):
       '''
       Assegna il colore dell'elemento
       cliccato nel canvas dei colori alla label di esempio,
       '''
       color = evt.widget.itemcget('current', 'fill')
       self.l_color.configure(bg=color)
       n_c = self.__negative_color(color, evt.widget)
       self.l_color.configure(fg=n_c)
       self.color = color

   def _on_title(self):
       if self.state: return
       self.bar_title = self.e_title.get()
       self.bar_y_label = self.e_ylabel.get()
       self._plot()
       
   def _add_bar(self):
       if not self.state:
           self.e_label.delete(0, 'end')
           self.e_value.delete(0, 'end')
           self.state = 'add'
           self.e_label.focus_set()
           return
       new_label = self.e_label.get()
       if new_label == '':
           msg = 'Etichetta non definita'
           msgb.showwarning('Carenza dati', msg)
           self.e_label.focus_set()
           return
       if new_label in [x[0] for x in self.bars]:
           msg = 'Etichetta già esistente'
           msgb.showwarning('Carenza dati', msg)
           self.e_label.focus_set()
           return
       value = self.e_value.get()
       if value == '' or not value.isnumeric():
           msg = 'Il valore deve essere un numero intero'
           msgb.showwarning('Dati difformi', msg)
           self.e_value.focus_set()
           return
       if not self.color:
           msg = 'Nessun colore corrente'
           msgb.showwarning('Carenza dati', msg)
           return            
       self.bars.append((new_label, int(value), self.color))
       self.data_p._update_data(self.bars)
       self._plot()
       self.state = ''
       
   def _del_bar(self):
       if self.state: return
       if self.sel_index is None: return
       del self.bars[self.sel_index]
       self.sel_index = None
       self.data_p._update_data(self.bars)
       self._plot()

   def selected_bar(self, item):
       if self.state: return
       self.sel_index = None
       for i in range(len(self.bars)):
           if self.bars[i][0] == item[0]:
               self.sel_index = i
               break
       if self.sel_index == None: return
       bar = self.bars[self.sel_index]
       self.e_label.delete(0, 'end')
       self.e_label.insert('end', bar[0])
       self.e_value.delete(0, 'end')
       self.e_value.insert('end', bar[1])
       color = bar[2]
       self.l_color.configure(bg=color)
       n_c = self.__negative_color(color, self.l_color)
       self.l_color.configure(fg=n_c)

   def _on_save(self):
       f_name = fdg.asksaveasfilename(title='Salva grafico',
                                      initialfile='grafico',
                                      defaultextension='.png',
                                      filetypes=[('immagine png', '*.png')])
       if not f_name: return
       self.canvas.print_png(f_name)


if __name__ == '__main__':
   app = MyApp()
   app.mainloop()
 
Web  Top
0 replies since 15/5/2022, 08:50   110 views
  Share