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

[Python] Messaggi per un ttk.Notebook

« Older   Newer »
  Share  
nuzzopippo
view post Posted on 14/10/2021, 16:33 by: nuzzopippo
Avatar

Nubbio x Sempre

Group:
Moderazione globale
Posts:
7,226

Status:


Riflettendo un po' mi son reso conto che è opportuno affrontare la modalità adottata nella gestione dei mici PRIMA di affrontare il discorso della gestione a livello di GUI utente. Parliamo ora della interazione GUI=>Modell implementata.

pypubsub: funzionamento elementare


Il modulo pubsub è una implementazione python dei pattern Publisher, in sostanza fornisce una API per lo scambio di messaggi tra elementi "registrati". Il modulo implementa il pattern "Singleton", il che vuol significare che esiste una ed una sola istanza del modulo attiva nella applicazione ... il che rende questo modulo molto comodo*, istanze diverse di pubsub effettuate in moduli diversi agiranno sullo stesso oggetto.

* rende anche disponibili delle possibilità difficilmente "pensabili", tipo istanziare il modello dati completamente al di fuori del mainloop tkinter, cosa testata apportando leggerissime modifiche a quanto qui esposto

Il funzionamento generale è concettualmente piuttosto semplice :
  1. si iscrive un metodo di un oggetto in un "gruppo";
  2. si invia un messaggio a pubsub indirizzato ad un dato "gruppo", pubsub provvederà a smistare il messaggio a tutti gli iscritti del "gruppo";
  3. si rimuove l'iscrizione del metodo di un oggetto quando non è più utile.
i "gruppi" possono essere più di uno, possono avere "sotto-gruppi e non necessariamente devono essere conosciuti a priori; inoltre, i "messaggi" possono essere oggetti qualsiasi.

Già l'utilizzo nella forma più elementare (per le avanzate vedere la docs) di pypubsub, ossia l'utilizzo dei metodi "subscribe(obj.method, group)", "sendMessage(group, message)" e "unsubscribe(obj.method, group)" di pub permette di definire un protocollo di gestione dati sufficientemente efficace per il nostro semplice "caso".

I Gruppi


Il passo preliminare è definire i gruppi di sottoscrizione, valutando le necessità della applicazione ho optato per la definizione di due gruppi, diciamo così, "fissi" e di un gruppo "composto", sotto descritti
  1. gruppo REQUEST : fisso, ascolta una "richiesta", unico iscritto il metodo "_requests(self, message)" della classe model.Cats, iscrizione permanente per tutta la durata della applicazione;
  2. gruppo LISTUPDATED : fisso, ascolta una comunicazione di aggiornamento dell'elenco dei gatti, unico iscritto il metodo "update_values(self, message)" della classe view.ListPanel, la sottoscrizione avviene ad instanziamento di un ListPanel e cancellata prima della sua distruzione;
  3. gruppo CATUPDATED.cat_name : composto, la parte variabile è "cat_name" ed individua un aggiornamento dati di uno specifico "gatto", unico iscritto il metodo "update_values(self, message)" della classe view.CatPanel cui viene assegnato il "cat_name", la sottoscrizione avviene ad instanziamento di un CatPanel e cancellata prima della sua distruzione.
Pur se nulla avrebbe impedito di effettuarle nelle classi Micio, ListPanel e CatPanel, si è scelto, per facilità di controllo, di effettuare quasi tutta la produzione dei messaggi nelle sole classi, diciamo principali, "Cats" e "MyApp", quest'ultima provvede anche alla sottoscrizione e cancellazione dei pannelli dai gruppi di sottoscrizione.

Anche se non ce ne sarebbe bisogno, si precisa che le sottoscrizioni possono essere multiple per ogni singolo gruppo, anche se nella nostra applicazione ciò non avviene.

Nello stralcio di codice sottostante, tratto dal metodo "view_cat(self, name)" della classe MyApp mostra la creazione e registrazione di un pannello per la visualizzazione dei dati di un dato "gatto" già esistente
CODICE
pnl = CatPanel(self.book)
       pnl.set_state('')
       self.book.add(pnl, text=name)
       self.book.select(pnl)
       pub.subscribe(pnl.update_values,
                     'CATUPDATED.' + '_'.join(name.split()))
       msg = ['SHOWCAT', name]
       pub.sendMessage('REQUEST', message=msg)

Mentre lo stralcio del callback di MyApp che segue mostra la cancellazione della sottoscrizione, eliminazione dal notebook e distruzione del pannello correntemente visualizzato e chiuso dall'utente
CODICE
def _on_close(self):
       label = self.book.tab(self.book.select(), 'text')
       item = self.book.nametowidget(self.book.select())
       self.book.forget(self.book.select())
       if label == 'Elenco gatti':
           pub.unsubscribe(item.update_values, 'LISTUPDATED')
       else:
           data = item.get_data()
           try:
               pub.unsubscribe(item.update_values, 'CATUPDATED.' +
                               '_'.join(data[0].split()))
           except:
               pass
       item.destroy()
       self.update()
       self._evaluate_self()


Il "Vocabolario" di comunicazione


Naturalmente, quando oggetti "diversi" devono scambiarsi messaggi deve essere stabilita una forma di dialogo conosciuta da tutti i soggetti interessati, contenente, per altro, i dati necessari per soddisfare le azioni opportune al messaggio ricevuto.
Nella implementazione realizzata, un elemento che abbia bisogno sia effettuata una certa azione invia al gruppo "REQUEST" un messaggio contenente il tipo di azione richiesta e gli eventuali dati necessari alla sua realizzazione.
Come detto in precedenza, nella nostra applicazione l'unico sottoscrittore al gruppo REQUEST è il metodo "_requests(self, message)" di Cats, il codice di tale metodo evidenzierà il "vocabolario" comune tra la view ed il modello dati
CODICE
def _requests(self, message):
       request = message[0]
       if request == 'GETLIST':
           self._pub_list()
       elif request == 'ADDCAT':
           self._add(message[1:])
       elif request == 'SHOWCAT':
           self._show(message[1])
       elif request == 'UPDATECAT':
           self._update(message[1:])
       elif request == 'CATDELETE':
           self._delete(message[1])


I Messaggi


dal codice sin qui esposto si evince facilmente che i messaggi costituenti le richieste sono delle liste, di uno o più oggetti, nel quale il primo degli elementi identifica l'azione richiesta, i restanti sono i dati indispensabili ad effettuare l'azione.
Segue ora una disamina di detto "vocabolario"

GETLIST
  • struttura :['GETLIST']
  • Azione : emissione messaggio con la lista di oggetti "Micio"
  • messaggio conclusivo emesso : ('LISTUPDATED', self.cats)

ADDCAT
  • struttura : ['ADDCAT', nome, immagine, descrizione]
  • Azione : aggiunge un nuovo gatto, memorizzando l'immagine nella directory di cache, aggiorna la persistenza dati. Se il gatto esiste già emette un ValueError
  • messaggio conclusivo emesso : ('LISTUPDATED', self.cats)

SHOWCAT
  • struttura : ['SHOWCAT', nome]
  • Azione : invia i dati di uno specifico gatto
  • messaggio conclusivo emesso : ('CATUPDATED._nome', [nome, immagine, descrizione])

UPDATECAT
  • struttura : ['UPDATECAT', nome, immagine, descrizione]
  • Azione : sostituisce i dati esistenti di uno specifico gatto con i nuovi, eventualmente elimina la vecchia immagine dalla cache e ci memorizza la nuova. Se non viene rintracciato il gatto emette un ValueError
  • messaggio conclusivo emesso : ('CATUPDATED._nome', [nome, immagine, descrizione]) e ('LISTUPDATED', self.cats)

CATDELETE
  • struttura : ['CATDELETE', nome]
  • Azione : elimina un gatto esistente ed aggiorna la persistenza dati. Se il gatto non viene trovato emette un ValueError
  • messaggio conclusivo emesso : ('LISTUPDATED', self.cats)

Naturalmente, i messaggi al gruppo "LISTUPDATE" vengono intercettati dal metodo "update_values" dell'oggetto "view.ListPanel", se un tale pannello è aperto, mentre i messaggi di gruppo "CATUPDATED._nome" verranno intercettati dal metodo "update_values" dell'istanza di "view.CatPanel" registrata al gruppo, i pannelli provvederanno ad aggiornare la loro rappresentazione.

Cosa avviene nella view


Credo sia ora di esaminare per sommi capi (complessivamente siamo a circa 650 righe di codice ;) ) come funzionano le cose nella view, con particolare attenzione sulla gestione del notebook.

Premettiamo che il codice in esame basa la sua funzionalità sullo scambio di messaggi e sul protocollo appena descritti, non avendo quasi per nulla rapporti diretti con le classi del modulo "model" (in una ulteriore versione qui non discussa neanche importato), si aspetta solo di ricevere degli oggetti che abbiano delle determinate proprietà, cosa siano non interessa, ed invia degli oggetti con certe caratteristiche, che fine facciano non riguarda la view. I punti focali sono i metodi "update_values(self, message)" definiti nei pannelli visti nel post precedente, per la ricezione dati, ed il sistema di messaggistica gestito dai callback dei pulsanti disponibili nella finestra, che danno all'utente la possibilità di decidere cosa vuole fare.

Un ulteriore elemento "importante" è la label dei tabs presenti nel notebook, utilizzata quale discriminante in diversi processi, di norma essa è il "nome" del "gatto" ed in ogni caso DEVE essere unica, non è consentito, in sostanza, avere due tabs con lo stesso testo ... potrebbe essere interessante lo stralcio di codice sottostante
CODICE
if name == 'Elenco gatti':
           msgb.showwarning('Nuovo micio', 'Nome gatto inaccettabile.',
                            parent=self)
           return
       labels = [self.book.tab(x, 'text') for x in
                 self.book.winfo_children()]
       if name in labels:
           msgb.showwarning('Nuovo micio', 'Micio già presente e visualizzato',
                            parent=self)
           return

esso è tratto dal callback "_on_new(self)" collegato al pulsante "Nuovo" di MyApp, che da all'user la possibilità di definire un nuovo gatto.
Si noti come il nome (name) richiesto all'utente nel callback venga verificato sia diverso dalla stringa "Elenco gatti", riservata al solo ListPanel, e come sia controllato non sia presente nella lista labels estratta tramite la list comprehension nel codice.
Riguardo la comprehension è banale che "self.book.tab(x, 'text')" legge la label del singolo tab dell'insieme dei tabs aperti nel notebook ed estratti con "self.book.winfo_children()".

A questo punto e forse il caso di esaminare qualcosa del resto del callback, la parte
CODICE
pnl = CatPanel(self.book)
       pnl.set_name(name)
       pnl.set_state(self.state)
       self.book.add(pnl, text=name)
       self.book.select(pnl)

definisce un nuovo CatPanel con assegnato il notebook quale suo parent, gli assegna alcune proprietà, lo aggiunge al notebook tramite il metodo add di quest'ultimo e lo rende quale corrente (self.book.select(pnl)).
Per altro, nella condizione di nuovo inserimento ('new') od anche in quella di modifica ('modify') sono disponibili dei comandi che possono modificare delle proprietà (solo l'immagine ;) ) del pannello corrente ... non è il caso che possa venir selezionato un'altro, questo stralcio di codice impedisce possa essere fatto
CODICE
for item in self.book.winfo_children():
           if str(item) != self.book.select():
               self.book.tab(item, state='disabled')

Noterete che qui utilizziamo la stessa istruzione self.book.winfo_children() per esaminarne ogni "item", che non è proprio il nostro pannello ma un oggetto, chiamato "window" nella documentazione del quale estrae la stringa descrittiva comparandola con quella restituita dal tab correntemente selezionato e se diversa disabilitando lo item

Quest'ultimo procedimento viene utilizzato anche in caso l'utente voglia modificare l'immagine o la descrizione del "gatto" e prema il pulsante "Modifica"
CODICE
def _on_modify(self):
       for item in self.book.winfo_children():
           if str(item) != self.book.select():
               self.book.tab(item, state='disabled')
       item = self.book.nametowidget(self.book.select())
       self.state = 'modify'
       item.set_state(self.state)
       self._evaluate_self()

nel cui callback vediamo come possiamo ottenere il nostro CatPanel, tramite l'istruzione "item = self.book.nametowidget(self.book.select())", ed operarci su.
Metodo "importante" nametowidget, utilizzata ogni volta si è dovuto operare con un CatPanel ... ovviamente, il pulsante "Modifica" non è disponibile se è selezionato un ListPanel.

Una volta entrati in modalità di inserimento o modifica dati, saranno disponibili i comandi "Immagine", "Salva" ed "Annulla" che permetteranno, rispettivamente, di inserire/cambiare l'immagine del gatto, salvare i dati oppure annullare il processo corrente, distruggendo il tab ed il CatPanel se nuovo inserimento. Si rimanda ai callback "_on_image(self)", "_on_save(self)" e "_on_cancel(self)" di MyApp per i relativi dettagli.

Lo OP del post da cui è scaturito il presente intendeva rappresentare una scheda per ogni suo gatto esistente ... discorso che va bene se si hanno solo pochi gatti ma nella macchina su cui sviluppo non ho gatti, ho però gattine tipo questa

png


e sono molte :lol: ho ritenuto opportuno, quindi, dare l'opportunità di aprire e chiudere la visualizzazione di una micetta ... ops, gatto a volontà dell'utente.

Nel contesto attuale dell'esposizione, diamo la precedenza alla chiusura della visualizzazione, ottenibile utilizzando il pulsante "Chiudi" che provocherà la chiusura del tab corrente ed il cui callback è interessante
CODICE
def _on_close(self):
       label = self.book.tab(self.book.select(), 'text')
       item = self.book.nametowidget(self.book.select())
       self.book.forget(self.book.select())
       if label == 'Elenco gatti':
           pub.unsubscribe(item.update_values, 'LISTUPDATED')
       else:
           data = item.get_data()
           try:
               pub.unsubscribe(item.update_values, 'CATUPDATED.' +
                               '_'.join(data[0].split()))
           except:
               pass
       item.destroy()
       self.update()
       self._evaluate_self()

nel codice vedete utilizzare metodi già noti per estrarre la label ed il pannello dal tag corrente e quindi eliminarlo dal notebook tramite il suo metodo (nuovo) "forget(item)", quindi distruggere il pannello ed aggiornare la finestra.
... invito, però, a porre molta attenzione alle istruzioni "unbsubscribe" di pub è necessario farle e prima di distruggere il pannello o avrete dei tentativi di notifica ad oggetti inesistenti che daranno errori non gestibili nella view o che necessiterebbero di un sistema intercettazione più complesso ed integrato nel model.

Per altro, la sottoscrizione dei pannelli avviene ad ogni loro apertura su dati esitenti, ovvero alla registrazione di nuovi dati con istruzioni tipo sotto
CODICE
pub.subscribe(pnl.update_values, 'LISTUPDATED')
       ...
       pub.subscribe(pnl.update_values,
                     'CATUPDATED.' + '_'.join(name.split()))

si vedano il callback "_on_list(self)" e "_on_save(self)" oltre ai metodi "view_cat(self, name)" e "cat_deleted(self, name)" di MyApp per dettagli in merito.

Memorizzazione della finestra


Una ulteriore richiesta del più volte citato OP era "salvare le schede aperte alla chiusura e riaprirle al nuovo avvio" ... beh, perché non farlo?
Ho quindi deciso di salvare le dimensioni della finestra e gli eventuali tabs aperti al momento della chiusura.

Salvataggio


per salvare lo "stato" della finestra nella sua inizializzazione ho dirottato l'handler del protocollo su di un apposito callback che interviene in fase di chiusura
CODICE
self.protocol('WM_DELETE_WINDOW', self._on_destroy)


e nel callback memorizzo in un dizionario i dati interessanti che salvo poi in un file json localizzato nella direttrice predefinita per i dati, chiudendo quindi la finestra
CODICE
def _on_destroy(self, evt=None):
       '''Memorizza lo stato corrente e chiude,'''
       conf_current = {}
       conf_current['width'] = self.winfo_reqwidth()
       conf_current['height'] = self.winfo_reqheight()
       labels = [self.book.tab(x, 'text') for x in
                 self.book.winfo_children()]
       conf_current['tabs'] = labels
       dirs = model.def_dirs()
       f_name = os.path.join(dirs['data'], 'appstate.json')
       try:
           with open(f_name, mode='w', encoding='utf-8') as f:
               json.dump(conf_current, f)
       except OSError:
           pass
       self.destroy()


Caricamento


per ricaricare i dati, verifico esista il file di salvataggio e nel caso apro il file e ricarico il dizionario con json, quindi ridefinisco le dimensioni della finestra e leggo le eventuali labels dei tag memorizzate, invocando il callback "_on_list()" od il metodo "view_cat(...)" di MyApp a seconda del caso.
CODICE
def _load_conf(self):
       dirs = model.def_dirs()
       f_name = os.path.join(dirs['data'], 'appstate.json')
       prec_conf = None
       if not os.path.exists(f_name) or not os.path.isfile(f_name):
           return
       try:
           with open(f_name, mode='r', encoding='utf-8') as f:
               prec_conf = json.load(f)
       except OSError:
           pass
       if prec_conf is None: return
       # ridimensiona la finestra e la centra
       l = self.winfo_screenwidth()
       a = self.winfo_screenheight()
       wx = prec_conf['width']
       wy = prec_conf['height']
       self.geometry('{}x{}+{}+{}'.format(wx, wy, (l-wx)//2, (a-wy)//2))
       # ricrea gli eventuali tabs
       labels = prec_conf['tabs']
       if not labels: return
       for t in labels:
           if t == 'Elenco gatti':
               self._on_list()
           else:
               self.view_cat(t)
       self.update()


... FINITO! :D

Giocattolo magari un po' rozzo, essendo la prima volta che sperimenti pypubsub e ttk.Notebbok, ma che ho trovato divertente realizzare, nel post successivo troverete il codice completo per provare quanto su esposto.

Ciao

Ed ora il codice completo.

Per poterlo vedere in azione dovrete creare un venv importando i moduli indicati nel primo post, quindi attivarlo e lanciare da terminale il comando
CODICE
python view.py


i moduli sono due e devono essere posizionati nella stessa directory (che, ovviamente, è la stessa in cui darete il comando di lacio)

model.py
CODICE
# -*- coding: utf-8 -*-

import appdirs
import os
from pubsub import pub
import sys
import shutil


def make_recursive_dir(pathname):
   dir_name = os.path.basename(pathname)
   dir_mater = os.path.dirname(pathname)
   if os.path.exists(pathname) and os.path.isdir(pathname):
       return
   else:
       make_recursive_dir(dir_mater)
       os.mkdir(pathname)    
   
def def_dirs():
   '''
Definisce le directory da utilizzare nella applicazione,
eventualmente non esistano le crea.'''
   default_dirs = {}
   appname = 'micetti'
   appauthor = 'nuzzopippo'

   default_dirs['data'] = appdirs.user_data_dir(appname, appauthor)
   default_dirs['cache'] = appdirs.user_cache_dir(appname, appauthor)
   default_dirs['log'] = appdirs.user_log_dir(appname, appauthor)
   try:
       for key in default_dirs.keys():
           make_recursive_dir(default_dirs[key])
   except OSError as e:
       exit(1)
   return default_dirs


class Micio:
   '''Un "Model" per un micio.'''
   def __init__(self, name='', image='', descr=''):
       self._name = name
       self._image = image
       self._description = descr

   @property
   def name(self):
       return self._name

   @name.setter
   def name(self, value):
       self._name = value

   @property
   def image(self):
       return self._image
   
   @image.setter
   def image(self, value):
       self._image = value

   @property
   def description(self):
       return self._description

   @description.setter
   def description(self, value):
       self._description = value

   def get_csv_data(self):
       if not self._name or not self._image or not self._description:
           raise ValueError('I dati del micio sono incompleti')
       return '{}|{}|{}'.format(self._name, self._image, self._description)

   def append_to_csv(self, file_name):
       if not self._name or not self._image or not self._description:
           raise ValueError('I dati del micio sono incompleti')
       try:
           f_exists = os.path.exists(file_name) and os.path.isfile(file_name)
           with open(file_name, 'a') as f:
               if f_exists:
                   f.write('\n')
               f.write(self.get_csv_data())
       except OSError as e:
           raise OSError(e)

   def get_cat(self):
       if not self._name or not self._image or not self._description:
           raise ValueError('I dati del micio sono incompleti')
       return [self._name, self._image, self._description]


class Cats:
   def __init__(self):
       dirs = def_dirs()
       if not dirs:
           raise RuntimeError('Errore di configurazione ambiente operativo')
       self.data = os.path.join(dirs['data'], 'cat.csv')
       self.d_cache = dirs['cache']
       self.cats = []
       self._load_data()
       pub.subscribe(self._requests, 'REQUEST')

   def _load_data(self):
       if os.path.exists(self.data) and os.path.isfile(self.data):
           with open(self.data, 'r') as f:
               self.cats = [Micio(*x.rstrip('\n').split('|')) for x in f.readlines()
                            if x!='\n']
   
   def _requests(self, message):
       request = message[0]
       if request == 'GETLIST':
           self._pub_list()
       elif request == 'ADDCAT':
           self._add(message[1:])
       elif request == 'SHOWCAT':
           self._show(message[1])
       elif request == 'UPDATECAT':
           self._update(message[1:])
       elif request == 'CATDELETE':
           self._delete(message[1])

   def _pub_list(self):
       pub.sendMessage('LISTUPDATED', message=self.cats)

   def _add(self, data):
       # verifica nome micio
       if data[0] in [x.name for x in self.cats]:
           raise ValueError('Nome micio già presente')
       # cheching del file immagine
       try:
           f_o = data[1]
           f_name = os.path.basename(f_o)
           f_d = os.path.join(self.d_cache, f_name)
           shutil.copy(f_o, f_d)
           data[1] = f_d
       except OSError as e:
           msg = 'Errore caching file immagine:\n' + repr(e)
           raise ValueError(msg)
       new = Micio(*data)
       new.append_to_csv(self.data)
       self.cats.append(new)
       self._pub_list()

   def _show(self, name):
       for item in self.cats:
           if item.name == name:
               group = 'CATUPDATED.' + '_'.join(name.split())
               msg = item.get_cat()
               pub.sendMessage(group, message=msg)
               break

   def _update(self, data):
       cat = Micio(*data)
       if not cat.name or not cat.image or not cat.description:
           raise ValueError('Gatto %s ha dati incompleti' % data[0])
       match = False
       for i in range(len(self.cats)):
           if self.cats[i].name == cat.name:
               # verifica e sostituzione dell'immagine
               if self.cats[i].image != cat.image:
                   try:
                       os.unlink(self.cats[i].image)
                       f_o = cat.image
                       f_name = os.path.basename(f_o)
                       f_d = os.path.join(self.d_cache, f_name)
                       shutil.copy(f_o, f_d)
                       cat.image = f_d
                   except OSError as e:
                       msg = 'Errore caching file immagine:<br>' + repr(e)
                       raise ValueError(msg)
               self.cats[i] = cat
               match = True
               break
       if not match:
           raise ValueError('Gatto %s inesistente' % data[0])
       with open(self.data, 'w') as f:
           f.writelines([x.get_csv_data()+'\n' for x in self.cats])
       self._show(cat.name)
       self._pub_list()
   
   def _delete(self, data):
       cat = None
       for item in self.cats:
           if item.name == data:
               cat = item
               break
       if cat is None:
           raise ValueError('Gatto %s inesistente' % data[0])
       self.cats.remove(cat)
       if not self.cats:
           os.unlink(self.data)
       else:
           with open(self.data, 'w') as f:
               f.writelines([x.get_csv_data()+'\n' for x in self.cats])
       self._pub_list()


view.py
CODICE
#-*- coding: utf-8 -*-

import tkinter as tk
from tkinter import ttk
from pubsub import pub
import model
import tkinter.messagebox as msgb
import tkinter.filedialog as fdlg
import tkinter.simpledialog as sdlg
from PIL import Image
from PIL import ImageTk
import os
import json


def get_tk_image(fname, wdg):
   image = Image.open(fname)
   img_x, img_y = image.size
   r_img = img_x / img_y
   r_wdg = wdg.winfo_width() / wdg.winfo_height()
   if r_wdg <= r_img:
       f_scala = (wdg.winfo_width()-10) / img_x
   else:
       f_scala = (wdg.winfo_height()-10) / img_y
   fx = int(img_x * f_scala)
   fy = int(img_y * f_scala)
   image = image.resize((fx, fy))
   return ImageTk.PhotoImage(image)
   

class CatPanel(ttk.Frame):
   '''Un pannello per mostrare un gatto.'''
   def __init__(self, parent, *args, **kwargs):
       super().__init__(parent, *args, **kwargs)
       self.parent = parent
       self.lbl_img = tk.Label(self, justify='center')
       self.lbl_img.grid(row=0, column=0, padx=10, pady=10, sticky='nsew')
       self.e_descr = tk.Entry(self, state='disabled')
       self.e_descr.grid(row=1, column=0, padx=10, pady=10, sticky='ew')

       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(0, weight=1)

       self.bind('<Configure>', self._on_resize)

       self.state = ''
       self.name = ''
       self.file = ''
       self.img = None

   def _on_resize(self, evt=None):
       self.update()
       self._show_image()
       
   def set_state(self, state):
       self.state = state
       if self.state == 'modify' or self.state == 'new':
           self.e_descr.configure(state='normal')
       else:
           self.e_descr.configure(state='disabled')

   def set_name(self, name):
       self.name = name

   def set_file(self, fname):
       self.file = fname
       self._show_image()

   def update_values(self, message):
       self.name = message[0]
       self.file = message[1]
       if self.state == '': self.e_descr.configure(state='normal')
       self.e_descr.delete(0, 'end')
       self.e_descr.insert('end', message[2])
       if self.state == '': self.e_descr.configure(state='disabled')
       self._show_image()
       self.update()

   def _show_image(self):
       if not self.file: return
       try:
           self.img = get_tk_image(self.file, self.lbl_img)
       except Exception as e:
           msgb.showerror('Caricamento immagine', repr(e))
           self.img = None
       self.lbl_img.configure(image=self.img)
       
   def get_data(self):
       descr = self.e_descr.get().replace('|', '')
       return [self.name, self.file, descr]

   
class ListPanel(ttk.Frame):
   '''Un pannello per elencare gatti... e gattine.'''
   def __init__(self, mater, parent, *args, **kwargs):
       super().__init__(parent, *args, **kwargs)
       self.parent = parent
       self.mater = mater
       self.data = []
       self.cat_list = tk.Listbox(self)
       self.cat_list.grid(row=0, column=0, rowspan=5,
                          padx=(5,2), pady=5, sticky='nsew')
       v_scroll = tk.Scrollbar(self, orient='vertical',
                               command=self.cat_list.yview)
       v_scroll.grid(row=0, column=1, rowspan=5,
                     padx=(2,5), pady=5, sticky='ns')
       self.cat_list.config(yscrollcommand=v_scroll.set)
       bt_open = tk.Button(self, text='Visualizza', command=self._on_open)
       bt_open.grid(row=0, column=2, padx=10, pady=5, sticky='ew')
       bt_del = tk.Button(self, text='Elimina', command=self._on_del)
       bt_del.grid(row=1, column=2, padx=10, pady=5, sticky='ew')
       self.vision = tk.BooleanVar()
       self.ck_vis = tk.Checkbutton(self, text='Anteprima', onvalue=True,
                                    offvalue=False, variable=self.vision,
                                    command=self._on_image)
       self.ck_vis.grid(row=2, column=2, padx=10, pady=10, sticky='w')
       self.lbl_tbn = tk.Label(self)
       self.lbl_tbn.grid(row=3, column=2, padx=10, pady=10, sticky='nsew')
       self.lbl_dida = tk.Label(self, justify='left')
       self.lbl_dida.grid(row=4, column=2, padx=10, pady=10, sticky='w')

       self.cat_list.bind('<ButtonRelease-1>', self._on_image)
       self.cat_list.bind('<Return>', self._on_image)

       self.grid_columnconfigure(2, weight=1)
       self.grid_rowconfigure(3, weight=1)

       self.img = None

   def _on_open(self):
       sel_name = self.cat_list.get(tk.ANCHOR)
       if not sel_name: return
       for m in self.data:
           if m.name == sel_name:
               micio = m
       self.mater.view_cat(micio.name)
   
   def _on_del(self):
       sel_name = self.cat_list.get(tk.ANCHOR)
       if not sel_name: return
       self.mater.cat_deleted(sel_name)
       msg = ['CATDELETE', sel_name]
       try:
           pub.sendMessage('REQUEST', message=msg)
       except ValueError as e:
           msgb.showerror('Avvenuto errore', repr(e))

   def _on_image(self, evt=None):
       if not self.vision.get():
           self.img = None
           self.lbl_tbn.configure(image='')
           self.lbl_dida.configure(text='')
           return
       sel_name = self.cat_list.get(tk.ANCHOR)
       if not sel_name: return
       for m in self.data:
           if m.name == sel_name:
               micio = m
       self.lbl_dida.configure(text=micio.description)
       self.img = get_tk_image(micio.image, self.lbl_tbn)
       self.lbl_tbn.configure(image=self.img)
   
   def update_values(self, message):
       sel_name = self.cat_list.get(tk.ANCHOR)
       if not sel_name:
           sel_name = None
       self.cat_list.delete(0, 'end')
       self.data = message
       for e in [x.name for x in self.data]:
           self.cat_list.insert('end', e)
       if sel_name is None: return
       index = 0
       for item in self.data:
           if item.name == sel_name:
               self.cat_list.index(index)
               break
           index += 1
   

class MyApp(tk.Tk):
   def __init__(self):
       super().__init__()
       self.protocol('WM_DELETE_WINDOW', self._on_destroy)
       self.title('Ron-Ron Land')
       try:
           self.cat_mng = model.Cats()
       except (RuntimeError, OSError) as e:
           msgb.showerror('Avvenuto errore', repr(e))
           self.destroy()
           exit(1)
       self.init_dir = os.path.expanduser('~')
       self.book = ttk.Notebook(self)
       self.book.grid(row=0, column=0, padx=10, pady=10, sticky='nsew')
       cmdnbp = tk.Frame(self)
       cmdnbp.grid(row=1, column=0, padx=5, pady=5, sticky='ew')
       self.bt_new = tk.Button(cmdnbp, text='Nuovo', command=self._on_new)
       self.bt_new.grid(row=0, column=0, padx=5, pady=5, sticky='ew')
       self.bt_change = tk.Button(cmdnbp, text='Modifica', command=self._on_modify)
       self.bt_change.grid(row=0, column=1, padx=5, pady=5, sticky='ew')
       self.bt_img = tk.Button(cmdnbp, text='Immagine', command=self._on_image)
       self.bt_img.grid(row=0, column=2 ,padx=5, pady=5, sticky='ew')
       self.bt_save = tk.Button(cmdnbp, text='Salva', command=self._on_save)
       self.bt_save.grid(row=0, column=3 ,padx=5, pady=5, sticky='ew')
       self.bt_cancel = tk.Button(cmdnbp, text='Annulla', command=self._on_cancel)
       self.bt_cancel.grid(row=0, column=4 ,padx=5, pady=5, sticky='ew')
       self.bt_close = tk.Button(cmdnbp, text='Chiudi', command=self._on_close)
       self.bt_close.grid(row=0, column=5 ,padx=5, pady=5, sticky='ew')
       for i in range(5):
           cmdnbp.grid_columnconfigure(i, weight=1, uniform='a')
       cmdp = tk.Frame(self)
       cmdp.grid(row=2, column=0, padx=5, pady=5, sticky='ew')
       self.bt_list = tk.Button(cmdp, text='Elenco', command=self._on_list)
       self.bt_list.grid(row=0, column=0, padx=5, pady=5, sticky='ew')
       bt_end = tk.Button(cmdp, text='Esci', command=self._on_destroy)
       bt_end.grid(row=0, column=1 ,padx=5, pady=5, sticky='ew')
       cmdp.grid_columnconfigure(0, weight=1, uniform='b')
       cmdp.grid_columnconfigure(1, weight=1, uniform='b')
       self.grid_columnconfigure(0, weight=1)
       self.grid_rowconfigure(0, weight=1)
       self.book.bind('<<NotebookTabChanged>>', self._on_tab)
       self.update()
       l = self.winfo_reqwidth()
       self.minsize(l, l)

       self.state = ''
       self.init_dir = os.path.expanduser('~')
       self._load_conf()
       self._evaluate_self()

   def _on_tab(self, evt=None):
       if self.state: return
       if not self.book.tabs(): return
       text = self.book.tab(self.book.select(), 'text')
       if not text:
           self.bt_change.configure(state='disabled')
       elif text == 'Elenco gatti':
           self.bt_change.configure(state='disabled')
       else:
           self.bt_change.configure(state='normal')
       
   def _on_list(self):
       pnl = ListPanel(self, self.book)
       pub.subscribe(pnl.update_values, 'LISTUPDATED')
       self.book.add(pnl, text='Elenco gatti')
       pub.sendMessage('REQUEST', message=['GETLIST'])
       self.book.update()
       self._evaluate_self()

   def _on_new(self):
       msg = 'Inserire nome del micio'
       name = sdlg.askstring('Nuovo micio', msg, parent=self)
       if name is None: return
       if name == 'Elenco gatti':
           msgb.showwarning('Nuovo micio', 'Nome gatto inaccettabile.',
                            parent=self)
           return
       labels = [self.book.tab(x, 'text') for x in
                 self.book.winfo_children()]
       if name in labels:
           msgb.showwarning('Nuovo micio', 'Micio già presente e visualizzato',
                            parent=self)
           return            
       self.state = 'new'
       pnl = CatPanel(self.book)
       pnl.set_name(name)
       pnl.set_state(self.state)
       self.book.add(pnl, text=name)
       self.book.select(pnl)
       for item in self.book.winfo_children():
           if str(item) != self.book.select():
               self.book.tab(item, state='disabled')
       self._evaluate_self()

   def _on_modify(self):
       for item in self.book.winfo_children():
           if str(item) != self.book.select():
               self.book.tab(item, state='disabled')
       item = self.book.nametowidget(self.book.select())
       self.state = 'modify'
       item.set_state(self.state)
       self._evaluate_self()

   def _on_image(self):
       f_name = fdlg.askopenfilename(parent=self,
                                     initialdir=self.init_dir,
                                     title='Seleziona immagine')
       if not f_name: return
       # memorizza la direttrice del file quale corrente
       if os.path.isfile(f_name):
           self.init_dir = os.path.dirname(f_name)
       item = self.book.nametowidget(self.book.select())
       item.set_file(f_name)

   def _on_save(self):
       item = self.book.nametowidget(self.book.select())
       data = item.get_data()
       if self.state == 'new':
           msg = ['ADDCAT'] + data
       elif self.state == 'modify':
           msg = ['UPDATECAT'] + data
       else:
           return
       try:
           pub.sendMessage('REQUEST', message=msg)
       except (ValueError, OSError) as e:
           msgb.showerror('Avvenuto errore', repr(e))
           return
       item.set_state('')
       if self.state == 'new':    
           pub.subscribe(item.update_values, 'CATUPDATED.' +
                         '_'.join(data[0].split()))
       for item in self.book.winfo_children():
           self.book.tab(item, state='normal')
       self.state = ''
       self._evaluate_self()

   def _on_cancel(self):
       if self.state == 'new':
           item = self.book.nametowidget(self.book.select())
           self.book.forget(self.book.select())
           item.destroy()
           self.update()
       else:
           item = self.book.nametowidget(self.book.select())
           item.set_state('')
           data = item.get_data()
           msg = ['SHOWCAT', data[0]]
           pub.sendMessage('REQUEST', message=msg)
       for item in self.book.winfo_children():
           self.book.tab(item, state='normal')
       self.book.select(self.book.winfo_children()[-1])
       self.state = ''
       self._evaluate_self()

   def _on_close(self):
       label = self.book.tab(self.book.select(), 'text')
       item = self.book.nametowidget(self.book.select())
       self.book.forget(self.book.select())
       if label == 'Elenco gatti':
           pub.unsubscribe(item.update_values, 'LISTUPDATED')
       else:
           data = item.get_data()
           try:
               pub.unsubscribe(item.update_values, 'CATUPDATED.' +
                               '_'.join(data[0].split()))
           except:
               pass
       item.destroy()
       self.update()
       self._evaluate_self()

   def _on_destroy(self, evt=None):
       '''Memorizza lo stato corrente e chiude,'''
       conf_current = {}
       conf_current['width'] = self.winfo_reqwidth()
       conf_current['height'] = self.winfo_reqheight()
       labels = [self.book.tab(x, 'text') for x in
                 self.book.winfo_children()]
       conf_current['tabs'] = labels
       dirs = model.def_dirs()
       f_name = os.path.join(dirs['data'], 'appstate.json')
       try:
           with open(f_name, mode='w', encoding='utf-8') as f:
               json.dump(conf_current, f)
       except OSError:
           pass
       self.destroy()
           
   def _evaluate_self(self):
       self.bt_new.configure(state='disabled')
       self.bt_change.configure(state='disabled')
       self.bt_img.configure(state='disabled')
       self.bt_save.configure(state='disabled')
       self.bt_cancel.configure(state='disabled')
       self.bt_close.configure(state='disabled')
       self.bt_list.configure(state='disabled')
       if self.state == 'modify' or self.state == 'new':
           self.bt_img.configure(state='normal')
           self.bt_save.configure(state='normal')
           self.bt_cancel.configure(state='normal')
       elif self.state == '':
           self.bt_new.configure(state='normal')
           if self.book.tabs():
               self.bt_close.configure(state='normal')
           labels = [self.book.tab(x, 'text') for x in
                     self.book.winfo_children()]
           if 'Elenco gatti' not in labels:
               self.bt_list.configure(state='normal')

   def view_cat(self, name):
       labels = [self.book.tab(x, 'text') for x in
                 self.book.winfo_children()]
       if name in labels:
           msgb.showinfo('Attento', 'Micio già visualizzato')
           return
       pnl = CatPanel(self.book)
       pnl.set_state('')
       self.book.add(pnl, text=name)
       self.book.select(pnl)
       pub.subscribe(pnl.update_values,
                     'CATUPDATED.' + '_'.join(name.split()))
       msg = ['SHOWCAT', name]
       pub.sendMessage('REQUEST', message=msg)
       self._evaluate_self()
   
   def cat_deleted(self, name):
       labelitem = None
       for i in self.book.winfo_children():
           label = self.book.tab(i, 'text')
           if label == name:
               labelitem = i
               break
       if labelitem is None: return
       item = self.book.nametowidget(labelitem)
       self.book.forget(labelitem)
       data = item.get_data()
       pub.unsubscribe(item.update_values, 'CATUPDATED.' +
                       '_'.join(data[0].split()))
       item.destroy()

   def _load_conf(self):
       dirs = model.def_dirs()
       f_name = os.path.join(dirs['data'], 'appstate.json')
       prec_conf = None
       if not os.path.exists(f_name) or not os.path.isfile(f_name):
           return
       try:
           with open(f_name, mode='r', encoding='utf-8') as f:
               prec_conf = json.load(f)
       except OSError:
           pass
       if prec_conf is None: return
       # ridimensiona la finestra e la centra
       l = self.winfo_screenwidth()
       a = self.winfo_screenheight()
       wx = prec_conf['width']
       wy = prec_conf['height']
       self.geometry('{}x{}+{}+{}'.format(wx, wy, (l-wx)//2, (a-wy)//2))
       # ricrea gli eventuali tabs
       labels = prec_conf['tabs']
       if not labels: return
       for t in labels:
           if t == 'Elenco gatti':
               self._on_list()
           else:
               self.view_cat(t)
       self.update()



if __name__ == '__main__':
   app = MyApp()
   app.mainloop()


Mi ci son divertito, provando diverse varianti, tra cui una in cui model non era neanche importato da view ... magari interesserà qualcuno.

Ciao :)

Edited by nuzzopippo - 23/10/2021, 08:25
 
Web  Top
1 replies since 3/10/2021, 11:00   224 views
  Share