Gianni Giaccaglini

Tricks & mini applics on WPF
posts - 46, comments - 0, trackbacks - 0

Visore di foglio Excel (spartano ma carino)

 

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:

 

A

B

C

D

E

F

1

 

 

 

 

 

 

2

 

 

 

 

 

 

3

 

 

 

 

 

 

4

 

 

 

 

 

 

5

 

 

 

 

 

 

6

 

 

 

 

 

 

 

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

Print | posted on giovedì 6 settembre 2012 20:40 |

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 8 and 2 and type the answer here:

Powered by:
Powered By Subtext Powered By ASP.NET