Friday, September 2, 2011

Using a MouseMotionListener to allow user interface with a grid.



In this tutorial, we will create a grid on a JPanel, and use the MouseMotionListener to make the panel aware of which grid space is currently highlighted by the user.  I came across this solution working on Project Hardin, in the process of developing the user interface for a grid-based map builder.

The principles used here can be applied to many formations other than grids, but you must be aware of where your interactive areas are, inside of your component, to use this method.  Obviously, there are 1,000 ways to do this.  Your use case might demand certain changes in coding and approach.


Overview


The following steps will be followed in this tutorial:

  1. Create the Listening Panel by extending JPanel.
  2. Create the properties that will allow us to draw tiles consistently, as well as keep track of the currently selected tile.
  3. Create / Overwrite the methods used to paint tiles on the panel.
  4. Add a MouseMotionListener to the panel, providing logic for the listener.
  5. Create public method to allow passing-in of MouseEvents.
  6. Create a Frame to display our work onto, then place our Listening Panel into a JScrollPane.
  7. Add a MouseMotionListener to the JScrollPane, sending MouseEvent signals to Listening Panel.
  8. Integrate, debug and Execute.


Create a Listening Panel




//This is easy! :)
static class ListenerDisplayPanel extends JPanel {

}


Create properties for Listening Panel



Next we have to set up our local properties for this class. These properties will be used like variable in the equations our drawing routine uses to draw tiles. We will also include a property that tracks the most recently pointed-ad tile.


static class ListenerDisplayPanel extends JPanel {

// Private member variables
private int mapWidth = 50; // Width of the map (in tiles)
private int mapHeight = 50; // Length of the map (in tiles)

// The width, in pixels, of a grid tile
private static final int TILE_WIDTH = 32;
private static final int TILE_HEIGHT = 32;

// The point (in grid format, not pixel) currently highlighted
private Point currentlyAt = new Point(0,0);

}


To insure that our panel is the correct size for its contents, we'll set the preferred size of the panel in our constructor, using our member variables.


public ListenerDisplayPanel() {

// Set the dimensions of the panel.
setPreferredSize(new Dimension(
mapWidth * TILE_WIDTH,
mapHeight * TILE_HEIGHT));
}

Already, our member variables are helping to define our panel.


Define grid-painting logic



At this point, we are ready to define how our panel should go about painting the grid and the grid highlighter. We haven't built the logic to update the currently selected tile yet, but we can use the currentlyAt property we crated in the last step in our equations.

We will add the logic to update the value of currentlyAt later in the tutorial.


/**
* Draws our graphics on the panel
*
* @param g
* The graphics component, which is something like the canvas
* you're painting on.
*/

protected void paintComponent(Graphics g) {

// Prevents images from getting gunky / forces full redraw
super.paintComponent(g);

// Initialize brush
g.setColor(Color.red);

// Draw a checkerboard on the screen based on height / weight value
for (int x = 0 ; x < mapWidth ; x++) {
for (int y = 0 ; y < mapHeight ; y++) {

if ( (0 == x % 2) && 0 == (y % 2) ||
(x % 2 > 0) && (y % 2 > 0 )) {

// draw a 32 x 32 rectangle
g.drawRect(x * TILE_WIDTH, y * TILE_HEIGHT,
TILE_WIDTH, TILE_HEIGHT);
}
else
{
g.fillRect(x * TILE_WIDTH, y * TILE_HEIGHT,
TILE_WIDTH, TILE_HEIGHT);
}
}
}

// Draw a green box around currently highligted square.
// Drawing two lines will thicken the line, making it
// more visible.

g.setColor(Color.green);
g.drawRect(
currentlyAt.x * TILE_WIDTH,
currentlyAt.y * TILE_HEIGHT,
TILE_WIDTH, TILE_HEIGHT);
g.drawRect(
(currentlyAt.x * TILE_WIDTH) + 1,
(currentlyAt.y * TILE_HEIGHT) + 1,
TILE_WIDTH - 2, TILE_HEIGHT - 2);
}

}



Note how our class-member variables, such as currentlyAt , TILE_WIDTH and TILE_HEIGHT are very present in this routine.


Add the MouseMotionListener to the Panel



There are two steps to this process. The first step is to create the MouseMotionListener, coding in logic for how the program should react to mouse movement over this panel. Because this panel wont be directly on the user interface (it will be inside of a JScrollPanel , we need to create a method to allow parenting containers to invoke our MouseMotionListener.

EventListeners can be intimidating to new programmers. If this is you, rest assured, with enough practice and practical application, they become much easier to use. In this tutorial, we're going to add our MouseMotionListener as an *anonymous* class, which is a fancy way of saying we'll make it up on the fly as we add it to our panel.

First, will code in our new MouseMotionListener as part of the ListenerDisplayPanel class. We'll also throw in a quick routine which will supply us with the *grid-based* position of the mouse, so we'll know which tile to highlight. That routine is defined after our we finish working on our constructor.


/**
* CONSTRUCTOR (Default)
*/

public ListenerDisplayPanel() {

// Set the dimensions of the panel.
setPreferredSize(new Dimension(
mapWidth * TILE_WIDTH,
mapHeight * TILE_HEIGHT));

// Add the mouse listener, which will process the mouse movement
// and record the current tile being highlighted

addMouseMotionListener(new MouseMotionListener() {

@Override
public void mouseDragged(MouseEvent e) {

// No code here.
}

@Override
public void mouseMoved(MouseEvent e) {

// Get the point where the mouse is located, in pixels,
// relative to this component.

Point relativePoint = e.getPoint();

// Translate the relativePoint into a point relative to
// the grid

Point gridPoint = mouseAtComponent_ToGrid(relativePoint);

// Assign this grid point to the currentlyAt variable,
// refreshing the currently selected tile space in
// memory.

currentlyAt = gridPoint;

// Refresh the invoking object and repaint the panel
Component source = (Component) e.getSource();
repaint();
source.validate();
}
});


setVisible(true);
}

/**
* Translates the position of the mouse point (in pixel units)
* into a grid position within a map.
*
* @param mouseAt
* The position of the mouse in Point format. You can get this
* from the e.getPoint() command of a MouseEvent object.
*
* @return
* The currently hovered-o'er grid.
*/

private Point mouseAtComponent_ToGrid(Point mouseAt) {

// divide the point by the tile size
return new Point(
(int) mouseAt.x / TILE_WIDTH,
(int) mouseAt.y / TILE_HEIGHT);
}

In brief, we made a very simple listener.

We simply had it report to us the position of the mouse (relative to the component - where the top left corner is (0,0)). Then we took that position, converted it with our mouseAtComponent_ToGrid() method, and updated the position of our currentlyAt property. Since this property is used by the paintComponent() method to draw a highlight around our selected square, all the dirty work is now done. We finished our listener logic by refreshing the object that made the call, as well as our panel
.


Exposing our Panel's MouseMotionListener



Finally, we need to give parent containers a way to pass MouseEvents to our panel so we can process them with our listener. A very simple way to do this is by creating a public method that takes a MouseEvent as an arguement and calls itself to process our event.


/**
* Allows calls to pass MouseEvents into this panel's
* MouseMotionListener.
*
* @param e
* The MouseEvent that was triggered by user action.
*/

public void mouseMoved(MouseEvent e) {
processMouseMotionEvent(e);
}


We have a little more work to do with listeners in this tutorial, but where the ListenerDisplayPanel is concerned, we're done with this section for now. In fact, this completes our construction of the


Create a Frame for display and Interaction



We'll begin, as always, by declaring our class and our class data members


/**
* We'll use this frame to show our display panel within a
* scroll panel.
*/

static class PanelDisplayFrame extends JFrame {

// Declare our listener panel and new Scroll pane as members.
ListenerDisplayPanel displayPanel = new ListenerDisplayPanel();
JScrollPane scrollPane;
}

Again, fairly simple stuff here.


Configuring the Frame



Next, we'll put some logic in the constructor to insert our completed listening panel ListenerDisplayPanel into a JScrollPane. Note that when we instantiate scrollPane, we add displayPanel as an argument to the JScrollPane constructor. This is the recommended way to add panels to your scrollpanes, as using other methods may get tricky.


/**
* CONSTRUCTOR (default)
*/

public PanelDisplayFrame() {

// Instantiate the scroll pane containing our display panel.
scrollPane = new JScrollPane(displayPanel);

// Update the scrollPane UI to prevent weird updating effects.
scrollPane.updateUI();

// Set up our scroll pane on the main panel.
setLayout(new BorderLayout());
add(scrollPane);
}



Add a listener to the frame



Finally, to get ourselves code-complete, we simply add another MouseMotionListener scrollPane. We're simply going to tell this listener to pass the MouseEvent signal down to the panel we created.

This completes the circuit, when we're done, we should be able to see our grid within a scrollable pane, and we should be able to see a highlight around the tile we are pointing to.


Since this code is also added to the constructor for the display frame, the new code will be bold-faced


public PanelDisplayFrame() {

// Instantiate the scroll pane containing our display panel.
scrollPane = new JScrollPane(displayPanel);

// Update the scrollPane UI to prevent weird updating effects.
scrollPane.updateUI();

// Set up our scroll pane on the main panel.
setLayout(new BorderLayout());
add(scrollPane);

// Add a mouse motion listener to pass messages to our
// display panel.

scrollPane.addMouseMotionListener(new MouseMotionListener() {

@Override
public void mouseDragged(MouseEvent e) {

// do nothing
}

@Override
public void mouseMoved(MouseEvent e) {

// Call the display panel to convey our MouseEvent.
displayPanel.mouseMoved(e);
}
});

}


That should do it! Your program should look something like this when it is finished (note the green square and the mouse pointer).


You can download the full code for this puppy at http://code.google.com/p/tutorialsbytimon/downloads/list or browse the tutorial repository at http://code.google.com/p/tutorialsbytimon

Happy Coding!

1 comment:

  1. CORRECTION!

    Step 7 - Add a Mouse Listener to the JScrollPane:


    Step 7 is not necessary. The JScrollPane appears to allow the contained component to grab focus. It's not necessarily a bad idea to pipeline your event listening ability through components, but in this case, it is not necessary.

    The tutorial file has been updated.

    ReplyDelete