Obsah
Základní charakteristika našeho snažení:
aktivita, kdy sami chceme vykreslit nějaký grafický obrazec (úsečku, kružnici, atd.) nebo vypsat text
nejedná se o vykreslování jednotlivých komponent, např. zobrazování tlačítek apod.
to zajišťuje Swing automaticky
začíná se od hierarchicky nejvyššího kontejneru (často
JFrame
) a postupuje k nižším
o překreslování se většinou nemusí žádat, stačí jen komponentu změnit
např. setText()
způsobí přepsání textu a i
případnou změnu velikosti komponenty
to je zařízeno tak, že změna vzhledu komponenty způsobí
automaticky vyvolání metody repaint()
z
java.awt.Component
ta zařídí, že požadavek na vykreslení je umístěn do
fronty čekajících a vykreslí se metodou
paintComponent()
, jakmile na něj přijde
řada
navíc, změní-li se pozice nebo velikost komponenty, je
před repaint()
zavolána ještě
revalidate()
je třeba si uvědomit, že ve Swingu
jsou všechna
volání součástí jednoho vlákna včetně volání repaint()
a
včetně reakcí na události
pokud jedna část trvá dlouho, ostatní musejí čekat, až doběhne
aby bylo vykreslování co nejplynulejší (netrhané), používá se implicitně metoda dvojího bufferování (double-buffered)
komponenty se překreslují do bufferu v pozadí a až když je hotov, jednorázově se přepne na obrazovku
existuje jednoduchý recept na vylepšení rychlosti vykreslování – nastavit neprůhledné komponenty (těch je prakticky v programu naprostá většina, ne-li všechny) na skutečně neprůhledné (opaque)
pro to slouží metoda setOpaque(boolean
isOpaque) z
javax.swing.JComponent
standardní nastavení je totiž false
(=
průhledné) což při vykreslování způsobí, že se musí kompletně
vykreslit i komponenty, které jsou pod průhlednou
komponentou
u některých komponent ale nastavení záleží na použitém L&F
je-li komponenta průhledná, nevykresluje se její pozadí
nastavené pomocí setBackground()
naopak okraje komponenty, jsou-li vykreslovány, se vykreslují vždy
jsou založené na primitivní (= minimální možnosti) třídě
java.awt.Graphics
od JKD 1.2 je k dispozici potomek
java.awt.Graphics2D
metody třídy Graphics2D
umožňují snadno:
kreslit čáry libovolné tloušťky
vyplňovat obrysy barvami, přechody, texturami
posouvat, rotovat, zmenšovat, zkosit grafická primitiva i text
pracovat s obrázky atp.
lze kreslit na jakýkoliv objekt zděděný od
javax.swing.JComponent
nelze kreslit na JFrame
pro kreslení je třeba překrýt metodu
protected void paintComponent(Graphics g)
to znamená, že musíme vytvořit vlastní třídu zděděnou od
JComponent
(abstraktní třída)
prakticky většinou dědíme od JPanel
parametrem je grafický objekt (grafický kontext), který se získá „automaticky“
o jeho vytvoření se nemusíme starat, protože tuto metodu nikdy přímo nevoláme
vyvolává se metodou repaint()
, která
dokáže vynutit okamžik kreslení
počátek souřadnic [0, 0] (typu int
= počet
pixelů) je v levém horním rohu
souřadnice x
(šířka,
width) roste doprava
souřadnice y
(výška,
height) roste dolů
max. hodnota x
a y
(velikost prostoru, kam lze kreslit = „plátna“) je dána
velikostí komponenty
x = getWidth()-1
, y =
getHeight()-1
odečtení 1 je nutné, protože souřadnice jsou od 0
protože se u komponenty mohou vyskytnout okraje, je vhodné
použít (Insets
je vycpavka mezi vnitřkem komonenty a
okrajem)
Insets insets = getInsets(); int minX = insets.left; int minY = insets.top; int maxX = getWidth() - insets.left - insets.right-1; int maxY = getHeight() - insets.top - insets.bottom-1;
objekt třídy Graphics
z historických důvodů
předávaný do paintComponent()
téměř nikdy nepoužíváme
přímo
přetypovává se na Graphics2D
Graphics2D g2 = (Graphics2D) g;
tato třída má mnohem větší možnosti
nese si s sebou následující defaultní nastavení renderovacích atributů
Paint
– barva popředí komponenty
(černá)
Font
– font popisu komponenty
(Dialog
, PLAIN
, 12 pt)
Stroke
– typ čáry – tloušťka 1, nepřerušovaná
čára
Transform
– žádná transformace
podrobnosti a ukázky viz v
tutorial\2d\overview\rendering.html
objekt třídy Graphics
předávaný do
paintComponent()
je pak také předáván do automaticky
volané metody paintBorder()
pokud tedy změníme nějaké nastavení, promítne se i do případného ohraničení
pokud to nechceme, používáme:
Graphics2D g2 = (Graphics2D) g.create(); // kopie // nějaké nastavení nad kopií g2.translate(x, y); ... g2.dispose(); // zrušení kopie
občas je vidět jako první příkaz metody
paintComponent()
volání
super.paintComponent(g);
tím se zajistí, že se nejdříve vykreslí vše, co bylo požadováno v předkovi
pokud na kreslící komponentu jen jednorázově kreslíme, pak je tento příkaz zbytečný
má-li komponenta reagovat na repaint()
, je
tento příkaz vhodný (viz dále)
Ukázka vytvoření kreslící komponenty a starého (z
Graphics
) a nového (z Graphics2D
) způsobu
kreslení
import javax.swing.*; import java.awt.*; import java.awt.geom.*; class MojeKresliciKomponenta extends JPanel { MojeKresliciKomponenta() { setOpaque(true); } public Insets getInsets() { return new Insets(10, 10, 10, 10); } protected void paintComponent(Graphics g) { Insets insets = getInsets(); int minX = insets.left; int minY = insets.top; int maxX = getWidth() - insets.left - insets.right - 1; int maxY = getHeight() - insets.top - insets.bottom - 1; // stary zpusob z Graphics g.setColor(Color.yellow); g.fillRect(minX, minY, maxX, maxY); Graphics2D g2 = (Graphics2D) g; // novy zpusob z Graphics2D g2.setPaint(Color.black); g2.draw(new Line2D.Double(0, 0, maxX, maxY)); } } public class GrafikaZaklad { public static void main(String[] args) { JFrame oknoF = new JFrame("GrafikaZaklad"); oknoF.add(new MojeKresliciKomponenta()); oknoF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); oknoF.setSize(250, 250); oknoF.setVisible(true); } }
z java.awt.geom
vykreslují se obrysově přednastavenou barvou pomocí
g2.draw(Shape s)
nebo vyplněně (jsou-li uzavřená) pomocí
g2.fill(Shape s)
od Shape
jsou zděděny např.
přímka – Line2D
obdélníkové tvary – Rectangle2D
,
RoundRectangle2D
, Ellipse2D
,
Arc2D
křivky – CubicCurve2D
,
QuadCurve2D
obecný tvar – GeneralPath
tutorial\2d\overview\shapes.html
toto jsou ale abstraktní třídy, které mají vždy dvě vnitřní
třídy .Double
a .Float
např. Line2D.Double
a
Line2D.Float
ty používají souřadnice x
a
y
buď typu double
nebo
float
prakticky je vykreslení přímky např.
g2.draw(new Line2D.Double(0.0, 0.0, 10.0,
10.0));
Přestože parametry Line2D.Double()
mají být typu
double
, je možné používat typ int
, protože
dojde k automatické konverzi na double
.
g2.draw(new Line2D.Double(0, 0, 10, 10));
vyplnění obdélníka
g2.fill(new Rectangle2D.Double(0.0, 0.0, 10.0,
10.0));
objekty primitiv nemusíme pokaždé vytvářet znovu (časově náročné), mají dostatečné množství nastavovacích metod, např.
setLine(double X1, double Y1, double X2, double Y2) setLine(Line2D jinaLine) setLine(Point2D p1, Point2D p2)
třída Graphics
(od které je zděděna
Graphics2D
) dává pro vykreslování primitiv k dispozici
použitelné metody typu drawLine()
,
drawRect()
, fillRect()
, ...
většinou to ale není výhodné, protože fill()
a draw()
z Graphics2D
dokáží využít
polymorfismu
dále je třeba si uvědomit, že „starým způsobem“ vykreslená primitiva byly jen „pixely na obrazovce“
primitiva z java.awt.geom
jsou kromě toho
navíc objekty v paměti s mnoha užitečnými metodami,
např.:
boolean intersectsLine(Line2D l)
tj. test, zda úsečka kříží jinou úsečku
nebo int outcode(double x, double
y)
která pro obdélník určí, kde testovaný bod leží – uvnitř nebo vně (vlevo, vpravo, nahoře, dole)
vytváříme-li aplikaci, která pouze jednorázově nevykresluje, jsou tyto metody velkou pomocí
jedná se o barvu obrysu či výplně a čáru obrysu
jsou to parametry typu rozhraní Paint
(barva) a
Stroke
(čára) nastavované metodami třídy
Graphics2D
void setPaint(Paint paint)
a void
setStroke(Stroke s)
toto rozhraní implementují třídy:
Color
– jednolitá barva
GradientPaint
– barevný přechod
TexturePaint
– textura
vlastnost se nastavuje pomocí g2.setPaint(Paint
p);
Color
má mnoho možností (viz dokumentace)
typické použití je Color(int r, int g, int b)
pro vytvoření libovolné neprůhledné
true-color barvy
nebo použití jedné ze 13 již přednastavených „základních“ konstantních barev
black
, blue
,
cyan
, darkGray
, gray
,
green
, lightGray
,
magenta
, orange
,
pink
, red
, white
,
yellow
použití je Color.red
a lze použít i velká
písmena Color.RED
dále lze používat i průhledné barvy (transparency) atd.
GradientPaint
má typický způsob použití:
GradientPaint(float x1, float y1, Color color1,
float x2, float y2, Color color2)
např. postupný vodorovný přechod od červené ke žluté:
GradientPaint redToYellow =
new GradientPaint(x, y, Color.red,
x + sirka, y, Color.yellow);
implementuje třída BasicStroke
vlastnost se nastavuje pomocí g2.setStroke(Stroke
s);
lze nastavit zejména tloušťku čáry (width)
typické použití: new
BasicStroke(2.0f);
parametr je typu float
, takže pouze výraz
(2.0)
způsobí chybu při překladu
pokud je čára přerušovaná, lze ji libovolně nakonfigurovat použitím třídy:
BasicStroke(float width, int cap, int join, float miterlimit, float[] dash, float dash_phase)
cap
tvar zakončení
CAP_BUTT
– pravoúhlé
bez přesahu
CAP_ROUND
–
zakulacené s přesahem
CAP_SQUARE
–
pravoúhlé s přesahem
join
– typ napojení dvou čar
JOIN_BEVEL
–
useknuté
JOIN_MITER
– do
špičky
JOIN_ROUND
– do
kulata
miterlimit
– maximální délka špičatého
napojení
dash
– vzor přerušované čáry
přerušovaná: float[] dash = {10.0f};
čerchovaná: float[] dash = {10.0f, 3.0f, 2.0f,
3.0f};
dash_phase
– offset v počtu bodů, kdy má začít
přerušovaná čára
float[] dash = {10.0f}; BasicStroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, dash, 0.0f);
Používáme-li základní primitiva a tenké čáry, je vhodné zapnout antialiasing, který často významně vylepší vzhled obrázku
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Ukázka grafických primitiv
import java.awt.*; import java.awt.geom.*; import javax.swing.*; public class Primitiva2D extends JPanel { public void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Dimension d = getSize(); int gridWidth = d.width / 6; int gridHeight = d.height / 2; g2.setPaint(Color.white); g2.fill(new Rectangle2D.Double(0, 0, d.width, d.height)); Font font = new Font("SansSerif", Font.PLAIN, 8); FontMetrics fontMetrics = g2.getFontMetrics(font); g2.setPaint(Color.lightGray); g2.draw3DRect(0, 0, d.width - 1, d.height - 1, true); g2.draw3DRect(3, 3, d.width - 7, d.height - 7, false); g2.setPaint(Color.black); int x = 5; int y = 8; int rectWidth = gridWidth - 2 * x; int stringY = gridHeight - 3 - fontMetrics.getDescent(); int rectHeight = stringY - fontMetrics.getMaxAscent() - y - 5; // Line2D g2.draw(new Line2D.Double(x, y+rectHeight-1, x + rectWidth, y)); g2.drawString("Line2D", x, stringY); x += gridWidth; // Rectangle2D g2.setStroke(new BasicStroke(2.0f)); g2.draw(new Rectangle2D.Double(x, y, rectWidth, rectHeight)); g2.drawString("Rectangle2D", x, stringY); x += gridWidth; // RoundRectangle2D // float vzor[] = {10.0f, 3.0f, 2.0f, 3.0f}; float vzor[] = {10.0f}; BasicStroke dashed = new BasicStroke(1.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER, 10.0f, vzor, 0.0f); g2.setStroke(dashed); g2.draw(new RoundRectangle2D.Double(x, y, rectWidth, rectHeight, 10, 10)); g2.drawString("RoundRectangle2D", x, stringY); x += gridWidth; // Arc2D g2.setStroke(new BasicStroke(8.0f, BasicStroke.CAP_BUTT, BasicStroke.JOIN_MITER)); g2.draw(new Arc2D.Double(x, y, rectWidth, rectHeight, 90, 135, Arc2D.OPEN)); g2.drawString("Arc2D", x, stringY); x += gridWidth; // Ellipse2D g2.setStroke(new BasicStroke(2.0f)); g2.draw(new Ellipse2D.Double(x, y, rectWidth, rectHeight)); g2.drawString("Ellipse2D", x, stringY); x += gridWidth; // GeneralPath uzavrena - polygon int x1Points[] = {x, x+rectWidth, x, x+rectWidth}; int y1Points[] = {y, y+rectHeight, y+rectHeight, y}; GeneralPath polygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, x1Points.length); polygon.moveTo(x1Points[0], y1Points[0]); for (int i = 1; i < x1Points.length; i++ ) { polygon.lineTo(x1Points[i], y1Points[i]); } polygon.closePath(); g2.draw(polygon); g2.drawString("GeneralPath", x, stringY); // druhy radek x = 5; y += gridHeight; stringY += gridHeight; // GeneralPath otevrena - polyline int x2Points[] = {x, x+rectWidth, x, x+rectWidth}; int y2Points[] = {y, y+rectHeight, y+rectHeight, y}; GeneralPath polyline = new GeneralPath(GeneralPath.WIND_EVEN_ODD, x2Points.length); polyline.moveTo (x2Points[0], y2Points[0]); for (int i = 1; i < x2Points.length; i++ ) { polyline.lineTo(x2Points[i], y2Points[i]); } g2.draw(polyline); g2.drawString("GeneralPath otevřená", x, stringY); x += gridWidth; // Rectangle2D g2.setPaint(Color.red); g2.fill(new Rectangle2D.Double(x, y, rectWidth, rectHeight)); g2.setPaint(Color.black); g2.drawString("Rectangle2D", x, stringY); x += gridWidth; // RoundRectangle2D GradientPaint redToYellow = new GradientPaint(x, y, Color.red, x + rectWidth, y, Color.yellow); g2.setPaint(redToYellow); g2.fill(new RoundRectangle2D.Double(x, y, rectWidth, rectHeight, 10, 10)); g2.setPaint(Color.black); g2.drawString("RoundRectangle2D", x, stringY); x += gridWidth; // Arc2D g2.setPaint(Color.red); g2.fill(new Arc2D.Double(x, y, rectWidth, rectHeight, 90, 135, Arc2D.OPEN)); g2.setPaint(Color.black); g2.drawString("Arc2D", x, stringY); x += gridWidth; // Ellipse2D redToYellow = new GradientPaint(x, y, Color.red, x+rectWidth, y,Color.yellow); g2.setPaint(redToYellow); g2.fill (new Ellipse2D.Double(x, y, rectWidth, rectHeight)); g2.setPaint(Color.black); g2.drawString("Ellipse2D", x, stringY); x += gridWidth; // GeneralPath int x3Points[] = {x, x+rectWidth, x, x+rectWidth}; int y3Points[] = {y, y+rectHeight, y+rectHeight, y}; GeneralPath filledPolygon = new GeneralPath(GeneralPath.WIND_EVEN_ODD, x3Points.length); filledPolygon.moveTo(x3Points[0], y3Points[0]); for (int i = 1; i < x3Points.length; i++ ) { filledPolygon.lineTo(x3Points[i], y3Points[i]); } filledPolygon.closePath(); g2.setPaint(Color.red); g2.fill(filledPolygon); g2.setPaint(Color.black); g2.draw(filledPolygon); g2.drawString("GeneralPath", x, stringY); } public static void main(String s[]) { JFrame f = new JFrame("Primitiva2D"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.getContentPane().add(new Primitiva2D()); f.setSize(750, 300); f.show(); } }
pokud chceme tvar vyplnit a navíc orámovat, musíme použít
nejdříve fill()
a pak draw()
existují dvě
QuadCurve2D
– křivka určená dvěma koncovými
body a jedním řídícím bodem
má jen jedno zakřivení
tutorial\2d\display\Quad.html
CubicCurve2D
– Beziérova křivka určená dvěma
koncovými body a dvěma řídícími body
má dvě zakřivení
tutorial\2d\display\Cubic.html
může být uzavřená (polygon) nebo otevřená (polyline)
typicky se vytváří tak, že se nejdříve definují pole souřadnic
x
a y
pak se vytvoří objekt třídy GeneralPath
, kterému
se určí způsob vyhodnocení, zda je nějaký bod uvnitř
(winding rule) – většinou se použije
WIND_EVEN_ODD
druhý parametr je počet bodů, ze kterých bude křivka skládat
třetím krokem je nastavení výchozího bodu pomocí
moveTo()
pak následuje spojení bodů pomocí
lineTo()
– spojení úsečkou
quadTo()
– spojení křivkou
poslední krok může být uzavření křivky pomocí
closePath()
Graphics2D
poskytuje možnosti transformací
translate
– posun souřadnic
rotate
– natočení
scale
– změna měřítka
shear
– zkosení
tyto transformace lze slučovat
lze je provést nad celým kreslícím prostorem jednotlivě, pomocí
metod z Graphics2D
translate()
rotate()
scale()
shear()
public class Transformace extends JPanel { protected void paintComponent(Graphics g) { Graphics2D g2 = (Graphics2D) g; Dimension d = getSize(); int sirka = d.width / 3; int vyska = d.height / 3; g2.setStroke(new BasicStroke(10.0f)); // g2.translate(sirka, vyska); // g2.rotate(Math.toRadians(45)); g2.rotate(Math.toRadians(45), 1.5 * sirka, 1.5 * vyska); // g2.scale(0.5, 0.5); // g2.shear(0.5, 0.0); g2.setPaint(Color.yellow); g2.draw(new Rectangle2D.Double(sirka, vyska, sirka, vyska)); g2.setPaint(Color.black); g2.draw(new Line2D.Double(sirka, 2 * vyska, 2 * sirka, vyska)); }
druhou možností je připravit si objekt třídy
AffineTransform
a pak pomocí
g2.transform(AffineTransform Tx)
tuto transformaci provést najednou
jak transformace správně řadit za sebe pro dosažení kýženého výsledku nás učí počítačová grafika a je to netriviální úloha
potřebujeme-li překreslit grafiku, aniž bychom měnili pozici
a/nebo velikost základní kreslící komponenty musíme při reakci na
událost volat metodu repaint()
z kreslící
komponenty
import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import javax.swing.*; class Kresleni extends JPanel { int pocet = 1; protected void paintComponent(Graphics g) { // super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; Dimension d = getSize(); int y = d.height; int krok = d.height / 20; g2.setStroke(new BasicStroke(10.0f)); g2.setPaint(Color.yellow); g2.draw(new Line2D.Double(d.width / 2, y, d.width / 2, y - (krok * pocet))); } } public class Klikani { Kresleni kresleni; Klikani() { JFrame oknoF = new JFrame("Klikani"); kresleni = new Kresleni(); JPanel jPN = new JPanel(); JButton tBT = new JButton("+1"); tBT.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { kresleni.pocet++; kresleni.repaint(); } }); jPN.add(tBT); oknoF.add(jPN, BorderLayout.NORTH); oknoF.add(kresleni, BorderLayout.CENTER); oknoF.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); oknoF.setSize(250, 250); oknoF.setVisible(true); } public static void main(String[] args) { new Klikani(); } }
občas potřebujeme, aby grafické objekty reagovaly na události od myši
grafická primitiva obsahují metodu contains()
,
která zjistí, zda jsme uvnitř objektu
pak stačí jen obsloužit události od
MouseListener
(kliknutí) a/nebo
MouseMotionListener
(pohyb)
import java.awt.*; import java.awt.event.*; import java.awt.geom.*; import javax.swing.*; ; class Kresleni extends JPanel implements MouseListener, MouseMotionListener { int pocet; Color barva; Rectangle2D cara; Kresleni() { setOpaque(true); pocet = 1; barva = Color.yellow; cara = new Rectangle2D.Double(); this.addMouseListener(this); this.addMouseMotionListener(this); } protected void paintComponent(Graphics g) { super.paintComponent(g); Graphics2D g2 = (Graphics2D) g; Dimension d = getSize(); int y = d.height; int krok = d.height / 20; g2.setPaint(barva); cara.setRect(d.width / 2, y - (krok * pocet), 10, (krok * pocet)); g2.fill(cara); } public void mouseClicked(MouseEvent e) { if (cara.contains(e.getX(), e.getY())) { if (barva.equals(Color.yellow)){ barva = Color.red; } else { barva = Color.yellow; } } repaint(); } public void mouseEntered(MouseEvent e) { } public void mouseExited(MouseEvent e) { } public void mousePressed(MouseEvent e) { } public void mouseReleased(MouseEvent e) { } public void mouseDragged(MouseEvent e) { } public void mouseMoved(MouseEvent e) { if (cara.contains(e.getX(), e.getY())) { barva = Color.green; } else { barva = Color.blue; } repaint(); } }