Spesso capita di dover esportare i dati di una Listbox su un foglio di Excel. Le soluzioni per farlo sono diverse a seconda delle esigenze, vediamo alcuni esempi, dal più generico a quello più sofisticato.
Innanzi tutto cosa vogliamo fare? Un copia e incolla o generare un file?
Come vogliamo interagire con la ListBox? Vogliamo mantenere la formattazione? Vogliamo gestire casi particolari come ad esempio le date? Vogliamo poter inserire volendo le formule?
La soluzione che andremo ad implementare sarà basata su un modulo che estende le funzionalità della Listbox, questo perché ci permette di utilizzarla anche in sottoclassi della Listbox senza dover creare una catena di ereditarietà particolare.
Per cui lanciamo Xojo e creiamo un modulo a cui diamo il nome ToExcel.
Primo metodo: Esportazione semplice
In questo caso vogliamo solo esportare il contenuto della listbox in celle di Excel senza badare alla formattazione. Visto che sia l’esportazione per copia e incolla che quella su file hanno entrambe bisogno del contenuto espresso come testo creiamo prima una funzione che ci restituisce questo testo e poi le due funzionalità per l’azione che vogliamo.
Private Function soloTesto(lb as Listbox) as Text return=lb.Cell(-1, -1).ToText End Function |
Tutto qui. Per avere il contenuto di tutte le celle della Listbox basta richiamare la proprietà Cell con i valori -1 per le riga e -1 per la colonna. Questa funzionalità di Cell ci permette, volendo di prendere l’intero contenuto di una riga con Cell(riga, -1) o di una colonna con Cell(-1, colonna).
Il testo restituito da Cell(-1, -1) è una Stringa i valori delle celle con le righe separate da EndOfLine e le colonne da tabulatori.
Il problema con questo metodo è che non vengono restituite le intestazioni.
Per cui modifichiamo il metodo per prevedere la possibilità di avere le intestazioni se desiderato e se sono presenti.
Private Function soloTesto(lb as Listbox, withHeading as Boolean=true) as Text dim allHeading as String //Se voglio le intestazioni e sono presenti riempio la variabile allHeading if withHeading and lb.HasHeading then allHeading=lb.Heading(-1)+EndOfLine end if //Ottengo il contenuto della Listbox dim allText as Text=lb.Cell(-1,-1).ToText //Restituisco header (che sarà eventualmente vuoto) e il contenuto return allHeading.toText+allText End Function |
Ora abbiamo la possibilità di volere o meno le intestazioni e se queste esistono saranno aggiunte al nostro risultato.
La chiamata Heading(-1) restituisce tutte le intestazioni separate dal carattere tabulatore.
La funzione è privata perché ci serve all’interno del modulo. La vera funzionalità che estende la Listbox sarà invece:
Public Sub copiaTesto(extends lb as Listbox, withHeading as Boolean=true) dim c as new Clipboard c.Text=soloTesto(lb, withHeading) End Sub |
In questo modo possiamo copiare il contenuto di una Listbox in una finestra, ad esempio LaMiaListbox, con il semplice comando: LaMiaListbox.copiaTesto e poi passare su Excel e incollare il contenuto.
Secondo metodo: Esportazione complessa
In questo caso l’esportazione vuole mantenere le stesse impostazioni visualizzate nella cella, quindi allineamento, grassetto e così via.
Ma visto che esportiamo con la formattazione magari vogliamo avere qualche funzionalità per poter esprimere altre informazioni particolari che magari rendiamo nell’evento CellTextPaint della ListBox, o vogliamo scrivere il testo in modo diverso perché è una data o una formula.
Le date e le formule sono cose ambigue in un file Excel, per sicurezza le date conviene crearle nel formato YYYY-MM-GG per evitare ambiguità e le formule sono in genere localizzate sul sistema (per cui in italiano avremo =SOMMA… in inglese =SUM…)
L’approccio generale è lo stesso di prima. Otteniamo un testo e poi lo rendiamo a disposizione degli appunti i in questo caso anche per salvare il file.
Il testo generato sarà un HTML in particolare un frammento HTML relativo ad una TABLE.
Iniziamo a scrivere la nostra funzionalità base e poi aggiungeremo i vari comandi.
dovendo scrivere del codice HTML, ma anche per semplificare la scrittura utilizziamo delle costanti:
kTableOpen as Text=”<table>”
kTableClose as Text=”</table>”
kRowOpen as Text=”<tr>”
kRowClose as Text=”</tr>”
kCellOpen as Text=”<td>”
kCellClose as Text=”</td>”
Private Function withFormat(lb as Listbox, withHeading as Boolean) as Text //Otteniamo il numero di righe presenti dim numRows as integer=lb.ListCount-1 //Otteniamo il numero di colonne presenti dim numCols as integer=lb.ColumnCount-1 //Creiamo un vettore che conterrà tutto il nostro risultato dim allText() as String //Creiamo la tabella allText.Append kOpenTable //Contenuto dell'header se desiderato e presente if withHeading and lb.HasHeading then allText.Append kOpenRow for col as integer=0 to numCols allText.Append kCellOpen allText.Append lb.Heading(col).toText allText.Append kCellClose next allText.Append kRowClose end if //Per le celle è la stessa cosa ripetuta per tutte le righe for row as integer=0 to numRows allText.Append kOpenRow for col as integer=0 to numCols allText.Append kCellOpen allText.Append lb.Cell(row, col).toText allText.Append kCellClose next allText.Append kRowClose next //Chiudiamo la tabella allText.Append kTableClose //Restituiamo il valore return Join(allText, "").ToText End Function |
Però in questo modo non abbiamo nulla per la formattazione.
Per formattare il contenuto della cella o dell’header utilizzeremo gli stili HTML per cui cambiamo la definizione di apertura della cella in:
kCellOpen as Text=”<td style='”
e introduciamo una nuova costante per chiudere questa apertura:
kCellOpenClose as Text=”‘>”
Per avere i diversi parametri degli stili utilizziamo una struttura per cui definiamo:
Structure cellStyle align as alignMode bold as boolean fontSize as integer fontColor as color background as color End Structure |
Una struttura di questo genere ci permette di poter espandere eventualmente le proprietà senza dover riscrivere il codice.
Definiamo una enumerazione per l’allineamento che tenga conto delle differenze tra l’allineamento della Listbox e dell’HTML:
Public Enum alignMode Left Center Right End Enum |
Per evitare di utilizzare inutilmente i colori definiamo una costante che rappresenta il nessun colore:
noColor as Color= &c000000FF |
Definiamo a questo punto una funzione per inizializzare correttamente la nostra struttura, per gli header useremo il parametro row=-1
Private Function standardFormat(lb as Listbox, column as integer, row as integer=-1) as cellStyle dim cs as cellStyle //Variabile d'appoggio per verificare l'allineamento utilizzato dim align as integer if row=-1 then //Heading align=lb.ColumnAlignment(column) else //Cella: verifichiamo quello della cella, se è Default utilizziamo //quello della colonna align=lb.CellAlignment(row, column) if align=Listbox.AlignDefault then align=lb.ColumnAlignment(column) end if end if //Trasformiamo l'allineamento trovato in quello previsto nella enumerazione select case align case Listbox.AlignRight cs.align=alignMode.Right case Listbox.AlignCenter cs.align=alignMode.Center case else cs.align=alignMode.Left end select //Le celle possono essere in grassetto gli header no //(almeno non possiamo leggerlo dalla Listbox) if row>-1 then cs.bold=lb.CellBold(row, column) //Assegnamo al colore del font e del background il nessun colore //Ovvero utilizzeremo dopo quello predefinito cs.fontColor=noColor cs.background=noColor //Restituiamo la struttura correttamente inizializzata Return cs End Function |
Scriviamo una funzione di supporto che converte un colore in RGB
Private function toWeb(c as color) as Text return "rgb("+c.Red.ToText)+", "+c.Green.ToText+", "+c.Blue.ToText+")" end function |
A questo punto scriviamo la funzione per convertire lo stile in informazioni per lo stile. Tutte queste informazioni saranno aggiunte al nostro vettore del risultato.
Public Sub creaStile(theText() as String, theStyle as cellStyle) //TheText è il vettore del risultato //TheStyle è lo stile corrente //Analizziamo l'allineamento //Se è left non scriviamo nulla Select case theStyle.align case alignMode.Right theText.Append "text-align:right;" case alignMode.Center theText.Append "text-align:center;" End Select //Grassetto if theStyle.bold then theText.Append "font-weight:bold;" //FontSize if theStyle.fontSize <> 0 then theText.Append "font-size:"+theStyle.fontSize.ToText+"px;" //FontColor if theStyle.fontColor <> noColor then theText.Append "color:"+theStyle.fontColor.toWeb+";" //BackgroundColor if theStyle.background <> noColor then theText.Append "background-color:"+theStyle.background.toWeb+";" End Sub |
A questo punto possiamo scrivere correttamente lo stile della cella o dell’header nel nostro testo HTML.
Ma come facciamo ad ottenere lo stile necessario?
Lo facciamo introducendo due delegate.
I delegate sono strutture delle chiamate a metodi o funzioni a cui possiamo assegnare dei metodi o funzioni che rispettano la stessa firma.
Per cui definiamo per gli stili:
Public Sub HeadingFormat(lb as Listbox, col as integer, byRef style as cellStyle) Public Sub CellFormat(lb as Listbox, row as integer, col as integer, byRef style as cellStyle) |
e per i contenuti:
Public Function HeadingText(lb as Listbox, col as integer, byRef theText as Text) Public Function CellText(lb as Listbox, row as integer, col as integer, byRef theText as Text) |
A questo punto possiamo riscrivere la nostra funzione withFormat in questo modo:
Private Function withFormat(lb as Listbox, withHeading as Boolean, cellContent as CellText=nil, cellStyleFormat as CellFormat=nil, headerText as HeadingText as nil, headerFormat as HeadingFormat=nil) as Text //Otteniamo il numero di righe presenti dim numRows as integer=lb.ListCount-1 //Otteniamo il numero di colonne presenti dim numCols as integer=lb.ColumnCount-1 //Dichiariamo una variabile per lo stile dim styleForCell as cellStyle //Dichiariamo una variable per il testo corrente dim aText as Text //Creiamo un vettore che conterrà tutto il nostro risultato dim allText() as String //Creiamo la tabella allText.Append kOpenTable //Contenuto dell'header se desiderato e presente if withHeading and lb.HasHeading then allText.Append kOpenRow for col as integer=0 to numCols allText.Append kCellOpen styleForCell=standardFormat(lb, col) if headerFormat <> nil then headerFormat.invoke(lb, col, styleForCell) creaStile(allText, styleForCell) allText.append kCellOpenClose //Aggiungiamo il testo utilizzando di base quello presente, //in questo modo la funzione eventualmente chiama dovrà solo modificarlo dove necessario aText=lb.Heading(col).toText if headerText <> nil then headerText.invoke(lb, col, aText) allText.append aText allText.Append kCellClose next allText.Append kRowClose end if //Per le celle è la stessa cosa ripetuta per tutte le righe for row as integer=0 to numRows allText.Append kOpenRow for col as integer=0 to numCols allText.Append kCellOpen styleForCell=standardFormat(lb, col, row) if cellStyleFormat <> nil then cellStyleFormat.invoke(lb, col, row, styleForCell) creaStile(allText, styleForCell) allText.append kCellOpenClose aText=lb.Cell(row, col).toText if cellContent <> nil then cellContent.invoke(lb, row, col, aText) allText.append aText allText.Append kCellClose next allText.Append kRowClose next //Chiudiamo la tabella allText.Append kTableClose //Restituiamo il valore return Join(allText, "").ToText End Function |
Questa soluzione ci da la flessibilità necessaria per avere o meno dei contenuti o stili speciali definiti a livello di finestra o classe dove si trova la nostra Listbox.
Inoltre il sistema è facilmente espandibile senza dover scrivere molto.
Ad esempio, poniamo di voler aggiungere i bordi con uno spessore e un colore.
Visto che i bordi sono 4 aggiungiamo una struttura per gestirli:
Structure border width as integer borderColor as color End Structure |
Aggiungiamo alla struttura cellStyle i 4 bordi:
//... al termine della struttura: borderLeft as border borderTop as border borderRight as border borderBottom as border |
Aggiungiamo una funzione per generare lo stile a seconda del bordo:
Private Sub addBorder(theText() as string, borderName as string, theBorder as border) if theBorder.borderColor <> noColor and theBorder.width>0 then theText.Append "border-"+borderName+":"+theBorder.width.ToText+"px solid "+theBorder.borderColor.toWeb+";" end if End Sub |
E infine aggiungiamo le chiamate a questa funzione nel generatore degli stili creaStile:
//...al termine della funzione addBorder theText, "left", theStyle.borderLeft addBorder theText, "top", theStyle.borderTop addBorder theText, "right", theStyle.borderRight addBorder theText, "bottom", theStyle.borderBottom |
La chiamata pubblica sarà per la copia:
Public Sub copiaConFormato(extends lb as Listbox, withHeading as Boolean, cellContent as CellText, cellStyleFormat as CellFormat, headerText as HeadingText, headerFormat as HeadingFormat) dim c as new Clipboard c.Text=withFormat(lb, withHeading, cellContent, cellStyleFormat, headerText, headerFormat) End Sub |
Mentre per il salvataggio:
Public Sub salvaConFormato(extends lb as Listbox, f as folderItem, withHeading as Boolean=true, cellContent as CellText=nil, cellStyleFormat as CellFormat=nil, headerText as HeadingText=nil, headerFormat as HeadingFormat=nil) if f <> nil then dim t as TextOutputStream try t=TextOutputStream.Create(f) t.Write withFormat(lb, withHeading, cellContent, cellStyleFormat, headerText, headerFormat) end try end if End Sub |