Visore di foglio Excel, spartano ma carino (e istruttivo)
Questo programmino fa seguito a due miei studi: un’analoga soluzione che sfrutta il controllo DataGridView di un Form (comparsa su Visual Basic Tips & tricks) e il post relativo all’utilizzo in varie salse di OLE Automation comparso su questo mio blog. Considerando, magari a torto, più macchinoso e per me ostico il DataGrid che ci passa il convento WPF, mi sono chiesto se non fosse possibile emularlo, almeno per una visura read-only, con una serie di volgari Label e TextBox disposte in una Grid WPF in modo da imitare uno spazio di celle dello spreadsheet.
Prima di procedere, vanità m’induce ad anticipare una variante originale, descritta in fondo a questo articolo. In due parole, consiste in queste mosse, valide (solo?) in ottica read-only:
1. Aprire con OLE una certa cartella di lavoro;
2. Inserire i valori delle celle da A1 all’ultima in una matrice di stringhe;
3. Chiudere Excel;
4. Procedere operando su tale matrice.
Tornando alla soluzione normale, con un (bel) po’ di pazienza ho ottenuto un layout dall’aspetto seguente:
In realtà nel mio caso ho previsto 16 righe con altrettante caselle di testo. Ma chi vuole può ampliare a piacere questa “finestra” di 16 righe x 6 colonne sul foglio Excel, dato che il codice VB (o C#, chi avrà interesse alla traduzione) proposto è in parte ma non del tutto indipendente da tale scelta.
XAML essenziale
A costoro, per innata pigrizia indico solo due brani XAML: Il primo è relativo alle etichette che descrivono le intestazioni di riga e colonna:
<Label Height="25" Grid.Row="1" HorizontalAlignment="Center" Name="Riga1"
Content="1" Width="50" HorizontalContentAlignment="Center" Background="LightBlue" Margin="0,2,0,1" />
<Label Height="25" Grid.Row="2" HorizontalAlignment="Center" Name="Riga2"
Eccetera
<Label Grid.Column="1" HorizontalAlignment="Center" Name="Colonna1"
Content="A" Width="95" HorizontalContentAlignment="Center" Background="LightBlue" Margin="0,2,0,0" />
<Label Grid.Column="2" HorizontalAlignment="Center" Name="Colonna2"
Eccetera
Spero sia chiaro che le label delle righe hanno nomi mnemonici Riga1, Riga2, ..., Riga16 e contenuti (Content) da 1 a 16. Analogamente, Colonna1, Colonna2, . . ., Colonna7 sono i nomi delle etichette intestatarie dai Content “A”, “B”,... “F”.
Il secondo brano è inerente alle (mie) TextBox 16 x 6:
<TextBox Grid.Row="1" Grid.Column="1" Height="25" HorizontalAlignment="Left" Margin="0,1" Name="Cella1_1"
Width="95" />
<TextBox Grid.Row="2" Grid.Column="1" Height="25" HorizontalAlignment="Left" Margin="0,2,0,1" Name="Cella2_1"
Width="95" />
<TextBox Grid.Row="3" Grid.Column="1" Height="25" HorizontalAlignment="Left" Margin="0,2,0,0" Name="Cella3_1"
Width="95" />
Eccetera.
I cui nomi, per farla breve, sono del tipo CellaN_M con N e M pari alla riga e colonna di appartenenza nella Grid.
Nella finestra WPF vanno poi collocati pulsanti e un paio di semplici controlli, volti a caricare via OLE Automation il Foglio1 di un certo file .xlsx, disporne i valori delle celle in A1:F15 nelle corrispondenti Texbox sopra accennate, modificare riga e colonna iniziale e altre faccende. Per avarizia... spaziale ne vedremo l’azione man mano che procede la disamina del codice scatenato dai vari clic.
Il codice VB
Premessa doverosa. In quel che segue non mi sono troppo preoccupato di ottimizzare il codice, sul presupposto che un utente non del tutto idiota compia azioni in una sequenza logica, ossia PRIMA caricare un certo file, POI spostare riga o colonna, INFINE chiudere il programmino.
Ciò detto esaminiamo senza ulteriori indugi la parte iniziale, invitando a tener presente le variabili comunitarie, da mioExcel a MatrValori escluso (che servirà a una variante interessante e, forse, originale... Suspence!)
Imports Microsoft.Office.Interop.Excel ' Si può anche evitare!
Class MainWindow
Dim mioExcel As Microsoft.Office.Interop.Excel.Application
Dim IntestRighe, IntestColonne
Dim MatrCelle
Dim Zona As Microsoft.Office.Interop.Excel.Range
Dim swPrimoGiro As Boolean = False
Dim swFileCaricato As Boolean = False
Dim ZonaFoglio As Microsoft.Office.Interop.Excel.Range ' Intervallo da A1 a ultimacella
Dim MatrValori(,) As String
Private Sub CaricaMatrCelle()
MatrCelle =
{{Cella1_1, Cella2_1, Cella3_1, Cella4_1, Cella5_1, Cella6_1, Cella7_1, Cella8_1, Cella9_1, Cella10_1, Cella11_1, Cella12_1, Cella13_1, Cella14_1, Cella15_1},
{Cella1_2, Cella2_2, Cella3_2, Cella4_2, Cella5_2, Cella6_2, Cella7_2, Cella8_2, Cella9_2, Cella10_2, Cella11_2, Cella12_2, Cella13_2, Cella14_2, Cella15_2},
{Cella1_3, Cella2_3, Cella3_3, Cella4_3, Cella5_3, Cella6_3, Cella7_3, Cella8_3, Cella9_3, Cella10_3, Cella11_3, Cella12_3, Cella13_3, Cella14_3, Cella15_3},
{Cella1_4, Cella2_4, Cella3_4, Cella4_4, Cella5_4, Cella6_4, Cella7_4, Cella8_4, Cella9_4, Cella10_4, Cella11_4, Cella12_4, Cella13_4, Cella14_4, Cella15_4},
{Cella1_5, Cella2_5, Cella3_5, Cella4_5, Cella5_5, Cella6_5, Cella7_5, Cella8_5, Cella9_5, Cella10_5, Cella11_5, Cella12_5, Cella13_5, Cella14_5, Cella15_5},
{Cella1_6, Cella2_6, Cella3_6, Cella4_6, Cella5_6, Cella6_6, Cella7_6, Cella8_6, Cella9_6, Cella10_6, Cella11_6, Cella12_6, Cella13_6, Cella14_6, Cella15_6}}
End Sub
Private Sub ApriFile()
If swFileCaricato Then Exit Sub
IntestRighe = _
{Riga1, Riga2, Riga3, Riga4, Riga5, Riga6, Riga7, Riga8, Riga9, Riga10, Riga11, Riga12, Riga13, Riga14, Riga15}
IntestColonne = {Colonna1, Colonna2, Colonna3, Colonna4, Colonna5, Colonna6}
CaricaMatrCelle()
mioExcel = New Microsoft.Office.Interop.Excel.Application
' Carica l’archivio .xls
mioExcel.Workbooks.Open(txtFile.Text)
' mioExcel.Visible = True ' Usato per debug. Il default occulta Excel
Dim RigaFormule = mioExcel.Range("RigaFormule")
With RigaFormule
.Copy(mioExcel.Range("F2:H15"))
End With
MatrCelle(1, 1).Text = IniCella.Value
Zona = mioExcel.Range("A1:F15")
If swPrimoGiro Then
For i = 0 To IntestRighe.Length - 1
IntestRighe(i).content = i + 1
Next
IntestColonne(0).Content = "A"
For i = 1 To IntestColonne.Length - 1
IntestColonne(i).Content = NextIntest(IntestColonne(i - 1).Content)
Next
swPrimoGiro = False
End If
InserZonaInGriglia()
swFileCaricato = True
mioExcel.DisplayAlerts = False
End Sub
Dopo quanto già detto, ai buoni intenditori non occorre dire che CaricaMatrCelle, come dichiara il suo nome, pone nelle varie righe e colonne di un oggetto (si badi bene!) MatrCelle le TextBox che sappiamo e va da sé che questa parte va modificata in conseguenza da chiunque scegliesse un diverso layout (idem per i brani relativi alle Label delle intestazioni).
NOTA. L’adozione del tipo Object per MatrCelle qui si è resa indispensabile, per motivi che vari guru interpellati non hanno saputo chiarire. Idem per IntestRighe e IntestColonne. Pazienza e prendiamone atto.
Quanto ad ApriFile, esordisce caricando le varie Label in IntestRighe e IntestColonne e la matrice di TextBox, richiamando CaricaMatrCelle poi pone in mioExcel una nuova istanza OLE di Excel il cui insieme WorkBooks viene arricchito col metodo Open dell’archivio .xlsx. Il pathname completo di quest’ultimo si trova nella casella di testo txtFile. Qui chiedo venia: è un esempio didattico e addirittura ho previsto per default un file particolare nel quale esiste una zona RigaFormule che viene un po’ rozzamente copiata in basso. Questo solo per ricordare che certi archivi (noti!) possono essere aggiornati dinamicamente persino in un caso read-only. Gl’interessati a qualcosa di più serio possono togliere queste righe e, meglio ancora, utilizzare il ben noto controllo che dà accesso a un qualsiasi file Excel nel file system ospite.
Tralasciando altri dettagli, passo senz’altro a sottoporre a chi legge la Sub che inserisce i valori dell’intervallo Zona (definito a livello Dichiarazioni, rivedere) ovvero A1:F15 nel nostro caso. Mi astengo, a questo punto, da commenti e lo stesso faccio un po’ antipaticamente con le routine dedicate ad aggiungere / togliere una riga. Trattasi di RigaSucc e RigaPrec entrambe lanciate da Button ad hoc (o più smaglianti oggetti WPF, magari a forma di freccia) e a loro volta facenti capo a CambiaRighe.
Private Sub InserZonaInGriglia()
For i = 0 To MatrCelle.GetUpperbound(0) ' UBound(MatrCelle, 1)
For j = 0 To MatrCelle.GetUpperBound(1) ' UBound(MatrCelle, 2)
MatrCelle(i, j).Text = Zona.Cells(j + 1, i + 1).Value
Next
Next
End Sub
Private Sub CambiaRighe(ByVal GiuSu As Boolean)
Dim Delta As Integer = IIf(GiuSu, 1, -1)
If Delta < 0 And IntestRighe(0).Content = 1 Then Exit Sub
For i = 0 To IntestRighe.Length - 1
IntestRighe(i).Content += Delta
Next
End Sub
Private Sub RigaSucc()
If IntestRighe Is Nothing Then Exit Sub
CambiaRighe(True)
Zona = Zona.Offset(1)
InserZonaInGriglia()
End Sub
Private Sub RigaPrec()
If IntestRighe Is Nothing Then Exit Sub
If IntestRighe(0).Content = "1" Then Exit Sub
Zona = Zona.Offset(-1)
InserZonaInGriglia()
CambiaRighe(False)
End Sub
Più complicato il discorso sulle Sub ColonnaSucc e ColonnaPrec, esse pure scatenate da controlli ad hoc. Stavolta il tormentone è come passare a un intestazione successiva, ossia da A a Z, poi AA, AB, …, AB, …, AZ , .., BA, BB, ..., ZZ, AAA eccetera eccetera (eredità dell’originario Visicalc) oppure precedendo a ritroso. Provvedono a tali scopi, rispettivamente, le routine NextIntest e PrecedIntest dal non ambiguo nomignolo.
Per pigrizia le affido all’esegesi autogestita dei più esperti a titolo di non disutile esercizio. Altri soggetti meno motivati le considerino come ricette. Che funzionano.
Private Sub ColonnaSucc()
If IntestColonne Is Nothing Then Exit Sub
For i = 0 To IntestColonne.Length - 1
IntestColonne(i).Content =
NextIntest(IntestColonne(i).Content)
Next
Zona = Zona.Offset(0, 1)
InserZonaInGriglia()
End Sub
Private Sub ColonnaPrec()
If IntestColonne Is Nothing Then Exit Sub
If IntestColonne(0).Content = "A" Then Exit Sub
For i = 0 To IntestColonne.Length - 1
IntestColonne(i).Content =
PrecedIntest(IntestColonne(i).Content)
Next
Zona = Zona.Offset(0, -1)
InserZonaInGriglia()
End Sub
Private Function NextIntest(ByVal OldIntest As String) As String
Dim LastCar As Char, NumCar As Integer
Dim NextCar As Char
Dim i As Integer
NumCar = OldIntest.Length
LastCar = OldIntest(NumCar - 1)
Select Case OldIntest
Case "Z"
Return "AA"
Exit Function
Case "ZZ"
Return "AAA"
Exit Function
Case Else
For i = NumCar To 1 Step -1
LastCar = OldIntest(i - 1)
If Asc(LastCar) < Asc("Z") Then ' Char.ConvertToUtf32(LastCar, 0) < Char.ConvertToUtf32("Z", 0) Then
NextCar = Chr(Asc(LastCar) + 1)
' OldIntest = OldIntest.Remove(i - 1, 1)
' OldIntest = OldIntest.Insert(i - 1, NextCar)
OldIntest = OldIntest.Remove(i - 1, 1).Insert(i - 1, NextCar)
' Mid(OldIntest, i) = NextCar
Return OldIntest
Exit Function
Else
NextCar = "A"
Mid(OldIntest, NumCar) = NextCar
End If
Next
Return OldIntest
End Select
End Function
Private Function PrecedIntest(ByVal OldIntest As String) As String
Dim LastCar As Char, NumCar As Integer
Dim NextCar As Char
Dim i As Integer
NumCar = OldIntest.Length
LastCar = OldIntest(NumCar - 1)
Select Case OldIntest
Case "AA"
Return "Z"
Exit Function
Case "AAA"
Return "ZZ"
Exit Function
Case Else
For i = NumCar To 1 Step -1
LastCar = OldIntest(i - 1)
If Asc(LastCar) > Asc("A") Then
NextCar = Chr(Asc(LastCar) - 1)
OldIntest = OldIntest.Remove(i - 1, 1).Insert(i - 1, NextCar)
' Mid(OldIntest, i) = NextCar
Return OldIntest
Exit Function
Else
NextCar = "Z"
Mid(OldIntest, NumCar) = NextCar
End If
Next
Return OldIntest
End Select
End Function
Tutto il codice si conclude come segue, anche qui senza commenti di sorta (salvo le critiche su cui ho messo le mani avanti in apertura).
Private Sub ChiudiFile()
If Not swFileCaricato Then Exit Sub
mioExcel.DisplayAlerts = False
mioExcel.Quit()
mioExcel = Nothing
swPrimoGiro = True
swFileCaricato = False
IntestRighe = Nothing
IntestColonne = Nothing
MatrCelle = Nothing
End Sub
End Class
Una variante curiosa, comunque elegante
L’ideuzza anticipata all’inizio l’ho sperimentata con un procedimento un po’ differente, che vado a proporre senza menare segugi nel cortile antistante la fattoria (alias can per l’aia).
Private Sub InserMatrInGriglia(ByVal rIniz As Long, ByVal cIniz As Long)
Dim i As Integer, j As Integer, r As Long, c As Long
r = rIniz : c = cIniz
Dim MaxRiga As Integer = MatrCelle.GetUpperbound(0)
Dim MaxCol As Integer = MatrCelle.GetUpperbound(1)
Dim MaxIndRiga As Long = MatrValori.GetUpperBound(0)
Dim MaxIndCol As Long = MatrValori.GetUpperBound(1)
For i = 0 To MaxCol
For j = 0 To MaxRiga
If r > MaxIndRiga Or c > MaxIndCol Then
MatrCelle(j, i).Text = ""
Else
MatrCelle(j, i).Text = MatrValori(r, c)
End If
c += 1
Next
c = cIniz
r += 1
Next
End Sub
Private Sub FoglioInMatrice()
Static swGiroUnico As Boolean = True
If swGiroUnico Then
IntestRighe = {Riga1, Riga2, Riga3, Riga4, Riga5, Riga6, Riga7, Riga8, Riga9, Riga10, Riga11, Riga12, Riga13, Riga14, Riga15}
IntestColonne = {Colonna1, Colonna2, Colonna3, Colonna4, Colonna5, Colonna6}
CaricaMatrCelle()
mioExcel = New Microsoft.Office.Interop.Excel.Application
' Carica l’archivio .xls
mioExcel.Workbooks.Open(txtFile.Text)
Dim IniCella = mioExcel.Range("A1")
Dim UltimaCella As Microsoft.Office.Interop.Excel.Range = _
mioExcel.Range("A1").SpecialCells(Microsoft.Office.Interop.Excel.XlCellType.xlCellTypeLastCell)
ZonaFoglio = mioExcel.Range(IniCella(1), UltimaCella(1))
Dim Nr As Long, Nc As Long
With ZonaFoglio
Nr = .Rows.Count
Nc = .Columns.Count
End With
ReDim MatrValori(Nr - 1, Nc - 1) ' Considera la base 0 di una matrice
Dim Cella As Microsoft.Office.Interop.Excel.Range
Dim i As Long = 0
Dim j As Long = 0
For Each Cella In ZonaFoglio ' celle spazzolate per righe / colonne!
' Sostituisci celle vuote con un blank in MatrValori
MatrValori(i, j) = IIf(Cella.Value Is Nothing, "", Cella.Value)
j += 1
If j = Nc Then
j = 0
i += 1
End If
Next
mioExcel.DisplayAlerts = False
mioExcel.Quit()
mioExcel = Nothing
Zona Foglio = Nothing
swGiroUnico = False
End If
End Sub
Cominciamo dalla... seconda che ho detto (disgustosa imitazione di Corrado Guzzanti). In sintesi essa dopo la solita apertura via OLE del solito file .xlsx nella già vista casella txtFile individua l’intervalloZonaFoglio le cui celle diagonali sono Inicella (che poi è A1) e UltimaCella, ottenuto con una speciale funzione (che ho un po’ sudato nel tradurla dal linguaggio macro a quello .NET). In tal modo tale zona si estende solo alle celle utili e non all’intero, enorme foglio di lavoro. Dopo di che un duplice ciclo For ... Next piazza i valori di ZonaFoglio nella MatrValori sostituendo le celle vuote (di valore Nothing) con stringhe vuote. Si noti una finezza frutto di una mia scoperta, ossia che un loop tipo For Each Cella In ZonaFoglio procede automaticamente per righe e colonne, ma poi il ricorso agli indici i e j è inevitabile per MatrValori.
Dimenticavo di dire che ZonaFoglio e MatrValori sono definite a livello Dichiarazioni (rivedere).
La procedura si conclude abbandonando Excel e le variabili mioExcel e ZonaFoglio. Da tale momento si agisce sulla MatrValori per il lavoro di inserimento nelle TextBox della nostra MatrCelle. Provvede al riguardo la InserMatrInGriglia secondo gli argomenti rIniz e cIniz Qui giunto lascio ogni commento interamente al paziente e solerte visitatore, cui propongo solo una banale routine di prova denominata Prova per sfrenata immaginazione, ove potrà sbizzarrirsi a cambiare l’istruzione evidenziata in grassetto, osservando non di nascosto l’effetto che fa:
Sub Prova()
Dim rIn As Long, cIn As Long
rIn = 3 : cIn = 4
IntestRighe(0).content = rIn + 1
For i = 1 To UBound(IntestRighe)
IntestRighe(i).Content = IntestRighe(i - 1).Content + 1
Next
IntestColonne(0).Content = "A"
' Cambia prima intestazione di colonna secondo cIn
For i = 0 To cIn
IntestColonne(0).Content = NextIntest(IntestColonne(0).Content)
Next
' Cambia le altre intestazioni
For i = 1 To UBound(IntestColonne)
IntestColonne(i).Content = NextIntest(IntestColonne(i - 1).Content)
Next
InserMatrInGriglia(rIn, cIn)
End Sub
(Va da sé che Prova ha senso solo dopo che è stata lanciata FoglioInMatrice.)
Conclusioni
Un giorno o l’altro proverò a rifare la soluzione descritta inizialmente utilizzando una MatrValori anziché MatrCelle e, conseguentemente, InserMatrInGriglia in luogo di InserZonaInGriglia. La qual cosa richiede tra l’altro una riscrittura delle routine RigaSucc, RigaPrec, ColonnaSucc e ColonnaPrec. Un compito che chi ne ha voglia potrebbe svolgere in proprio.
A questo punto il dibattito è aperto su un paio di punti: a) la reale utilità, a fronte delle complicanze aggiuntive, di tradurre una zona Excel in una normale matrice; b) persino la vera necessità di un visore Excel.
Circa il primo punto ritengo che il vantaggio stia nella minor occupazione di RAM, piuttosto che in termini velociferi. Sul secondo non mi pronuncio, insisto però nel dire che il trattamento di fogli di lavoro quasi sempre sottintende una tabella classica, con soli campi e record, mentre uno spreadsheet è un modello a celle sparse. Già a suo tempo ho molto sofferto per “ficcarlo” in una DatagridView. Non me la sono sentita di ripetere tale faticaccia con la DataGrid di WPF, ritenendo tale controllo, forse non a torto, ancor meno flessibile.
Ogni commento in merito sarà gradito, qui o tramite email.
giannigiac@tin.it
REPETITA IUVANT
Rivisitando il mio vecchio Visore di Excel su VB T&T ho riscoperto due cose (ah, la memoria! A volte vacilla...): a) che allora utilizzai su una DatagridView il formato Open XML (OOXML) di Office; b) che sviluppai una più compatta versione delle funzioni che danno le intestazione di colonna successiva e precedente. Circa il primo punto che OOXML è sì più moderno e veloce rispetto alla tecnologia OLE Automation, ma non si applica a precedenti formati XLS.
Sul secondo, per comodità degli interessati riporto le due funzioni:
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
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