„Rysowamy” czyli jak w C# z pomocą GDI+ wykonać wskaźnik.

Tym razem trochę inaczej, bardziej desktopowo (z wykorzystaniem Windows Forms). Tworząc aplikacje czasem jest konieczność użycia kontrolki dokładnie takiej jak sobie zamarzymy. Nie zawsze jest taka pod ręką albo trzeba za nią płacić (co o ile w komercyjnych rozwiązaniach nie jest problemem to w prywatnych już pewnie może być) grubą kasę (może trochę przesadzam).

Tak na marginesie, to owo „rysowamy” jest autorstwa mojej córki gdy była mała … oznacza „rysujemy”. To taka mała dygresja i już wracamy do głównego wątku związanego z utworzeniem kontrolki.

Tutaj przychodzi nam z pomocą C# oraz GDI+ (info o GDI na stronach Microsoft -> https://msdn.microsoft.com/pl-pl/library/windows/desktop/ms533798%28v=vs.85%29.aspx), które razem tworzą dobre narzędzie do realizacji własnych pomysłów.

By pomysł swój wcielić w życie potrzebujemy Visual Studio oraz pomysł. Tworzymy w VS nowy projekt (w tym wypadku Window Forms Control Library) tak jak na poniższym obrazku:

np1

Naszym oczom pokaże się niepozorny obrazek, który nie przypomina w niczym naszej kontrolki.

np2

Następnie w oknie Solution Explorer zmieniamy nazwę klasy z UserControl1.cs na Wskaznik1.cs (prawym przyciskiem na nazwie klasy -> rename) i zgadzamy się na to co proponuje nam Visual Studio.

np3

Teraz naciskamy F7 lub lub klikamy prawym przyciskiem na nazwie klasy „Wskaznik .cs” -> View Code. Otwiera się nam okno z kodem:

np4

Do naszego okna dodajemy poniższy kod co pozwoli nam na rysowanie 2D z wykorzystaniem GDI+.

using System.Drawing.Drawing2D;

Przed projektowaniem kontrolki powinniśmy się zastanowić jaką funkcjonalność ma ona oferować. Jakie właściwości będą dostępne z poziomu Visual Studio a które uczynimy prywatnymi.

Załóżmy, że będziemy chcieli mieć kontrolkę w postaci typowego „zegara”, takiego jak są w samochodach, manometrach itp. Funkcjonalność będzie bardzo prosta, będzie możliwość ustawienia:

  • wskazówki na wartości bieżącej;
  • wartości minimalnej
  • wartości maksymalnej;
  • szerokości wskazówki;
  • koloru wskazówki;
  • koloru tła kontrolki;
  • koloru obwódki tła kontrolki;
  • rodzaju czcionki wyświetlającej wartość;

By móc sprawdzać jak rzeczywiście działa nasza kontrolka dodajemy nowy projekt do naszej solucji. W tym celu w oknie „Solution Explorer” klikamy prawym przyciskiem myszy na nazwie solucji (Solution 'Wskaznik1′) -> Add -> New Project i dodajemy projekt „Window Forms Application” tak jak pokazano to na poniższym rysunku:

np5

Po tym w naszej solucji pojawia się nowy projekt i zmienia się okno edytora:

np6

Dodajemy naszą kontrolkę na okno formy, poprzez dwuklik lub przeciągnięcie z toolboxa.

np7

No i nareszcie mamy naszą kontrolkę/wskaźnik na formie i możemy zobaczyć jak działa (to znaczy moglibyśmy zobaczyć gdyby była utworzona a na razie nie jest). W miejscu kontroli widzimy tylko delikatne obramowanie.

np8

No to lecimy dalej. Dodajemy kod naszej kontrolki do pliku  „Wskaznik.cs”

        #region zmienne programu
        private float valueActual = 0;
        private float valueMin = 0;
        private float valueMax = 100;
        private float tipWidth = 2.0f;          
        private Color tipColor = Color.Red;
        private Color circleColor = Color.Black;
        private Color backgroundColor = Color.White;
        private Font controlFont = new Font("Arial", 14, FontStyle.Bold);
        #endregion

        #region właściwości kontrolki
        
        [Browsable(true), Category("Ustawienia"), Description("wartość minimalna")]
        public float valuemin {
            get { return valueMin; }
            set
            {
                if (valuemin != value)
                {
                    valueMin = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość maksymalna")]
        public float valuemax
        {
            get { return valueMax; }
            set
            {
                if (valuemax != value)
                {
                    valueMax = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("szerokość wskazówki")]
        public float tipwidth
        {
            get { return tipWidth; }
            set
            {
                if (tipwidth != value)
                {
                    tipWidth = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość aktualna")]
        public float valueactual
        {
            get { return valueActual; }
            set
            {
                if (valueactual != value)
                {
                    valueActual = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor wskazówki")]
        public Color tipcolor
        {
            get { return tipColor; }
            set
            {
                if (tipcolor != value)
                {
                    tipColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor tła")]
        public Color backgroundcolor
        {
            get { return backgroundColor; }
            set
            {
                if (backgroundcolor != value)
                {
                    backgroundColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor wskazówki")]
        public Color circlecolor
        {
            get { return circleColor; }
            set
            {
                if (circlecolor != value)
                {
                    circleColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość aktualna")]
        public Font controlfont
        {
            get { return controlFont; }
            set
            {
                if (controlfont != value)
                {
                    controlFont = value;
                    Invalidate();
                }
            }
        } 
        #endregion

Po dodaniu tego kodu i skompilowaniu kontrolki (ctrl + shift + B) właściwości pojawiają się w naszej kontrolce – możemy je zmieniać.

np9

No dobrze, mamy możliwość ustawiania właściwości kontrolki ale tak właściwie to jeszcze jej nie mamy. Więc do dzieła. Na początek narysujmy to co się nie zmienia czyli okrąg na obwodzie i tło kontrolki. Wykorzystamy metodę:

protected override void OnPaint(PaintEventArgs e)

gdzie umieścimy kod związany z rysowaniem kontrolki.

No a jaki kod? Np. taki jak poniżej (pełny kod z pliku „Wskaznik.cs”):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Data;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

using System.Drawing.Drawing2D;

namespace Wskaznik1
{
    public partial class Wskaznik: UserControl
    {
        #region zmienne programu
        private float valueActual = 0;
        private float valueMin = 0;
        private float valueMax = 100;
        private float tipWidth = 2.0f;          
        private Color tipColor = Color.Red;
        private Color circleColor = Color.Black;
        private Color backgroundColor = Color.White;
        private Font controlFont = new Font("Arial", 8, FontStyle.Italic);

        #endregion

        #region właściwości kontrolki
        
        [Browsable(true), Category("Ustawienia"), Description("wartość minimalna")]
        public float valuemin {
            get { return valueMin; }
            set
            {
                if (valuemin != value)
                {
                    valueMin = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość maksymalna")]
        public float valuemax
        {
            get { return valueMax; }
            set
            {
                if (valuemax != value)
                {
                    valueMax = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("szerokość wskazówki")]
        public float tipwidth
        {
            get { return tipWidth; }
            set
            {
                if (tipwidth != value)
                {
                    tipWidth = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość aktualna")]
        public float valueactual
        {
            get { return valueActual; }
            set
            {
                if (valueactual != value)
                {
                    valueActual = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor wskazówki")]
        public Color tipcolor
        {
            get { return tipColor; }
            set
            {
                if (tipcolor != value)
                {
                    tipColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor tła")]
        public Color backgroundcolor
        {
            get { return backgroundColor; }
            set
            {
                if (backgroundcolor != value)
                {
                    backgroundColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("kolor wskazówki")]
        public Color circlecolor
        {
            get { return circleColor; }
            set
            {
                if (circlecolor != value)
                {
                    circleColor = value;
                    Invalidate();
                }
            }
        }

        [Browsable(true), Category("Ustawienia"), Description("wartość aktualna")]
        public Font controlfont
        {
            get { return controlFont; }
            set
            {
                if (controlfont != value)
                {
                    controlFont = value;
                    Invalidate();
                }
            }
        } 
        #endregion


        public Wskaznik()
        {
            InitializeComponent();

        }



        protected override void OnPaint(PaintEventArgs e)
        {
            //tutaj będziemy rysować kontrolkę
            this.Height = this.Width;

            try
            {
                float x_max = this.ClientRectangle.Width - 10;
                float y_max = this.ClientRectangle.Height - 10;
                int xb = 5;
                int yb = 5;

                Pen penCircleBlackThick = new Pen(circleColor, 2.0f);
                Pen penTip = new Pen(tipColor, tipWidth);

                Point center = new Point(this.ClientRectangle.Width / 2, this.ClientRectangle.Height / 2);
                Point tipBase = new Point(xb, this.ClientRectangle.Height / 2);
                
                SolidBrush brushBackground = new SolidBrush(backgroundColor);
                base.OnPaint(e);

                //wygładzamy kontrolkę
                e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
                e.Graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;

                //wyrysowanie tła i obwódki
                e.Graphics.FillEllipse(brushBackground, xb, yb, x_max, y_max);
                e.Graphics.DrawEllipse(penCircleBlackThick, xb, yb, x_max, y_max);

                //wyrysowanie wartości aktualnej pod osią wskazówki
                StringFormat formatCenter = new StringFormat();
                formatCenter.Alignment = StringAlignment.Center;

                e.Graphics.DrawString("" + valueActual, controlFont, new SolidBrush(Color.Black), center.X, center.Y+5,formatCenter);

                //wyrysowanie skali i wypisanie wartości
                int step = (int)((valueMax - valueMin) / 10);
                int value = 0;

                for (int i = 180; i <= 360; i++)
                {
                    if (i % 18 == 0)
                    {
                        double x1 = center.X - 10 * Math.Cos(degToRad(i)) + x_max / 2 * Math.Cos(degToRad(i));
                        double y1 = center.Y - 10 * Math.Sin(degToRad(i)) + y_max / 2 * Math.Sin(degToRad(i));
                        double x2 = center.X + x_max / 2 * Math.Cos(degToRad(i));
                        double y2 = center.Y + y_max / 2 * Math.Sin(degToRad(i));

                        double xt = center.X - 20 * Math.Cos(degToRad(i)) + x_max / 2 * Math.Cos(degToRad(i));
                        double yt = center.Y - 20 * Math.Sin(degToRad(i)) + y_max / 2 * Math.Sin(degToRad(i));

                        e.Graphics.DrawLine(penCircleBlackThick, new Point((int)x1, (int)y1), new Point((int)x2, (int)y2));
                        e.Graphics.DrawString("" + value, controlFont, new SolidBrush(Color.Black), (int)xt, (int)yt, formatCenter);
                        value += step;
                    }

                }

                //wyrysowanie wskazówki
                double xw = center.X + x_max / 2 * Math.Cos(degToRad(180 + valueActual * 180 / (valueMax - valueMin)));
                double yw = center.Y + y_max / 2 * Math.Sin(degToRad(180 + valueActual * 180 / (valueMax - valueMin)));
                e.Graphics.DrawLine(penTip, center, new Point((int)xw, (int)yw));
                
            }
            catch (Exception)
            {
                
                throw;
            }
        
        }


        //do obliczeń sin, cos musimy przeliczyć stopnie na radiany
        private float degToRad(float stopnie)
        {
            return stopnie * (float)Math.PI / 180;
        }

    }
}

By za każdym razem po kompilacji widzieć efekt w postaci działającej naszej kontrolki należy zmienić ustawienia naszej solucji tak aby uruchamiany był projekt, w którym kontrolka działa – w tym przypadku TestKontrolki. By tego dokonać klikamy w SolutionExplorer prawym przyciskiem myszy na nazwie solucji i wybieramy „Properties”, co spowoduje ukazanie się poniższego okna:

np11

Po wpisaniu całego dotychczasowego kodu i kompilowaniu kontrolki oraz uruchomieniu projektu (F5) z kontrolką otrzymujemy obraz działającej kontrolki na formie:

ws1Niestety kontrolka jest „poszarpana” i taka mało przyjemna. Zmienimy to włączając antyaliasing. Po włączeniu antyaliasingu kontrolka wygląda lepiej – widać to szczególnie dobrze na wskazówce i okręgu wskaźnika.

ws2

Odrobinę zmodyfikowana kontrolka, której właściwości zmienione zostały w oknie projektu testowego.

ws3

Porównanie kontrolki zmodyfikowanej z kontrolką „surową” wstawioną do projektu prosto z toolboxa przedstawia poniższy obrazek:

ws4

No a teraz trzeba przełączyć się z trybu „Debug” w tryb „Release” i skompilować kontrolkę,  która będzie po kompilacji w pliku Wskaznik1.dll.

Potem w projekcie docelowym wystarczy przeciągnąć plik dll kontrolki z Windows Explorer (lub innego managera plików) na toolbox.

Niestety można zauważyć coś mało ciekawego – przy zmianach wartości aktualnej kontrolki cała kontrolka miga nam … no ale i na to jest lekarstwo/sposób.

Dodajemy zamieszczony poniżej kod (poniżej InitializeComponent();). Kod ten pozwala nam skorzystać z DoubleBufferingu – kontrolka jest przerysowywana w tle i dopiero wtedy wyświetlana.

this.SetStyle(ControlStyles.DoubleBuffer |
ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint,true);
this.UpdateStyles();

Po tym zabiegu kompilujemy naszą solucję (ctrl + shift + b) i uruchamiamy projekt testowy (F5) – no i możemy cieszyć się działającą kontrolką (samodzielnie stworzoną).

 

Inne kontrolki stworzone tą samą metodą:

gauges1

Autor: gervee

Pełnoetatowy ojciec małej gromadki, programista(?), "amator" fotograf, "dłubacz" lubiący DIY, miłośnik chmielonego napitku. "Żartowniś" bez poczucia humoru ;).

Dodaj komentarz