Programowanie w języku C#

Obsługa Graphics i rysowanie po WinForms.

Wstęp


Pisząc jakieś aplikacje na WinForms często potrzebujemy narysować coś ręcznie na formie. Z pomocą przychodzi nam przestrzeń nazw System.Drawing. Jest tam kilkanaście klas pomocnych nam do rysowania po formie. Na początek omówię najważniejsze z nich:

KlasaOpis
BitmapPo prostu bitmapa
BrushPędzel, którym wypełniamy obszary
BufferedGraphicsBuforowane grafiki, dzięki nim obraz będzie stabilny przy szybkich ruchach i wszystko będzie normalnie wyglądać
FontCzcionka
GraphicsKlasa, z pomocą której możemy rysować i nie tylko poprzez GDI+
ImageAbstrakcyjna podstawa dla pochodnych typów obrazowych np. bitmapy
PenOłówek, który używany jest do podstawowego rysowania konturów
StrukturaOpis
ColorReprezentuje kolor w postaci ARGB
PointPunkt
RectangleProstokąt
SizeWielkośc (dł., wys.)
EnumOpis
KnownColorZawiera podstawowe kolory systemu


Przestrzeń nazw zawiera jeszcze kilka innych klas np. konwerterów, ale ja wymieniłem tylko te podstawowe. Przejdźmy teraz do podstaw rysowania.

Podstawy


Musimy utworzyć klasę Graphics lub jeżeli operujemy na metodach wewnątrz klasy kontrolki z Windows Forms, możemy pobrać Graphics z argumentu. Metody te związane są najczęściej z rysowaniem danej kontrolki, tak więc jeżeli chcemy nieco zmodyfikować działanie rysowania kontrolki, możemy zmienić treść jej metod.
Obiekt klasy Graphics możemy utworzyć poprzez kilka jej statycznych metod lub poprzez metodę CreateGraphics() dla kontrolek WinForms, wtedy przynależność Graphics jest do obszaru tej właśnie kontrolki. Jak wspomniałem są jeszcze metody statyczne, oto one:
Graphics.FromImage(Image); // tworzy Graphics na rzecz obrazka, na którym możemy wykonywać operacje
Graphics.FromHwnd(IntPtr); // tworzy Graphics na rzecz uchwytu
Graphics.FromHdc(IntPtr, IntPtr); // tworzy Graphics na rzecz uchwytu kontekstu urządzenia


Utwórzmy teraz obiekt Graphics, którym będziemy operowali kopią obrazka wczytanego z pliku:
Graphics g = Graphics.FromImage(Image.FromFile("test.bmp"));

Mamy teraz dostęp do właściwości i metod obiektu Graphics. Właściwość VisibleClipBounds zawiera automatycznie przy wczytaniu obrazka, obszar obrazka - czyli też obszar, po którym rysujemy. Możemy ten obszar zmieniać za pomocą metody SetClip(...). Klasa zawiera też inne właściwości np. te związane z jakością wykonywanych operacji graficznych. Są to:
Graphics.CompositionQuality;
Graphics.InterpolationMode;
Graphics.PixelOffsetMode;
Grahpics.SmoothingMode;

Ponadto, mamy jeszcze do dyspozycji właściwość TextContrast, którą można regulować korekcję gamma dla rysowanego tekstu.
Przejdźmy do możliwości klasy. Za pomocą Graphics możemy narysować:
•        Łuk (Arc)
•        Krzywą Beziera (Bezier)
•        Krzywą (Curve)
•        Elipsę (Ellipse)
•        Ikonę (Icon)
•        Obraz (Image)
•        Linię (Line)
•        Ścieżkę (Path)
•        Wielokąt (Polygon)
•        Prostokąt (Rectangle)
•        Tekst (String)
•        Wycinek koła (Pie)

Nie trudno domyślić się, że w przypadku chęci narysowania trójkąt skorzystamy z narysowania wielokąta o 3 wierzchołkach.
Klasa posiada jeszcze metodę CopyFromScreen(...), która kopiuje obszar z ekranu do naszego obszaru. Możemy dowolnie ustawić w parametrach metody ten właśnie obszar, który chcemy skopiować i dokładne miejsce „wklejenia”;

Rysowanie


Linia


Znamy już ogólną budowę i działanie klasy Graphics, więc wypadałoby coś teraz wykonać praktycznie. Zacznijmy od narysowania linii na wcześniejszym wczytanym obrazku. Należy zrobić to tak:
g.DrawLine(Pens.Blue, 20, 20, 60, 60);

Klasa Pens jest klasą statyczną i zawiera podstawowe „ołówki”. Jest jeszcze do dyspozycji Brushes, która zawiera podstawowe pędzle. Oczywiście możemy tworzyć własne narzędzia:
Pen grubyCzerwonyMarker = new Pen(Color.Red, 10);

Warto poczytać opisy konstruktorów poszczególnych klas, aby zapoznać się z nimi i wiedzieć jak ich używać. Dokumentację znajdziecie na http://msdn.microsoft.com/en-us/library/system.drawing.aspx.

Tekst


Spróbujmy teraz napisać coś na obrazku. Kod będzie wyglądał mniej-więcej tak:
g.DrawString("Obrazek nr 1", new Font("Segoe UI Light", 24), Brushes.Blue, 20, 20);

Pierwszy parameter to łańcuch, który chcemy umieścić; drugi to czcionka – możemy skorzystać z czcionki rodzica, jeżeli kontrolka go posiada, ale możemy też wczytać nową, wystarczy podać nazwę i wielkość; trzeci argument to pędzel, który zostaanie użyty do malowania tekstu, a ostatnie 2 argumenty to współrzędne początku rysowania (lewy-górny róg).
Pragnę zaznaczyć, że często argumenty przedstawiające współrzędne, wielkość etc., możemy zastąpić jednym – klasą, dla punktu będzie to klasa Point.
Mając takie narzędzie jak Graphics i trochę umiejętności programistycznych, możemy ułatwić sobie pracę, np. potrzeba nam wstawić do internetu zdjęcia, ale na każdym musi znaleźć się nasze imię i nazwisko oraz data wykonania. Istnieją od tego programy, ale czemu nie zrobić tego samemu? :) Wykonanie tego ręcznie dla 300 zdjęć z jakiejś wycieczki byłoby udręką. Postarajmy się ten proces zautomatyzować.
Wybierzmy miejsce dla napisu, np. prawy dolny róg zdjęcia, tam powinien znaleźć się napis. Najpierw musimy wczytać nasze zdjęcia i na każdym zrobić tą samą operację – narysować tekst. Wszystko ok, ale skoro napis ma być w prawym dolnym rogu, to jak znaleźć współrzędne punktu, od którego zacznie się rysować teskt? Musimy też mieć długość tekstu, aby dokładnie wymierzyć offset tekstu przed prawą „barierą” zdjęcia. Aby znaleźć długość łańcucha pisaną konkretną czcionką w pikselach, musimy wywołać metodę MeasureString(...), która w podstawowe wersji jako pierwszy parametr przyjmuje łańcuch, a drugi czcionkę.
g.MeasureString("Chuck Norris, 621r.p.n.e.", new System.Drawing.Font("Arial", 12));

Metoda zwraca wynik jako SizeF, mówiliśmy już o Size, a to ‘F’ oznacza, że zmienne określające lokację punktu są jako float. Musimy teraz odczytać wys. i szer. napisu, aby dobrze wymierzyć pozycję napisu. Przyjmiemy też dla estetyki, że za napisem na się znaleźć 10px w pionie i tyle samo w poziomie. Więc kod będzie taki:
SizeF sizeStr = g.MeasureString("Chuck Norris, 621r.p.n.e.", new System.Drawing.Font("Arial", 12));
Point offset = new Point(10, 10);
PointF pos = new PointF(g.VisibleClipBounds.Width - sizeStr.Width - offset.X, g.VisibleClipBounds.Height - sizeStr.Height - offset.Y);
 
g.DrawString("Obrazek nr 1", new Font("Segoe UI Light", 24), Brushes.Blue, pos);

Tak powinniśmy zrobić dla całej kolekcji naszych zdjęć. Jeszcze jedna uwaga: Graphics rysuje w czasie rzeczywistym i nie da się z niego „wyciągnąć” obrazka, tak więc uprzednio powinniśmy zapisać referencję do obrazka, na którym działamy, bo potem nie będzie w stanie nic z nim zrobić.
Dla całej kolekcji powinniśmy dodać pętlę foreach i wcześniej zadeklarować tablicę dla typu Image, do której wczytamy po kolei wszystkie zdjęcia. Po wykonaniu operacji na każdym obrazku powinien nastąpić zapis do pierwotnego pliku. Ten sam pomysł możemy użyć, udostępniając zdjęcia w internecie i chemy żeby miał znak wodny, możemy go umieścić półprzezroczsto na środku obrazku i mamy pewność, że nikt sobie łatwo i bez bólu nie przywłaszczy. O półprzezroczystości napiszę w następnych rozdziałach.
Jeszcze jedna rada: im lepsze ustawienia damy właściwiościom od jakości, o których wspomniałem, tym napis będzie ładniej wyglądał.

Kształty


Różne kształty rysujemy analogicznie jak linię czy tekst, dla prostokąta podajemy współrzędne lewego górnego rogu i wymiary, zaś dla wielokąta, podajemy zbiór punktów w takiej kolejności, jak mają być połączone i np. ich kolor, zależy od argumentów danej przeciążonej metody. Są też funkcje, które przyjmują za argument tego co chcemy narysować w formie tablicy. Metoda ta wtedy rysuje każdy element tablicy, tak więc zamiast wywoływać 10x metodę DrawRectangle(...), możemy zadeklarować tablicę, umieścić w niej to co chcemy i przekazać tablicę to metody DrawRectangles(...).

Operacje na obszarze rysowania


Obszar, na którym pracujemy możemy dowolnie zmienić. Możemy np. wyłączyć część obszaru metodą ExcludeClip(...), która przyjmuje prostokąt lub Region, który opisuje obszar do usunięcia z obecnego. Możemy też zresetować obszar Clip metodą ResetClip(), która ustawia obecny obszar na nieskończony (praktycznie jest on skończony, ale i tak jest bardzo duży). Bardzo prosto też możemy zmienić cały obszar rysowania na inny metodą SetClip(...).

Wypełnianie


Chcąc coś narysować, chcemy często wypełnić dany obszar. Z pomocą przychodzą nam metody zaczynające się na Fill...(...). Zależnie od tego jaką figurę chcemy wypełnić. Aby wypełnić narysowant prostokąt wystarczy wywołać FillRectangle(...) z takimi samymi parametrami współrzędnych, jakie podaliśmy rysując. Czasem chcemy wypełnić niekoniecznie regularny kształt, może więc użyć metody FillRegion(Brush, Region). Wypełni dany obszar danym kolorem. Aby ustawić obszar tworzymy nowy obiekt typu Region i podajemy albo prostokąt albo ścieżkę – ścieżka to zbiór punktów określający obszar, niekoniecznie zamknięty. Tworząc obszar mamy do wyboru różne opcje wypełniania i przede wszystkim zbiór punktów.

Czyszczenie


Chcąc wyczyścić nasz obszar, powinniśmy wywołać metodę Clear(Color), która wypełnia cały obszar danym kolorem.

Macierze


Chcąc wykonywać trochę bardziej skomplikowane rzeczy z udziałem obrazka, musimy użyć macierzy. Na szczęście w .NET framework’u mamy już gotowe, zaimplementowane klasy do obsługi macierzy. Sama klasa Graphics posiada kilka właściwości i metod, którymi posłużymy się, aby przekształcać macierz do porządanego efektu. Z pomocą macierzy możemy zrobić kilka rzeczy: obrócić, przeskalować, przesunąć, przekształcić o inną macierz lub ręcznie zmieniać wartości macierzy, aby powstało nam homo-niewiadomo :D Po wykonanych operacjach warto zresetować macierz przekształceń metodą ResetTransform().

Wszelkie operacje na macierzy przekształceń wykonujemy PRZED rysowaniem i po kolei, np. najpierw translacja, potem obrót, nigdy nie na odwrót, bo nie uzyskamy wtedy takiego efektu jak chcemy.

Translacja


Chcąc przesunąc równolegle macierz przekształceń, na której operujemy obracajać i skalując, wywołujemy metodę TranslateTransform(Float, Float). Parametry to odpowiednio x i y przesunięcia. Translacja przydaje się przy obrocie, gdy musimy ustawić środek obrotu.

Obrót


Obrót aplikujemy za pomocą metody RotateTransform(Float). Parametr informuje o kącie obrotu, kąt jest prawoskrętny. Zatem chcąc obrócić napis „ala ma kota”, o 90° w prawo względem środka napisu wykonujemy:
g = this.CreateGraphics();
str = "ala ma kota";
PointF pos = new PointF(100, 100); // pozycja napisu (lewy górny róg)
SizeF size = g.MeasureString(str, this.Font); // wielkość napisu
PointF pivot = new PointF(pos.X + size.Width / 2, pos.Y + size.Height / 2); // punkt obrotu (środek napisu)
 
g.TranslateTransform(pivot.X, pivot.Y); // translacja przekształcenia o ‘pivot’
g.RotateTransform(90.0f); // obracamy o 90'  
g.DrawString(str, this.Font, Brushes.Blue, new PointF(-size.Width / 2, -size.Height / 2)); // rysujemy

W DrawString za ostatni parametr podałem dlatego takie wartości, bo po translacji napis rysował by się przy ustawieniu (100, 100) daleko od faktycznej pozycji, dlatego, że teraz domyślną pozycją jest właśnie pivo – pozycja translacji. Aby umieścić napis na środku pivot’u, odjąłem połowę długości w prawo i połowę wys. w górę. Napis teraz narysuje nam się 90° w prawo względem siebie. To samo oczywiście możemy zrobić z innymi figurami czy obrazkami.

Skalowanie


Skalowanie odbywa się za pomocą metody ScaleTransform(Float, Float), gdzie parametry to wspołczynniki skalowania obiektu, odpowiednio 1. to długość, 2. to wysokość. Skoro to współczynniki to ustawienie (1,1) nic nie da, skala zostanie taka jak była. By powiększyć obiekt 2x, ustawiamy współczynniki na (2,2). Chcąc pomniejszyć dwukrotnie użyjemy (0.5f, 0.5f). Literał ‘f’ występuje dlatego, że są to liczby typu float.




To na razie tyle, nad dalszą cześcią cały czas pracuję :) Dlasza część artykułu to będą konkretne rzeczy typu animacja.

2 komentarze

Varran 2012-03-13 16:50

Grzesiu zapomniałeś o linku ;)