Re: Draw a rectangle keeping aspect ratio of viewport
- From: "Peter Duniho" <NpOeStPeAdM@xxxxxxxxxxxxxxxx>
- Date: Mon, 26 May 2008 11:22:43 -0700
On Sun, 25 May 2008 15:52:32 -0700, RichT <someone@xxxxxxxxxxxxx> 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 to spot the problem, but when you highlighted the problem it was blatantly 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 person 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 JComponent 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 allows 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 can use an AffineTransform, and in some places doing so makes the code a bit more concise. But it also complicates the code conceptually, and because 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 than 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. When a variable is named simply "p" (for example), it makes it very hard to do 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 dealing with scaling, and using an AffineTransform. Java doesn't have a pre-processor that I could use to optionally compile one or the other, so 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 code 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 explicit version, but with slightly different parameters, calculations, etc.)
I could in fact use some sort of flag to conditionally execute one or the other, or I could just post two different versions of the class, so if my current method of differentiating the two is too complicated, let me know 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 probably 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 the 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 keeps 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 specific 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 reader to easily provide their own input.
I know that's a lot of notes, and the code may seem to be a bit long. But 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 AffineTransform();
public ImageSelectComponent()
{
this.addMouseListener(this);
this.addMouseMotionListener(this);
}
public void setImage(BufferedImage image)
{
// I'm only copying the provided image because later on, I found 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 file, 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 in
// the image coordinates describing the whole image, and transform
// 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_OPTION)
{
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 greater 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 result
if ((transform.getType() & AffineTransform.TYPE_GENERAL_ROTATION) != 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()));
}
}
.
- Follow-Ups:
- References:
- Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: Peter Duniho
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: Peter Duniho
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: Peter Duniho
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: Peter Duniho
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: Peter Duniho
- Re: Draw a rectangle keeping aspect ratio of viewport
- From: RichT
- Draw a rectangle keeping aspect ratio of viewport
- Prev by Date: Re: 64 bits
- Next by Date: Re: JDBC Connect Error Postgresql
- Previous by thread: Re: Draw a rectangle keeping aspect ratio of viewport
- Next by thread: Re: Draw a rectangle keeping aspect ratio of viewport
- Index(es):