domenica 29 gennaio 2012

[C#] 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.cs :

- Struttura :


- Codice :

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;

namespace UserControlGrafici
{
    public partial class ControlGrafico : UserControl
    {
        public ControlGrafico()
        {
            InitializeComponent();
        }

        private Pen m_penassi = new Pen(Color.Black, 2); //Pen per disegno Assi XY
        private Pen m_pengrafico = new Pen(Color.Red, 2); //Pen per disegno Grafico
        private Font m_fontassi = new Font(FontFamily.GenericSansSerif, 11, FontStyle.Regular);
        private float m_tensionecurva; //valore consigliato : 0

        private int m_scalax; //Scala X : il valore esprime in pixels l'unità su Asse X
        private int m_scalay; //Scala Y : il valore esprime in pixels l'unità su Asse Y

        private int m_traslazionex;
        private int m_traslazioney;
        private int m_originex;
        private int m_originey;

        private List<PtFx> m_puntifx = new List<PtFx>();

        public void SetPuntiFx(List<PtFx> fx, float tensioneCurva)
        {
            m_tensionecurva = tensioneCurva;
            m_puntifx = fx;
            pbfx.Refresh();
        }

        private void ControlGrafico_Load(object sender, EventArgs e)
        {
            for (int i = 30; i <= 200; i += 10)
            {
                cmb_scalax.Items.Add(i);
                cmb_scalay.Items.Add(i);
            }
            cmb_scalax.Text = cmb_scalax.Items[cmb_scalax.Items.Count - 1].ToString();
            cmb_scalay.Text = cmb_scalay.Items[cmb_scalay.Items.Count - 1].ToString();

            for (int i = -10; i <= 10; i++)
            {
                cmb_traslax.Items.Add(i);
                cmb_traslay.Items.Add(i);
            }
            cmb_traslax.Text = "0";
            cmb_traslay.Text = "0";
        }

        private void pbfx_Paint(object sender, PaintEventArgs e)
        {
            if (this.DesignMode == true) return;
           
            //Traslazione Origine Graphics pbfx
            m_originex =  Convert.ToInt32(pbfx.Width / 2.0 + m_traslazionex * m_scalax);
            m_originey = Convert.ToInt32(pbfx.Height / 2.0 + m_traslazioney * m_scalay);
            e.Graphics.ResetTransform();
            e.Graphics.TranslateTransform(m_originex, m_originey);

            //Disegno Assi
            Point p1x = new Point(-pbfx.Width - m_originex, 0);
            Point p2x = new Point(pbfx.Width - m_originex, 0);
            Point p1y = new Point(0, pbfx.Height - m_originey);
            Point p2y = new Point(0, -pbfx.Height - m_originey);
            string sxy;
            float x;
            float y;
            e.Graphics.DrawLine(m_penassi, p1x, p2x);
            e.Graphics.DrawLine(m_penassi, p1y, p2y);
            for (int i = m_scalax; i <= (pbfx.Width - m_originex); i += m_scalax)
            {
                e.Graphics.DrawEllipse(m_penassi, i, -2, 2, 2);
                sxy = (i / m_scalax).ToString();
                x = i - e.Graphics.MeasureString(i.ToString(),m_fontassi).Width / 2.0f;
                y = -e.Graphics.MeasureString(i.ToString(), m_fontassi).Height;
                e.Graphics.DrawString(sxy, m_fontassi, Brushes.Black, x, y);
            }
            for (int i = -m_scalax; i >= (-pbfx.Width - m_originex); i -= m_scalax)
            {
                e.Graphics.DrawEllipse(m_penassi, i, -2, 2, 2);
                sxy = (i / m_scalax).ToString();
                x = i;
                y = -e.Graphics.MeasureString(i.ToString(), m_fontassi).Height;
                e.Graphics.DrawString(sxy, m_fontassi, Brushes.Black, x, y);
            }
            for (int i = m_scalay; i <= (pbfx.Height + m_originey); i += m_scalay)
            {
                e.Graphics.DrawEllipse(m_penassi, -2, -i, 2, 2);
                sxy = (i / m_scalay).ToString();
                x = 0;
                y = -i;
                e.Graphics.DrawString(sxy, m_fontassi, Brushes.Black, x, y);
            }
            for (int i = -m_scalay; i >= (-pbfx.Height + m_originey); i -= m_scalay)
            {
                e.Graphics.DrawEllipse(m_penassi, -2, -i, 2, 2);
                sxy = (i / m_scalay).ToString();
                x = 0;
                y = -i - e.Graphics.MeasureString(i.ToString(), m_fontassi).Height / 2.0f;
                e.Graphics.DrawString(sxy, m_fontassi, Brushes.Black, x, y);
            }

            //Disegno Curva F(x)
            if (m_puntifx == null) return;

            Point[] puntiFxGraf = new Point[m_puntifx.Count];
            for (int i = 0; i < puntiFxGraf.Length; i++)
            {
                try
                {
                    puntiFxGraf[i].X = Convert.ToInt32(m_puntifx[i].X * m_scalax);
                    puntiFxGraf[i].Y = Convert.ToInt32(m_puntifx[i].Y * m_scalay) * -1;
                }
                catch (Exception ex)
                {
                }
            }
            e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
            if (puntiFxGraf.Length > 0)
            {
                e.Graphics.DrawCurve(m_pengrafico, puntiFxGraf, m_tensionecurva);
            }
        }

        private void pbfx_Resize(object sender, EventArgs e)
        {
            pbfx.Refresh();
        }

        private void cmb_scalax_SelectedIndexChanged(object sender, EventArgs e)
        {
            m_scalax = Convert.ToInt32(cmb_scalax.SelectedItem);
            pbfx.Refresh();
        }

        private void cmb_scalay_SelectedIndexChanged(object sender, EventArgs e)
        {
            m_scalay = Convert.ToInt32(cmb_scalay.SelectedItem);
            pbfx.Refresh();
        }

        private void cmb_traslax_SelectedIndexChanged(object sender, EventArgs e)
        {
            m_traslazionex = Convert.ToInt32(cmb_traslax.SelectedItem) * -1;
            pbfx.Refresh();
        }

        private void cmb_traslay_SelectedIndexChanged(object sender, EventArgs e)
        {
            m_traslazioney = Convert.ToInt32(cmb_traslay.SelectedItem);
            pbfx.Refresh();
        }

    }
}

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.cs :
E' la Structure che contiene i punti originari di ogni Funzione.

namespace UserControlGrafici
{
    public struct PtFx
    {
        public double X;
        public double Y;
        public PtFx(double valueX, double valueY) : this()
        {
            X = valueX;
            Y = valueY;
        }
    }
}

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 :
            List<PtFx> fx = new List<PtFx>();
            double Y = 0;
            for (double X = -10; X <= 10; X += 0.01)
            {
                Y = (Math.Pow(X, 2)) / 3 + X - 1;
                fx.Add(new PtFx(X, Y));
            }
            controlGrafico1.SetPuntiFx(fx, 0);


2. Spezzata con valori Random :
            Random R = new Random();
            List<PtFx> fx = new List<PtFx>();
            double Y = 0;
            for (double X = 0; X <= 20; X++)
            {
                Y = R.Next(0, 3);
                fx.Add(new PtFx(X, Y));
            }
            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

            List<PtFx> fx = new List<PtFx>();
            double Y = 0;
            for (double X = -20; X <= 20; X += 0.01)
            {
                Y = (Math.Pow(Math.E, (5 * X - 3 * (Math.Pow(X, 2))))) - (Math.Pow(X, 3));
                fx.Add(new PtFx(X, Y));
            }
            controlGrafico1.SetPuntiFx(fx, 0);


+ Fine Articolo.


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





7 commenti:

mfeo15 ha detto...

Mi potresti dare una mano con il tuo algoritmo?

MarcoGG ha detto...

Ciao. Ho deciso di riservare questi commenti solo a segnalazioni e brevi discussioni, sia perchè non voglio che la parte dei commenti divenga una specie di "prosieguo" dell'Articolo, sia perchè non sono adatti a contenere testo misto a codice. Per approfondimenti veri e propri in merito ad un Articolo, c'è la mia Pagina FaceBook :
https://www.facebook.com/pages/MarcoGG/176216775722284

Anonimo ha detto...

O buffonciello! Potevi anche mette il download diretto del progetto, almeno ci capivo a modo! Un si 'apisce na segaccia nulla!

Anonimo ha detto...

Cazzi tua, t'attacchi.

MarcoGG ha detto...

Tra i due commenti "Anonimi" quoto decisamente il secondo. :). Quanto al "buffonciello", l'unico buffone qui sei tu. Se non riesci a replicare l'esempio del presente Articolo, lascia pur perdere la programmazione e dedicati a passatempi più elementari. Come già spiegato in altre sedi, la scelta è per un Blog che vada letto oltre che "copia/incollato".

Anonimo ha detto...

Te sai invece...

MarcoGG ha detto...

La cosa più stupida e inutile del mondo ( che mai mi sarei aspettato di dover gestire qui ) è innescare polemiche su un Blog come questo. E' proprio vero che la realtà supera l'immaginazione. :-D Difficile immaginare gente tanto STRONZA da nascondersi dietro ad un profilo "Anonimo" ( davvero molto comodo ), andando a scrivere critiche tutt'altro che costruttive sulla pagina e sull'autore DA CUI SI STA COPIANDO CODICE FUNZIONANTE. Mi riferisco chiaramente al commento Anonimo datato 13 dicembre 2012 12:39, e al suo mittente, nonchè al suo successivo commento che già ho censurato. Dal momento che non ho alcuna intenzione di continuare la "discussione" con uno STRONZO, se la cosa continua, cestinerò tutti gli interventi molesti e/o inutili.

Posta un commento

 
Design by Free WordPress Themes Modificato da MarcoGG