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.
9 commenti:
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?
Ooopss! scusa per la mia domanda, ho risolto!!! complimenti per il controllo è molto utile.. :)
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. ;-)
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..
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.
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.
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.
Ottimo.
Sarebbe bello avere un .zip del progetto, perchè con il copia e incolla del codice che hai messo è un casino.
Ciao :)
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