Visore di fogli Excel 2007 in una griglia DatagridView

Transcript

Visore di fogli Excel 2007 in una griglia DatagridView
Visore di f ogli Excel 2007 in una griglia DatagridView
Di Gianni Giaccaglini
Il piccolo ma utile programma, di cui vanto la paternità e l’originalità è una versione didattica (che mi sono
proposto di migliorare). Con esso si visualizza in una Form che incorpora un controllo DataGridView i
contenuti del primo foglio di una cartella di lavoro Excel 2007, con tanto di intestazioni delle righe e
colonne, che si aggiornano nel corso della navigazione nelle celle con opportuni pulsanti.
Figura 1 L’utility Visore di Excel con tasti di navigazione su un foglio Excel attinto con l’API OpenXml SDK.
Si noti anche il pulsante per la selezione anche discontinua di celle con conseguente calcolo, e segnalazione,
della somma dei soli dati numerici ivi contenuti.
Nota. Questa versione, ripeto, si limita al primo foglio. La versione (semi) professionale permette di
navigare, visualizzandoli, su tutti i fogli della cartella di lavoro.
Considerazioni preliminari
L’avvento del formato Open XML delle versioni 2007 di Word, Excel (e Project) ha aperto orizzonti, che chi
scrive ha in particolare illustrato sul libro FAG Open XML, guida allo sviluppo nonché sul prestigioso sito
Visual Studio Tips & Tricks.
Tale formato, è per l’appunto, “aperto” in quanto i nuovi file .docx/.docm o .xlsx/.xlsm sono una collezione
di testi puri che aderiscono a uno schema XML. Conoscendone la composizione e l’articolazione si può così
accedere, modificare e persino creare tali archivi anche senza che Office 2007 – o Office tout court! - sia
installato sulla macchina ospite.
Nota. Cito qui fugacemente due miei articoli specifici, il primo relativo a una tabella incorporata in un
documento Word 2007, trasferita in un foglio elettronico (tramite i VSTO, Visual Studio Tools per Office, ed.
2008), il secondo descrive un procedimento (da me escogitato) per accedere ai contenuti relativi a un
intervallo di date coordinate di un foglio di lavoro Excel.
In due parole, comunque, il mio algoritmo crea una dictionary avente come chiave il riferimento delle celle
e il contenuto (non formattato) di ciascuna come valore. Questo avviene grazie allo sfruttamento della
specifica proprietà r del nodo <c> che fornisce il riferimento della cella, mentre il valore è quello del suo
figlio <v>:
<c r="B4">
..<v>100.5</v>
</c>
Per massima comodità dei meno zelanti (ma buoni intenditori), riporto senza commenti il cuore
dell’algoritmo (d’altronde ripreso più avanti quasi di peso, salvo una modifica):
Dim
Dim
Dim
Dim
For
ListaConChiaveRif As New Dictionary(Of String, String)
MioFile = "C:\MiaCart\Semplice.xlsx"
Cella1 = "B2", Cella2 = "G7"
ZonaValori = ZonaAcquist(MioFile, Cella1, Cella2)
Each CellaVal In ZonaValori
Dim Rifer = CellaVal.Parent.@r
Dim Dato As String
If CellaVal.Parent.@t = "s" Then
Dato = VettSharStrings(CellaVal.Value)
Else
Dato = CellaVal.Value
End If
ListaConChiaveRif.Add(Rifer, Dato)
Next
Il rompicapo del data binding
Nella fase di analisi mi sono presto reso conto che il pur comodo data binding ottenibile in modo
automatico nel familiare controllo DataGridView è a dir poco estremamente difficoltoso se non impossibile.
Sull’impossibilità non oso esprimere giudizi lapidari, lasciando aperta la discussione nei caffè più o meno
web bazzicati dai professionisti più accreditati. Uno di costoro, da me interpellato, mi ha un po’ deluso. Non
dirò di chi si tratta ma è davvero un grosso guru di Visual Studio... Di fatto mi è parso che non si rendesse
ben conto che:

Il famoso data binding automatico si applica senza problemi a tabelle relazionali e a liste di oggetti
in memoria, in entrambi i casi caratterizzati da precisi nomi dei campi, le une, e proprietà
dell’oggetto, le altre, tutte e due tradotte in testa alle colonne della griglia;

Uno spreadsheet, ahimè, può essere un insieme anche molto vasto, sovente dotato di molti buchi
con intere colonne che possono mancare (e a me interessava visualizzare qualsiasi spreadsheet);

Né va dimenticato che l’OOXML, tramite l’SDK specifica Open XML (ver. 1.1 o 2.0) fornisce un file
XML, a sua volta contenente nodi relativi alle sole celle “piene”!, ora non risulta che il data “binder”
del DataGridView sia in grado di esibire oggetti XML.
La mia soluzione, ovvero il toro per le corna
Il guru di cui sopra suggeriva la traduzione di tale file XML in una tabella o lista di oggetti. Entrambe le
strade non nego in assoluto che siano percorribili. Ma come ottenere le intestazioni di colonne e righe? E il
limite di colonne gestibili col DataGridView (ampio, circa 500, ma di gran lunga inferiore a quelle possibili in
un foglio che in Excel 2007 sono ben 16.384)?
Nota. D’ora in avanti si parlerà quasi sempre semplicemente di “griglia” anziché “DataGridView”.
Ci sarebbe comunque da scrivere codice a monte (per tradurre lo spreadsheet in una lista o simili) e magari
a valle (per inserire intestazioni di colonne e righe). Così ho deciso di prendere il toro per le corna, dopo
aver appreso una cosa che molti sanno e tutti si dimenticano: l’oggetto DataGridView è programmabile,
adattandolo ovviamente con codice ad hoc alle caratteristiche di una qualsiasi fonte di dati, inclusa quella
particolare chiamata spreadsheet.
Nota. Detto di passaggio, quanto verrà esposto costituisce un utile richiamo alle proprietà e metodi del
controllo griglia, tutt’altro che ben documentati in giro. Utilizzabile altrove, direi.
Prima di illustrare le varie routine, oso anticipare ai buoni intenditori, dotati altresì di una certa fantasia, le
grandi linee del progetto, a valle della creazione di una Form con controllo DataGridView incorporato più i
pulsanti per caricare un file xlsx o xlsm, mostrarne una “finestra” nelle colonne e righe predisposte (una
sola riga, v. figura 2).
Figura 2 L’utility VisoreDiExcel al lancio del programma.
Il codice si articola nelle linee seguenti:
1. Caricamento in memoria del foglio e creazione della dictionary riferimento / valore (
ListaConChiaveRif nella fattispecie);
2. Creazione nella griglia di un certo numero di righe, con contestuale assegnazione delle intestazioni
laterali di riga da 1 in avanti e di colonna, “A”, “B”, ... ”Z”, “AA”, “AB”,... eccetera (sequenza
familiare quanto atipica);
3. Esplorazione delle celle di tale griglia, assegnando a ciascuna il valore attinto dalla dictionary sulla
chiave riferimento.
Il clou del punto 3 corrisponde a una inattesa (almeno per me) ma piacevole sorpresa:
il DataGridView offre due utili proprietà HeaderText e .HeaderCell, relativa l’una all’intestazione di colonna,
l’altra a quella di riga. Concatenandole (operatore &) si ottiene proprio il riferimento di quella cella!
Risulta così possibile inserire nella cella giusta della griglia tutti e soli i valori prelevati dalla dictionary con
l’esatto riferimento.
Caso didattico introduttivo: caricamento di una matrice
Dando per scontate le operazioni di creazione a project-time della Form col controllo griglia e gli altri
pulsanti dalle etichette di limpida semantica, fornisco subito la sezione Dichiarazioni a monte della classe
UserForm1:
Imports
Imports
Imports
Imports
System.IO
<xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main">
Packaging = DocumentFormat.OpenXml.Packaging
System.Xml.Linq
Circa tali direttive ricordo ancora a ignari e immemori che prima si debbono fissare i riferimenti giusti,
previo caricamento della libreria DocumentFormat.OpenXml (v. precedenti articoli su Visual Basic Tips &
Trics).
Ed ecco subito l’esordio della Classe Form1:
Public Class Form1
Dim Matr(,) As String = {{"1", "2", "3", "4", "5", "6",
{"10", "11", "12", "13", "14",
{"19", "20", "21", "22", "23",
{"28", "29", "30", "31", "32",
{"37", "38", "39", "40", "41",
{"46", "47", "48", "49", "50",
{"55", "56", "57", "58", "59",
{"64", "65", "66", "67", "68",
"7", "8", "9"}, _
"15", "16", "17",
"24", "25", "26",
"33", "34", "35",
"42", "43", "44",
"51", "52", "53",
"60", "61", "52",
"69", "70", "71",
Function SuccIntest(ByVal Intest As String) As String
Dim i As Integer
Dim Car As Char
i = Intest.Length
While i >= 1
Car = Intest(i - 1)
If Car <> "Z"c Then
Dim CodAsc = Char.ConvertToUtf32(Car, 0)
Dim SuccCar = Char.ConvertFromUtf32(Codasc + 1)
Intest = Intest.Substring(0, i - 1) & _
SuccCar & Intest.Substring(i)
Return Intest
Else ' Car è "Z"
Intest = Intest.Substring(0, i - 1) & "A" _
& Intest.Substring(i)
If i = 1 Then
Return "A" & Intest
End If
i = i - 1
End If
End While
Return Intest
End Function
Function PrecedIntest(ByVal Intest As String) As String
Dim i As Integer
Dim Car As Char
i = Intest.Length
While i >= 1
Car = Intest(i - 1)
If Car <> "A"c Then
Dim CodAsc = Char.ConvertToUtf32(Car, 0)
Dim PrecCar = Char.ConvertFromUtf32(CodAsc - 1)
Intest = Intest.Substring(0, i - 1) & _
PrecCar & Intest.Substring(i)
Return Intest
Else
If Intest.Length = 1 Then Return Intest
"18"},
"27"},
"36"},
"45"},
"54"},
"63"},
"72"}}
_
_
_
_
_
_
If i = 1 Then
Intest = Intest.Remove(0, 1)
Return Intest
End If
Intest = Intest.Substring(0, i - 1) & "Z" _
& Intest.Substring(i)
i = i - 1
End If
End While
Return Intest
End Function
Function IntestCol(ByVal Rifer As String) As String
Dim Col As String = Nothing
Dim Lung As Integer = Rifer.Length
Dim i = 0
Dim Car = Rifer(i)
While Not Integer.TryParse(Car, 0) And i < Lung - 1
Col &= Car
i += 1
Car = Rifer(i)
End While
Return Col
End Function
La matrice bidimensionali di costanti Matr è stata inserita a scopo dimostrativo. Il caso didattico
introduttivo prevede infatti di caricarla nella griglia con inserimento sperimentale di intestazioni in stile
Excel. Il pulsante specifico è quello di sinistra, etichettato “Carica matrice”. Comunque si focalizzi
l’attenzione sulle tre funzioni (originali) utilizzate in tutto il progetto. Scopo delle prime due è quello di
fornire l’intestazione di colonna successiva e precedente a un’intestazione data, mentre la terza, lasciata al
commento autogestito, preleva da una stringa tipo “A12”, “BC123” la parte alfabetica - “A” e “BC” nei due
casi – ovvero l’intestazione di colonna del riferimento Rifer.
Commenti veloci sulla funzione SuccIntest (mentre la PrecedIntest, che esercita l’opposto mestiere è
affidata al commento fai-da-te), che deve generare, successivamente, A, B, ..., Z poi AA, AB,... , AZ, BA,... ,
BZ,... , ZZ, AAA, AAB ecc., fino all’estrema testata che in Excel 2007 è XFD.
Nota. Sottolineo che tale particolare sequenza (ereditata dal progenitore degli spreadsheet, Visicalc) non
segue una progressione alfabetica, perché ad esempio AA precede Z nell’ordinamento alfabetico.
Il procedimento spazzola con l’indice i che parte dall’ultimo carattere Car dell’argomento Intest
percorrendoli tutti (finché i >= 1). Se Car non è “Z” se ne prende l’ASCII (CodAsc) con la proprietà
ConvertToUtf32 applicata come si deve all’oggetto Car per subito tradurlo nel carattere successivo con
l’opportuno ricorso a ConvertFromUtf32, infilandolo al posto dell’altro con un gioco di concatenamenti che
sfruttano la proprietà Substring di una stringa.
Di conseguenza, con Return Intest il carattere esaminato di quest’ultima passa da A a B, da B a C, ... , da Y a
Z.
Lascio all’esame autonomo del paziente lettore, l’impiego adottato della proprietà SubStringa. Cito solo il
seguente caso, che sostituisce con un asterisco la lettera poszta nel mezzo del cammin di nostra stringa
(indice i):
NostraStr = NostraStr.Subtring(0, i - 1) & "*" & NostraStr.SubString(i)
Se invece Car è “Z” esso viene tradotto in “A” con anteposizione di un’altra “A” ottenendo così “AA” a
seguito di Z. Che cosa accade con due o più caratteri? Chi legge può sincerarsene con la riflessione e/o il
debug sulla carta o nell’IDE Visual Studio.
Con questi prolegomeni la routine dell’evento Click del pulsante di caricamento matrice Matr nella griglia
non dovrebbe destare sorprese:
Private Sub btnMostraMatrice_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnMostraMatrice.Click
Dim DGridV As DataGridView = DataGridView1
Dim Intestaz = InizIntest & "A" ' Per test. Provare anche con "AA" o altro
For i = 0 To DGridV.Columns.Count - 1
With DGridV.Columns(i)
.HeaderText = Intestaz
MessageBox.Show("Intestazione " & .Name & _
" è stata modificata in " & .HeaderText)
Intestaz = SuccIntest(Intestaz)
End With
Next
With DGridV
.Rows.Add(Matr.GetUpperBound(0))
MessageBox.Show("Numero attuale di righe:" & .Rows.Count())
Dim Riga = InizRiga ' Per maggior chiarezza...
For i = 0 To Matr.GetUpperBound(0)
With .Rows(i)
.HeaderCell.Value = Riga.ToString
For j = 0 To Matr.GetUpperBound(1)
.Cells(j).Value = DGridV.Columns(j).HeaderText & _
.HeaderCell.Value & ": " & Matr(i, j)
Next
End With
Riga = Riga + 1
Next
End With
End Sub
Lo scopo, ripeto, didattico è quello di esibire l’inserimento delle testate di colonna e riga della griglia
nonché dei valori di Matr nelle sue celle. A tal fine la routine presenta messaggi opportuni nelle varie fasi.
Va prima detto che – per semplicità, ma anche per motivi meglio chiariti più avanti – il progetto prevede un
numero fisso, 18, di colonne della griglia, adeguato comunque a quello della Matr, mentre il numero delle
righe della griglia stessa (che parte con 1, ricordate?) è eguagliato alla prima dimensione di Matr, con
l’istruzione Rows.Add(Matr.GetUpperBound(0)) dell’oggetto DGridV ossia la nostra griglia DataGridView1.
A questo punto è opportuno precisare che l’oggetto DataGridView si articola negli insiemi Columns e Rows
le sue colonne e righe ovviamente. Le due proprietà dei membri Column e Row che più ci stanno a cuore
sono HeaderText (intestazione di colonna) e rispettivamente HeaderCell (intestazione di riga, più che di
“cella”).
Nota. Non disturbi l’istruzione Riga = InizRiga. Come si vedrà InizRiga è un valore default = 1. Andava bene
anche Riga = 1? Beh è una finezza per eventuali esperimenti alternativi... Analogamente, InizIntest è un
default definito = “A”.
Dopo aver posto la variabile Intestaz = InizIntest & "A" in modo da esordire con “AA” (un valore come un
altro, modificabile in sede di debugging per ogni variante), il primo ciclo spazzola le Columns fissandone via
via la proprietà HeaderText a Intestaz¸ quest’ultima predisposta alla successiva con Intestaz =
SuccIntest(Intestaz) per il prossimo giro. Così l’utente assiste alla trasformazione delle varie “Column1”,
“Column2”, ecc. le testate standard a project time, in “AA”, “AB”, “AC” e via di seguito.
Il successivo loop che percorre le Rows inserendo sulla sinistra i numeri progressivi delle righe lo lascio per
pigrizia all’esegesi autogestita. Preciso però che ogni oggetto Row è dotato di celle, insieme Cells e faccio
notare che nel ciclo più interno di indice j che percorre le celle della riga di indice i il valore di ciascuno non
è semplicemente il corrispettivo di Matr(i, j) bensì quest’ultimo con anteposizione del concatenamento fra
l’header di colonna e di riga più “:”. Insomma la situazione è del tipo seguente:
AA
AB
AC
AD
AE
AF
AG
AH
1
AA1:1
AB1:2
AC1:3
AD1:4
AE1:5
AF1:6
ecc.
ecc.
2
AA2:10
AB2:11
AC2:12
AD2:13
AE2:14
AF2:15
ecc.
ecc.
3
AA3:19
AB3:20
AC3:21
ecc.
ecc.
ecc.
ecc.
ecc.
L’ammaestramento che se ne ricava, già anticipato, è che con tale concatenamento si possono sempre
ottenere le coordinate in stile Excel delle celle della griglia! (buono a sapersi, anche in futuro).
Caricamento di un foglio di lavoro
Prima di proseguire, esaminiamo le variabili definite a livello modulo (anziché Dim qui andrebbe bene
anche Private o persino Public) e che pertanto si possono gestire conservando il loro valore nel corso
dell’elaborazione:
Dim ListaConChiaveRif As New Dictionary(Of String, String)
Dim SpreadSh As String = Nothing ' Il file Excel 2007 da caricare
Dim PrecedSpreadCaricato As String = Nothing
Dim SpreadCaricato As Boolean = False
Dim UltimaIntestFoglio As String
Dim NumRigheFoglio As Integer
' Valori default
Dim NumRigheDataGrid As Integer = 50 ' Un numero come un altro
Dim InizRiga As Integer = 1 ' o altri valore, per varianti di debug
Dim InizIntest As String = "A" ' oppure "ZY" e altre
' intestazioni usabili nei vari debug
Di InizRiga e InizIntest si è già parlato. Un altro valore default è NumRigheFoglio che poi di fatto viene
adattata a quelle del foglio caricato. Le altre variabili, diciamo così, comunitarie hanno un nome che ne
lascia intuire la missione. Le esamineremo assieme al codice che le utilizza.
Cominciamo con la dictionary ListaConChiaveRif già citata, nella quale viene stoccata la coppia riferimento
/ valore. Ecco la prima funzione che carica un Array VettSharedStrings, sulla base dell’argomento
SharedStringPart del tipo relativo alla parte del pacco (Packaging) dell’SDK per Open XML:
Function VettSharedStrings(ByVal SharedStringpart As _
Packaging.SharedStringTablePart) As Array
'Accesso alle SharedStrings di un file xls*
Using StrRead = New StreamReader(SharedStringpart.GetStream())
Dim SharStrDoc = XDocument.Load(StrRead)
Dim SharStrs = SharStrDoc...<t>
Dim VettShStrs(0) As String
ReDim VettShStrs(SharStrs.Count - 1)
Dim i As Integer = 0
For Each SharStr In SharStrs
VettShStrs(i) = SharStr.Value.ToString
i += 1
Next
'Dim Vettshstrs As Array = SharStrs.ToArray
StrRead.Close()
Return VettShStrs
End Using
End Function
Poiché tale Sub è identica a quella dell’articolo precedente per accesso a un intervallo di una cartella Excel
2007, rimando ad esso per i commenti.
Allo stesso articolo rinvio per l’analisi della seguente routine volta a caricare col metodo Open del “pacco” il
file definito dall’argomento SpreadSh (prima variabile a livello modulo, passata come si vedrà fra poco) e
attingerne un foglio di lavoro, il Foglio1 per semplicità e che a sua volta richiama la precedente funzione,
per creare tra l’altro la ListaConChiaveRif:
Sub CaricaFoglioEListaConChiave()
Try
Using PackExcel = _
Packaging.SpreadsheetDocument.Open(SpreadSh, True)
Dim SharedStrPart = PackExcel.WorkbookPart.SharedStringTablePart
Dim VettSharStrings = VettSharedStrings(SharedStrPart)
Dim PrimoFoglio = PackExcel.WorkbookPart.WorksheetParts.LastOrDefault
' N.B. LastOrDefault in realtà punta al PRIMO foglio di lavoro
Using StrRead = New StreamReader(PrimoFoglio.GetStream())
Dim Foglio = XDocument.Load(StrRead)
'MessageBox.Show(Foglio.ToString) ' Servito per debug
Dim DimensFoglio = Foglio...<dimension>.@ref
Dim PrimaCella = "", i As Integer = 0
While DimensFoglio(i) <> ":"
PrimaCella &= DimensFoglio(i)
i += 1
End While
Dim UltimaCella = DimensFoglio.Replace(PrimaCella & ":", Nothing)
UltimaIntestFoglio = IntestCol(UltimaCella)
NumRigheFoglio = UltimaCella.Replace(UltimaIntestFoglio, Nothing)
Dim CelleValori = Foglio...<v>
For Each CellaVal In CelleValori
Dim Rifer = CellaVal.Parent.@r
Dim Dato As String
If CellaVal.Parent.Attributes.Last.Value = "s" Then
Dato = VettSharStrings(CellaVal.Value)
Else
Dato = CellaVal.Value
Dato = Dato.Replace("."c, ","c) 'Per l'edizione italiana
End If
ListaConChiaveRif.Add(Rifer, Dato)
Next
' Servito per debug:
' MessageBox.Show("Numero elementi ListaConChiaveRif: " & _
' ListaConChiaveRif.Count)
Foglio = Nothing
StrRead.Close()
End Using
PackExcel.Close()
End Using
Catch
MessageBox.Show("File di tipo errato o corrotto", "Probabile errore",
MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
End Sub
E se voglio accedere a uno qualsiasi dei fogli di lavoro dell’archivio Excel (che potrebbe averne anche
molti)? Il segreto di cui mi ero dimenticato è subito visto (e davvero semplice quanto inatteso):
Dim MioFoglio = PackExcel.WorkbookPart.WorksheetParts(indFgl)
con (indFgl) in luogo di .Last. Si tenga solo presente che indFoglio opera per così dire a ritroso, per cui in
uno spreadsheet con tre fogli Foglio1, Foglio2 e Foglio3 l’indice indFgl va posto =3, 2 e 1, rispettivamente.
(Insomma l’ampliamento a tutti i possibili fogli di lavoro è possibile, però con la pazienza del caso (tenendo
presente la proprietà Count dell’insieme WorksheetParts e, magari, adottando una casella a discesa
opportuna)
Tornando alla versione precedente, rispetto ad essa, sono state apportate alcune modifiche e adattamenti
al nuovo caso. La prima è la struttura Try / Catch / End Try introdotta per catturare gli errori nel
caricamento, indicandone i più probabili. Ma la più sostanziosa è data dalle righe in grassetto, che per
massima comodità di chi legge riporto nuovamente:
Dim DimensFoglio = Foglio...<dimension>.@ref
Dim PrimaCella = "", i As Integer = 0
While DimensFoglio(i) <> ":"
PrimaCella &= DimensFoglio(i)
i += 1
End While
Dim UltimaCella = DimensFoglio.Replace(PrimaCella & ":", Nothing)
UltimaIntestFoglio = IntestCol(UltimaCella)
NumRigheFoglio = UltimaCella.Replace(UltimaIntestFoglio, Nothing)
Vi si sfrutta la proprietà ref del nodo <dimension> del foglio .xml. Davvero utile e da non dimenticare, offre
una stringa del tipo "A1:BC27” o “C2:AF123” e simili che indica l’intervallo compreso fra la prima e l’ultima
cella non vuote. L’analisi del procedimento è lasciata al paziente lettore, che si renderà conto magari col
tasto F8 che esso fornisce l’UltimaIntestFoglio e il NumRigheFoglio variabili a livello modulo (rivedere) di
eloquente significato. Ad esempio nei due casi testé citati si avranno la coppia “BC” e “27” e,
rispettivamente, “AF” e “123”.
Nota. Come sarà più chiaro fra poco, la PrimaCella viene usata solo come passaggio intermedio, avendo
preferito indicare in ogni casi A1 come prima cella della griglia.
Caricamento del foglio
Il cerchio si chiude con la centralissima routine CaricaFoglio che giustappunto carica il Foglio1 della cartella
di lavoro Excel 2007 inserita nella casella di testo della nostra Form (Me), indicata con la label “File Excel da
caricare” e denominata txtFileExcel:
Sub CaricaFoglio()
SpreadSh = Me.txtFileExcel.Text
If SpreadSh = Nothing Then Exit Sub
If SpreadSh = PrecedSpreadCaricato And Not SpreadCaricato Then
MessageBox.Show("Spreadsheet " & SpreadSh & " già caricato!", "", _
MessageBoxButtons.OK, MessageBoxIcon.Information)
Exit Sub
Else
ListaConChiaveRif.Clear()
End If
Dim DGridV As DataGridView = DataGridView1
DGridV.ScrollBars = ScrollBars.Both
If ListaConChiaveRif.Count = 0 Then
CaricaFoglioEListaConChiave()
End If
Dim Intestaz = InizIntest
For i = 0 To DGridV.Columns.Count - 1
With DGridV.Columns(i)
.HeaderText = Intestaz
Intestaz = SuccIntest(Intestaz)
End With
Next
With DGridV
.Rows.Clear() ' Elimina righe create in precedenza (e.g. con Carica Matrice)
.Rows.Add(NumRigheDataGrid)
Dim Riga = InizRiga ' Per maggior chiarezza...
For i = 0 To .Rows.Count - 1
With .Rows(i)
.HeaderCell.Value = Riga.ToString
Dim RifCella = ""
For j = 0 To .Cells.Count - 1
RifCella = _
DGridV.Columns(j).HeaderText & .HeaderCell.Value
If ListaConChiaveRif.ContainsKey(RifCella) Then
.Cells(j).Value = ListaConChiaveRif(RifCella)
End If
Next
End With
Riga = Riga + 1
Next
End With
PrecedSpreadCaricato = Me.txtFileExcel.Text
End Sub
La precedente Sub viene utilizzata da vari pulsanti, a partire da quello sottostante la casella di testo,
etichettato “Carica foglio Excel” e denominato btnCaricaFoglio:
Private Sub btnCaricaFoglio_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnCaricaFoglio.Click
SpreadCaricato = False
CaricaFoglio()
End Sub
Commenti stringati a CaricaFoglio. Esso esordisce prelevando in SpreadSh il contenuto della casella di testo
già vista. Se vengono superati controlli vari, dati per comprensibili, viene richiamata la già vista
CaricaFoglioEListaConChiave rivedendo la quale ci si rende conto che utilizzerà SpreadSh come si deve.
A quel punto si innestano due cicli, il primo analogo a quello esaminati nella routine didattica relativa al
caricamento della Matr nella griglia (notare che per maggior compattezza l’oggetto DataGridView1 viene
stoccato in DGridV di tipo DataGridView). Anche qui l’iterazione For i = 0 To DGridV.Count – 1 ... Next
genera le intestazioni relative alle 18 colonne prefissate nel progetto.
Dopo il Clear delle righe attuali e l’aggiunta di un NumRigheDataGrid pari a quelle del Foglio1, parte il
secondo loop For i = 0 To DGridV.Rows.Count ... Next in cui per ogni DGridV.Rows(i) viene posto un valore
progressivo di Riga nella testata di riga (HeaderCell.Value). Ma lo snippet di maggior pregio è quello che
sfrutta il trucco originale anticipato fin dall’apertura. Ripetiamolo:
For j = 0 To DGridV.Cells.Count - 1
RifCella = _
DGridV.Columns(j).HeaderText & .HeaderCell.Value
If ListaConChiaveRif.ContainsKey(RifCella) Then
.Cells(j).Value = ListaConChiaveRif(RifCella)
End If
Next
Come oramai tutti dovrebbero capire, in RifCella viene inserito il riferimento, come dire?, de facto della
cella corrente per concatenamento dei due HeaderText e HeaderCell già “scoperti” nell’esempio didattico
di caricamento Matr. Il ciclo in questione spazzola tutte le celle della griglia, inserendovi il valore dell’ormai
celebre dictionary sulla chiave chiave RifCella però se e solo se la RifCella è presente in ListaConChiaveRif.
Nota. Non si poteva procedere spazzolando invece la dictionary? Alternativa a dir poco problematica,
perché l’accesso a una cella della griglia in base alle sue (pseudo) coordinate non è direttamente
supportato. Si noti poi che la proprietà ContainsKey(RifCella) di non immediata reperibilità con l’Help on
line è risultata qui davvero provvidenziale!
Navigazione nella griglia
Avviene con quattro pulsanti etichettati e denominati in modo che indica inequivocabilmente il mestiere di
ciascuno, pertanto ne riporto il codice facendolo seguire da annotazioni essenziali. Prima un’osservazione
circa il già menzionato limite di 18 colonne della griglia. Esso si è reso necessario, dopo diversi benchmark,
non tanto per il numero massimo (500) supportato dal DataGridView, insufficiente solo con fogli davvero
grandi, bensì perché il refresh di un numero elevato di celle crea rallentamenti notevoli. D’altra parte
all’utente la vista parziale del foglio basta e avanza.
Colonna avanti. Aggiunge una colonna a destra, eliminando quella di sinistra (conservando le 18 colonne
prefissate) e aggiornando opportunamente i contenuti.
Private Sub btnColonnaAvanti_Click(ByVal sender As Object, ByVal e As
System.EventArgs) Handles btnColonnaAvanti.Click
Dim DGridV = DataGridView1
With DGridV
Dim Ncol = .Columns.Count
If .Columns(Ncol - 1).HeaderText = UltimaIntestFoglio Then
MessageBox.Show("Siamo all'ultima colonna del foglio")
Exit Sub
End If
End With
If DGridV.Rows.Count = 1 Then
CaricaFoglio()
Exit Sub
End If
Dim Nc = DGridV.Columns.Count
With DGridV.Columns
Dim NuovaIntest = SuccIntest(.Item(Nc - 1).HeaderText)
.RemoveAt(0)
Nc = .Count ' Dà il prec.te Nc - 1 (istr.ne messa per > chiarezza)
.Add(.Item(Nc - 1).Name, NuovaIntest) ' Senza l'istr. prec. si poteva fare _
' item(Nc - 2)
End With
With DGridV.Rows
For i As Integer = 0 To .Count - 1
Dim RifCella = _
DGridV.Columns(Nc - 1).HeaderText & .Item(i).HeaderCell.Value
If ListaConChiaveRif.ContainsKey(RifCella) Then
.Item(i).Cells(0).Value = ListaConChiaveRif(RifCella)
End If
Next
End With
End Sub
È la più sofisticata delle quattro routine d’evento, perché richiama CaricaFoglio solo al primo clic su tale
Button, mentre in seguito aggiunge una colonna aggiornando solamente questa ed eliminando poi la prima.
Colonna indietro. Aggiunge una colonna a sinistra, eliminando quella di destra (conservando le 18 colonne
prefissate) e aggiornando i contenuti.
Private
Sub
btnColonnaIndietro_Click(ByVal
sender
System.EventArgs) Handles btnColonnaIndietro.Click
Dim DGridV = DataGridView1
If DGridV.Rows.Count = 1 Then
CaricaFoglio()
Exit Sub
End If
With DGridV.Columns
If .Item(0).HeaderText = "A" Then Exit Sub
SpreadSh = Nothing ' NON VA...???!
InizIntest = PrecedIntest(.Item(0).HeaderText)
SpreadCaricato = True
CaricaFoglio()
End With
End Sub
As
Object,
ByVal
e
As
Stavolta, purtroppo, non sono riuscito a limitare l’aggiornamento in analogia al caso precedente, per cui è
stato giocoforza l’applicazione brute-force di CaricaFoglio (peraltro senza rallentamenti sensibili). Questo
perché, si direbbe, non c’è verso di inserire una colonna a sinistra. Se qualcuno vi riesce, faccia un fischio.
Riga avanti e Riga indietro. Le più semplici da capire, entrambe col ricorso brutale a CaricaFoglio. Ergo le
trascrivo senza commenti di sorta.
Private Sub btnRigaAvanti_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Handles btnRigaAvanti.Click
InizRiga += 1
If InizRiga + NumRigheDataGrid > NumRigheFoglio Then
MessageBox.Show("Siamo all'ultima riga del foglio")
End If
SpreadCaricato = True
CaricaFoglio() ' Soluzione brute-force...
End Sub
Private
Sub
btnRigaIndietro_Click(ByVal
sender
System.EventArgs) Handles btnRigaIndietro.Click
If InizRiga > 1 Then InizRiga -= 1
SpreadCaricato = True
CaricaFoglio() ' Soluzione brute-force...
End Sub
As
Object,
ByVal
e
As
Altre procedure
Somma celle selezionate. Queste routine derivano dalla scoperta della proprietà SelectedCells di chiara
semantica. Ho così ottenuto abbastanza la funzione SommaCelleSelez, che totalizza i valori numerici di una
selezione anche discontinua di celle della griglia. Il pulsante denominato btnSommaCelleSel la richiama
segnalando al mondo il risultato (con indicazione delle celle via via esaminate, istruzioni tutoriali che si
possono togliere).
Private Function SommaCelleSelez() As Double
'Somma Celle selzionate
Dim strCella As String = Nothing
Dim S = 0.0
With DataGridView1
If .SelectedCells.Count = 0 Then Exit Function
' Ma di fatto c'è sempre una cella selez.ta
For Each Cella In .SelectedCells
Try
strCella = Cella.Value.ToString
Catch ex As Exception
Continue For
End Try
MessageBox.Show(strCella) ' Servita per debug
' strCella = strCella.Replace(","c, "."c) _
' Per l'edizione italiana? Ma di fatto NON serve
If Double.TryParse(strCella, 0) Then
S = S + CType(strCella, Double)
End If
Next
Return S
End With
End Function
Private Sub btnSommaCelleSel_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnSommaCelleSel.Click
MsgBox("Somma celle numeriche Selezionate: " & SommaCelleSelez.ToString)
End Sub
Carica cella. Viene lanciata dal clic sul pulsante omonimo, e fa sì che nella griglia venga caricato il Foglio1
avente come cella d’angolo in alto a sinistra la cella di coordinate pari a quelle della casella Prima cella. Si
richiede una digitazione esatta, non avendo previsto di intrappolare errori. La sua routine d’evento Click è
interamente affidata all’autoanalisi (non freudiana!) dei più o meno esperti.
Private Sub btnCaricaCella_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles btnCaricaCella.Click
Dim Rif = TxtPrimaCella.Text.ToUpper ' Solo per > chiarezza
If Rif = "" Then Exit Sub
If Integer.TryParse(Rif(0), 0) Then
MessageBox.Show("Cifra all'inizio", "ERRORE!", MessageBoxButtons.OK,
MessageBoxIcon.Error)
TxtPrimaCella.Text = ""
Exit Sub
End If
Dim Intest = IntestCol(Rif)
Dim Riga = Rif.Replace(Intest, Nothing)
If Not Integer.TryParse(Riga, 0) Then
MessageBox.Show("L'indicazione di riga (" & Riga & ") deve contenere solo
cifre", "ERRORE!", MessageBoxButtons.OK, MessageBoxIcon.Error)
TxtPrimaCella.Text = ""
Exit Sub
End If
' MsgBox("Colonna: " & Intest & vbLf & "Riga: " & Riga) 'Servito pèer debug
InizIntest = Intest
InizRiga = Riga
SpreadCaricato = True
CaricaFoglio()
End Sub
Scelta del file, last but not least. Questo pulsante si affida a un controllo OpenFileDialog, equivalente alla
finestra di Dialogo Apri di Windows, caratterizzato in più da un filtro che limita l’esplorazione ai soli file
Excel 2007 (*) presenti sul PC ospite.
Nota. Per l’esattezza il filtro *.xls? potrebbe catturare anche i particolari file binari .xlsb di fatto illeggibili.
Facile il rimedio, chi ci tenesse, ma si tratta di bestie davvero rare...
Il controllo OpenFileDialog lo do per noto, ricordando, ai più distratti, che va inserito nella Form
definendone le proprietà essenziali, dopo di che risulta invisibile a run-time per cui il più delle volte
conviene sfruttarlo con una routine, che ne impone i caratteri a run-time. Come quella da me usata,
connessa all’ultimo bottone del progetto, per pigrizia Button1:
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Handles Button1.Click
' Attiva il controllo OpenFileDialog, sui soli file .xlsx / . xlsm
With OpenFileDialog1
.Filter = "File Excel 2007(*.xls?)|*.xls?"
.FileName = ""
.ShowDialog() ' Mostra la finestra dei file nel ns. PC
If .ShowDialog = Windows.Forms.DialogResult.OK Then
Me.txtFileExcel.Text = .FileName
End If
End With
End Su
Si riveda la figura 1. Come dovrebbe essere evidente, sulla scelta del file e il clic sul pulsante Apri della
finestra omonima il nome del file (pathname completo) viene riversato nella variabile txtFileExcel.Text e a
quel punto l’utente può decidersi a lanciare tale file cliccando sul pulsante Carica foglio Excel.
Qualcuno troverà farraginoso questo doppio passaggio. Non ha tutti i torti benché ciò consenta pure la
scorciatoia della digitazione diretta del file nella casella. Eliminare quest’ultima e il suo pulsante
btnCaricaFoglio e modificare sia la precedente routine che la routine CaricaFoglio ecco un non arduo
esercizio che lascio ai più volonterosi.
Bibliografia essenziale
-
Per Microsoft LINQ:
A. Del Sole Programmare Microsoft LINQ – Ed. FAG
http://www.fag.it/scheda.aspx?ID=28759
P. Pialorsi, M. Russo Programmare Microsoft LINQ – Ed. Mondadori
(http://education.mondadori.it/ v. anche, sul sito devleap.com, http://introducinglinq.com/ )
-
Per nozioni generali e articoli su OOXML nonché download dell’SDK 1.0
http://openxmldeveloper.org/articles/1970.aspx
-
Per esempi applicativi (gestiti in DOM + XPath in ambiente VBA)
G. Giaccaglini Open XML Guida allo sviluppo – Ed. FAG
http://www.fag.it/scheda.aspx?ID=28500