Wednesday, 16 December 2009

Playing with the Mouse

In this article I'll be discussing how we can you can enhance an graphical component by handling mouse events such as mouse clicks and movements.

This article will not discuss the graphical aspect and assumes that you know how to create a simple UI component extending the JComponent and adding this component into a JFrame. For more information about how to do this, please read the article: Practical Example of Simple Java UI Component.

A note of caution to the reader: This is not an exhaustive article about events and some topics are simple mentioned while others not discussed at all. While you're encouraged to learn as much as you can, it's best to first read this topic as provided here in the order provided and then expand your knowledge once you have read it.

Create an Environment

Let's start by create a simple components (that does nothing) which will add into a frame.

package com.albertattard.pwtm;

import java.awt.Graphics;
import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyComponent extends JComponent {
  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);
  }
}

Always start with a super simple class and then slowly evolve it testing each change on the way. Don't wait for bulk changes to test as then it may be too late to understand what's wrong.

Now it's time to create the frame and add the above component to it.

package com.albertattard.pwtm;

import java.awt.Color;
import javax.swing.BorderFactory;
import javax.swing.JFrame;

@SuppressWarnings("serial")
public class MyFrame extends JFrame {

  private MyComponent myComp;

  public MyFrame() {
    initComponents();
  }

  private void initComponents() {
    setLayout(null);

    myComp = new MyComponent();
    myComp.setBorder(
      BorderFactory.createLineBorder(Color.RED));
    myComp.setBounds(10, 10, 400, 200);
    add(myComp);
  }
}

Now let's kick off this example by creating another with the main method and have this show rolling

package com.albertattard.pwtm;

import javax.swing.JFrame;
import javax.swing.SwingUtilities;

public class MyMain {
  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        createShowFrame();
      }
    });
  }

  private static void createShowFrame() {
    JFrame frame = new MyFrame();
    frame.setTitle("Playing with the Mouse");
    frame.setSize(600, 400);
    frame.setDefaultCloseOperation(
      JFrame.EXIT_ON_CLOSE);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }
}

Now that we have all the require components in place let's run. The above three classes should produce the following (or something very similar).


Let's start playing

Java provides three methods/mechanisms that allow us, the developers, to use to handle mouse events. Let say that we want to change the background colour of my component whenever the mouse hovers over it and rest the colour when the mouse moves away from the component.

We want that our component (the class called MyComponent defined above) to do something when the mouse is moved over it. Then, we have to register a mouse listener within this component and act accordingly. The JComponent class (which MyComponent inherits from) has all the logic we need, that is, methods that allow us to register mouse listeners. Whenever mouse events are occur, these methods will advise/notify our listeners with the changes and we can act accordingly. We first start by registering a mouse listener and then implement the methods we're interested in as illustrated below.

package com.albertattard.pwtm;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyComponent extends JComponent {

  public MyComponent() {
    initListeners();
  }

  private void initListeners() {
    addMouseListener(new MouseAdapter() {
      public void mouseExited(MouseEvent e) {
        setBackground(Color.WHITE);
        repaint();
      }

      public void mouseEntered(MouseEvent e) {
        setBackground(Color.RED);
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    g.setColor(getBackground());
    g.fillRect(0, 0, getWidth(), getHeight());
  }
}

Hold your horses, I'm lost.

There's lot of new code in this class, so let's start understanding this bit by bit. Let's start with the initListeners() method. This method is registering a mouse listener by calling the addMouseListener () method inherited from the JComponent class and pass our listener. For simplicity, some code was removed.

  private void initListeners() {
    addMouseListener(/* The Mouse Listener*/);
  }

After removing all the other code, this method becomes very simple and understandable. If you refer to the previous example, we also included a constructor which is calling this method, which I don't believe it requires explanation. The paintComponent () method was also enhanced which now fills a rectangle with the set colour. This rectangle has the same size of our component which makes it appear as the background colour.

Let's now turn our attention to the MouseListener. What is it? Whenever a mouse event is fired, (in laymen terms) the JVM advise our listener about the change. It does so by execute one of the five methods defined by the MouseListener listed below:

  • mouseClicked()
  • mouseEntered()
  • mouseExited()
  • mousePressed()
  • mouseReleased()

The method names are self explanatory but the process may not be. We first create an instance of the MouseListener, which is an interface, and override these methods. Then we register our listener with the component. When the mouse is clicked on our component, the method mouseClicked() will be executed. When the mouse hovers on our component, the method mouseEntered() is executed and when the mouse moves away from our component, the mouseExited().

One last thing before we return to our example. How many from the mouse listener's methods do we need to use to fulfil our requirement? Only two, as the requirement is to change the background when the mouse hovers in and out of our component. Java provides what are referred to adaptors. WTF? Java provides a skeleton class that implement this interfaces and provide default methods (methods that do nothing) for all the interface methods. We can then extend this class and override the method we want. Since the adaptor overrides all the interface methods, we only have to override those we're interested in, which simplifies our work.

Back to our initListeners() method. We're creating an instance of MouseAdapter, and registering it with our component.

  private void initListeners() {
    addMouseListener(new MouseAdapter() {
      public void mouseExited(MouseEvent e) {
        setBackground(Color.WHITE);
        repaint();
      }

      public void mouseEntered(MouseEvent e) {
        setBackground(Color.RED);
        repaint();
      }
    });
  }

As you can see from the above code fragment, we're only overriding two methods, the mouseEntered() and mouseExited() methods. In these methods we're changing the background colour and calling the repaint method.

Straight forward he

Getting it to the next level

That was fun, but let's improve it. Let's draw (fill) a circle following the mouse motion.

Before we can do so, we have to discuss another thing, the MouseEvent.

Before the event is fired and our listeners' methods invoked, information about the event is gathered (by something else which you don't need to care about, at least for now) and forwarded to you (the developer) as an event object, the MouseEvent in this case. This object contains information pertaining to our event, such as the mouse position, the time of the event and other things which may be useful when processing such event. We can (need to) use such information to achieve our goal as shown below.

package com.albertattard.pwtm;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyComponent extends JComponent {

  private Point mousePoint; 

  public MyComponent() {
    setForeground(Color.BLUE); 
    initListeners();
  }

  private void initListeners() {
    addMouseListener(new MouseAdapter() {
      public void mouseExited(MouseEvent e) {
        setBackground(Color.WHITE);
        repaint();
      }

      public void mouseEntered(MouseEvent e) {
        setBackground(Color.RED);
        repaint();
      }
    });

    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    g.setColor(getBackground());
    g.fillRect(0, 0, getWidth(), getHeight());

    if (mousePoint != null) {
      int ballSize = 15;
      g.setColor(getForeground());
      g.fillOval(mousePoint.x - ballSize / 2, 
                 mousePoint.y - ballSize / 2,
                 ballSize, ballSize);
    }
  }
}

The changes from the previous class are highlighted and discussed below.

An instance field was added to our component. Why? We need to transfer the mouse position, magically provided to us by the mouse events (discussed later on), to the paint method. Since we cannot call the paint method directly from the event (the reapint() call is something else), we need to save this information in the instance field, mousePoint, and the use it from the paint method. This instance field is accessible by both methods. First this field is updated by the method

      public void mouseMoved(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }

within our new adaptor, . The method mouseMoved() is invoked every time the mouse is moved within our component. This is important to understand. Only the mouse events that are related with our component are captured by our listeners. So when the mouse moves out of our component, this method will stop being invoked.

Next we'll discuss the mouse clicks

Handling the clicks

Let say that we also want to handle the mouse click that happen on our component. For simplicity we'll display a simple text in the top left corner listing the number of click that occure as shown below.

A long story short, add a new instance field (called clickCount) of type int and paint its value in the paint method as highlighted below. Also, override the appropriate method in the mouse adaptor.

package com.albertattard.pwtm;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyComponent extends JComponent {

  private Point mousePoint;
  private int clickCount; 

  public MyComponent() {
    setForeground(Color.BLUE);
    initListeners();
  }

  private void initListeners() {
    addMouseListener(new MouseAdapter() {
      public void mouseExited(MouseEvent e) {
        setBackground(Color.WHITE);
        repaint();
      }

      public void mouseEntered(MouseEvent e) {
        setBackground(Color.RED);
        repaint();
      }

      @Override
      public void mouseClicked(MouseEvent e) {
        clickCount++;
        repaint();
      }
    });

    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    g.setColor(getBackground());
    g.fillRect(0, 0, getWidth(), getHeight());

    if (mousePoint != null) {
      int ballSize = 15;
      g.setColor(getForeground());
      g.fillOval(mousePoint.x - ballSize / 2,
          mousePoint.y - ballSize / 2, ballSize, ballSize);
    }

    if (clickCount > 0) {
      g.setColor(getForeground());
      g.drawString("Click Count: " + clickCount, 10, 20);
    } else {
      g.setColor(getForeground());
      g.drawString("No Clicks!!!", 10, 20);
    }
  }
}

The above code is not rocket science but it has a little flaw. If you drag the mouse inside the component, the blue circle will not follow the mouse pointer as illustrated below.

Why not? The MouseMotionListener, which the MouseMotionAdapter implements has two methods but we're only overriding one as illustrated below.

    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }
    });

The other method, mouseDragged(), is used to handle mouse dragging events as illustrated code fragment below.

     addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseDragged(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }
      
      @Override
      public void mouseMoved(MouseEvent e) {
        mousePoint = e.getPoint();
        repaint();
      }
    }); 

Dragging is more complex than one may think and is discussed further in the following section

Dragging the mouse

For this example, we'll create a new component and will make use of it rather than keep changing the other one. Like before, we'll start with a simple example and keep building on it. We'll start with the following simple component.

package com.albertattard.pwtm;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyDragComponent extends JComponent {

  private Rectangle draggableShape;

  public MyDragComponent() {
    draggableShape = new Rectangle(20, 20, 50, 50);
    setForeground(Color.BLUE);
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2d = (Graphics2D) g;

    g2d.setColor(getBackground());
    g2d.fillRect(0, 0, getWidth(), getHeight());

    g2d.setColor(getForeground());
    g2d.fill(draggableShape);
  }
}

This component simply draws a solid blue 50 by 50 rectangle at the x and y coordinates 20 as illustrated below

Remember to add this component to the frame before you test it out

Let's start be arm our rectangle with a red border when the mouse enters the box as shown below. Note that the component has a red border and the mouse events are triggered for any mouse activity within the component. We need to filter out movements outside the rectangle and only arm the rectangle when the mouse is on it.

How can we do that? Quiet simple. Like before, we create a field (called mouseInside) and will set it to true when the mouse is within the rectangle and false otherwise. The updates are highlighted below.

package com.albertattard.pwtm;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;

import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyDragComponent extends JComponent {

  private Rectangle draggableShape;
  private boolean mouseInside; 

  public MyDragComponent() {
    draggableShape = new Rectangle(20, 20, 50, 50);
    setForeground(Color.BLUE);
    initListeners();
  }

  private void initListeners() {
    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2d = (Graphics2D) g;

    g2d.setColor(getBackground());
    g2d.fillRect(0, 0, getWidth(), getHeight());

    g2d.setColor(getForeground());
    g2d.fill(draggableShape);

    if (mouseInside) {
      g2d.setColor(Color.RED);
      g2d.setStroke(new BasicStroke(3));
      g2d.draw(draggableShape);
      g2d.setStroke(new BasicStroke(1));
    }
  }
}
Why are we resetting the stroke at the end of the paint component method? Ideally you rest all changes made to the graphics component once done as other paint methods for this object such paint border method will inherit your changes. In this case, if you do not reset the stroke the border will be painted with this stroke.

The following code fragment does the trick. The method mouseMoved () is continually called by the underlying system (so to say) every time the mouse is moved over our component. When the mouse point (retrieved from the mouse event provided as the method parameter) is within the rectangle area, the mouseInside field is set to true.

      public void mouseMoved(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        repaint();
      }

Let's not stop here. Let's make the rectangle draggable. By draggable I mean that the user can drag the rectangle around our component as shown below.

This is not an easy task and some may find it hard to understand but let's not panic. The following code does the job.

package com.albertattard.pwtm;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import javax.swing.JComponent;

@SuppressWarnings("serial")
public class MyDragComponent extends JComponent {

  private Rectangle draggableShape;
  private boolean mouseInside;
  private Point lastMousePoint; 

  public MyDragComponent() {
    draggableShape = new Rectangle(20, 20, 50, 50);
    setForeground(Color.BLUE);
    initListeners();
  }

  private void initListeners() {
    addMouseMotionListener(new MouseMotionAdapter() {
      @Override
      public void mouseMoved(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        lastMousePoint = e.getPoint();
        repaint();
      }

      @Override
      public void mouseDragged(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        if (mouseInside) {
          if (lastMousePoint != null) {
            draggableShape.x += 
                e.getPoint().x - lastMousePoint.x;
            draggableShape.y += 
                e.getPoint().y - lastMousePoint.y;
          }
        }
        lastMousePoint = e.getPoint();
        repaint();
      }
    });
  }

  @Override
  protected void paintComponent(Graphics g) {
    super.paintComponent(g);

    Graphics2D g2d = (Graphics2D) g;

    g2d.setColor(getBackground());
    g2d.fillRect(0, 0, getWidth(), getHeight());

    g2d.setColor(getForeground());
    g2d.fill(draggableShape);

    if (mouseInside) {
      g2d.setColor(Color.RED);
      g2d.setStroke(new BasicStroke(3));
      g2d.draw(draggableShape);
      g2d.setStroke(new BasicStroke(1));
    }
  }
}

Let's starts discussing the code.

When the mouse is dragged within the rectangle, we're also moving it. The rectangle can be dragged from anywhere within. So we have to take under consideration where the mouse was clicked within the rectangle and shift the rectangle by the offset, that is, by the amount the mouse was moved. We cannot simply set the rectangle's x and y coordinates to match those of the event. The following sketch highlights this (hopefully).

To do this we added a new field called lastMousePoint, which is used to save the last mouse position (as the name infers). Instead of setting the rectangle coordinates equal to the mouse coordinates we adjust them by how much the mouse was moved using the following (copy and paste from above) logic.

      @Override
      public void mouseDragged(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        if (mouseInside) {
          if (lastMousePoint != null) {
            draggableShape.x += 
                e.getPoint().x - lastMousePoint.x;
            draggableShape.y += 
                e.getPoint().y - lastMousePoint.y; 
          }
        }
        lastMousePoint = e.getPoint();
        repaint();
      }

It is important to also update the lastMousePoint field before we exit the above method.

A tip for the champions. If you observe the previous class, you notice that the field lastMousePoint is only used within the adaptor. Ideally we move this field within the adaptor as highlighted below.

 addMouseMotionListener(new MouseMotionAdapter() {
      
      private Point lastMousePoint; 
      
      @Override
      public void mouseMoved(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        lastMousePoint = e.getPoint();
        repaint();
      }

      @Override
      public void mouseDragged(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        if (mouseInside) {
          if (lastMousePoint != null) {
            draggableShape.x += 
                e.getPoint().x - lastMousePoint.x;
            draggableShape.y += 
                e.getPoint().y - lastMousePoint.y;
          }
        }
        lastMousePoint = e.getPoint();
        repaint();
      }
    });

Too keep it simple, the above change will not apply to the rest of the examples

One last thing before we go. Note that we're able to drag the rectangle outside the component printable area. This is not always good and we would like to prevent this as illustrated below.

This is not a difficult change. All we need to do is cap the changes to the rectangle as illustrated in the following code fragment.

      @Override
      public void mouseDragged(MouseEvent e) {
        mouseInside = draggableShape.contains(e.getPoint());
        if (mouseInside) {
          if (lastMousePoint != null) {
            draggableShape.x += 
                e.getPoint().x - lastMousePoint.x;
            draggableShape.y += 
                e.getPoint().y - lastMousePoint.y;

            draggableShape.x = Math.max(draggableShape.x, 0);
            draggableShape.x = Math.min(draggableShape.x,
                getWidth() - draggableShape.width);
            draggableShape.y = Math.max(draggableShape.y, 0);
            draggableShape.y = Math.min(draggableShape.y,
                getHeight() - draggableShape.height);
          }
        }
        lastMousePoint = e.getPoint();
        repaint();
      }

The rest of the class is not listed for brevity.

No comments:

Post a Comment