Re: Draw a rectangle keeping aspect ratio of viewport
On Sun, 25 May 2008 15:52:32 -0700, RichT <someone@somewhere.org> wrote:=
[...]
Yes thank you, you fixed my code that was never going to work properly=
=
in a lifetime, why is it that when someone points out the obvious it =
becomes obvious?
Because the only reason it's "obvious" is that it's been pointed out to =
=
you. :)
I must have looked at this code 20 times if not a hundred and failed t=
o =
spot the problem, but when you highlighted the problem it was blatantl=
y =
obvious and jumping out at me saying here I am ner ner nah ner ner :)
That's just how the brain works. If you are the person who created =
something, your brain does a lot of filtering when you look at it (even =
=
more filtering than it normally is doing, which is already a lot). In =
many situations, this is beneficial -- it reduces the amount of "noise" =
=
your brain has to process. But for this sort of thing, it means your =
brain is basically lying to you, making you see what you _thought_ you =
created, and not what you actually _did_ create.
It's a common problem, and it's one of the best reasons for posting code=
=
when you've run into a brick wall. Another set of eyes is very often =
helpful (it's also why authors don't edit their own manuscripts :) ).
Don't worry about it...we all have the exact same problem.
For what it's worth, I find it helpful to have another person look at my=
=
code when I run into something like this. But often there's not another=
=
person around to ask, or the code is more complicated than what the pers=
on =
has time to review. This is where a debugger comes in.
In a lot of ways, a code debugger is like a second set of eyes. It =
"reads" the code to you in completely unambiguous, flawless (we hope :) =
) =
terms. The most common scenario for me when I'm using a debugger is to =
=
step through code until I find that the code causes a result other than =
=
what I expected. This is sort of like the debugger reading the code and=
=
telling me what it _really_ does, which I can compare to what I _thought=
_ =
it does. Where the two don't match, then I've found the problem. :)
[...]
I am still unsure how the best way to track the different coordinate =
systems, AffineTransform seems useful, but I will admit I never really=
=
understood matrix stuff at school and how it relates to coordinate =
translations? that said I use an AffineTransform to scale the image to=
=
the canvas.
AffineTransform is what I'm describing, yes. It's certainly helpful to =
=
know how matrices work with respect to transforming between coordinate =
spaces, but to some extent if you simply look at the docs for =
AffineTransform and see what methods it offers, that can help you =
understand what sorts of things a transform can do for you.
That said, if the only "world coordinate" object you're displaying in =
"screen coordinates" is the image itself, using an AffineTransform might=
=
be overkill.
I did some playing around with a test project, writing a simple JCompone=
nt =
that displays a given image at a given scale. I combined this with a =
JScrollPane so that I could learn more about how the JScrollPane works. =
=
One important thing I found is that, as I'd hoped, the JScrollPane allow=
s =
for scrolling without the contained component having to know _anything_ =
=
about it.
In particular, all mouse input is translated according to the scrolling,=
=
so that when dealing with mouse input you can ignore that your component=
=
is in a JScrollPane.
So if the only other thing your component is doing is scaling, all you =
really need to do is multiple or divide by the scale as appropriate to =
convert back and forth between "world" and "screen" coordinates. You ca=
n =
use an AffineTransform, and in some places doing so makes the code a bit=
=
more concise. But it also complicates the code conceptually, and becaus=
e =
of that could be considered overkill for the problem at hand.
I'm including that test code at the end of this message, in case it's =
useful to you.
[...]
There is still a single problem with the code I posted, if you select =
=
the x3 option it does not centre the rectangle within he scrollbar =
viewport, the rectangle I am drawing is supposed to be representative =
to =
an area of the image I select with the selection rectangle.
I'm not really clear on the above problem description. I ran the code, =
=
and I saw a rectangle that isn't centered in any scenario, not just when=
=
zoomed to 3x. As I mentioned before, a good problem description will be=
=
very clear about how to use the program to cause the problem to happen, =
=
what happens when you do that, and what you expect to happen instead. =
Without that information, even with a working code sample it can be hard=
=
to know exactly what's being asked about.
I do have some comments regarding the code you posted:
* I have a suspicion that the "image width/height larger/smaller th=
an =
viewport" stuff is superfluous. It's hard to know for sure without a =
clear description of what the code is supposed to do, but it seems to me=
=
(especially based on my own tests with JScrollPane) that the custom =
component should mostly be indifferent to whatever's going on with the =
JScrollPane.
* You have a lot of code in each menu action handler that I think =
belongs elsewhere. In particular, there's a lot of management of the =
custom component that IMHO belongs in the component itself, based on =
simple property changes to the component. Hopefully the code I'm =
including illustrates what I mean.
* In your 3x action handler, you queue some kind of centering logic=
=
for later execution. This seems odd...why not just execute that after =
revalidating the container (happens implicitly when you revalidate your =
=
custom component).
* No doubt people will give me grief for using Hungarian, but IMHO =
=
your practice of using single-letter variable names is worse. I'd =
recommend strongly against that, especially for non-local variables. Wh=
en =
a variable is named simply "p" (for example), it makes it very hard to d=
o =
a simple text search to try to figure out exactly where it's used.
Anyway, that's all I have for now. :)
Pete
Here's the code I mentioned. Some notes:
* The code is in three sections: the custom component, the main =
application, and a utility class. I realize this is less convenient, =
since you have to make three different .java files to use it, but I'm =
hoping that the more clear division of functionality compensates for the=
=
inconvenience.
* I wrote the custom component two different ways: explicitly deali=
ng =
with scaling, and using an AffineTransform. Java doesn't have a =
pre-processor that I could use to optionally compile one or the other, s=
o =
I simply commented out the lines having to do with the explicit version.=
=
There aren't really that many different places where the code varies =
according to explicit vs. transform, so hopefully it's not too confusing=
=
or labor-intensive to see the difference. Obviously to enable the =
explicit version, you'd have to uncomment that code, plus comment the co=
de =
specific to the transform approach. (Generally speaking, in this =
situations, the transform code is obvious because of its use of an =
AffineTransform instance, or because it's a near-duplicate of the explic=
it =
version, but with slightly different parameters, calculations, etc.)
I could in fact use some sort of flag to conditionally execute one or th=
e =
other, or I could just post two different versions of the class, so if m=
y =
current method of differentiating the two is too complicated, let me kno=
w =
and I can try to make it clearer where the division in the code is.
* I kind of "bailed" on the component size stuff. There are probabl=
y =
layout-friendlier ways to manage the size of the component, but because =
my =
intent was just to put this component in a JScrollPane, I just set all t=
he =
various "sizes" for the control to the desired size. This is not =
necessarily considered a "good Java habit". :)
* Note that the client of my custom component doesn't know anything=
=
about the consequences of changing its properties. It just changes them=
, =
and then the custom component itself manages the consequences as =
appropriate (changing the size and repainting, in this case). This keep=
s =
the code simpler and less repetitive.
* Finally, very minor note: your code sample had an explicit file =
path, while mine prompts the user. Inasmuch as a code sample should be =
=
_complete_ and inasmuch as you can't really post an image file with your=
=
sample, IMHO it makes more sense for the sample to not rely on a specifi=
c =
data file for input (the image file in this case). If there's some =
specific characteristic of the input file (image dimensions, bit depth, =
=
etc. for example), describe those but then allow for a way for the reade=
r =
to easily provide their own input.
I know that's a lot of notes, and the code may seem to be a bit long. B=
ut =
in reality, I think it's reasonably simple...something like half the =
custom component exists to manage things other than the specific =
coordinate-mapping issues, so hopefully it does a good job of =
demonstrating how uncomplicated that part of the problem really needs to=
=
be. :)
Anyway, here are the classes:
The custom JComponent:
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.event.*;
public class ImageSelectComponent extends JComponent implements =
MouseInputListener
{
private BufferedImage _image;
private float _scale = 1.0f;
private boolean _fDragging;
private Point2D _ptDragStart;
private Point2D _ptDragCur;
private Rectangle2D _rectDragBounds;
private AffineTransform _transformToClient = new AffineTransform(=
);
private AffineTransform _transformFromClient = new AffineTransfor=
m();
public ImageSelectComponent()
{
this.addMouseListener(this);
this.addMouseMotionListener(this);
}
public void setImage(BufferedImage image)
{
// I'm only copying the provided image because later on, I foun=
d =
that
// the entire image got cleared to black when I tried to write =
to =
it.
// Seems to be related to the fact that the image is from a fil=
e, =
and
// this might be a Mac-only issue (bug?). In any case, copying=
the
// passed-in image to a new one allows me to write to it later =
=
without
// any trouble.
_image = new BufferedImage(image.getWidth(), image.getHeight(=
), =
BufferedImage.TYPE_INT_ARGB);
Graphics2D gfx = _image.createGraphics();
gfx.drawImage(image, 0, 0, null);
gfx.dispose();
_UpdateMetrics();
}
public BufferedImage getImage()
{
return _image;
}
public void setScale(float scale)
{
_scale = scale;
_transformToClient = AffineTransform.getScaleInstance(_scale,=
=
_scale);
_transformFromClient = AffineTransform.getScaleInstance(1 / =
_scale, 1 / _scale);
_UpdateMetrics();
}
public float getScale()
{
return _scale;
}
protected void paintComponent(Graphics gfxArg)
{
Graphics2D gfx = (Graphics2D)gfxArg;
// Draw the image with the current transformation
if (_image != null)
{
AffineTransform transformSav = gfx.getTransform();
gfx.transform(_transformToClient);
// gfx.drawImage(_image, 0, 0, Math.round(_image.getWidth() *=
=
_scale), Math.round(_image.getHeight() * _scale), null);
gfx.drawImage(_image, 0, 0, null);
gfx.setTransform(transformSav);
}
// Drag data is already in client coordinates
if (_fDragging)
{
gfx.draw(_RectFromDrag());
}
}
private void _UpdateMetrics()
{
if (_image != null)
{
Rectangle2D rectBounds =
Util.RectTransform(_transformToClient, new Rectangle(0,=
0, =
_image.getWidth(), _image.getHeight()));
// Dimension sizeNew = new =
Dimension(Math.round(_image.getWidth() * _scale), =
Math.round(_image.getHeight() * _scale));
Dimension sizeNew = new =
Dimension((int)Math.round(rectBounds.getWidth()), =
(int)Math.round(rectBounds.getHeight()));
setMinimumSize(sizeNew);
setMaximumSize(sizeNew);
setPreferredSize(sizeNew);
setSize(sizeNew);
}
}
public void mouseClicked(MouseEvent arg0)
{
}
public void mouseEntered(MouseEvent arg0)
{
}
public void mouseExited(MouseEvent arg0)
{
}
public void mousePressed(MouseEvent arg0)
{
_fDragging = true;
_ptDragStart = arg0.getPoint();
// To determine limits of mouse input, start with a rectangle i=
n
// the image coordinates describing the whole image, and transf=
orm
// that rectangle into client coordinates
// _rectDragBounds = new Rectangle(0, 0,
// Math.round(_image.getWidth() * _scale), =
Math.round(_image.getHeight() * _scale));
_rectDragBounds = Util.RectTransform(_transformToClient,
new Rectangle(0, 0, _image.getWidth(), =
_image.getHeight()));
}
public void mouseReleased(MouseEvent arg0)
{
_fDragging = false;
if (_image != null)
{
// Rectangle2D rectComponent = _RectFromDrag(),
// rectImage = new =
Rectangle((int)Math.round(rectComponent.getX() / _scale),
// (int)Math.round(rectComponent.getY() / _scale),
// (int)Math.round(rectComponent.getWidth() / _scale)=
,
// (int)Math.round(rectComponent.getHeight() / _scale=
));
Graphics2D gfx = _image.createGraphics();
gfx.transform(_transformFromClient);
gfx.setColor(Color.WHITE);
//gfx.fill(rectImage);
gfx.fill(_RectFromDrag());
gfx.dispose();
}
this.repaint();
}
public void mouseDragged(MouseEvent arg0)
{
_ptDragCur = Util.PointConstrained(_rectDragBounds, =
arg0.getPoint());
this.repaint();
}
public void mouseMoved(MouseEvent arg0)
{
}
private Rectangle2D _RectFromDrag()
{
int x = (int)Math.min(_ptDragStart.getX(), _ptDragCur.getX())=
,
y = (int)Math.min(_ptDragStart.getY(), _ptDragCur.getY())=
,
dx = (int)Math.abs(_ptDragStart.getX() - _ptDragCur.getX(=
)),
dy = (int)Math.abs(_ptDragStart.getY() - _ptDragCur.getY(=
));
return new Rectangle(x, y, dx, dy);
}
}
The main application frame:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import java.io.IOException;
import javax.imageio.*;
import javax.swing.*;
public class ImageSelectFrame extends JFrame
{
/**
* @param args
*/
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
public void run()
{
JFrame frame = new ImageSelectFrame();
frame.pack();
frame.setVisible(true);
}
});
}
private ImageSelectFrame()
{
super("TestImageSelect");
this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
final ImageSelectComponent isc = new ImageSelectComponent();
JScrollPane pane = new JScrollPane(isc);
this.add(pane);
JMenuBar mbar = new JMenuBar();
JMenu menu = new JMenu("File");
JMenuItem mitem = new JMenuItem("Open...");
mitem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent e)
{
_PromptImage(isc);
}
});
menu.add(mitem);
menu.addSeparator();
mitem = new JMenuItem("Exit");
mitem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent arg0)
{
ImageSelectFrame.this.dispose();
}
});
menu.add(mitem);
mbar.add(menu);
menu = new JMenu("Image");
mitem = new JMenuItem("Set Scale...");
mitem.addActionListener(new ActionListener()
{
public void actionPerformed(ActionEvent arg0)
{
_PromptScale(isc);
}
});
menu.add(mitem);
mbar.add(menu);
this.setJMenuBar(mbar);
}
private void _PromptImage(ImageSelectComponent isc)
{
JFileChooser chooser = new JFileChooser();
if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OP=
TION)
{
try
{
BufferedImage image = =
ImageIO.read(chooser.getSelectedFile());
if (image == null)
{
throw new Exception("Unrecognized image file =
contents");
}
isc.setImage(image);
}
catch (Exception e)
{
JOptionPane.showMessageDialog(this,
"Unable to open image file. Error: \"" =
+ e.getMessage() + "\"",
"Invalid image",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
}
private void _PromptScale(ImageSelectComponent isc)
{
try
{
float scale = Float.parseFloat(
JOptionPane.showInputDialog(
this,
"Enter the new scale as a decimal:",
Float.toString(isc.getScale())));
if (scale <= 0)
{
throw new Exception("Scale must be a decimal value grea=
ter =
than 0");
}
isc.setScale(scale);
}
catch (Exception e)
{
JOptionPane.showMessageDialog(this,
e.getMessage(),
"Invalid scale",
JOptionPane.ERROR_MESSAGE);
e.printStackTrace();
}
}
}
The utility class:
import java.awt.*;
import java.awt.geom.*;
public class Util
{
public static Rectangle2D RectTransform(AffineTransform transform, =
=
Rectangle2D rect)
{
// A non-quadrant rotation would create a non-rectangular resul=
t
if ((transform.getType() & AffineTransform.TYPE_GENERAL_ROTATIO=
N) =
!= 0)
{
throw new IllegalArgumentException("the only valid rotation=
=
type for transform is TYPE_QUADRANT_ROTATION");
}
Shape shapeNew = transform.createTransformedShape(rect);
Rectangle2D rectBounds = shapeNew.getBounds2D();
return new Rectangle((int)Math.round(rectBounds.getMinX()),
(int)Math.round(rectBounds.getMinY()),
(int)Math.round(rectBounds.getWidth()),
(int)Math.round(rectBounds.getHeight()));
}
public static Point2D PointConstrained(Rectangle2D rectConstraint, =
=
Point2D ptSource)
{
return new Point.Double(Math.min(Math.max(ptSource.getX(), =
rectConstraint.getMinX()), rectConstraint.getMaxX()),
Math.min(Math.max(ptSource.getY(), =
rectConstraint.getMinY()), rectConstraint.getMaxY()));
}
}