Z rysowaniem figur, a właściwie figury, spotkaliśmy się już przy okazji pierwszej lekcji poświęconej JavieFX. Możliwości JavyFX w tym względzie nie kończą się oczywiście na rysowaniu kółek.
Ogólne informacje
JavaFX udostępnia zestaw klas umożliwiający dość łatwe rysowanie dwuwymiarowych kształtów, są one dostępne w pakiecie javafx.scene.shape. Warto na początku przygody z grafiką 2D przejrzeć choćby pobieżnie dokumentację pakietu, żeby dowiedzieć się jakie figury możemy wykorzystać. Należą do nich na przykład linie (Line
), koła (Circle
), elipsy (Ellipse
), prostokąty (Rectangle
), łuki (Arc
) czy krzywe Béziera (CubicCurve
). W tej lekcji nie będziemy wszystkich omawiać, spróbuję jednak pokazać jak zacząć zabawę w rysowanie figur i trochę bardziej złożonych obrazków.
Przeglądając dokumentację pakietu zauważysz zapewne wskazującą na możliwości tworzenia także trójwymiarowych obiektów ale nimi, przynajmniej na razie, nie będziemy się zajmować. Nie będziemy też w tej lekcji zajmować się grafiką bitmapową, taką jak zdjęcia. Skupimy się po prostu na rysowaniu prostych figur i bardziej złożonych kształtów.
Obiekty reprezentujące figury mają oczywiście swoje właściwości, do podstawowych należą oczywiście te, które określają ich wymiary i położenie, wypełnienie, parametry obrysu. Co ważne, w przeciwieństwie do wielu kontrolek, kształty nie zmieniają automatycznie swoich wymiarów, kiedy zmienia się wielkość okna.
Zanim zaczniemy rysować na ekranie, dobrze by było zapoznać się z układem współrzędnych który będziemy wykorzystywać. W przeciwieństwie do tego znanego nam z lekcji matematyki, zaczyna się w lewym górnym rogu obszaru rysowania, a kolejne wartości współrzędnych odpowiadające pikselom zwiększają się w prawo (oś X) i w dół (oś Y). Rysując w scene
, mamy do dyspozycji obszar określony wielkością tego węzła. Szerokość i wysokość scene
(podobnie jak wielu innych elementów w JavieFX), można pobrać za pomocą metod: scene.getWidth()
i getHeight()
.
Stwórz nowy projekt JavaFX w NetBeans, nazwij go Grafika2D
i zmodyfikuj domyślny kod:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
package grafika2d; import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.paint.Color; import javafx.stage.Stage; public class Grafika2D extends Application { @Override public void start(Stage primaryStage) { // Użyjemy węzła Group Group grupa = new Group(); Scene scene = new Scene(grupa, 400, 300); Button btn = new Button(); btn.setText("Kliknij"); btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { // Pobranie i wydruk wymiarów scene System.out.println("szerokość: " + scene.getWidth()); System.out.println("wysokość: " + scene.getHeight()); } }); grupa.getChildren().add(btn); primaryStage.setTitle("Figury"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } |
Po uruchomieniu programu pojawi się okienko z przyciskiem, kiedy w niego klikniesz w terminalu zostanie wypisana szerokość i wysokość węzła scene
. Zmieniaj rozmiary okna i wciskaj przycisk, za każdym razem otrzymasz aktualne rozmiary. Pobieranie bieżących wymiarów może się przydać na przykład jeśli chcemy umieścić obiekt w określonym położeniu w relacji do brzegów okna, na przykład dokładnie w środku.
Teraz pokażę na kilku przykładach jak rysować proste figury geometrycznych a przy okazji jak modyfikować ich wygląd. Zaczniemy od rysowania linii.
Linie
Linie to najprostsze z figur. Kropka to w JavieFX też linia tyle, że zaczynająca się i kończąca w tym samym miejscu. O położeniu i długości linii decydują dwie pary parametrów: startX
, startY
, endX
i endY
. Dwa pierwsze oznaczają początek linii, druga jej koniec.
Uzupełnimy zatem kod metody start()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
public void start(Stage primaryStage) { // Użyjemy węzła Group Group grupa = new Group(); Scene scene = new Scene(grupa, 400, 300); Button btn = new Button(); btn.setText("Kliknij"); btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { // Pobranie i wydruk wymiarów scene System.out.println("szerokość: " + scene.getWidth()); System.out.println("wysokość: " + scene.getHeight()); } }); int startX = 10; int startY = 20; int koniecX = 100; int koniecY = 200; // Współrzędne początku i końca linii ustawiane za pomocą konstruktora Line linia1 = new Line(startX, startY, koniecX, koniecY); // Druga linia Line linia2 = new Line(); // Ustawianie współrzędne początku i końca za pomocą modyfikatorów linia2.setStartX(50); linia2.setStartY(50); linia2.setEndX(350); linia2.setEndY(250); grupa.getChildren().addAll(btn, linia1, linia2); primaryStage.setTitle("Figury"); primaryStage.setScene(scene); primaryStage.show(); } |
Oczywiście należy jak zawsze zadbać o zaimportowanie odpowiednich klas.
Pokazałem tu dwa sposoby tworzenia linii, korzystające z dwu konstruktorów. Pierwszy z nich przyjmuje jako argumenty współrzędne początku i końca linii, drugi konstruktor nie przyjmuje żadnych argumentów a wartości odpowiednich właściwości ustawiamy używając odpowiednich metod: setStartX()
, setStartY()
, setEndX()
oraz setEndY()
.
Nasza aplikacja będzie teraz zawierała dwie linie:
Jak widać domyślnie linie mają grubość 1 piksela i są czarne. Spróbujemy teraz nieco zmienić jedną z nich.
Poniżej ostatniej linii ustawiającej parametry linii linia2
dodaj taki kod:
1 2 3 4 5 6 7 |
// Zmieniamy parametry linii // kolor linia2.setStroke(Color.CHOCOLATE); // rodzaj zakończenia (tu bez dekoracji) linia2.setStrokeLineCap(StrokeLineCap.BUTT); // szerokość linia2.setStrokeWidth(15); |
Efekt wygląda tak:
Zwróć uwagę, że nazwy metod których powyżej używałem mają w nazwie człon Stroke
oznaczający kreskę. W przypadku figur, jak zaraz pokażę oznacza kreskę obrysu. Najpierw użyliśmy metody setStroke
przekazując jej jako argument kolor. Więcej na temat kolorów napiszę za chwilę. Na razie wpisując w NetBeans Color.
możesz skorzystać z podpowiedzi i zobaczyć inne nazwy kolorów, które można wykorzystać.
Metoda setStrokeLineCap()
pozwala określić rodzaj zakończenia linii. Mamy tu trzy możliwości:
BUTT
– brak dekoracjiSQUARE
– na końcach zostają dodane prostokątyROUND
– końce są zaokrąglone
Należy pamiętać, że SQUARE
i ROUND
wydłużają nieco linie.
Ostatnia z metod, setStrokeWidth()
odpowiada oczywiście za ustawienie grubości linii.
Teraz dodajmy kropkę:
1 2 3 4 5 6 7 8 9 10 11 |
// Kropka - linia o takich samych parametrach poczatku i końca // Ustawiamy ją w środku sceny Line kropka = new Line (scene.getWidth()/2, scene.getHeight()/2, scene.getWidth()/2, scene.getHeight()/2 ); kropka.setStroke(Color.FORESTGREEN); // szerokość kropka.setStrokeWidth(20); grupa.getChildren().addAll(btn, linia1, linia2, kropka); |
Upss… dziwnie kwadratowa ta kropka. Zmieńmy zatem zakończenia linii:
1 2 3 4 5 |
// rodzaj zakończenia (tu zaokrąglony) kropka.setStrokeLineCap(StrokeLineCap.ROUND); // szerokość kropka.setStrokeWidth(20); |
Teraz jest już lepiej:
Linia niekoniecznie musi być ciągła, można tu zastosować różne wzorki, które definiujemy jako obserwowalną listę. Kolejne liczby oznaczają długość kolejnych linii i przerw między nimi. Zwróć uwagę, że jeśli liczba liczb jest nieparzysta to dana liczba oznacza na zmianę długość odcinka kreski i przerwy:
1 2 3 4 |
// zmieniamy wzór linii ObservableList wzor = FXCollections.observableArrayList(10.0, 20.0, 30.0); linia2.getStrokeDashArray().addAll(wzor); |
Można też zrobić to krócej:
1 |
linia2.getStrokeDashArray().addAll(10.0, 20.0, 30.0); |
Efekt wygląda tak:
Prostokąty
Aby narysować typowy prostokąt, o ostrych kątach podajemy cztery parametry: x
i y
wyznaczające lewy górny róg oraz width
i height
określający jego szerokość i wysokość.
Możemy w tym celu używać jednego z czterech konstruktorów zdefiniowanych w klasie Rectangle
, który wygląda tak:
Rectangle(double x, double y, double width, double height)
Można też użyć konstruktora Rectangle()
a później używając odpowiednich metod (modyfikatorów
) ustawić odpowiednie wartości. Trzecią możliwością jest przekazanie konstruktorowi szerokości i wysokości prostokąta, wtedy zostanie on utworzony w domyślnym miejscu. Użyjemy jednak pierwszej opcji:
1 2 |
Rectangle prostokat = new Rectangle(100, 20, 150, 100); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat); |
Domyślnie prostokąt ma czarne wypełnienie, co oczywiście można zmienić:
1 |
prostokat.setFill(Color.BROWN); |
Można też zmienić obrys:
1 2 |
prostokat.setStroke(Color.SLATEGRAY); prostokat.setStrokeWidth(10); |
Jak widać, zastosowaliśmy te same metody co w przypadku linii. Podobnie możesz modyfikować inne parametry obrysu.
Prostokąt może mieć zaokrąglone rogi, za co odpowiadają dwie właściwości: arcWidth
i arcHeight
.
Ustawiamy je używając odpowiednich metod – modyfikatorów:
1 2 |
prostokat.setArcWidth(40); prostokat.setArcHeight(40); |
Koła
Klasa Circle
oferuje pięć konstruktorów służących tworzeniu kół. Położenie i rozmiar koła określają trzy parametry: centerX
, centerY
oraz radius
. Jak same nazwy wskazują, dwa pierwsze określają położenie środka koła a trzeci jego promień.
Dodajmy zatem koło do naszego obrazka:
1 2 3 |
Circle kolo = new Circle(300, 200, 50); kolo.setFill(Color.CADETBLUE); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat, kolo); |
Elipsy
Przy rysowaniu elipsy, jak wynika z dokumentacji, oprócz współrzędnych środka, musimy określić dwie średnice, pionową i poziomą (radiusX
, radiusY
).
Nasza elipsa będzie dla odmiany pozbawiona wypełnienia, za to będzie z obrysem:
1 2 3 4 5 6 7 |
Ellipse elipsa = new Ellipse(100, 200, 75, 50); elipsa.setFill(null); elipsa.setStrokeWidth(5); elipsa.setStroke(Color.CORAL); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat, kolo, elipsa); |
Wielokąty
Klasa Polygon
pozwala rysować dowolne figury zamknięte. Za pomocą konstruktora, lub metod (add
, addAll
) podajemy współrzędne kolejnych punktów, między którymi rysowane są linie, po czym ostatni punkt zostaje połączony z pierwszym.
Stwórzmy więc pięciokąt:
1 2 3 4 5 6 7 8 9 |
Polygon pieciokat = new Polygon(80, 80, 120, 120, 100, 160, 60, 160, 40, 120); pieciokat.setFill(Color.BLUEVIOLET); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat, kolo, elipsa, pieciokat); |
Mamy pięciokąt:
Można dodać do wielokąta kolejny punkt:
1 |
pieciokat.getPoints().addAll(80.0, 180.0); |
Ale trzeba pamiętać, że punkt dodaje się na końcu, więc do tego punktu biegnie krawędź z punktu, który był poprzednio ostatni (40, 120) i od dodanego punkty pobiegnie odcinek zamykający figurę do punktu pierwszego (80, 80). Dlatego efekt, który otrzymamy będzie nieco dziwny:
Linia łamana
Polyline
to klasa służąca do rysowania linii łamanych, działa podobnie do wielokąta z tym, że ostatni punkt nie łączy się z pierwszym.
1 2 3 4 5 6 7 8 |
Polyline lamana = new Polyline(300, 200, 350, 200, 300, 250, 250, 270); lamana.setStrokeWidth(4); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat, kolo, elipsa, pieciokat, lamana); |
Można też używać wypełnienia:
1 |
lamana.setFill(Color.BEIGE); |
Wtedy efekt będzie taki:
Kolory
Dotychczas nadając kolor obrysom czy wypełnienia robiliśmy to w taki sposób: Color.NAZWAKOLORU
. Czyli używaliśmy nazw kolorów, które chcieliśmy zastosować. Teraz zajrzyj do dokumentacji klasy Color
. Można tam znaleźć, w sekcji „Field Summary” nazwy wszystkich nazw kolorów, które możemy użyć, ich próbki a także ich wartości RGB.
Jak wynika z dokumentacji kolory można definiować też inaczej, jako wartości RGB, przy czym można stosować zakres 0-1 lub 0-255 dla każdej składowej barw. Można też regulować kanał alfa, który oznacza stopień krycia (wartość 0 oznacza całkowitą przeźroczystość). Możliwe są też inne sposoby określania barw, ale nie będę ich tu poruszał, jak zwykle zachęcam do przestudiowania dokumentacji.
Można na przykład tak zmienić wypełnienie obiektu lamana
:
1 2 |
Color col = Color.rgb(0, 100, 0, 0.5); lamana.setFill(col); |
Krótsza wersja tego kodu wygląda tak:
1 2 |
lamana.setFill(Color.rgb(0, 100, 0, 0.5); |
Teraz jest ona ciemnozielona, półprzeźroczysta. Użyliśmy tu metody statycznej rgb
, przyjmującej trzy wartości odpowiadające kanałom RGB (Red, Green, Blue) w zakresie 0-255 oraz kanału alfa z zakresu 0-1. Metoda rgb
jest przeciążona, można nie podawać wartości kanału alfa, wtedy domyślnie ma on wartość 1.
Następnie zrobimy rzecz bardziej zabawną, dodaj taki kod:
1 2 3 4 5 |
kolo.setOnMouseClicked((e) -> { Color c = new Color(Math.random(), Math.random(), Math.random(), Math.random()); kolo.setFill(c); }); |
Od tej chwili, jeśli klikniemy na obiekt kolo
to przyjmie on losową barwę i stopień krycia, ponieważ wszystkie wartości kanału RGB oraz alfa są losowane. Zauważ, że w tym przypadku używamy konstruktora klasy Color
, który przyjmuje wszystkie wymienione wartości z zakresu 0-1.
Kod pliku Grafika2D.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
package grafika2d; import javafx.application.Application; import javafx.event.ActionEvent; import javafx.event.EventHandler; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.control.Button; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.scene.shape.Ellipse; import javafx.scene.shape.Line; import javafx.scene.shape.Polygon; import javafx.scene.shape.Polyline; import javafx.scene.shape.Rectangle; import javafx.scene.shape.StrokeLineCap; import javafx.stage.Stage; public class Grafika2D extends Application { @Override public void start(Stage primaryStage) { // Użyjemy węzła Group Group grupa = new Group(); Scene scene = new Scene(grupa, 400, 300); Button btn = new Button(); btn.setText("Kliknij"); btn.setOnAction(new EventHandler<ActionEvent>() { @Override public void handle(ActionEvent event) { // Pobranie i wydruk wymiarów scene System.out.println("szerokość: " + scene.getWidth()); System.out.println("wysokość: " + scene.getHeight()); } }); int startX = 10; int startY = 20; int koniecX = 100; int koniecY = 200; // Współrzędne początku i końca linii ustawiane za pomocą konstruktora Line linia1 = new Line(startX, startY, koniecX, koniecY); // Druga linia Line linia2 = new Line(); // Ustawianie współrzędne początku i końca za pomocą modyfikatorów linia2.setStartX(50); linia2.setStartY(50); linia2.setEndX(350); linia2.setEndY(250); // Zmieniamy parametry linii // kolor linia2.setStroke(Color.CHOCOLATE); // rodzaj zakończenia (tu bez dekoracji) linia2.setStrokeLineCap(StrokeLineCap.BUTT); // szerokość linia2.setStrokeWidth(15); // Kropka - linia o takich samych parametrach poczatku i końca // Ustawiamy ją w środku sceny Line kropka = new Line (scene.getWidth()/2, scene.getHeight()/2, scene.getWidth()/2, scene.getHeight()/2 ); kropka.setStroke(Color.FORESTGREEN); // szerokość kropka.setStrokeWidth(20); // rodzaj zakończenia (tu zaokrąglony) kropka.setStrokeLineCap(StrokeLineCap.ROUND); // zmieniamy wzór linii //ObservableList wzor = FXCollections.observableArrayList(10.0, 20.0, 30.0); linia2.getStrokeDashArray().addAll(10.0, 20.0, 30.0); Rectangle prostokat = new Rectangle(100, 20, 150, 100); prostokat.setFill(Color.BROWN); prostokat.setStroke(Color.SLATEGRAY); prostokat.setStrokeWidth(10); prostokat.setArcWidth(40); prostokat.setArcHeight(40); Circle kolo = new Circle(300, 200, 50); kolo.setFill(Color.CADETBLUE); Ellipse elipsa = new Ellipse(100, 200, 75, 50); elipsa.setFill(null); elipsa.setStrokeWidth(5); elipsa.setStroke(Color.CORAL); Polygon pieciokat = new Polygon(80, 80, 120, 120, 100, 160, 60, 160, 40, 120); pieciokat.setFill(Color.BLUEVIOLET); // dodajemy punkt pieciokat.getPoints().addAll(80.0, 180.0); Polyline lamana = new Polyline(300, 200, 350, 200, 300, 250, 250, 270); lamana.setStrokeWidth(4); //lamana.setFill(Color.BEIGE); // Kolor przez wskazanie wartości kanałów RGM i krycia (alfa) Color col = Color.rgb(0, 100, 0, 0.5); lamana.setFill(col); // Po kliknięciu w koło, losowo zmienia się kolor wypełnienia i krycie kolo.setOnMouseClicked((e) -> { Color c = new Color(Math.random(), Math.random(), Math.random(), Math.random()); kolo.setFill(c); }); grupa.getChildren().addAll(btn, linia1, linia2, kropka, prostokat, kolo, elipsa, pieciokat, lamana); primaryStage.setTitle("Figury"); primaryStage.setScene(scene); primaryStage.show(); } public static void main(String[] args) { launch(args); } } |
Inne sposoby wypełniania kształtów
Kształty niekoniecznie musimy wypełniać jednolitym kolorem. Klasa Paint
, która jest klasą nadrzędną klasy Color
posiada też inne klasy potomne: LinearGradient
, RadialGradient
oraz ImagePattern
, które odpowiednio umożliwiają wypełnienie obiektów gradientem liniowym, radialnym (kołowym) oraz bitmapami lub wzorkiem. Niestety nie będziemy tu szerzej omawiać tych możliwości (może innym razem).
Przykład i zadanie
Na koniec wykorzystamy dotychczas zdobyte umiejętności do narysowania wzoru o charakterze fraktalnym. Wykorzystamy przy tym technikę rekurencji.
Stwórz kolejny projekt o nazwie KolkaRekurencjaFX
a w nim umieść kod (w pliku KolkaRekurencjaFX.java
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
package kolkarekurencjafx; import javafx.application.Application; import javafx.scene.Group; import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.scene.shape.Circle; import javafx.stage.Stage; public class KolkaRekurencjaFX extends Application { @Override public void start(Stage primaryStage) { Group grupa = new Group(); malujWzorek(grupa, 200, 100, 80); Scene scene = new Scene(grupa, 400, 200); primaryStage.setTitle("Fraktal"); primaryStage.setScene(scene); primaryStage.show(); } public void malujWzorek(Group grupa, int x, int y, int r) { Circle kolko = new Circle(x , y, r, Color.rgb(0,100,0,0.25)); kolko.setStroke(Color.BLACK); grupa.getChildren().add(kolko); if (r > 1) { malujWzorek(grupa, x + r, y, r / 2); malujWzorek(grupa, x - r, y, r / 2); } } public static void main(String[] args) { launch(args); } } |
Po uruchomieniu programu otrzymasz taki rezultat:
Jak widać za pomocą dość krótkiego programu można uzyskać całkiem ciekawe efekty.
Przeanalizuj teraz powyższy kod, tak aby zrozumieć jak działa. Zmodyfikuj go tak aby uzyskać inne ciekawe efekty, na przykład:
- Otrzymaj z kółek „krzyż” taki jak na ilustracji poniżej
- Dodaj do interfejsu kontrolki pozwalające zmieniać pewne parametry rysunku, np. początkową szerokość koła, dzielnik promienia, możesz też dodać dodatkowe parametry.
Leave a Reply
You must be logged in to post a comment.