| 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 elementareIl 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 espostoIl funzionamento generale è concettualmente piuttosto semplice : - si iscrive un metodo di un oggetto in un "gruppo";
- si invia un messaggio a pubsub indirizzato ad un dato "gruppo", pubsub provvederà a smistare il messaggio a tutti gli iscritti del "gruppo";
- 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 GruppiIl 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 - 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;
- 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;
- 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 comunicazioneNaturalmente, 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 Messaggidal 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 viewCredo 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 itemQuest'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 e sono molte 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 finestraUna 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. Salvataggioper 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() Caricamentoper 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! 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 i moduli sono due e devono essere posizionati nella stessa directory (che, ovviamente, è la stessa in cui darete il comando di lacio) model.pyCODICE # -*- 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.pyCODICE #-*- 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
|