venerdì 4 novembre 2011

[VB.NET] UserControl per Grafici

Descrizione :
Un mio UserControl che permette di rappresentare il grafico di funzioni Y = f(x).

+ Articolo :


Questo Controllo è in grado di ricevere in input un insieme di punti X e Y, in cui tipicamente Y è funzione di X, ma anche senza che le due variabili siano necessariamente correlate, e rappresentarli su un grafico con un classico piano cartesiano XY.
Tutto ciò di cui il Controllo ha bisogno dall'esterno è perciò un insieme di punti.

Il Controllo si occupa di :

--> disegnare gli assi X e Y e le scale dei valori.

--> gestire separatamente l'insieme dei punti del grafico contenuti da quello dei punti rappresentati sul grafico : l'insieme dei punti X e Y passati in input non viene banalmente modificato e riportato sul grafico, ma il Controllo provvede a creare un secondo insieme di punti, dedotto dal primo e adatto alla rappresentazione grafica, e ad unire i punti stessi con una curva in AntiAliasing, ad alta qualità.
Ovviamente la qualità della curva sarà sempre inversamente proporzionale alla distanza tra due punti successivi, nell'insieme dei punti passati in input.

--> permettere di definire due fattori di scala separati per X e Y :
agendo sulle ComboBox dedicate a questo scopo, è possibile scalare il grafico a piacere nelle direzioni X ed Y. La curva eventualmente già contenuta nel Controllo verrà automaticamente ridisegnata, e "deformata" adattandosi al nuovo rapporto delle misure X/Y.

--> permettere di definire due fattori di traslazione dell'origine separati per X e Y :
agendo sulle ComboBox dedicate a questo scopo, è possibile personalizzare la posizione dell'origine nel grafico. Si può traslare lungo X ed Y, per visualizzare la parte di curva che interessa, sempre al centro del Controllo...

--> Autosize : basta impostare l'Anchor della PictureBox interna "pbfx" su tutti i 4 valori ( Top, Bottom, Left, Right ) e fare lo stesso sull'istanza del ControlGrafico aggiunto alla Form di test, e il grafico si adatterà a qualsiasi dimensione e risoluzione desiderata.

ControlGrafico.vb :

- Struttura :


- Codice :
Public Class ControlGrafico

    Private m_penassi As New Pen(Color.Black, 2) 'Pen per disegno Assi XY
    Private m_pengrafico As New Pen(Color.Red, 2) 'Pen per disegno Grafico
    Private m_fontassi As New Font(FontFamily.GenericSansSerif, 11, FontStyle.Regular)
    Private m_tensionecurva As Single 'valore consigliato : 0

    Private m_scalax As Integer  'Scala X : il valore esprime in pixels l'unità su Asse X
    Private m_scalay As Integer  'Scala Y : il valore esprime in pixels l'unità su Asse Y

    Private m_traslazionex As Integer
    Private m_traslazioney As Integer
    Private m_originex As Integer
    Private m_originey As Integer

    Private m_estremosn As Double = -5 'Estremo sinistro incluso : [m_estremosn <= X
    Private m_estremodx As Double = 5 'Estremo destro incluso : X <= m_estremodx]

    Private m_puntifx As New List(Of PtFx)

    Public Sub SetPuntiFx(ByVal fx As List(Of PtFx), ByVal tensioneCurva As Single)

        m_tensionecurva = tensioneCurva
        m_puntifx = fx
        pbfx.Refresh()

    End Sub

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

        For i As Integer = 30 To 200 Step 10
            cmb_scalax.Items.Add(i)
            cmb_scalay.Items.Add(i)
        Next
        cmb_scalax.Text = cmb_scalax.Items(cmb_scalax.Items.Count - 1)
        cmb_scalay.Text = cmb_scalay.Items(cmb_scalay.Items.Count - 1)

        For i As Integer = -10 To 10
            cmb_traslax.Items.Add(i)
            cmb_traslay.Items.Add(i)
        Next
        cmb_traslax.Text = 0
        cmb_traslay.Text = 0

    End Sub

    Private Sub pbfx_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles pbfx.Paint

        If Me.DesignMode = True Then Exit Sub

        'Traslazione Origine Graphics pbfx
        m_originex = pbfx.Width / 2 + m_traslazionex * m_scalax
        m_originey = pbfx.Height / 2 + m_traslazioney * m_scalay
        With e.Graphics
            .ResetTransform()
            .TranslateTransform(m_originex, m_originey)
        End With

        'Disegno Assi
        Dim p1x As New Point(-pbfx.Width - m_originex, 0)
        Dim p2x As New Point(pbfx.Width - m_originex, 0)
        Dim p1y As New Point(0, pbfx.Height - m_originey)
        Dim p2y As New Point(0, -pbfx.Height - m_originey)
        With e.Graphics
            .DrawLine(m_penassi, p1x, p2x)
            .DrawLine(m_penassi, p1y, p2y)
            For i As Integer = m_scalax To (pbfx.Width - m_originex) Step m_scalax
                .DrawEllipse(m_penassi, i, -2, 2, 2)
                .DrawString(i / m_scalax, m_fontassi, Brushes.Black, _
                            i - .MeasureString(i, m_fontassi).Width / 2, -.MeasureString(i, m_fontassi).Height)
            Next
            For i As Integer = -m_scalax To (-pbfx.Width - m_originex) Step -m_scalax
                .DrawEllipse(m_penassi, i, -2, 2, 2)
                .DrawString(i / m_scalax, m_fontassi, Brushes.Black, _
                            i, -.MeasureString(i, m_fontassi).Height)
            Next
            For i As Integer = m_scalay To (pbfx.Height + m_originey) Step m_scalay
                .DrawEllipse(m_penassi, -2, -i, 2, 2)
                .DrawString(i / m_scalay, m_fontassi, Brushes.Black, 0, -i)
            Next
            For i As Integer = -m_scalay To (-pbfx.Height + m_originey) Step -m_scalay
                .DrawEllipse(m_penassi, -2, -i, 2, 2)
                .DrawString(i / m_scalay, m_fontassi, Brushes.Black, 0, -i - .MeasureString(i, m_fontassi).Height / 2)
            Next
        End With

        'Disegno Curva F(x)
        If m_puntifx Is Nothing Then Exit Sub
        Dim puntiFxGraf(m_puntifx.Count - 1) As Point
        For i As Integer = 0 To puntiFxGraf.Length - 1
            Try
                puntiFxGraf(i).X = m_puntifx(i).X * m_scalax
                puntiFxGraf(i).Y = m_puntifx(i).Y * m_scalay * -1
            Catch ex As Exception
            End Try
        Next
        With e.Graphics
            .SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
            If puntiFxGraf.Length > 0 Then .DrawCurve(m_pengrafico, puntiFxGraf, m_tensionecurva)
        End With

    End Sub

    Private Sub pbfx_Resize(ByVal sender As Object, ByVal e As System.EventArgs) Handles pbfx.Resize
        pbfx.Refresh()
    End Sub

    Private Sub cmb_scalax_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_scalax.SelectedIndexChanged
        m_scalax = cmb_scalax.SelectedItem
        pbfx.Refresh()
    End Sub

    Private Sub cmb_scalay_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_scalay.SelectedIndexChanged
        m_scalay = cmb_scalay.SelectedItem
        pbfx.Refresh()
    End Sub

    Private Sub cmb_traslax_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_traslax.SelectedIndexChanged
        m_traslazionex = cmb_traslax.SelectedItem * -1
        pbfx.Refresh()
    End Sub

    Private Sub cmb_traslay_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_traslay.SelectedIndexChanged
        m_traslazioney = cmb_traslay.SelectedItem
        pbfx.Refresh()
    End Sub

End Class

Ovviamente è possibile esporre molte delle caratteristiche interne, come colori delle Pen di disegno, e svariati altri aspetti grafici e non, come ad esempio i valori da caricare di default nelle ComboBox ecc... Tutte cose di facile implementazione che non mi sono soffermato a realizzare, e che lascio a chi vorrà personalizzare questo Control come meglio crede.
Per ora il succo del discorso c'è tutto.

Il Metodo essenziale è SetPuntiFx().
Ho preferito esporre la Tension della curva, utile al Metodo interno di disegno .DrawCurve().
Questo parametro permette di influire in un certo modo su come il Controllo "inventa" i punti mancanti nell'insieme della funzione, e di conseguenza su come la curva verrà disegnata.
Personalmente consiglio di tenere questo valore sempre a 0. Con TensioneCurva a 0, il Controllo unirà due punti consecutivi con la retta più breve possibile.
Si possono sperimentare valori >0, ad esempio per "ammorbidire" le spezzate, ma senza eccedere, perchè può portare ad effetti indesiderati...
Nella rappresentazione di una classica funzione matematica, stile "studio di funzione", meglio creare una serie di valori chiave molto ravvicinati, piuttosto che fare troppo affidamento sulla Tension.

PtFx.vb :
E' la Structure che contiene i punti originari di ogni Funzione.
Public Structure PtFx

    Public X As Double
    Public Y As Double
    Public Sub New(ByVal valueX As Double, ByVal valueY As Double)
        X = valueX
        Y = valueY
    End Sub

End Structure

E ora finalmente, un po' di esempi e il ControlGrafico in azione.
Compilare - aggiungere un "ControlGrafico1" alla Form di test - pronti - via :

1. Una classica parabola :
        Dim fx As New List(Of PtFx)
        Dim Y As Double
        For X As Double = -10 To 10 Step 0.01
            Y = X ^ 2 / 3 + X - 1
            fx.Add(New PtFx(X, Y))
        Next
        ControlGrafico1.SetPuntiFx(fx, 0)


2. Spezzata con valori Random :
        Dim R As New Random
        Dim fx As New List(Of PtFx)
        Dim Y As Double
        For X As Double = 0 To 20
            Y = R.Next(0, 3)
            fx.Add(New PtFx(X, Y))
        Next
        ControlGrafico1.SetPuntiFx(fx, 0)


3. La Funzione di WikiPedia :
Ovvero, la funzione al momento sulla pagina Wiki : "Studio di Funzione".
http://it.wikipedia.org/wiki/File:Studio_funzione_esempio_con_derive.jpg
        Dim fx As New List(Of PtFx)
        Dim Y As Double
        For X As Double = -20 To 20 Step 0.01
            Y = Math.E ^ (5 * X - 3 * X ^ 2) - X ^ 3
            fx.Add(New PtFx(X, Y))
        Next
        ControlGrafico1.SetPuntiFx(fx, 0)


+ Fine Articolo.

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



6 commenti:

matty ha detto...
Questo commento è stato eliminato dall'autore.
MarcoGG ha detto...

Ciao. Non offro consulenze di questo genere. I miei Articoli Blog sono disponibili così come sono per chiunque ne voglia fare uso. Chiunque fosse interessato ad approfondire alcuni argomenti trovati in questo Blog, può farlo liberamente sulla mia Pagina FaceBook :
https://www.facebook.com/pages/MarcoGG/176216775722284

Anonimo ha detto...

Ciao,
bellissimo ed interessantissimo esercizio. Vorrei provarlo ma.. cos'è "ControlGrafico1"?

MarcoGG ha detto...

Ciao e grazie. E' molto semplice : ControlGrafico1 è il nome del Controllo creato dalla Classe di tipo UserControl, una volta che è stato aggiunto alla Form. L'articolo spiega come crearlo.
Public Class ControlGrafico e relativo codice è quanto devi scrivere nel codice dello UserControl. I suoi componenti, come spiegato in figura, vanno aggiunti allo UserControl stesso in design :
- cmb_scalax
- cmb_scalay
sono due ComboBox che servono a modificare la Scala di visualizzazione sui due assi X e Y.
- cmb_traslax
- cmb_traslay
sono due ComboBox che servono a modificare la Traslazione del grafico lungo i due assi X e Y.
- pbfx è una PictureBox destinata al disegno del grafico.

Giorgio ha detto...

Ciao, veramente molto interessante. Ho inserito il controllo VB in una mia solution fatta di progetti in C#. Funziona perfettamente. Una domanda... per farlo funzionare ho dovuto inizializzare m_scalax e m_scalay a 1, altrimenti andava in loop. Grazie. Giorgio

MarcoGG ha detto...

Ciao, probabilmente nel tuo progetto non hai incluso gli Eventi delle ComboBox che vanno ad aggiornare i valori di m_scalax ed m_scalay :
Private Sub cmb_scalax_SelectedIndexChanged(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles cmb_scalax.SelectedIndexChanged
m_scalax = cmb_scalax.SelectedItem
pbfx.Refresh()
End Sub
Questi valori dovrebbero essere maggiori di zero al primo Load del Controllo.

Posta un commento

 
Design by Free WordPress Themes Modificato da MarcoGG