Darmowy kurs Java

Proste rysowanie - JPanel i JComponent

Pakiet Swing oprócz gotowych komponentów takich jak przyciski, suwaki, czy pola tekstowe dostarcza również klasy, na których sami możemy coś rysować. Oczywiście nie napiszemy od razu Painta, ale zaczniemy od rysowania prostych kształtów i wyświetlania własnego tekstu.

Kurs Programowania Java

Do opisanych czynności mogą nam posłużyć dwie klasy JComponent, lub JPanel (która rozszerza tą pierwszą). Istnieje jednak między nimi kilka subtelnych różnic

    • JComponent jest domyślnie przezroczysty, natomiast JPanel nie (posiada domyślny kolor tła)

    • JPanel jako domyślnego zarządce rozkładem ma ustawionego FlowLayout, JComponent, nie ma żadnego

    • JComponent jest klasą abstrakcyjną, więc nie można utworzyć jej instancji (nie można storzyć obiektu new JComponent() )

    Poza tym ich funkcjonalność jest niemal identyczna. Obie posiadają kontenery, do których możemy dodawać inne komponenty, dzięki czemu, można w wygodny sposób stworzyć kilka paneli, które dopiero później gromadzimy w ramce. Co istotniejsze posiadają metody paint() oraz paintComponent(), w których możemy rysować różne rzeczy.

    Dwie wspomniane metody posiadają jeden parametr typu Graphics, jednak trzeba sobie zdawać sprawę, że obecnie jest to tak naprawdę obiekt Graphics2D i jeśli coś rysujemy, to powinniśmy najpierw utworzyć obiekt tej klasy i przypisać mu zrzutowany na tę klasę argument, czyli:

    public void paintComponent(Graphics g) {
    	Graphics2D g2d = (Graphics2D) g;
             //dalsza część metody, rysowanie itp.
    }

    Rysowanie podstawowych kształtów możemy zrealizować na dwa sposoby.

    1. Nieco przestarzały, raczej niezalecany, choć można go używać. Konkretnie klasa Graphics posiada cały zestaw metod do rysowania różnych kształtów, np:

      • drawLine(int x1, int y1, int x2, int y2) - rysowanie linii

      • drawRect(int x, int y, int width, int height) - rysowanie prostokąta

      • drawOval(int x, int y, int width, int height) - rysowanie owalnych kształtów (elipsy)

      Istnieją też podobne metody, różniące się tym, że wypełniają dodatkowo obszar zdefiniowany w ten sposób jednolitym kolorem, rozpoczynają się przedrostkiem fill, zamiast draw i korzystają z ustawionego, lub domyślnego koloru.

      Metodę tę odradzam głównie z jednego powodu - kłóci się ona (może poza metodą drawString() ) z programowaniem obiektowym. Po co przekazywać jako argumenty zestawy nawet 10 wartości, gdy można je zastąpić przykładowo 2 prostokątami, czy 2 punktami. Rozwiązanie takie jest czytelniejsze, zajmuje mniej miejsca i angażuje mniej zmiennych. Przykład na końcu lekcji nie obrazuje tego może tak dobrze, ale wyobraźmy sobie, że chcemy narysować nie 2, a 200 figur, wtedy ciężko jest to zautomatyzować, a w przypadku 2 przedstawionego sposobu bez problemu można używać pętli i sprawić, że klasa będzie bardziej elastyczna.

      Jeżeli już korzystamy z tego rozwiązania to korzystajmy z obiektu Graphics2D tak jak wcześniej wspomnieliśmy. Uzyskamy dzięki temu dużo lepszą elastyczność i dostęp do dodatkowych metod.

       

      2. Podejście bardziej eleganckie z użyciem tego co daje nam Java2D, a konkretnie gotowych kształtów rozszerzających klasę Shape, znajdziemy tam:

        • Ellipse2D

        • Rectangle2D

        • Line2D

        • ... i kilka innych, np łuki, czy zaokrąglony prostokąt

        Co ciekawe każda klasa posiada 2 implementacje:

          • z pojedynczą precyzją np Rectangle2D.Float

          • z dokładnością double np Rectangle2D.Double

          Analogicznie jest ze wszystkimi innymi. Zapis może się wydawać na początku niezbyt przyjazny, ale po napisaniu kilku przykładów wykorzystujących takie obiekty zapomina się o tym.

          UWAGA Nie wolno wywoływać metody paint(), ani paintComponent() bezpośrednio. Aby odświeżyć widok komponentu należy wywołać na jego instancji metodę repaint().

          Podsumujmy więc tę lekcję i napiszmy dwa przykładowe programy robiące dokładnie to samo - narysujmy na panelu kwadrat wewnątrz którego będzie koło. Wykorzystamy podejście z wykorzystaniem obu przedstawionych metod. Dodatkowo zarówno klasa Ramki oraz klasa Testowa będą identyczne, więc napiszemy je tylko raz:

          import javax.swing.JFrame;
          import javax.swing.JPanel;
          
          public class MyFrame extends JFrame {
          	public MyFrame() {
          		super("Rysowanie");
          		JPanel panel = new MyPanel();
          
          		add(panel);
          
          		pack();
          		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
          		setVisible(true);
          	}
          }
          
          import java.awt.EventQueue;
          
          public class Test {
          
          	public static void main(String[] args) {
          		EventQueue.invokeLater(new Runnable() {
          
          			@Override
          			public void run() {
          				new MyFrame();
          			}
          		});
          	}
          }
          

          Sposób pierwszy:

          import java.awt.*;
          
          import javax.swing.JPanel;
          
          public class MyPanel extends JPanel {
          	public MyPanel() {
          		setPreferredSize(new Dimension(400, 400));
          	}
          
          	@Override
          	protected void paintComponent(Graphics g) {
          		super.paintComponent(g);
          		Graphics2D g2d = (Graphics2D) g;
          
          		// prostokat
          		g2d.drawRect(10, 10, 380, 380);
          		// kolo
          		g2d.drawOval(10, 10, 380, 380);
          	}
          }
          

          Kurs Programowania Java

          Sposób drugi:

          import java.awt.*;
          import java.awt.geom.*;
          
          import javax.swing.JPanel;
          
          public class MyPanel extends JPanel {
          	public MyPanel() {
          		setPreferredSize(new Dimension(400, 400));
          	}
          
          	@Override
          	protected void paintComponent(Graphics g) {
          		super.paintComponent(g);
          		Graphics2D g2d = (Graphics2D) g;
          
          		// prostokat
          		Rectangle2D rectangle = new Rectangle2D.Double(10, 10, 380, 380);
          		// kolo
          		Ellipse2D circle = new Ellipse2D.Double(10, 10, 380, 380);
          
          		g2d.draw(rectangle);
          		g2d.draw(circle);
          	}
          }
          

          Nowinką jest wykorzystanie metody pack(). Dzięki jej wywołaniu następuje automatyczne dopasowanie zarówno do rozmiarów (prefered size) dodanych komponentów oraz zdefiniowanego zarządcy rozkładem.

          Zapisz się do newslettera

          Otrzymuj nasz Newsletter z przykładowymi pytaniami rekrutacyjnymi, wyzwaniami programistycznymi i nowościami ze świata Javy, a także informacje o nowych kursach i promocjach.

          Traktujemy Twoją prywatność poważnie. Nikomu nie udostępniamy Twojego maila no i zawsze możesz się wypisać.

          Komentarze do artykułu

          Wyłączyliśmy możliwość dodawania komentarzy. Poniżej znajdziesz archiwalne wpisy z czasów gdy strona była jeszcze hobbystycznym blogiem. Zapraszamy natomiast do zadawnia pytań i dyskusji na naszej grupe na facebooku.

          s4ncho

          w przykładach chyba nigdzie nie wywołujesz metody "paintComponent(Graphics g)"

          Slawek

          Cała magia polega na tym, że tej metody bezpośrednio wręcz nie wolno wywoływać. Robi, się to poprzez metodę repaint() wywołaną na rzecz obiektu, który przeciąża metodę paintComponent, lub paint. Powinienem o tym wspomnieć i gdzieś to dopiszę.

          tomek

          JPanel panel = new MyPanel(); Czy tu nie powinno byc JPanel panel = new JPanel();

          tomek

          ok, juz widze, jest ok

          Mariusz

          Jestem ekstra początkujący i tak trochę nad tym rozmyślałem, że warto wspomnieć o tym żeby oprócz klas Test i MyFrame stworzyć klase MyPanel do rysowania tego kwadratu z wpisanym kołem, ponieważ na początku było to trochę dla mnie nie zrozumiałe :)

          blezus

          1. Po co jest wywołana metoda paintComponent() z nad klasy (JPanel)? super.paintComponent(g); Usunąłem tą linię i program też zadziałał. 2. Nadal nie rozumiem tego o co pytano w 1 komentarzu. Skoro nigdzie nie jest wywołana metoda paintComponent() to skąd program wie że ma narysować ten okrąg w kwadracie?

          Hashed

          Pytanie stare, ale dla potomnych odpowiem: 1. Klasa MyPanel dziedziczy z JPanel, która dziedziczy jeszcze z kilku innych klas. Przeciążamy jej metodę paintComponent i chcemy, aby _oprócz tego co robiła dotychczas_ rysowała jakiś prostokąt i koło. Być może dotychczas ta metoda nic nie robiła, może coś robiła (można to sprawdzić, ale to nie ma żadnego znaczenia) - nie chcemy usunąć jej dotychczasowego zachowania. Programista nie może zakładać że klasa po której dziedziczy zachowuje się tak jak nie inaczej, chyba że sam ją implementował, choćby dla tego że w innej implementacji javy (np u twojego kolegi czy klienta przyszłego) klasa może być zaimplementowana inaczej. Dla tego żeby niezależnie jakie było zachowanie klasy nadrzędnej było ono zachowane, należy ręcznie wywołać przeciążaną funkcję w odpowiednim momencie. Btw - zastanawiam się, czy bez tej linii po zakryciu okienka innym i odsłonięciu go nie stała by się magia. 2. Zapewniam Cię, że metoda paintComponent jest wywoływana, tyle że nie bezpośrednio przez programistę. W konstruktorze klasy MyFrame wywoływana jest metoda add, która dodaje w jakiś sposób obiekt klasy MyPanel do okienka (jak nie ma znaczenia dla programisty - ważna jest abstrakcja: obiekt klasy MyFrame posiada teraz obiekt klasy MyPanel). Cała reszta dzieje się w konstruktorze obiektu JPanel (wywoływanym w kostruktorze MyFrame) który zapewnia nas, że od teraz to okienko będzie obsługiwane. Co to znaczy obsługiwane? To znaczy, że jak będziemy klikać to będzie ono reagować, i że cyklicznie będzie wywoływana metoda repaint wszystkich obiektów należących do obiektu tej klasy (pamiętamy, że obiekt klasy MyPanel jest też obiektem klasy JPanel), np w oddzielnym wątku GUI. Metoda repaint z kolei wywołuje metoda paintComponent (która dalej zapewne wywołuje paint, ale to już nie ma tu znaczenia). Proste jak Perl. W skrócie - metoda paintComponent jest wywoływana cyklicznie przez środowisko Javy, a przeładowując ją definiujesz sposób w jaki ma się przerysować, kiedy się od niej tego zarząda (czy to ręcznie przez wywołanie repaint, czy w momencie kiedy zrobi to jvm).

          Swierszcz

          mam takie pytanie: jak to jest, ze jak nadam rozmiar w klasie MyFrame rozmiar okna np.: setSize(300,300); i wykasuje w klasie MyPanel linijkę zawierającą rozmiary panelu setPreferredSize(new Dimension(400, 400)); to zamiast wyświetlić mi okienko w rozmiarach zadeklarowanych wyświetla mi skompresowane okienko, i żebym mógł coś zobaczy, muszę je rozciągnąć??

          Slawek

          Jeśli usuniesz metodę pack() problem powinien zniknąć. Powoduje ona, dopasowanie rozmiaru okna do komponentów i layoutów zdefiniowanych w ramce.

          darth2012

          Napisałem prosty program rysujący kwadrat, który następnie przesuwa się w prawy dolny róg. protected void paintComponent(Graphics g){ x++; super.paintComponent(g); Graphics2D g2d = (Graphics2D) g; g2d.drawRect(x,x,380,380); } jednak obrazek aktualizował się tylko przy zmienianiu rozmiarów okna, więc zmodyfkowałem program dodając na końcu tej metody repaint();. Okno aktualizowało się za szybko, a w dodatku migotało. Jak spowolnić aktualizowanie i sprawić by nadal samo to robiło?

          tomek

          Najprościej byłoby dodać przez repaint(); coś takiego Thread.sleep(100); Na ile to jest optymalne i czy da się lepiej to nie wiem

          Mikolaj

          w tej linijce JPanel panel = new MyPanel(); (jest to w kodzie 5 linijek pod UWAGA) działało mi to tylko wtedy gdy zmieniłem MyPanel na JPanel i nie wiem czy tak może być bo dopiero się uczę javy

          Mikolaj

          Dobra już widze miałem literówke w klasie MyPanel

          Patryk

          Mam problem, ponieważ kiedy wykonuje import: import java.swing.JPanel; pokazuje że jest błąd, a jak chce poprawić to chce zrobić nową klase pt. "JPanel"

          Patryk

          Ach, mój błąd miałem literówke w imporcie. Lecz mam te 3 klasy napisane, ale żadnek okienko mi nie wyskakuje.

          fdgfd

          to tu

          fazusia

          superusio jest ten programusik

          Gerlos

          Siema Siema Siema

          KILLER

          jak zrobić linie?

          Kamil

          Jak eclipsa włączyć i pisać program? dajcie kody gotowe do zadania od łysego

          Beata

          Jak sprawić, żeby kółko było narysowane np. czerwoną linią? Jak sprawić, żeby kółko miało wypełnienie np. żółte? g2d.fillOval(10,10,380,380); ? Gdzie ustawić kolor?

          Beata

          Sorry, już mam. Graphics2D g2d = (Graphics2D) g; Color c1 = new Color(36,217,36); g2d.setColor(c1); // prostokat g2d.drawRect(10, 10, 380, 380); Color c2 = new Color(101,101,101); g2d.setColor(c2); g2d.fillRect(10, 10, 380, 380); // kolo g2d.setColor(c1); g2d.drawOval(10, 10, 380, 380); g2d.fillOval(10, 10, 380, 380);

          AKA

          Do bani jest ten poradnik, za dużo niewiadomych(dla początkujących). Poradnik miał być "dla każdego",a niestety nawet w wyjaśnieniach używacie pojęć, które nie zostały wcześniej wyjaśnione, przez co dalej nie wiadomo o co chodzi.

          snt.banzai

          mam pytanie, zapewne głupie ale cóż. Na jakiej zasadzie w klasie MyFrame tworzymy instancję obiektu z klasy MyPanel, skoro obie klasy nie pozostają z sobą w stosunku dziedziczenia (jedna nie rozszerza drugiej)?

          abc

          Ten kurs nie powinien sie nazywać "Javastart" patrząc na to jak tutaj jest tłumaczone... Niestety jeśli ktoś nie miał styczności z javą i chce zacząć to na pewno nie będzie to dobry kurs... Niestety autor kursu podaje tutaj gotowe kody, nie tłumacząc tak naprawde co dane funkcje robią, a chyba o takie coś chodzi w kurse... zawiodłem się

          aleksanderwiel

          Jak używać tej metody repaint(), o której pisałeś?

          Lolo

          Używasz jej poprzez wywołanie repaint() na rzecz obiektu, który przeciąża metodę paintComponent, lub paint.

          aleksanderwiel

          A więc mogę tej metody użyć w ten sposób?: "@Override public void paintComponent(Graphics g) { super.paintComponent(g); repaint(); } " Mógłbym prosić o jakiś kod (raczej jestem wzrokowcem :)), żeby to lepiej zrozumieć?

          Lolo

          Nie, tak nie należy tego używać. Możesz tego tak użyć MyPanel myPanel = new MyPanel(); . . . myPanel.repaint();

          Sławek Ludwiczak

          repaint() należy wywołać wtedy, kiedy chcesz zaktualizować wygląd aplikacji, ale NIGDY nie należy wywoływać tej metody wewnątrz paintComponent(). Jeżeli zrobisz to tak jak napisałeś, to wywołasz nieskończoną pętlę rekurencyjną co jest generalnie bez sensu :) W lekcji z obsługą zdarzeń myszy jest co prawda kilka rzeczy dodatkowych,. ale jest tam pokazane gdzie wywołać repaint() - http://javastart.pl/grafika_awt_swing/obsluga-zdarzen-mysz/ W skrócie: 1. Elementy do wyświetlenia (np. listę kształtów) przechowuj poza metodą paintComponent() 2. Po zaktualizowaniu elementów (poza metodą paintComponent) wywołaj metodę repaint() 3. W tym momencie wykona się metoda paintComponent, która wyświetli elementy w zaktualizowanej formie

          Szkolenie Java Wrocław