mercoledì 2 novembre 2011

[VB.NET] UserControl DataGridView con Riga Totali

Descrizione :
Una mia tecnica su UserControl per aggiungere un DataGridView Totali ad un DataGridView Dati.

+ Articolo :

L'articolo seguente non vuole essere un "how-to" su "come creare un UserControl", ma principalmente uno spunto pratico e funzionante in risposta al problema di avere una riga ( o più righe... ) destinata a contenere i totali ( ma anche altri dati riassuntivi... ) calcolati su alcune delle colonne di un DataGridView popolato tramite DataTable.

Il Progetto di Test ( una semplice Applicazione Windows Forms ) consta di questi componenti :

- Form di avvio : "FormMain"
--> Button : btn_test
--> Button : btn_testnothing
--> Button : btn_testfont

- UserControl "TDataGridView"
--> DataGridView : DGV
--> DataGridView : DGV_totals

A design basta aggiungere a TDataGridView i due DataGridView sopra indicati, senza preoccuparsi troppo di impostarne le caratteristiche in quanto vengono definite e re-impostate dinamicamente via codice.

--> Codice per TDataGridView :

Public Class TDataGridView

    'DataSource e Indici delle Colonne di cui si desiderano i Totali
    Private m_datasource As DataTable
    Private m_totcolindices As List(Of Integer)
    'Font comune 
    Private m_dgvfont As New Font(Drawing.FontFamily.GenericSansSerif, 12)
    Private m_dgvheadersfont As New Font(m_dgvfont, FontStyle.Bold)

    Public Event DGVCellRightClick(ByVal column As Integer, ByVal row As Integer, ByVal value As Object)

    Public Property DgvFont As Font
        Get
            Return m_dgvfont
        End Get
        Set(ByVal value As Font)
            Try
                m_dgvheadersfont = New Font(value, FontStyle.Bold)
                m_dgvfont = value
            Catch ex As Exception
                MessageBox.Show("Impossibile usare questo Font.", "Azione annullata", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
                Exit Property
            End Try
            DGV.ColumnHeadersDefaultCellStyle.Font = m_dgvheadersfont
            DGV.Font = m_dgvfont
            DGV_totals.ColumnHeadersDefaultCellStyle.Font = m_dgvheadersfont
            DGV_totals.Font = m_dgvfont
            ResizeDGV()
        End Set
    End Property

    Private Sub AggiornaTotali()

        If DGV_totals.RowCount = 0 Or m_datasource Is Nothing Then Exit Sub
        Me.Validate()
        DGV.EndEdit()
        Dim oTot As Object
        Dim dTot As Double
        For i As Integer = 0 To m_totcolindices.Count - 1
            oTot = m_datasource.Compute("SUM(" & m_datasource.Columns(m_totcolindices(i)).ColumnName & ")", Nothing)
            Try
                dTot = Convert.ToDouble(oTot)
            Catch ex As Exception
                dTot = 0
            End Try
            DGV_totals.Rows(0).Cells(m_totcolindices(i)).Value = dTot
        Next

    End Sub

    Public Sub SetDataSource(ByVal dataSource As DataTable, ByVal totColIndices As List(Of Integer))

        m_totcolindices = totColIndices
        m_datasource = dataSource
        DGV.DataSource = m_datasource

        With DGV_totals

            .Rows.Clear()
            .Columns.Clear()
            If m_datasource Is Nothing Or m_totcolindices Is Nothing Then Exit Sub
            For i As Integer = 0 To DGV.Columns.Count - 1
                .Columns.Add(DGV.Columns(i).Name, "")
                .Columns(i).Width = DGV.Columns(i).Width
                If m_totcolindices.Contains(i) Then .Columns(i).HeaderText = "Totale"
            Next
            Dim dgvr As New DataGridViewRow
            dgvr.CreateCells(DGV)
            .Rows.Add(dgvr)

        End With

        AggiornaTotali()
        ResizeDGV()

    End Sub

    Public Sub SetColumnFormat(ByVal columnIndex As Integer, ByVal format As String)

        Try
            DGV.Columns(columnIndex).DefaultCellStyle.Format = format
            DGV_totals.Columns(columnIndex).DefaultCellStyle.Format = format
        Catch ex As Exception
            MessageBox.Show("Impossibile applicare questo Formato.", "Azione annullata", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        End Try

    End Sub

    Private Sub TDataGridView_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load

        Me.BorderStyle = Windows.Forms.BorderStyle.FixedSingle
        Me.MinimumSize = New Size(200, 100)

        With DGV
            .AllowUserToResizeRows = False
            .ScrollBars = ScrollBars.Vertical
            .Top = 1
            .Left = 1
            .Width = Me.Width - 4
            .Anchor = AnchorStyles.Left Or AnchorStyles.Top Or AnchorStyles.Right
        End With

        With DGV_totals
            .ReadOnly = True
            .AllowUserToOrderColumns = False
            .AllowUserToResizeColumns = True
            .AllowUserToResizeRows = False
            .ScrollBars = ScrollBars.Horizontal
            .ColumnHeadersVisible = True
            .RowHeadersVisible = DGV.RowHeadersVisible
            .Left = DGV.Left
            .Width = DGV.Width
            .Anchor = AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Bottom
        End With

    End Sub

    Private Sub ResizeDGV()

        DGV.AutoResizeRows()
        With DGV_totals
            .AutoResizeRows()
            .Width = DGV.Width
            If DGV.Controls.OfType(Of VScrollBar).SingleOrDefault.Visible = True Then
                .Width -= SystemInformation.VerticalScrollBarWidth
            End If
            If .Rows.Count > 0 Then
                .Height = .Rows(0).Height + 1
            Else
                .Height = .RowTemplate.Height + 1
            End If
            If .ColumnHeadersVisible = True Then
                .Height += .ColumnHeadersHeight
            End If
            If .Controls.OfType(Of HScrollBar).SingleOrDefault.Visible = True Then
                .Height += SystemInformation.HorizontalScrollBarHeight
            End If
            .Top = Me.Height - .Height - 3
            DGV.Height = Me.Height - DGV.Top - .Height - 2
        End With

    End Sub

    Protected Overrides Sub OnResize(ByVal e As System.EventArgs)

        ResizeDGV()
        Me.Refresh()
        MyBase.OnResize(e)

    End Sub

    Private Sub DGV_ColumnWidthChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) Handles DGV.ColumnWidthChanged

        DGV_totals.Columns(e.Column.Index).Width = DGV.Columns(e.Column.Index).Width
        ResizeDGV()
        DGV_totals.HorizontalScrollingOffset = DGV.HorizontalScrollingOffset

    End Sub

    Private Sub DGV_ColumnHeaderMouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles DGV.ColumnHeaderMouseClick

        DGV.HorizontalScrollingOffset = DGV_totals.HorizontalScrollingOffset

    End Sub

    Private Sub DGV_CellMouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellMouseEventArgs) Handles DGV.CellMouseClick

        If e.Button = Windows.Forms.MouseButtons.Right Then RaiseEvent DGVCellRightClick(e.ColumnIndex, e.RowIndex, DGV(e.ColumnIndex, e.RowIndex).Value)

    End Sub

    Private Sub DGV_totals_ColumnWidthChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewColumnEventArgs) Handles DGV_totals.ColumnWidthChanged

        DGV.Columns(e.Column.Index).Width = DGV_totals.Columns(e.Column.Index).Width
        ResizeDGV()
        DGV_totals.HorizontalScrollingOffset = DGV.HorizontalScrollingOffset

    End Sub

    Private Sub DGV_totals_Scroll(ByVal sender As Object, ByVal e As System.Windows.Forms.ScrollEventArgs) Handles DGV_totals.Scroll

        DGV.HorizontalScrollingOffset = DGV_totals.HorizontalScrollingOffset

    End Sub

    Private Sub DGV_CellValueChanged(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewCellEventArgs) Handles DGV.CellValueChanged

        AggiornaTotali()

    End Sub

    Private Sub DGV_RowsRemoved(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewRowsRemovedEventArgs) Handles DGV.RowsRemoved

        AggiornaTotali()

    End Sub

    Private Sub DGV_RowsAdded(ByVal sender As Object, ByVal e As System.Windows.Forms.DataGridViewRowsAddedEventArgs) Handles DGV.RowsAdded

        AggiornaTotali()

    End Sub

End Class

A questo punto basta compilare, e su Form "FormMain" possiamo trascinare un nuovo Controllo "TDataGridView1" direttamente dalla ToolBar di Visual Studio. come si nota subito, grazie all'Overrides Sub OnResize(), e al Metodo interno ResizeDGV(), le posizioni e proporzioni dei due DataGridView nel nuovo Controllo si adattano anche in design ad ogni operazione manuale.

Nell'esempio ho scelto di gestire :

- DataSource a livello di TDataGridView : definito semplicemente assieme agli indici di colonna destinati a contenere i totali.
- Font a livello di TDataGridView : una Font unica.
- Resize : sia a Design, sia a Runtime. Posizioni e proporzioni, nonchè variazioni della larghezza Colonne, sia sul DGV principale, sia sul secondario, sono completamente automatiche.
- Scrolling : lo scrolling viene sempre propagato dal DGV secondario ( Totali ), mentre la HScrollBar sul principale non è visibile. Visivamente il Controllo sembra una sorta di "DataGridView avanzato" diviso in due sezioni...
- DGVCellRightClick() : un esempio di come implementare un Evento e i suoi parametri a livello di UserControl.
- SetColumnFormat : per impostare in modo unificato il formato di visualizzazione dei campi numerici soggetti a calcolo del totale.
- Calcolo dei Totali Colonna : NON eseguito banalmente con cicli sulle celle, MA a livello di DataSource, grazie al Metodo Compute() del DataTable.

A questo punto non resta altro che aggiungere un nuovo "TDataGridView1" a FormMain, per vederlo all'opera.

--> Codice per FormMain :

Public Class FormMain

    Private Sub btn_test_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_test.Click

        'DataTable
        Dim DT As New DataTable
        DT.Columns.Add("ID", GetType(Integer))
        DT.PrimaryKey = New DataColumn() {DT.Columns("ID")}
        DT.Columns.Add("Campo1", GetType(String))
        DT.Columns.Add("Campo2", GetType(Integer))
        DT.Columns.Add("Campo3", GetType(Single))
        DT.Columns.Add("Campo4", GetType(Double))
        For i As Integer = 1 To 50
            DT.Rows.Add({i, "Campo1_" & i, i, 1.23 * i, 12.3456 * i})
        Next

        'TDataGridView1
        With TDataGridView1
            .SetDataSource(DT, New List(Of Integer)({2, 3, 4}))

            'Formati e altre proprietà ...
            '...
            .SetColumnFormat(3, "N2")
            .SetColumnFormat(4, "N4")
            '...
        End With

        'Test Nomi Colonne
        Dim SB As New System.Text.StringBuilder
        With TDataGridView1
            For i As Integer = 0 To DT.Columns.Count - 1
                SB.Append(DT.Columns(i).ColumnName & " / " & .DGV.Columns(i).Name & " / " & .DGV_totals.Columns(i).Name & _
                          Environment.NewLine)
            Next
        End With
        MessageBox.Show(SB.ToString)

    End Sub

    Private Sub btn_testnothing_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_testnothing.Click

        TDataGridView1.SetDataSource(Nothing, Nothing)

    End Sub

    Private Sub TDataGridView1_DGVCellRightClick(ByVal column As System.Int32, ByVal row As System.Int32, ByVal value As System.Object) Handles TDataGridView1.DGVCellRightClick

        MessageBox.Show("[" & column & ", " & row & "] = " & value.ToString)

    End Sub

    Private Sub btn_testfont_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_testfont.Click

        Using FD As New FontDialog
            FD.Font = TDataGridView1.DgvFont
            If FD.ShowDialog = Windows.Forms.DialogResult.OK Then TDataGridView1.DgvFont = FD.Font
        End Using

    End Sub

End Class


--> Come si nota, il blocco di codice destinato all'impostazione del TDataGridView1 da parte del codice Form è ridotto all'osso : imposto il DataSource, gli indici dei Campi da riportare in Totale, e la formattazione sui Campi con virgole. Tutto il resto, sia a design, sia a runtime, è gestito all'interno della Classe UserControl.

--> Il ciclo "Test Nomi Colonne" serve solo allo scopo di verficare la corrispondenza tra indici e nomi dei Campi, che sono gli stessi per DT, DGV e DGV_totals. Quando servirà leggere i totali da DGV_totals questo fatto tornerà utile...

--> TDataGridView1_DGVCellRightClick() è l'utilizzo del nuovo Evento personalizzato creato a livello di UserControl.

--> btn_testfont permette di impostare la Font comune per TDataGridView1. Da notare che questa impostazione funziona anche nel caso in cui il DataSource sia a Nothing ( cmd_testnothing ).

+ Fine Articolo.

Un Click su "Mi Piace" è il modo migliore per ringraziare l'autore di questo articolo.



9 commenti:

Pietro Bono ha detto...

Ciao MarcoGG, ho dato un occhiata al tuo usercontrol datagridview con righe totali, lo vorrei usare in uan mia applicazione, fin qui nessun problema, ma lo volevo pure usare come un controllo che eredita da datagridview, è possibile?

Pietro Bono ha detto...

Ooopss! scusa per la mia domanda, ho risolto!!! complimenti per il controllo è molto utile.. :)

MarcoGG ha detto...

Ciao. Come tu stesso hai notato, si tratta di un UserControl che include due DataGridView. Pertanto non può ereditare da DataGridView stesso. Chiedi pure se hai dubbi. - Grazie. In effetti mi sono divertito su questo Controllo. Ed è stato apprezzato da diversi utenti. ;-)

Pietro Bono ha detto...

Ciao MarcoGG volevo chiederti una cosa su questo controllo;
ho notato che quando elimino l'intera riga selezionandola e premendo canc da tastiera, i totali non si aggiornano, si può fare qualcosa? per risolvere? grazie sempre..... anticipatamente..

MarcoGG ha detto...

Ciao. Anzitutto ti ringrazio per il feedback sul presente Articolo. Lo scopo di questo spazio per i commenti è anche quello di segnalare dubbi e perplessità. In questo modo posso migliorare le soluzioni proposte anche sulla scorta delle segnalazioni degli utenti. In particolare questa tua è opportuna, e ne ho approfittato per rivedere l'intero codice della Classe TDataGridView. Penso che ora il funzionamento generale del controllo sia decisamente migliorato. Invito te e chiunque sia in possesso della vecchia versione, di passare a quest'ultima.

Pietro Bono ha detto...

Ciao MarcoGG ho visto la nuova versione del tuo controllo perfetto, era quello che mancava, mi sono permesso di aggiungere altri metodi al controllo, dato che si tratta di un contenitore che contiene dei numeri, ho voluto inserire la gestione della virgola decimale, e delle operazioni tra celle, ti invio il codice tramite email, ho fatto tutto in base alle mie conoscenze, spero che lo guardi e che migliori il codice ove è necessario, fammi sapere ciao.

MarcoGG ha detto...

Ottimo. :) Sì, adesso sicuramente è più funzionale e stabile di prima, anche nei casi in cui l'utente arrivi a lavorare su una sola Row, o anche ad eliminare tutte le Row e a reinserirne di nuove a mano. Darò senz'altro un'occhiata alla tua Mail. Ma intanto, anche grazie ai feedback come il tuo, aspetterei di avere la certezza sulla definitiva stabilità di quanto già c'è, e inoltre ad inserire anche la corrispondente versione C#.
A differenza della quasi-totalità dei miei Articoli, questo è nato in risposta a richieste di altri utenti su Forum tecnici, e non ho avuto ancora modo di usarlo ( e quindi "stressarlo" di persona ;) ) all'interno di applicazioni mie.

Anonimo ha detto...

Ottimo.
Sarebbe bello avere un .zip del progetto, perchè con il copia e incolla del codice che hai messo è un casino.
Ciao :)

MarcoGG ha detto...

Ciao :).
Esattamente cosa trovi di così difficoltoso nel Copia/Incolla del codice ?
C'è una Guida all'uso del Blog :
http://marcoggblog.blogspot.com/p/informazioni.html
in cui ho chiaramente spiegato che basta fare Doppio-Click in ogni Code-Box per selezionare tutto il codice. Non è necessario scrollare...
Detto questo, arriveranno anche gli allegati agli Articoli, ma si vedrà più avanti.

Posta un commento

 
Design by Free WordPress Themes Modificato da MarcoGG