domenica 13 novembre 2011

[VB.NET] Dati Controllo Chart

Descrizione :
Esempio pratico di associazione dati e loro rappresentazione con il controllo Chart.

+ Articolo :

Anzitutto, dal momento che di "controlli chart" in giro ce ne sono davvero tanti, voglio precisare che l'argomento in esame riguarda esclusivamente il controllo incluso in Visual Studio ( in questo caso : 2010 ), e per l'esattezza quello in Forms :
System.Windows.Forms.DataVisualization.Charting.Chart().

Ho creato questo articolo per un duplice scopo :
1. Mostrare una tecnica valida per collegare un Chart con le sue varie Serie, ad una unica fonte dati, come un DataTable.
2. Illustrare 4 diversi modi per evidenziare ciascun dato, una volta che è rappresentato graficamente nel Chart.

Oltre a questo, si noterà come diversi controlli, come le ComboBox per la scelta di ciascun Punto-Dati, vengano automaticamente messe in relazione all'atto della loro associazione con una fonte dati comune.

Per replicare l'articolo occorre una semplice Form "FormMain", con i seguenti controlli essenziali :
Chart --> Chart1
ComboBox --> cmb_x
ComboBox --> cmb_serie2
ComboBox --> cmb_serie3
Le Label sono a scopo descrittivo.

Un'immagine rende bene l'idea della struttura desiderata e del funzionamento ottenuto :


--> Codice completo FormMain :

Public Class FormMain

    Private fontLab As New Font(FontFamily.GenericSansSerif, 14, FontStyle.Regular)
    Private DT As New DataTable("TabellaDati")
    Private percorsoBmp As String = Application.StartupPath & "\"
    Private nomeBmp As String = "arrow.bmp"

    Private Function RandomInteger(ByVal min As Integer, ByVal max As Integer, ByVal seed As Integer) As Integer
        Return (New Random(seed)).Next(min, max)
    End Function

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

        'Formattazione DataTable Tabella Dati per contenere due serie
        'Asse X comune alle Serie Dati
        DT.Columns.Add("X", GetType(System.Int32))
        DT.PrimaryKey = {DT.Columns("X")}
        'Asse Y Serie1
        DT.Columns.Add("YSerie1", GetType(System.Single))
        'Asse Y Serie2
        DT.Columns.Add("YSerie2", GetType(System.Int32))
        'Asse Y Serie3
        DT.Columns.Add("YSerie3", GetType(System.Int32))
        'Asse Y Serie4
        DT.Columns.Add("YSerie4", GetType(System.Int32))

        'Creazione Dati ( Pseudo-Random )
        For i As Integer = 1 To 20
            DT.Rows.Add({i, RandomInteger(10, 25, i ^ 2) * 1.2, _
                            RandomInteger(25, 50, i ^ 3), _
                            RandomInteger(50, 75, i ^ 4), _
                            RandomInteger(75, 100, i ^ 5)})
        Next
        'Creazione Serie
        Chart1.Series.Clear()
        Chart1.DataSource = DT
        'Serie1
        Dim serie1 As New DataVisualization.Charting.Series("Serie1")
        With serie1
            .ChartType = DataVisualization.Charting.SeriesChartType.Column
            .Color = Color.Goldenrod
            .BorderWidth = 1
            .BorderColor = Color.Black

            .XValueMember = DT.Columns("X").ToString
            .YValueMembers = DT.Columns("YSerie1").ToString
        End With
        Chart1.Series.Add(serie1)
        serie1.ToolTip = serie1.Name & " [ #VALX{F0} | #VALY{F2} ]"

        'Serie2
        Dim serie2 As New DataVisualization.Charting.Series("Serie2")
        With serie2
            .ChartType = DataVisualization.Charting.SeriesChartType.Line
            .Color = Color.Red
            .BorderWidth = 2
            .MarkerSize = 12
            .MarkerBorderColor = Color.Black
            .MarkerBorderWidth = 2
            .MarkerStyle = DataVisualization.Charting.MarkerStyle.Diamond

            .XValueMember = DT.Columns("X").ToString
            .YValueMembers = DT.Columns("YSerie2").ToString
        End With
        Chart1.Series.Add(serie2)
        'Serie3
        Dim serie3 As New DataVisualization.Charting.Series("Serie3")
        With serie3
            .ChartType = DataVisualization.Charting.SeriesChartType.Line
            .Color = Color.SlateBlue
            .BorderWidth = 2
            .MarkerSize = 10
            .MarkerBorderColor = Color.Blue
            .MarkerBorderWidth = 2
            .MarkerStyle = DataVisualization.Charting.MarkerStyle.Circle

            .XValueMember = DT.Columns("X").ToString
            .YValueMembers = DT.Columns("YSerie3").ToString
        End With
        Chart1.Series.Add(serie3)
        'Serie4
        Dim serie4 As New DataVisualization.Charting.Series("Serie4")
        With serie4
            .ChartType = DataVisualization.Charting.SeriesChartType.Line
            .Color = Color.Green
            .BorderWidth = 2
            .MarkerSize = 8
            .MarkerBorderColor = Color.DarkGreen
            .MarkerBorderWidth = 2
            .MarkerStyle = DataVisualization.Charting.MarkerStyle.Square

            .XValueMember = DT.Columns("X").ToString
            .YValueMembers = DT.Columns("YSerie4").ToString
        End With
        Chart1.Series.Add(serie4)

        With cmb_x
            .DropDownStyle = ComboBoxStyle.DropDownList
            .DataSource = DT
            .ValueMember = DT.Columns("X").ToString
            .DisplayMember = DT.Columns("X").ToString
        End With

        With cmb_serie2
            .DropDownStyle = ComboBoxStyle.DropDownList
            .DataSource = DT
            .ValueMember = DT.Columns("X").ToString
            .DisplayMember = DT.Columns("YSerie2").ToString
        End With

        With cmb_serie3
            .DropDownStyle = ComboBoxStyle.DropDownList
            .DataSource = DT
            .ValueMember = DT.Columns("X").ToString
            .DisplayMember = DT.Columns("YSerie3").ToString
        End With

    End Sub

    Private Sub cmb_serie2_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_serie2.SelectedIndexChanged

        With Chart1.Series("Serie2")
            If .Points.Count = 0 Then Exit Sub
            For i As Integer = 0 To .Points.Count - 1
                .Points(i).Label = String.Empty
                .Points(i).MarkerStyle = DataVisualization.Charting.MarkerStyle.Diamond
            Next
        End With

        With Chart1.Series("Serie2").Points(cmb_serie2.SelectedIndex)
            .Font = fontLab
            .Label = "Serie2 - " & "X = " & cmb_serie2.SelectedValue & " | Y = " & cmb_serie2.Text
            .LabelBackColor = Color.Gold
            .LabelBorderColor = Color.Black
            .LabelBorderWidth = 1
            .LabelForeColor = Color.Black
            .MarkerStyle = DataVisualization.Charting.MarkerStyle.Star10
        End With

    End Sub

    Private Sub cmb_serie3_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_serie3.SelectedIndexChanged

        With Chart1.Series("Serie3")
            If .Points.Count = 0 Then Exit Sub
            For i As Integer = 0 To .Points.Count - 1
                .Points(i).MarkerImage = String.Empty
            Next
        End With

        With Chart1.Series("Serie3").Points(cmb_serie3.SelectedIndex)
            .MarkerImage = percorsoBmp & nomeBmp
            .MarkerImageTransparentColor = Color.White
        End With

    End Sub

    Private Sub Chart1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Chart1.MouseMove

        Dim HTR As DataVisualization.Charting.HitTestResult = Chart1.HitTest(e.X, e.Y)
        'Controllo sul fatto che sia un DataPoint valido
        If Not HTR.ChartElementType = DataVisualization.Charting.ChartElementType.DataPoint Then Exit Sub
        'Controllo sul fatto che sia la Serie desiderata ( Serie4 )
        If Not HTR.Series.Name = "Serie4" Then Exit Sub

        With Chart1.Series("Serie4")
            For i As Integer = 0 To .Points.Count - 1
                .Points(i).Label = String.Empty
                .Points(i).MarkerStyle = DataVisualization.Charting.MarkerStyle.Square
            Next
        End With
        With Chart1.Series("Serie4").Points(HTR.PointIndex)
            .Font = fontLab
            .Label = "Serie4 - " & "X = " & .XValue & " | Y = " & .YValues(0)
            .LabelBackColor = Color.YellowGreen
            .LabelBorderColor = Color.Black
            .LabelBorderWidth = 1
            .LabelForeColor = Color.Black
            .MarkerStyle = DataVisualization.Charting.MarkerStyle.Star10
        End With

    End Sub

End Class

Note :

--> Il Chart, le Serie e anche le ComboBox puntano al medesimo DataTable, chiaramente ciascuno al suo o suoi campi di pertinenza.

--> Serie1 : evidenzia il dato al passaggio del Mouse come semplice ToolTip.

--> Serie2 : evidenzia il dato ( scelto in cmb_serie2 ) con una Label. A mio avviso la scelta più completa e flessibile.

--> Serie3 : evidenzia il dato ( scelto in cmb_serie3 ) con una bitmap caricata da disco. In questo caso sarà necessario predisporre un'immagine che abbia al suo centro il Marker, e uno o più indicatori esterni. La figura seguente mostra come ho creato la mia "arrow.bmp" in 3 semplici passi, aiutandomi semplicemente con le Forme di Word 2007 :


--> Serie4 : evidenzia il dato al passaggio del Mouse con una Label. Comportamento molto simile al ToolTip della Serie1, ma molto più flessibile nella gestione.

--> Come già accennato, una selezione su ciascuna delle 3 ComboBox si propaga alle altre in automatico e senza che sia necessario estendere gli Handles nelle routine di evento, o scrivere alcun codice aggiuntivo, perchè ciò accade solo grazie alle associazioni fatte in precedenza con i Campi del DataTable.

+ Fine Articolo.

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



[Excel VBA] DDE

Descrizione :
Un mio esempio pratico di utilizzo DDE da Excel VBA completo di generatore DDE ( VB6 ) per simulazioni.

+ Articolo :

Tempo fa ho creato questo esempio per un Forum, dopo un buon bombardamento di richieste da parte di utenti che ( purtroppo per loro ) devono avere a che fare con il DDE in Excel.

Per chi non sapesse di cosa sto parlando, DDE sta per "Dynamic Data Exchange" :
http://en.wikipedia.org/wiki/Dynamic_Data_Exchange

L'esempio simula un generatore di valori, che ho scritto in VB6, dal momento che VB6 ha un supporto nativo al DDE, e un progetto Excel che "ascolta" e registra i dati DDE in arrivo...
Chiaramente il punto su cui focalizzare l'attenzione è il lato-Excel della tecnica in quanto, nel caso reale, i dati arriveranno tipicamente da links remoti e non da un generatore/simulatore locale.

--> Listener_DDE.xls :
WorkBook Excel con il codice VBA essenziale per ricevere i dati DDE e inoltre per verificare se e quando i dati arrivano effettivamente.
L'essenziale per il Listener_DDE.xls è che ci sia un Foglio "DDE" e un Modulo VBA.
Su Foglio "DDE" ho inserito due CommandButton ( del tipo ActiveX, come sempre ), cmd_attivadde e cmd_disattivadde.
La loro funzione, come suggeriscono i nomi, è di attivare/disattivare l'ascolto di eventuali dati DDE in arrivo.

Il codice per il Foglio "DDE" è il seguente :

Private Sub cmd_attivadde_Click()

    ApplicaFormuleDDE

    Dim SorgentiDDE As Variant
    SorgentiDDE = ActiveWorkbook.LinkSources(xlOLELinks)
    Dim i As Integer
    If Not IsEmpty(SorgentiDDE) Then
        For i = 1 To UBound(SorgentiDDE)
            ActiveWorkbook.SetLinkOnData SorgentiDDE(i), "SuArrivoDatiDDE"
        Next i
    Else
        MsgBox "Nessuna Sorgente DDE Rilevata", vbExclamation, "Errore"
    End If

End Sub

Private Sub ApplicaFormuleDDE()

    'A1
    Sheets("DDE").Range("A1").FormulaR1C1 = "=GeneratoreValori_DDE|Form1!txt_dati01"

End Sub

Private Sub cmd_disattivadde_Click()

    'A1
    Sheets("DDE").Range("A1").FormulaR1C1 = ""

End Sub

"SuArrivoDatiDDE", come si nota è il nome della Routine, passato come stringa al Metodo SetLinkOnData.
Per questo ho creato un Modulo VBA e vi ho inserito il codice seguente :

Public Sub SuArrivoDatiDDE()

    Sheets("DDE").Range("B1").FormulaR1C1 = Sheets("DDE").Range("A1").Text

End Sub

La Public Sub SuArrivoDatiDDE() è la Routine in cui inserire le varie operazioni da eseguire sui dati DDE in arrivo.
In questo caso l'unica operazione eseguita è la copia dell'ultimo valore ricevuto da A1 a B1.
Ovviamente si possono inserire istruzioni di copia in righe e colonne, registrazione dati in file di testo, inserimenti a DB, grafici e quant'altro...

--> GeneratoreValori_DDE.exe :
L'applicazione VB6 che genera, in questo caso, una semplice serie di valori numerici crescenti, ed è costituita da una sola Form ed alcuni semplici controlli.
Un'immagine rende subito l'idea della struttura :


ComboBox --> cmb_secondi
CommandButton --> cmd_attivainviodde
CommandButton --> cmd_disattivainviodde
TextBox --> txt_dati01
Timer --> Timer1

Il codice completo del Progetto / Form VB6 di GeneratoreValori_DDE è il seguente :
'****************************************************
'***** Generatore Dati DDE Locale *******************
'***** MARCOGG 2011 *********************************
'****************************************************

' Impostare a Design le seguenti proprietà su Form1 :
' LinkMode = 1 - Source
' LinkTopic = Form1

Private num As Long

Private Sub cmb_secondi_Click()

    Timer1.Interval = CInt(cmb_secondi.Text) * 1000

End Sub

Private Sub Form_Load()

    Dim i As Integer
    For i = 1 To 60
        cmb_secondi.AddItem (i)
    Next i
    cmb_secondi.ListIndex = 0

End Sub

Private Sub cmd_attivainviodde_Click()

    cmb_secondi.Enabled = False
    Timer1.Enabled = True
    
End Sub

Private Sub cmd_disattivainviodde_Click()

    cmb_secondi.Enabled = True
    Timer1.Enabled = False
    
End Sub

Private Sub Timer1_Timer()

    num = num + 1
    txt_dati01.Text = num

End Sub

Naturalmente potrei avere un Generatore VB6 con N controlli che generano N valori, anche su tempi diversi e il WorkBook-Listener potrebbe avere le corrispondenti N Celle che li ricevono...

--> Run :

1. Lanciare in esecuzione GeneratoreValori_DDE.exe e Listener_DDE.xls.

2. Selezionare sul Generatore l'intervallo in secondi desiderato tra un invio dati DDE e il successivo, e poi Click su "Attiva Invio Dati DDE".

3. Sul WorkBook, Click su "Attiva Ascolto DDE".

A questo punto c'è tutto l'essenziale per approfondire ed eseguire test, e ovviamente per completarlo e complicarlo a piacere.

+ Fine Articolo.

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



martedì 8 novembre 2011

[VB.NET] Gestione Avanzata Controlli

Descrizione :
Un mio progetto con numerose idee sulla gestione avanzata di controlli Windows Forms.

+ Articolo :

In questo articolo mi sono divertito un po' a mettere insieme una tecnica sulla gestione di controlli a runtime in VB.NET.
Nella fattispecie parliamo di una Classe, la "PBox", che eredita da PictureBox, cercando di sfruttare, o almeno di dare un'indicazione a tutti quelli che vorranno provare, su come sfruttare l'enorme potenzialità di VB.NET, soprattutto se confrontato con il "fratellino" VB6, dal quale moltissimi ancora stentano a separarsi.

Lo scopo dell'articolo, che vuole essere soprattutto propositivo, e che quindi certamente risente di scelte personali e soggettive ( salterà all'occhio in questo caso, quella di aver fatto a meno dei DataBindings, BindingSource, ecc, e di aver gestito diversamente l'aggiornamento del DataTable... ) , è perciò quello di fornire qualche tecnica interessante sulla gestione in generale dei Controls a runtime. Il tutto inserito in un qualcosa a cui ho voluto dare uno scopo finale.
Di carne al fuoco ce n'è parecchia, perciò cercherò di essere il più stringato possibile.

Il Progetto consente tutte le seguenti operazioni a runtime su oggetti della Classe PBox :

--> Aggiungere nuova PBox in 3 modi :
PBox vuota / PBox da file / PBox con Drag & Drop.

--> Eliminare PBox.

--> Clonare PBox da esistente.

--> Spostare e ridimensionare PBox con il Mouse :
Il Container è uno ScrollableControl ( in questo caso un Panel ), e le operazioni di spostamento possono portare a gestire un'area delle dimensioni ben superiori a quella della sola Form visibile.

--> Aggiungere 2 Immagini distinte per ogni PBox, una di sfondo e una principale.

--> Modificare ogni modalità di disegno disponibile per ognuna delle 2 Immagini :
5 Layout diversi per lo sfondo e 5 SizeMode diversi per la principale.

--> Modificare l'ordine di sovrapposizione "ZOrder" con 4 funzioni :
Porta Avanti / Indietro / Primo Piano / Dietro a Tutti.

--> Modificare il colore di sfondo.

--> Salvataggio degli oggetti PBox in formato XML.

--> Caricamento degli oggetti PBox.

--> Salvataggio dell'intero container in formato immagine Bitmap :
Il salvataggio non avviene con un banale capture screen, bensì l'intera area dello ScrollableControl viene trasferita su file Bitmap, comprendendo anche le aree soggette a Scroll e al momento non visibili.

Il tutto viene gestito via Mouse e con una speciale "Form Menu", richiamata con un Click Destro sulla PBox, che non è necessario aprire e chiudere ogni volta per ogni PBox che si vuole modificare, ma che una volta aperta, sta in primo piano sulla Form dell'applicazione e si ancora automaticamente alla PBox selezionata.

Per ragioni di semplicità, e visto anche che le funzioni che volevo implementare erano ben altre, ho volutamente scelto di non gestire controlli inseriti a design. Dal momento che tutta la situazione si può salvare, credo sia un aspetto di importanza minima. Ovviamente la gestione degli errori è quella essenziale. Ho evitato di inserire Try Catch in ogni situazione passibile di errore. In ogni caso l'applicazione funziona perfettamente.

Dato che ho deciso di allegare direttamente i sorgenti, evito la lunga descrizione dei controlli con nomi e proprietà da impostare a design.

Come spesso accade, un'immagine vale più di mille parole :



Il DataGridView in alto è solo a scopo descrittivo e mostra come ogni operazione mappata sugli oggetti PBox viene registrata in una Classe apposita, che eredita da DataTable, e che si occupa di tutte le operazioni esterne, aggiunta, eliminazione, clonazione, salvataggio e caricamento XML.

Al di sotto dei pulsanti c'è il Panel, ossia il contenitore in cui inserire e gestire i Controls PBox.
L'immagine mostra 4 PBox con altrettante immagini PNG trasparenti, due con immagine di sfondo, e due senza. Questo per dare un'idea di come l'oggetto "capisca" che tipo di immagine contiene e la mostri correttamente. Per le GIF è ancora meglio, in quanto, se una .gif inserita come immagine principale, è animata, allora la PBox mostrerà anche l'animazione.

FILE e CLASSI :

1. DTPBoxes.vb :
La classe che eredita da DataTable e si occupa della memorizzazione e della gestione degli Oggetti Pbox.

'***********************************************************************************
'************** Gestione Avanzata Controlli PBox ***** MARCOGG 2011 ****************
'***********************************************************************************

Public Class DTPBoxes
    Inherits DataTable

    Private m_parentcontrol As Control
    Private m_iddisp As Integer
    Private m_cloneoffsetxy As Integer = 20
    Private m_propertyname As String
    Private m_propertyvalue As String

    Public ReadOnly Property IdDisp() As Integer
        Get
            If Me.Rows.Count = 0 Then Return 1
            m_iddisp = 0
            Do
                m_iddisp += 1
                If Me.Rows.Find(m_iddisp) Is Nothing Then Return m_iddisp
            Loop
        End Get
    End Property

    Public Sub New(ByVal parentControl As Control)

        m_parentcontrol = parentControl
        Me.TableName = "PBox"

        'PK
        Me.Columns.Add("ID") '0
        Me.PrimaryKey = New DataColumn() {Me.Columns("ID")}

        'Campi
        With Me.Columns

            .Add("ZOrder", System.Type.GetType("System.Int32")) '1
            '-----------------------------
            .Add("BackColor")
            .Add("FileImmagineSfondo")
            .Add("BackgroundImageLayout")
            .Add("FileImmagine")
            .Add("SizeMode")

            .Add("Left")
            .Add("Top")
            .Add("RealLeft")
            .Add("RealTop")
            .Add("Width")
            .Add("Height")
            .Add("MinimumSize")

        End With

    End Sub

    Public Sub Aggiorna(ByVal pbx As PBox)

        Dim pbxRow As DataRow = Me.Rows.Find(pbx.ID)
        If pbxRow IsNot Nothing Then
            For i As Integer = 1 To Me.Columns.Count - 1
                m_propertyname = Me.Columns(i).ColumnName
                pbxRow.Item(m_propertyname) = pbx.GetPropertyValue(m_propertyname)
            Next
        End If

    End Sub

    Public Sub AggiornaTutti()

        For Each C As Control In m_parentcontrol.Controls
            If TypeOf (C) Is PBox Then Me.Aggiorna(DirectCast(C, PBox))
        Next

    End Sub

    Public Sub Aggiungi(ByVal pbx As PBox)

        With pbx
            .ID = Me.IdDisp
            m_parentcontrol.Controls.Add(pbx)
            .BringToFront()
            Me.Rows.Add(New Object() {.ID})
        End With

        Me.AggiornaTutti()

    End Sub

    Public Sub Clona(ByVal pbx As PBox)

        Dim newPbx As New PBox

        Me.Aggiungi(newPbx)

        newPbx.AggiornaDTPEnabled = False
        For i As Integer = 2 To Me.Columns.Count - 1
            m_propertyname = Me.Columns(i).ColumnName
            m_propertyvalue = pbx.GetPropertyValue(m_propertyname)
            newPbx.SetPropertyValue(m_propertyname, m_propertyvalue)
        Next
        newPbx.Left += m_cloneoffsetxy
        newPbx.Top += m_cloneoffsetxy
        newPbx.AggiornaDTPEnabled = True

    End Sub

    Public Sub Rimuovi(ByVal pbx As PBox)

        pbx.AggiornaDTPEnabled = False
        Me.Rows.Remove(Me.Rows.Find(pbx.ID))
        m_parentcontrol.Controls.Remove(pbx)
        Me.AggiornaTutti()

    End Sub

    Public Sub RimuoviTutti()

        For i As Integer = m_parentcontrol.Controls.Count - 1 To 0 Step -1
            If TypeOf (m_parentcontrol.Controls(i)) Is PBox Then Me.Rimuovi(m_parentcontrol.Controls(i))
        Next

    End Sub

    Public Sub Salva()

        Me.DefaultView.Sort = "ZOrder DESC"
        Me.DefaultView.ToTable.WriteXml(percorso & nomeFileSalvataggio)

    End Sub

    Private Sub CaricaXml(ByVal nomeFileXml As String)

        Me.RimuoviTutti()
        Me.Clear()
        Me.ReadXml(nomeFileXml)

        Dim pbx As PBox

        For row As Integer = 0 To Me.Rows.Count - 1

            pbx = New PBox
            pbx.AggiornaDTPEnabled = False
            m_parentcontrol.Controls.Add(pbx)
            For col As Integer = 0 To Me.Columns.Count - 1
                m_propertyname = Me.Columns(col).ColumnName
                m_propertyvalue = Me.Rows(row).Item(m_propertyname)
                pbx.SetPropertyValue(m_propertyname, m_propertyvalue)
            Next
            pbx.PrimoPiano()
            pbx.Left = pbx.RealLeft
            pbx.Top = pbx.RealTop
            pbx.AggiornaDTPEnabled = True

        Next

        AggiornaTutti()

    End Sub

    Public Sub Carica()
        Try
            CaricaXml(percorso & nomeFileSalvataggio)
        Catch ex As Exception
        End Try
    End Sub

    Public Sub SelezionaSuDgv(ByVal pbx As PBox)

        With DirectCast(m_parentcontrol.Parent, FormMain).dgvtest
            For Each r As DataGridViewRow In .Rows
                If r.Cells("ID").Value = pbx.ID Then r.Selected = True
            Next
        End With

    End Sub

End Class


2. FormMain.vb :
La Form di avvio.


L'apparente complessità della routine sotto il Button cmd_salvasnapshot su FormMain, è dovuta al workaround di un bug che affligge, a quanto pare, il Metodo DrawToBitmap(). Perciò non è imputabile al mio codice. Effettivamente, DrawToBitmap sballa l'ordine degli ZOrders ( Primo Piano, piani intermedi, Ultimo Piano... ). L'ordine dei piani delle PBox nel PNL viene invertito, nell'immagine bitmap risultante. Il problema perciò viene qui risolto con un workaround che inverte temporaneamente gli ZOrder delle PBox, per poi riportarli ai valori originari.

Public Class FormMain

    Private Sub FormMain_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        With PNL
            .AllowDrop = True
            .AutoScroll = True
            .Controls.Clear()
        End With
        DTP = New DTPBoxes(PNL)
        With dgvtest
            .MultiSelect = False
            .SelectionMode = DataGridViewSelectionMode.FullRowSelect
            .DataSource = DTP
        End With
    End Sub

    Private Sub PNL_DragDrop(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs) Handles PNL.DragDrop
        Dim fn As String = CType(e.Data.GetData(DataFormats.FileDrop), Array).GetValue(0).ToString
        Try
            Dim newPbx As New PBox
            newPbx.FileImmagine = fn
            DTP.Aggiungi(newPbx)
        Catch ex As Exception
            MessageBox.Show("File non supportato.", "Operazione Annullata", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        End Try
    End Sub

    Private Sub PNL_DragEnter(ByVal sender As Object, ByVal e As System.Windows.Forms.DragEventArgs) Handles PNL.DragEnter
        If (e.Data.GetDataPresent(DataFormats.FileDrop)) Then e.Effect = DragDropEffects.Copy
    End Sub

    Private Sub cmd_aggiungivuota_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_aggiungivuota.Click
        DTP.Aggiungi(New PBox)
    End Sub

    Private Sub cmd_carica_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_carica.Click
        DTP.Carica()
    End Sub

    Private Sub cmd_salva_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_salva.Click
        DTP.Salva()
    End Sub

    Private Sub cmd_aggiungifile_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_aggiungifile.Click
        Dim fn As String = GetFileImmagine()
        If Not fn = String.Empty Then
            Dim newPbx As New PBox
            newPbx.FileImmagine = fn
            DTP.Aggiungi(newPbx)
        End If
    End Sub

    Private Sub cmd_salvasnapshot_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_salvasnapshot.Click

        '-----------------------------------------
        'Inversione degli ZOrder per Bug ZOrder :
        Dim ids As New List(Of Integer)
        Dim zorders As New List(Of Integer)
        For Each pbx As PBox In PNL.Controls
            ids.Add(pbx.ID)
            zorders.Add(pbx.ZOrder)
        Next
        zorders.Reverse()
        For i As Integer = 0 To ids.Count - 1
            For Each pbx As PBox In PNL.Controls
                If pbx.ID = ids(i) Then pbx.ZOrder = zorders(i)
            Next
        Next
        '-----------------------------------------

        PNL.AutoScrollPosition = New Point(0, 0)
        My.Application.DoEvents()

        Dim pnlW As Integer = PNL.Width
        Dim pnlH As Integer = PNL.Height
        Dim W As Integer
        Dim H As Integer
        For Each C As Control In PNL.Controls
            If C.Right > W Then W = C.Right
            If C.Bottom > H Then H = C.Bottom
        Next

        Try
            W += SystemInformation.VerticalScrollBarWidth
            H += SystemInformation.HorizontalScrollBarHeight
            PNL.Width = W
            PNL.Height = H
            Using bmp As New Bitmap(W, H)
                PNL.DrawToBitmap(bmp, New Rectangle(Point.Empty, bmp.Size))
                bmp.Save(percorso & nomeFileSnapShot, Imaging.ImageFormat.Bmp)
            End Using
        Catch ex As Exception
            MessageBox.Show("Impossibile creare l'immagine.", "Errore", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
        Finally
            PNL.Width = pnlW
            PNL.Height = pnlH
        End Try

        '-----------------------------------------
        'Ripristino Zorders per Bug Zorder
        zorders.Reverse()
        For i As Integer = 0 To ids.Count - 1
            For Each pbx As PBox In PNL.Controls
                If pbx.ID = ids(i) Then pbx.ZOrder = zorders(i)
            Next
        Next
        '-----------------------------------------

        MessageBox.Show("File SnapShot Bitmap creato.", "OK", MessageBoxButtons.OK, MessageBoxIcon.Information)

    End Sub

End Class

3. FormMenu.vb :
Form per il Menu "Mobile".


Public Class FormMenu

    Private m_pbx As PBox

    Public Property PBX() As PBox
        Get
            Return m_pbx
        End Get
        Set(ByVal value As PBox)
            m_pbx = value
            Me.Text = m_pbx.ID

            'Valore Combo per BackgroundImageLayout di m_pbx
            cmb_backgroundimagelayout.SelectedItem = [Enum].Format(GetType(ImageLayout), m_pbx.BackgroundImageLayout, "F")
            'Valore Combo per SizeMode di m_pbx
            cmb_sizemode.SelectedItem = [Enum].Format(GetType(PictureBoxSizeMode), m_pbx.SizeMode, "F")
            'Label BackColor
            lbl_backcolor.BackColor = m_pbx.BackColor

            Riposiziona()
        End Set
    End Property

    Private Sub FormMenu_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        'Proprietà
        Me.ShowInTaskbar = False
        lbl_backcolor.Cursor = Cursors.Hand
        'Combo per BackgroundImageLayout
        With cmb_backgroundimagelayout
            .Items.AddRange([Enum].GetNames(GetType(ImageLayout)))
            .DropDownStyle = ComboBoxStyle.DropDownList
        End With
        'Combo per SizeMode
        With cmb_sizemode
            .Items.AddRange([Enum].GetNames(GetType(PictureBoxSizeMode)))
            .DropDownStyle = ComboBoxStyle.DropDownList
        End With
    End Sub

    Private Sub Riposiziona()

        Me.Visible = False
        Dim scrw As Integer = Screen.PrimaryScreen.Bounds.Width
        Dim scrh As Integer = Screen.PrimaryScreen.Bounds.Height
        Me.Left = m_pbx.PointToScreen(New Point(0, 0)).X + m_pbx.Width
        Me.Top = m_pbx.PointToScreen(New Point(0, 0)).Y
        If Me.Right > scrw Then Me.Left = m_pbx.PointToScreen(New Point(0, 0)).X - Me.Width
        If Me.Bottom > scrh Then Me.Top = m_pbx.PointToScreen(New Point(0, 0)).Y - Me.Height
        If Me.Left < 0 Then Me.Left = m_pbx.PointToScreen(New Point(0, 0)).X + m_pbx.Width
        If Me.Top < 0 Then Me.Top = 0
        Me.Visible = True

    End Sub

    Private Sub FormMenu_MouseClick(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Me.MouseClick
        If e.Button = Windows.Forms.MouseButtons.Right Then Me.Close()
    End Sub

    Private Sub cmd_elimina_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_elimina.Click
        DTP.Rimuovi(m_pbx)
        Me.Close()
    End Sub

    Private Sub cmd_clona_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_clona.Click
        DTP.Clona(m_pbx)
    End Sub

    Private Sub lbl_backcolor_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles lbl_backcolor.Click
        Dim CD As New ColorDialog
        If CD.ShowDialog() = DialogResult.OK Then
            lbl_backcolor.BackColor = CD.Color
            m_pbx.BackColor = CD.Color
        End If
    End Sub

    Private Sub cmd_immaginesfondo_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_immaginesfondo.Click
        Dim fn As String = GetFileImmagine()
        If Not fn = String.Empty Then m_pbx.FileImmagineSfondo = fn
    End Sub

    Private Sub cmd_immagine_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_immagine.Click
        Dim fn As String = GetFileImmagine()
        If Not fn = String.Empty Then m_pbx.FileImmagine = fn
    End Sub

    Private Sub cmb_backgroundimagelayout_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_backgroundimagelayout.SelectedIndexChanged
        Dim strSel As String = DirectCast(cmb_backgroundimagelayout.SelectedItem, String)
        m_pbx.BackgroundImageLayout = DirectCast([Enum].Parse(GetType(ImageLayout), strSel), ImageLayout)
    End Sub

    Private Sub cmb_sizemode_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_sizemode.SelectedIndexChanged
        Dim strSel As String = DirectCast(cmb_sizemode.SelectedItem, String)
        m_pbx.SizeMode = DirectCast([Enum].Parse(GetType(PictureBoxSizeMode), strSel), PictureBoxSizeMode)
    End Sub

    Private Sub cmd_primopiano_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_primopiano.Click
        m_pbx.PrimoPiano()
    End Sub

    Private Sub cmd_portaavanti_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_portaavanti.Click
        m_pbx.PortaAvanti()
    End Sub

    Private Sub cmd_dietrotutti_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_dietrotutti.Click
        m_pbx.DietroTutti()
    End Sub

    Private Sub cmd_portaindietro_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_portaindietro.Click
        m_pbx.PortaIndietro()
    End Sub

End Class


4. ModuloPublics.vb :

Module ModuloPublics

    Public DTP As DTPBoxes
    Public percorso As String = Application.StartupPath & "\"
    Public nomeFileSalvataggio As String = "SalvataggioPBoxes.xml"
    Public nomeFileSnapShot As String = "SnapShot_PBoxes.bmp"

    Public Function GetFileImmagine() As String

        Dim fn As String = String.Empty
        Using OFD As New OpenFileDialog
            OFD.Title = "Selezionare File Immagine"
            OFD.Filter = "Immagini Supportate (*.bmp,*.jpg,*.jpeg,*.gif,*.png)" & _
                                             "|*.bmp;*.jpg;*.jpeg;*.gif;*.png"
            OFD.FilterIndex = 1
            OFD.InitialDirectory = My.Computer.FileSystem.SpecialDirectories.MyPictures
            If OFD.ShowDialog = Windows.Forms.DialogResult.OK Then fn = OFD.FileName
        End Using
        Return fn

    End Function

    Public Function GetImmagine(ByVal nomeFile As String) As Image

        Dim MS As New IO.MemoryStream(IO.File.ReadAllBytes(nomeFile))
        Return Image.FromStream(MS)

    End Function

End Module

5. PBox.vb :
In particolare vorrei far notare i due Metodi Reflection GetPropertyValue e SetPropertyValue, con cui è possibile leggere e impostare proprietà attraverso le relative definizioni in formato Stringa. Questo rende ancora più facile l'eventuale aggiunta di altre proprietà da mappare e gestire, e la conseguente serializzazione su file XML.

'***********************************************************************************
'************** Gestione Avanzata Controlli PBox ***** MARCOGG 2011 ****************
'***********************************************************************************

Public Class PBox
    Inherits PictureBox

    Private m_id As Integer 'ID univoco in DTI per salvataggio / caricamento
    Private m_realleft As Integer 'Left effettivo in caso di Scroll orizzontale
    Private m_realtop As Integer 'Top effettivo in caso di Scroll verticale

    Private m_aggiornadtpenabled As Boolean = True 'Switch abilita/disabilita aggiornamento row DTI

    Private m_tiposelezione As TipiSelezione = TipiSelezione.NS
    Private m_resizewidthheight As Integer = 10 'Altezza/Larghezza Rect Resize
    Private m_rectmovexy As Rectangle 'Rect per Move XY
    Private m_rectresizewe As Rectangle 'Rect per Resize WE
    Private m_rectresizens As Rectangle 'Rect per Resize NS
    Private m_rectresizenwse As Rectangle 'Rect per Resize NWSE
    Private m_openmenu As Boolean 'Check per Menu Opzioni
    Private m_selx As Integer
    Private m_sely As Integer

    Private m_fileimmaginesfondo As String = String.Empty
    Private m_fileimmagine As String = String.Empty

    Public Property ID() As Integer
        Get
            Return m_id
        End Get
        Set(ByVal value As Integer)
            m_id = value
        End Set
    End Property

    Public Property ZOrder() As Integer
        Get
            If Me.Parent IsNot Nothing Then
                Return Me.Parent.Controls.GetChildIndex(Me)
            Else
                Return 0
            End If
        End Get
        Set(ByVal value As Integer)
            If Me.Parent IsNot Nothing Then
                Me.Parent.Controls.SetChildIndex(Me, value)
                Me.AggiornaTuttiDTP()
            End If
        End Set
    End Property

    Public Property RealLeft() As Integer
        Get
            Return m_realleft
        End Get
        Set(ByVal value As Integer)
            m_realleft = value
        End Set
    End Property

    Public Property RealTop() As Integer
        Get
            Return m_realtop
        End Get
        Set(ByVal value As Integer)
            m_realtop = value
        End Set
    End Property

    Public Property AggiornaDTPEnabled() As Boolean
        Get
            Return m_aggiornadtpenabled
        End Get
        Set(ByVal value As Boolean)
            m_aggiornadtpenabled = value
        End Set
    End Property

    Public Property TipoSelezione() As TipiSelezione
        Get
            Return m_tiposelezione
        End Get
        Set(ByVal value As TipiSelezione)
            m_tiposelezione = value
            Select Case m_tiposelezione
                Case TipiSelezione.MoveXY
                    Cursor = Cursors.SizeAll
                Case TipiSelezione.ResizeWE
                    Cursor = Cursors.SizeWE
                Case TipiSelezione.ResizeNS
                    Cursor = Cursors.SizeNS
                Case TipiSelezione.ResizeNWSE
                    Cursor = Cursors.SizeNWSE
                Case Else
                    Cursor = Cursors.Default
            End Select
        End Set
    End Property

    Public Property FileImmagineSfondo() As String
        Get
            Return m_fileimmaginesfondo
        End Get
        Set(ByVal value As String)
            m_fileimmaginesfondo = value
            If Not m_fileimmaginesfondo = String.Empty Then
                Me.BackgroundImage = GetImmagine(m_fileimmaginesfondo)
                AggiornaMeDTP()
            End If
        End Set
    End Property

    Public Property FileImmagine() As String
        Get
            Return m_fileimmagine
        End Get
        Set(ByVal value As String)
            m_fileimmagine = value
            If Not m_fileimmagine = String.Empty Then
                Dim sizeTemp As Size = Me.Size
                Me.Image = GetImmagine(m_fileimmagine)
                Me.Size = sizeTemp
                AggiornaMeDTP()
            End If
        End Set
    End Property

#Region "Costruttore"

    Public Sub New()

        Me.BackColor = Color.White
        Me.MinimumSize = New Size(m_resizewidthheight * 2, m_resizewidthheight * 2)
        Me.BackgroundImageLayout = ImageLayout.None
        m_rectmovexy = New Rectangle(0, 0, Me.Width - m_resizewidthheight, Me.Height - m_resizewidthheight)
        m_rectresizewe = New Rectangle(Me.Width - m_resizewidthheight, 0, m_resizewidthheight, Me.Height - m_resizewidthheight)
        m_rectresizens = New Rectangle(0, Me.Height - m_resizewidthheight, Me.Width - m_resizewidthheight, m_resizewidthheight)
        m_rectresizenwse = New Rectangle(Me.Width - m_resizewidthheight, Me.Height - m_resizewidthheight, m_resizewidthheight, m_resizewidthheight)

    End Sub

#End Region

#Region "Metodi"

    'Metodo Reflection GetPropertyValue : lettura valore proprietà
    Public Function GetPropertyValue(ByVal propertyName As String) As String

        Dim propertyValue As Object
        Dim PI As System.Reflection.PropertyInfo
        PI = Me.GetType.GetProperty(propertyName)
        propertyValue = PI.GetValue(Me, Nothing)

        With PI.PropertyType
            If .IsEnum Then propertyValue = Convert.ToInt32(propertyValue)
            If .Name = "Color" Then propertyValue = ColorTranslator.ToWin32(Me.BackColor)
            If .Name = "Size" Then propertyValue = CType(propertyValue, Size).Width & "|" & CType(propertyValue, Size).Height
        End With
        If propertyValue IsNot Nothing Then
            Return propertyValue.ToString
        Else
            Return String.Empty
        End If

    End Function

    'Metodo Reflection SetPropertyValue : assegnazione valore proprietà
    Public Sub SetPropertyValue(ByVal propertyName As String, ByVal propertyValue As Object)

        Dim PI As System.Reflection.PropertyInfo
        PI = Me.GetType.GetProperty(propertyName)
        If Not PI.CanWrite Then Exit Sub

        With PI.PropertyType
            If .IsEnum Then propertyValue = Convert.ToInt32(propertyValue)
            If .Name = "Int32" Then propertyValue = Convert.ToInt32(propertyValue)
            If .Name = "Color" Then propertyValue = ColorTranslator.FromWin32(propertyValue)
            If .Name = "Size" Then propertyValue = New Size(propertyValue.ToString.Split("|")(0), _
                                   propertyValue.ToString.Split("|")(1))
        End With
        PI.SetValue(Me, propertyValue, Nothing)

    End Sub

    Private Sub AggiornaMeDTP()
        If DTP IsNot Nothing And Me.AggiornaDTPEnabled = True Then DTP.Aggiorna(Me)
    End Sub

    Private Sub AggiornaTuttiDTP()
        If DTP IsNot Nothing And Me.AggiornaDTPEnabled = True Then DTP.AggiornaTutti()
    End Sub

    Public Sub PortaAvanti()
        Me.ZOrder -= 1
    End Sub

    Public Sub PortaIndietro()
        Me.ZOrder += 1
    End Sub

    Public Sub PrimoPiano()
        Me.BringToFront()
        Me.AggiornaTuttiDTP()
    End Sub

    Public Sub DietroTutti()
        Me.SendToBack()
        Me.AggiornaTuttiDTP()
    End Sub

#End Region

#Region "Eventi"

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

        'Aggiorna posizione e dimensione dei Rect di selezione
        m_rectmovexy.Width = Me.Width - m_resizewidthheight
        m_rectmovexy.Height = Me.Height - m_resizewidthheight

        m_rectresizewe.X = Me.Width - m_resizewidthheight
        m_rectresizewe.Height = Me.Height - m_resizewidthheight

        m_rectresizens.Y = Me.Height - m_resizewidthheight
        m_rectresizens.Width = Me.Width - m_resizewidthheight

        m_rectresizenwse.X = Me.Width - m_resizewidthheight
        m_rectresizenwse.Y = Me.Height - m_resizewidthheight

        Me.AggiornaMeDTP()
        Me.Refresh()

    End Sub

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

        Me.RealLeft = Me.Left - DirectCast(Me.Parent, ScrollableControl).AutoScrollPosition.X
        Me.RealTop = Me.Top - DirectCast(Me.Parent, ScrollableControl).AutoScrollPosition.Y
        Me.AggiornaMeDTP()

    End Sub

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

        MyBase.OnBackColorChanged(e)
        Me.AggiornaMeDTP()

    End Sub

    Protected Overrides Sub OnMouseClick(ByVal e As System.Windows.Forms.MouseEventArgs)

        m_openmenu = False
        For Each F As Form In My.Application.OpenForms
            If TypeOf (F) Is FormMenu Then
                m_openmenu = True
                DirectCast(F, FormMenu).PBX = Me
                Exit For
            End If
        Next

        If e.Button = Windows.Forms.MouseButtons.Left Then
            If DTP IsNot Nothing Then DTP.SelezionaSuDgv(Me)
        End If

        If e.Button = Windows.Forms.MouseButtons.Right Then
            If m_openmenu = False Then
                Dim FM As New FormMenu
                FM.Show()
                FM.PBX = Me
            End If
        End If

    End Sub

    Protected Overrides Sub OnMouseDown(ByVal e As System.Windows.Forms.MouseEventArgs)

        m_selx = e.X
        m_sely = e.Y
        If m_rectmovexy.Contains(m_selx, m_sely) Then Me.TipoSelezione = TipiSelezione.MoveXY
        If m_rectresizewe.Contains(m_selx, m_sely) Then Me.TipoSelezione = TipiSelezione.ResizeWE
        If m_rectresizens.Contains(m_selx, m_sely) Then Me.TipoSelezione = TipiSelezione.ResizeNS
        If m_rectresizenwse.Contains(m_selx, m_sely) Then Me.TipoSelezione = TipiSelezione.ResizeNWSE

    End Sub

    Protected Overrides Sub OnMouseUp(ByVal e As System.Windows.Forms.MouseEventArgs)

        Me.TipoSelezione = TipiSelezione.NS

    End Sub

    Protected Overrides Sub OnMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs)

        'MouseMove senza selezione
        If Me.TipoSelezione = TipiSelezione.NS Then
            If m_rectmovexy.Contains(e.X, e.Y) Then Cursor = Cursors.SizeAll
            If m_rectresizewe.Contains(e.X, e.Y) Then Cursor = Cursors.SizeWE
            If m_rectresizens.Contains(e.X, e.Y) Then Cursor = Cursors.SizeNS
            If m_rectresizenwse.Contains(e.X, e.Y) Then Cursor = Cursors.SizeNWSE
        End If

        'MouseMove con selezione
        If Me.TipoSelezione = TipiSelezione.MoveXY Then
            Me.Left -= (m_selx - e.X)
            Me.Top -= (m_sely - e.Y)
        End If
        If Me.TipoSelezione = TipiSelezione.ResizeWE Then
            Me.Width += e.X - m_selx
            m_selx = e.X
        End If
        If Me.TipoSelezione = TipiSelezione.ResizeNS Then
            Me.Height += e.Y - m_sely
            m_sely = e.Y
        End If
        If Me.TipoSelezione = TipiSelezione.ResizeNWSE Then
            Me.Width += e.X - m_selx
            Me.Height += e.Y - m_sely
            m_selx = e.X
            m_sely = e.Y
        End If

    End Sub

#End Region

End Class

6. TipiSelezione.vb :
Public Enum TipiSelezione

    NS = 0
    MoveXY = 1
    ResizeWE = 2
    ResizeNS = 3
    ResizeNWSE = 4

End Enum

Più che un singolo How-To, lo considero un insieme di tanti spunti, come ad esempio :

--> dotare una Classe di Metodi di accesso generico con la Reflection
--> popolare una ComboBox ( quelle su Form Menu ) direttamente con una Enum e farsi restituire il valore corrispondente
--> salvare l'intero contenuto di un Panel in una bitmap, comprese le aree non visibili
--> una parziale serializzazione dei Controls Forms, ecc, ecc...

+ Fine Articolo.

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



[VB.NET] Stampare Immagini con PrintDocument

Descrizione :
Un esempio pratico di stampa immagini / etichette usando PrintDocument e PrintPreviewDialog.

+ Articolo :

Si tratta di stampare un certo numero prefissato di immagini / etichette prendendo nomi e quantità da una lista in un DataGridView.

Nel foglio deve essere possibile determinare :
--> Numero di Righe e Colonne ( numero immagini per foglio ).
--> Alcune misure necessarie per l'adattamento alle varie situazioni ( come il cambiamento delle dimensioni immagini ).

Le immagini / etichette devono sempre riempire ogni Foglio alla sua capienza massima possibile ( economizzare carta... ), anche se su uno stesso foglio dovessero trovarsene di tipi diversi.

Come sempre, un'immagine per rendere subito l'idea del risultato desiderato ( Preview di Stampa ) :


Per replicare questa soluzione basta creare una nuova Applicazione Windows Forms con una sola Form "FormMain".
Tutto quello che serve a design è la FormMain ( Form di avvio ) con un DataGridView "DGV", e un Button "cmd_stampa".


--> Codice completo per FormMain :
Public Class FormMain

    'Cartella Immagini : modificare...
    Private percorsoImmagini As String = Application.StartupPath & "\Immagini\"

    Private WithEvents PD As New Printing.PrintDocument
    Private rigaDoc As Integer 'indice riga 
    Private colonnaDoc As Integer 'indice colonna
    Private nomiImgs As New List(Of String)
    Private indiceImg As Integer
    Private nuovaPag As Boolean
    Private bmpTemp As Bitmap

    'Variabili di controllo : posizionamento e quantità immagini su foglio...
    Private stepRiga As Integer = CP(60) 'distanza tra righe
    Private stepColonna As Integer = CP(70) 'distanza tra colonne
    Private scartoX As Integer = CP(15) 'scarto iniziale sinistro
    Private scartoY As Integer = CP(10) 'scarto iniziale superiore
    Private maxRigheDoc As Integer = 5 'max righe per pag.
    Private maxColonneDoc As Integer = 3 'max colonne per pag.

    Private Function CP(ByVal millimetri As Integer) As Integer 'CP = Centesimi di Pollice
        Return millimetri * 100 / 25.4
    End Function

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

        Dim r1() As Object = {"150", "BarCode", "A", "5"}
        Dim r2() As Object = {"150", "BarCode", "B", "8"}
        Dim r3() As Object = {"150", "BarCode", "C", "15"}

        With DGV
            .Columns.Add("Size", "Size")
            .Columns.Add("Categoria", "Categoria")
            .Columns.Add("Nome", "Nome")
            .Columns.Add("Quantità", "Quantità")

            .Rows.Add(r1)
            .Rows.Add(r2)
            .Rows.Add(r3)
        End With

    End Sub

    Private Sub cmd_stampa_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmd_stampa.Click

        'Caricamento nomiImgs
        'Qui eventuali controlli su reale esistenza directory e files...
        Dim nomeImg As String
        Dim qta As Integer
        nomiImgs.Clear()

        For i As Integer = 0 To DGV.RowCount - 1
            nomeImg = percorsoImmagini & DGV(2, i).Value & ".jpg"
            '... Eventuale controllo su esistenza nomeImg ...
            qta = DGV(3, i).Value
            For j As Integer = 1 To qta
                nomiImgs.Add(nomeImg)
            Next
        Next

        If nomiImgs.Count = 0 Then Exit Sub

        'Preview
        Dim PPD As New PrintPreviewDialog
        PPD.Document = PD
        PPD.Show()

        'Stampa diretta
        'PD.Print()

    End Sub

    Private Sub PD_BeginPrint(ByVal sender As Object, ByVal e As System.Drawing.Printing.PrintEventArgs) Handles PD.BeginPrint

        'Inizializzo valori di partenza per PD_PrintPage
        indiceImg = 0
        rigaDoc = 1
        colonnaDoc = 1
        nuovaPag = False
        bmpTemp = New Bitmap(nomiImgs(0))

    End Sub

    Private Sub PD_PrintPage(ByVal sender As System.Object, ByVal e As System.Drawing.Printing.PrintPageEventArgs) Handles PD.PrintPage

        Do

            'Disegno Img
            e.Graphics.DrawImageUnscaled(bmpTemp, (colonnaDoc - 1) * stepColonna + scartoX, (rigaDoc - 1) * stepRiga + scartoY)

            'Controlli / avanzamento righe, colonne, pagine
            If indiceImg = nomiImgs.Count - 1 Then
                nuovaPag = False
                Exit Do
            Else
                indiceImg += 1
                'Caricamento da disco solo se l'immagine è cambiata rispetto alla precedente
                If nomiImgs(indiceImg) <> nomiImgs(indiceImg - 1) Then bmpTemp = New Bitmap(nomiImgs(indiceImg))
                If colonnaDoc = maxColonneDoc Then
                    colonnaDoc = 1
                    If rigaDoc = maxRigheDoc Then
                        rigaDoc = 1
                        nuovaPag = True
                        Exit Do
                    Else
                        rigaDoc += 1
                    End If
                Else
                    colonnaDoc += 1
                End If
            End If

        Loop

        e.HasMorePages = nuovaPag

    End Sub

End Class

Note :

--> In questo test il caricamento dati in DGV è ovviamente fittizio e avviene nel Form_Load().

--> Viene sempre usato un solo oggetto Bitmap e viene caricato da disco una volta sola per tipo di immagine.

--> Da segnalare l'uso opportuno dell'Evento BeginPrint() : il posto giusto in cui inserire le varie istruzioni di reset delle variabili che controllano la stampa.

--> Il processo è personalizzabile a piacere : l'ultimo blocco di dichiarazioni "Variabili di controllo" permette di definire in millimetri ( Function CP() ) tutte le misure necessarie per adattare la stampa alle dimensioni delle immagini, al numero di righe / colonne desiderato e agli spazi inter-riga e inter-colonna desiderati.

Nel mio caso ho usato 3 Immagini jpg 150x150 pixel.
Le immagini usate nel test erano esattamente queste :































-->
Per testare più precisamente la disposizione delle immagini su foglio ovviamente meglio usare immagini con riquadro, e inoltre va ricordato di non trascurare i margini di stampa, che possono variare da stampante a stampante...
Passo successivo potrebbe essere riportare su Form il controllo diretto su queste variabili con NumericUpDown ecc...

--> PD.Print() è volutamente commentato : si può provare sia la preview di stampa, sia la stampa diretta su carta, sia la stampa indiretta tramite la PPD.

+ Fine Articolo.

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



 
Design by Free WordPress Themes Modificato da MarcoGG