I'm working on Java Swing.
I need an application which lets the user scale a drawing.
The drawing is done inside a custom JPanel's paintComponent method.
A drawing is, lets say, a drawRect method call.
There are two ways (let me call them "modes"), in the application, of scaling the drawing:
Simply increment the width and the height of the drawing when its method is called. For example:
graphics.drawRect(x, y, width + addX, height + addY);
where
addX
andaddY
are offsets of the user's mouse drag gesture.Use/Alter an AffineTransform which handles the scaling of the drawing, just before the drawing is drawn. For example:
graphics.transform(at); graphics.drawRect(x, y, width, height);
The two modes of scaling are combined together!
The user drags the mouse on the edges of the drawing to scale the drawing.
The user selects which mode is enabled each time via two buttons: 1 button for the first mode ("Draw scale") and 1 button for the second mode ("AffineTransform scale").
Here is my code so far:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Main {
//The panel which handles drawing:
private static class PaintPanel extends JPanel {
private final double drawX, drawY;
private final AffineTransform at;
private double drawWidth, drawHeight;
public PaintPanel(final double drawX,
final double drawY,
final double drawWidth,
final double drawHeight) {
at = new AffineTransform();
this.drawX = drawX;
this.drawY = drawY;
this.drawWidth = drawWidth;
this.drawHeight = drawHeight;
}
public AffineTransform getAffineTransform() {
return at;
}
public double getDrawX() {
return drawX;
}
public double getDrawY() {
return drawY;
}
public double getDrawWidth() {
return drawWidth;
}
public double getDrawHeight() {
return drawHeight;
}
public double getTotalWidth() {
return getDrawWidth() * getAffineTransform().getScaleX();
}
public double getTotalHeight() {
return getDrawHeight() * getAffineTransform().getScaleY();
}
@Override
public void paintComponent(final Graphics g) {
super.paintComponent(g);
final Graphics2D g2d = (Graphics2D) g.create();
//Red rectangle for clarity.
g.setColor(Color.RED);
g.drawRect((int)getDrawX(), (int)getDrawY(), (int)getTotalWidth()-1, (int)getTotalHeight()-1);
//The drawing!
g2d.transform(getAffineTransform());
g2d.drawRect((int)getDrawX(), (int)getDrawY(), (int)getDrawWidth()-1, (int)getDrawHeight()-1);
g2d.drawOval((int)getDrawX(), (int)getDrawY(), (int)getDrawWidth()-1, (int)getDrawHeight()-1);
g2d.dispose();
}
//The scale(...) method scales the drawing.
//The scale(...) method needs changes.
public void scale(final boolean drawScale,
final double addX,
final double addY) {
if (drawScale) {
drawWidth += (addX / getAffineTransform().getScaleX());
drawHeight += (addY / getAffineTransform().getScaleY());
}
else {
final double tmpAddX = addX / getAffineTransform().getScaleX(),
tmpAddY = addY / getAffineTransform().getScaleY();
final double sx = (getDrawWidth() + tmpAddX) / getDrawWidth(),
sy = (getDrawHeight() + tmpAddY) / getDrawHeight();
getAffineTransform().scale(sx, sy);
final double tmpAddX2 = addX / getAffineTransform().getScaleX(),
tmpAddY2 = addY / getAffineTransform().getScaleY();
final double sx2 = (getDrawWidth() + tmpAddX2) / getDrawWidth(),
sy2 = (getDrawHeight() + tmpAddY2) / getDrawHeight();
getAffineTransform().translate(getDrawX() - getDrawX()*sx2,
getDrawY() - getDrawY()*sy2);
}
repaint();
}
}
private static boolean drawScale;
public static void main(final String[] args) {
final Dimension dim = new Dimension(300, 300);
final int drawOff = 100;
final PaintPanel paintPanel = new PaintPanel(drawOff/2, drawOff/2, dim.width - 2*drawOff, dim.height - 2*drawOff);
paintPanel.setPreferredSize(dim);
final MouseAdapter ma = new MouseAdapter() {
private Point dragLocation;
private boolean dragX, dragY;
//Tells if the width is dragged:
private boolean isDragX(final Point loc) {
return (loc.getX() > paintPanel.getTotalWidth()-10+paintPanel.getDrawX()
&& loc.getX() < paintPanel.getTotalWidth()+10+paintPanel.getDrawX()
&& loc.getY() < paintPanel.getTotalHeight()+paintPanel.getDrawY()+10
&& loc.getY() > paintPanel.getDrawY());
}
//Tells if the height is dragged:
private boolean isDragY(final Point loc) {
return (loc.getY() > paintPanel.getTotalHeight()-10+paintPanel.getDrawY()
&& loc.getY() < paintPanel.getTotalHeight()+10+paintPanel.getDrawY()
&& loc.getX() < paintPanel.getTotalWidth()+paintPanel.getDrawX()+10
&& loc.getX() > paintPanel.getDrawX());
}
//Only used to set the cursor:
@Override
public void mouseMoved(final MouseEvent mevt) {
final Point loc = mevt.getPoint();
if (isDragX(loc) && isDragY(loc))
paintPanel.setCursor(Cursor.getPredefinedCursor(Cursor.SE_RESIZE_CURSOR));
else if (isDragX(loc))
paintPanel.setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
else if (isDragY(loc))
paintPanel.setCursor(Cursor.getPredefinedCursor(Cursor.S_RESIZE_CURSOR));
else if (paintPanel.getCursor() != Cursor.getDefaultCursor())
paintPanel.setCursor(Cursor.getDefaultCursor());
}
//Determines (on mouse click) which sides are dragged:
@Override
public void mousePressed(final MouseEvent mevt) {
final Point loc = mevt.getPoint();
dragX = isDragX(loc);
dragY = isDragY(loc);
dragLocation = loc;
}
//Scales while dragging:
@Override
public void mouseDragged(final MouseEvent mevt) {
final Point loc = mevt.getPoint();
if (dragX && dragY)
paintPanel.scale(drawScale, loc.getX()-dragLocation.getX(), loc.getY()-dragLocation.getY());
else if (dragX)
paintPanel.scale(drawScale, loc.getX()-dragLocation.getX(), 0);
else if (dragY)
paintPanel.scale(drawScale, 0, loc.getY()-dragLocation.getY());
dragLocation = loc;
}
};
paintPanel.addMouseListener(ma);
paintPanel.addMouseMotionListener(ma);
final JButton drawScaleButton = new JButton("Draw scale"),
affineTransformScaleButton = new JButton("AffineTransform scale");
drawScaleButton.addActionListener(e -> {
drawScale = true;
drawScaleButton.setEnabled(false);
affineTransformScaleButton.setEnabled(true);
});
affineTransformScaleButton.addActionListener(e -> {
drawScale = false;
drawScaleButton.setEnabled(true);
affineTransformScaleButton.setEnabled(false);
});
drawScale = false;
drawScaleButton.setEnabled(true);
affineTransformScaleButton.setEnabled(false);
final JPanel buttons = new JPanel(/*FlowLayout*/);
buttons.add(affineTransformScaleButton);
buttons.add(drawScaleButton);
final JPanel contents = new JPanel(new BorderLayout());
contents.add(buttons, BorderLayout.PAGE_START);
contents.add(paintPanel, BorderLayout.CENTER);
final JFrame frame = new JFrame("Scaling shape test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(contents);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
The problem is I cannot combine the two modes properly. I need the AffineTransform object to change accordingly to the user's drag gesture such that when the transform is applied to the drawing, then the drawing scales where the user's mouse is.
The code draws the drawing inside a red rectangle for clarity. The size of the drawing after the scaling modes must be still inside the red rectangle, but it's not and this is the problem.
Here is a screenshot demonstrating the problem:
To recreate the problem you can do the following steps:
- Drag the drawing to enlarge it until it reaches the frame's sides.
- Select "Draw scale".
- Drag the drawing as small as possible (but also visible).
- Select "AffineTransform scale".
- Repeat the steps above once or two more times and the problem should be visible.