Malen in Swing Teil 2: ein einfaches Malprogramm
Inhaltsverzeichnis
Der erste Teil dieses Tutorials erklärt den grundlegenden Malmechanismus: Malen in Swing Teil 1: der grundlegende Mechanismus.
Das folgende weiterführende Beispielprogramm erlaubt das Malen von farbigen Linien verschiedener Stärke. Das Malen geschieht freihändig durch Ziehen mit der Maus. Durch dieses Beispiel lernen wir:
- wie mehrfache repaint Anträge zusammengefasst werden
- wie wir den Bereich, der dargestellt wird, einschränken können
Mehrfache repaint Anträge zusammenfassen
Jede gemalte Linie setzt sich zusammen aus einer Folge von vielen Kreisen (Graphics#fillOval), die jeweils durch Interpolation zwischen zwei Mauspunkten festgelegt werden. Dabei wird repaint(x,y,brushSize,brushSize) mehrfach in einer Schleife aufgerufen:
@Override
public void mouseDragged(final MouseEvent e) {
double xDelta = e.getX() - lastPoint.getX();
double yDelta = e.getY() - lastPoint.getY();
double delta = Math.max(Math.abs(xDelta), Math.abs(yDelta));
double xIncrement = xDelta / delta;
double yIncrement = yDelta / delta;
double xStart = lastPoint.getX();
double yStart = lastPoint.getY();
for (int i = 0; i < delta; i++) {
Point interpolated = new Point((int) xStart, (int) yStart);
draw(interpolated);
xStart += xIncrement;
yStart += yIncrement;
}
draw(e.getPoint());
lastPoint = e.getPoint();
}
private void draw(final Point start) {
int brushSize = drawingPanel.getBrushSize();
int x = start.x - (brushSize / 2) + 1;
int y = start.y - (brushSize / 2) + 1;
drawingPanel.getG2d().fillOval(x, y,brushSize, brushSize);
drawingPanel.repaint(x, y,brushSize, brushSize); //<<---------------------
}
Erfolgen mehrfache repaint() Aufrufe auf einer Komponente bevor der erste Antrag verarbeitet wird, kann der Swing Repaintmanager die mehrfachen Anträge zu einem einzigen Aufruf zusammenfassen. Genauer gesagt: der RepaintManager wird eine Serie von Malanträgen dann zusammenfassen, wenn der "Anführer" der Serie schon mit invokeLater verschickt wurde, aber noch nicht auf dem EDT verarbeitet wurde (der Vorgang bleibt also "threadsafe"). Wenn mehrfache Anträge zusammengefasst werden, ist das resultierende Cliprechteck gleich der Vereinigung der Rechtecke, die in den zusammengefassten Anträgen enthalten sind.
BEMERKUNG:
Diese Zeile:
drawingPanel.getG2d().fillOval(x, y,brushSize, brushSize);
holt mit getG2d() nicht das Graphics Objekt der Komponente, sondern das Graphics Objekt des Bildes. In paintComponent wird dann mit "drawImage" das Bild auf das Graphics Objekt der Komponente übertragen, wie der folgende Abschnitt zeigt. Daher auch der Name des Hauptprogramms "DrawOnImage". Man spricht in diesem Fall auch von der "passiven Maltechnik".
Den Bereich, der dargestellt wird, einschränken
Während Swing versucht, den Prozess der Darstellung der Komponenten so leistungsfähig wie möglich zu machen, kann die paintComponent() Implementierung einer Komponente selbst eine bedeutende Auswirkung auf die gesamte Leistung haben. Wir können diesen Prozess beeinflussen, indem wir die Clipinformationen verwenden, um den Bereich, der dargestellt wird, einzuschränken.
Wenn deine Komponente einfach ist -- zum Beispiel, wenn es eine Drucktaste ist -- dann ist sie die Mühe nicht wert, die Darstellung zu bearbeiten, um nur den Teil zu malen, der das Cliprechteck überschneidet; es ist vorzuziehend, die gesamte Komponente zu malen und die Graphics passend einschränken zu lassen. Wenn du jedoch eine Komponente mit komplizierter Ausgabe erzeugt hast, wie eine Textkomponente, dann ist es notwendig, dass dein Code die Clipinformationen gebraucht, um den Umfang der Darstellung einzuschränken. Die Methode Graphics#getClipBounds gibt uns Zugriff auf das Cliprechteck:
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
// initialises the image with the first paint
// or checks the image size with the current panelsize
if (image == null || image.getWidth(this) < getSize().width || image.getHeight(this) < getSize().height) {
resetImage();
}
Rectangle r = g.getClipBounds(); //<<----------------------
g.drawImage(image, r.x, r.y, r.width + r.x, r.height + r.y, r.x, r.y, r.width + r.x, r.height + r.y, null);
}
Der drawImage(..)-Aufruf, mit den angegebenen Parametern, zeichnet nur den Teil des Bildes neu, der sich mit dem Cliprechteck überschneidet. Man spricht in diesem Zusammenhang auch von der "intelligenten Maltechnik" (smart painting).
Gesamter Quellcode
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class DrawOnImage {
final private DrawingPanel drawingPanel;
final private JPanel buttonPanel;
final private JButton clearButton, upSize, downSize;
private final ActionListener actionHandler;
public DrawOnImage() {
JFrame frame = new JFrame("DrawOnImage");
drawingPanel = new DrawingPanel();
actionHandler = new ActionListener() {
@Override
public void actionPerformed(final ActionEvent e) {
String s = e.getActionCommand();
if (s.equals("Paint")) {
JButton button = (JButton) e.getSource();
drawingPanel.setPaintColor(button.getBackground());
} else if (s.equals("Clear")) {
drawingPanel.clearPaint();
} else if (s.equals("+")) {
drawingPanel.increaseBrushSize();
} else {
drawingPanel.decreaseBrushSize();
}
}
};
buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(2, 0, 2, 2));
addButton(Color.BLACK);
addButton(Color.BLUE);
addButton(Color.GREEN);
upSize = addButton(null);
upSize.setText("+");
addButton(Color.RED);
addButton(Color.ORANGE);
clearButton = addButton(null);
clearButton.setText("Clear");
downSize = addButton(null);
downSize.setText("-");
frame.getContentPane().add(new JScrollPane(drawingPanel));
frame.getContentPane().add(buttonPanel, BorderLayout.SOUTH);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
private JButton addButton(final Color color) {
JButton button = new JButton();
button.setBackground(new Color(230, 240, 250));
button.setBorder(BorderFactory.createEtchedBorder());
if (color != null) {
button.setForeground(Color.WHITE);
button.setBackground(color);
}
button.setText("Paint");
buttonPanel.add(button);
button.addActionListener(actionHandler);
return (button);
}
public static void main(final String[] args) {
Runnable gui = new Runnable() {
@Override
public void run() {
DrawOnImage drawOnImage = new DrawOnImage();
}
};
//GUI must start on EventDispatchThread:
SwingUtilities.invokeLater(gui);
}
}
import java.awt.Point;
import java.awt.event.MouseEvent;
import javax.swing.event.MouseInputAdapter;
class MouseHandler extends MouseInputAdapter {
private final DrawingPanel drawingPanel;
private Point lastPoint;
MouseHandler(final DrawingPanel drawingPanel) {
this.drawingPanel = drawingPanel;
}
@Override
public void mousePressed(final MouseEvent e) {
lastPoint = e.getPoint();
draw(lastPoint);
}
@Override
public void mouseDragged(final MouseEvent e) {
double xDelta = e.getX() - lastPoint.getX();
double yDelta = e.getY() - lastPoint.getY();
double delta = Math.max(Math.abs(xDelta), Math.abs(yDelta));
double xIncrement = xDelta / delta;
double yIncrement = yDelta / delta;
double xStart = lastPoint.getX();
double yStart = lastPoint.getY();
for (int i = 0; i < delta; i++) {
Point interpolated = new Point((int) xStart, (int) yStart);
draw(interpolated);
xStart += xIncrement;
yStart += yIncrement;
}
draw(e.getPoint());
lastPoint = e.getPoint();
}
private void draw(final Point start) {
int brushSize = drawingPanel.getBrushSize();
int x = start.x - (brushSize / 2) + 1;
int y = start.y - (brushSize / 2) + 1;
drawingPanel.getG2d().fillOval(x, y, brushSize, brushSize);
drawingPanel.repaint(x, y, brushSize, brushSize);
}
}
import java.awt.*;
import javax.swing.*;
public class DrawingPanel extends JComponent {
private Image image;
private Graphics2D g2d;
private int brushSize = 5;
public DrawingPanel() {
super();
setPreferredSize(new Dimension(300, 300));
MouseHandler mh = new MouseHandler(this);
addMouseListener(mh);
addMouseMotionListener(mh);
}
@Override
public Dimension getPreferredSize() {
Dimension size = super.getPreferredSize();
if (image != null) {
size.width = image.getWidth(this);
size.height = image.getHeight(this);
}
return size;
}
public void setPaintColor(final Color color) {
g2d.setColor(color);
}
public void clearPaint() {
g2d.setBackground(Color.WHITE);
g2d.clearRect(0, 0, getWidth(), getHeight());
repaint();
g2d.setColor(Color.BLACK);
}
public void increaseBrushSize() {
brushSize += 2;
}
public void decreaseBrushSize() {
brushSize -= 2;
if (brushSize <= 0) {
brushSize = 1;
}
}
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
// initialises the image with the first paint
// or checks the image size with the current panelsize
if (image == null || image.getWidth(this) < getSize().width
|| image.getHeight(this) < getSize().height) {
resetImage();
}
Graphics2D g2 = (Graphics2D) g;
Rectangle r = g.getClipBounds();
g2.drawImage(image, r.x, r.y, r.width + r.x, r.height + r.y,
r.x, r.y, r.width + r.x, r.height + r.y, this);
}
private void resetImage() {
Image saveImage = image;
Graphics2D saveG2d = g2d;
image = createImage(getWidth(), getHeight());
g2d = (Graphics2D) image.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setBackground(Color.WHITE);
g2d.clearRect(0, 0, getWidth(), getHeight());
g2d.setColor(Color.BLACK);
if (saveG2d != null) {
g2d.setColor(saveG2d.getColor());
g2d.drawImage(saveImage, 0, 0, this);
saveG2d.dispose();
}
}
public Graphics2D getG2d() {
return g2d;
}
public int getBrushSize() {
return brushSize;
}
}