In today's exercise, you will look at how a GUI program can react to mouse events. In Lab #11b, you provided computation classes for computing different mathematical functions. One thing that this program lacked was an easy way to select a new region to plot. Sure, you could enter new values into the x0, x1, y0, and y1 text fields, but how fun is that?
Instead, it would be nicer if you could use the mouse to select an area of an image that you want to zoom into.
It turns out that this is perhaps surpringly easy to add. Granted, you'll implement this on top of the GUI already provided to you, but all of that code implements many different features. In this lab you need to add just one very useful feature, and it really doesn't take that much code.
Do this...
edu.institution
.username
.hotj.gui22
Exercise Questions/gui22.txt
.Design/gui22.txt
.hotj.gui22
package. (If you don't
have this code, you can import the original code and work on this
lab anyway; your graphs just won't be as interesting.)Also, a word of warning: I have not bothered to list where the various library classes come from in this lab exercise. If you have not become friends with Source -> Organize Imports in Eclipse, now's the time.
Mouse events are detected by our Java code with listeners, just the same as with textboxes and buttons and other widgets. The only question with mouse events is who should do the listening? The answer, in this case, is the panel that draws the image.
So consider what we want the behavior of the mouse listener to be:
When the mouse button is pressed in the panel containing the image, the point where the mouse was clicked specifies one corner of the "selection rectangle". As the mouse is dragged, the current location of the mouse determines the other corner of the "selection rectangle".
So there are two events you need to worry about: a pressed mouse button and a dragged mouse. Normally, the Swing library would separate those into two separate methods; in this case, its two separate methods in two different interfaces!
Pressing mouse button is considered just a "normal" mouse event. Dragging the mouse which is considered a "mouse motion" event. So you need to implement a normal mouse-event listener for the pressed mouse button and a mouse-motion listener for the dragged mouse.
You want to listen to the panel containing the image, and that
panel is created in the PointComputationFrame
class.
This class has access to the text fields that you want to update
based on the location of the mouse click.
The panel itself is a PointComputationPanel
which
is created in PointComputationFrame#addImagePanel()
.
Listeners can be added to myImagePanel
right after its
been declared. Here's the new algorithm:
Algorithm of PointComputationFrame#addImagePanel()
- Let
plotter
be an image plotter...- Let
image
be an image from the plotter...- Set
myImagePanel
to be a plot-computation panel usingimage
.- Let
selectCornerListener
be a new select-corner listener constructed with myself.- Add
selectCornerListener
tomyImagePanel
as a mouse listener.- Let
selectOtherCornerListener
be a new select-other-corner listener constructed with myself.- Add
selectOtherCornerListener
tomyImagePanel
as a mouse-motion listener.- Add
myImagePanel
to the frame.
This algorithm is mostly implemented already except for the emphasized steps which you now need to implement. But first the objects and operations for those two steps need some explaining:
Objects of two steps of PointComputationFrame#addImagePanel()
Description Type Kind Movement Name the listener to select the first corner of the selection rectangle SelectCornerListener
variable local selectCornerListener
the listener to select the second corner of the selection rectangle SelectOtherCornerListener
variable local selectOtherCornerListener
myself PointComputationFrame
keyword instance this
Neither of the listeners have been written yet.
Operations of two steps of PointComputationFrame#addImagePanel()
Description Predefined? Name Library create a new SelectCornerListener
with aPointComputationFrame
no SelectCornerListener(PointComputationFrame)
SelectCornerListener
add a mouse listener to a point-computation panel yes addMouseListener(MouseListener)
JPanel
create a new SelectOtherCornerListener
with aPointComputationFrame
no SelectOtherCornerListener(PointComputationFrame)
SelectOtherCornerListener
add a mouse-motion listener to a point-computation panel yes addMouseMotionListener(MouseMotionListener)
JPanel
Check out the inheritance: PointComputationPanel
is a JPanel
, so there's no problem invoking
the two add-mouse-listener methods on
myImagePanel
. Implicit in these lists is that
SelectCornerListener
will have to implement the
MouseListener
interface.
Do this...
Implement Steps 4 and 5 of the algorithm for
PointComputationFrame#addImagePanel()
. You'll get compilation errors for the missing
class.
Before you implement SelectCornerListener
, though,
you should be prepared for how much work this will take. If you
look at the MouseListener
interface, there are
five methods you need to implement. Only one method is
used for listening for a pressed mouse button. But since it's an
interface, you're required to define them all in your listener
subclass, even if you leave the body of the method empty.
Fortunately, the Java people knew this was going to be common,
so they also created a MouseAdapter
class which
implements the MouseListener
interface, each method
with an empty body. So if you extend MouseAdapter
, you
indirectly will implement the MouseListener
interface, and you'll be given "do nothing" definition for each
method in the interface. You won't have to do anything else for the
events you're not interested in.
Do this...
Create a class named SelectCornerListener
which
extends the MouseAdapter
class.
The compiler will still complain about a missing
SelectCornerListener
constructor. It's perhaps not
clear at this point, but the listener will need access to the image
panel and to the control panel, and it can get those accesses
through the frame.
Do this...
myFrame
instance variable for
SelectCornerListener
.SelectCornerListener#SelectCornerListener(PointComputationFrame)
constructor which initializes its instance variable
appropriately.Hint: see how ReplotListener
does these same
things.
The terminology used by the two mouse listeners is important. A mouse click is defined as a mouse press and a mouse release. A dragged mouse motion is defined as a mouse press and a moved mouse. Your first goal is to select one corner when the mouse is pressed (not clicked or released).
Do this...
Everytime you press the mouse in the display area, your program will print "Hey!" to the console screen. This indicates that the listener is doing its job. Notice that it does not print anything if you click somewhere outside the image panel.
Helpful hint: Watch the terminology closely
here! Unfortunately, a MouseEvent
talks about x- and
y-coordinates instead of column and row coordinates. For our
purposes, x- and y-coordinates are for the real numbers being
plotted. (That's the x
and
y
in your Computation
subclasses.) But when dealing with a MouseEvent
, x and
y will refer to column and row, respectively.
Now you should make it do something useful! The useful thing it
should do is select new x- and y-coordinates for the
myX0
and myY0
text
fields. The MouseEvent
passed into
mousePressed(Event)
has the column and
row screen coordinates of the place where the mouse was
pressed. The screen coordinates need to be translated into
real-number coordinates for the user to see in the text fields.
Consider this algorithm:
Algorithm of SelectCornerListener#mousePressed(MouseEvent)
- Receive
event
.- Let
plotter
be the real-number plotter frommyFrame
.- Let
column
be the x value gotten fromevent
.- Let
x
be the x-equivalent ofcolumn
(usingcolumnToX(int)
ofplotter
).- Get the coordinate control from my frame, and set X0 of this coordinate control to be
x
.- Let
row
be the y value gotten fromevent
.- Let
y
be the y-equivalent ofrow
(usingrowToY(int)
ofplotter
).- Get the coordinate control from my frame, and set Y0 of this coordinate control to be
y
.
Do this...
Implement this algorithm, replacing the println
statement. Your code should compile and run without problems; as you click in
the image area, the X0 and Y0 textfields should change
appropriately. Make sure that the numeric conversion is
correct.
It turns out handling a dragged click is similar to handling a pressed mouse-button. The main difference is that you need to react to a different event, a "dragging" event. For whatever reason, a dragging event is handled with a different listener, but what you do with that listener and how you implement it is identical to what you did before.
Do this...
Implement Steps 6 and 7 of
PointComputationFrame#addImagePanel()
.
The new class for a mouse-motion listener doesn't exist, so you need to create it. This time, it extends a different adapter which helps you with a different listener interface.
Do this...
SelectOtherCornerListener
which extends the MouseMotionAdapter
class.myFrame
instance variable for
SelectOtherCornerListener
.SelectOtherCornerListener#SelectOtherCornerListener(PointComputationFrame)
constructor.And now you need to do something when the mouse is dragged:
Do this...
Override
SelectOtherCornerListener#mouseDragged(MouseEvent)
.
The algorithm is identical to the one you used for
SelectCornerListener#mousePressed(MouseEvent)
except
that you should replace mentions of x0 with
x1 and mentions of y0 with y1.
Do this...
It should compile and run just fine. Remember to hit the
Replot button after selecting a new area.
If you feel inspired, you might want to try improving this new feature.
One thing you might find disconcerting is that there's no visual indication of the selection box as you press and drag the mouse. It's up to you to provide this, if you want it!
It actually isn't too difficult.
PointComputationPanel
would need four more instance
variables (and mutators) for column0, row0,
column1, and row1. As either mouse listener
changes the text fields, it should also set these new
instance variables. Then
PointComputationPanel#paintComponent(Graphics)
should
draw a box based on those coordinates.
You may also want some way to toggle the drawing of the box. Draw the box when the mouse is pressed and while it's dragged; don't draw the box when the mouse is released.
It's easy to select a rectangle whose width-to-height ratio does not match the width-to-height ratio of the panel itself. For example, select and plot a very narrow and very tall rectangle. The picture will be distorted because this skinny rectangle is zoomed to a square.
There are several ways to deal with this. The easiest
possibility is to ignore the issue; if a user really needs square
selections, the user will have to be careful when making a
selection. Another possibility would be to force the selection
rectangle to be square; you could enforce this by tweaking the row
and column (or x and y) values in the
SelectOtherCornerListener
. Alternately, you could
compute a new aspect ratio, and then change the size of the image
panel (and the GUI) to match this new ratio.
The image in this GUI does not change size, even if you resize the GUI itself. To allow the user to resize the GUI leads to the same aspect-ratio issues described in the previous section.
If you zoom in enough times, you will reach a point where you
can't zoom in any further. One option is to change the precision
implied by ControlPanel#COORDINATE_FORMATTER
, but this
may make the numbers hard to read at other times (because of too
much precision). Alternatively, you could increase or
decrease the precision based on the difference between the
coordinates. This is non-trivial.
Whatever way you use to get this to work, eventually you will
run into the fundamental limits of floating point numbers. You can
perhaps increase your precision options by switching to
BigDecimals
, but that would require changing a
lot of code and it would undoubtedly slow things down
significantly.