Kurs Java Podstawy - rozszerzony

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.

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);
	}
}

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.

<- Poprzednia LekcjaNastępna Lekcja ->

Komentarze

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.

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ę

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)?

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ć?

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

Lolo

Nie, tak nie należy tego używać.
Możesz tego tak użyć

MyPanel myPanel = new MyPanel();
.
.
.
myPanel.repaint();