Kurs Java Podstawy - rozszerzony

Obsługa zdarzeń - Mysz

Ostatnim typem prostych zdarzeń generowanych przez użytkownika są kliknięcia, przeciągnięcia, lub po prostu zmiana położenia kursora myszy. Jak się w tej lekcji przekonamy ich obsługa również nie jest zbyt skomplikowana, a po napisaniu kilku aplikacji z wykorzystaniem tego mechanizmu będziemy mogli sprawnie się nim posługiwać. Postaramy się napisać prostą aplikację, która w odpowiedzi na kliknięcia w obszarze panelu będzie rysowała różne kształty, natomiast Twoim zadaniem będzie jego rozbudowa o dalszą funkcjonalność.

W przypadku zdarzeń generowanych przez klawiaturę używaliśmy interfejsu KeyListener, tym razem, jak nietrudno zgadnąć, schemat będzie podobny. Możemy jednak wyróżnić dwa interfejsy które służą do przechwytywania dwóch typów zdarzeń, które mogą zaistnieć

  • MouseListener - interfejs słuchacza zdarzeń myszy. Odpowiedzialny za kliknięcia i pojawianie się kursora nad komponentem nasłuchującym.
  • MouseMotionListener - interfejs, który pozwala przechwytywać zdarzenia związane z ruchem kursora.

Warto również wspomnieć o tym, że istnieją klasy MouseAdapter i MouseMotionAdapter, dzięki którym nie musimy implementować wszystkich metod, a tylko te, które są nam potrzebne - minusem jest oczywiście to, że w Javie nie ma dziedziczenia wielobazowego, więc jeśli rozszerzamy przykładowo klasę JPanel, nie będziemy mogli użyć klasy adaptera.

Przejdźmy do rzeczy. Interfejs MouseListener posiada pięć metod:

//metoda wywoływana, gdy następuje kliknięcie, czyli wciśnięcie i zwolnienie przycisku myszy, ale uwaga, obie te operacje muszą zajść w jednym położeniu.
public void mouseClicked(MouseEvent event)

//metoda wywoływana, gdy zostaje wciśnięty przycisk myszy\
public void mousePressed(MouseEvent event)

//metoda wywoływana, gdy następuje zwolnienie przycisku myszy
public void mouseReleased(MouseEvent event)

//metoda wywoływana, gdy kursor pojawia się w obszarze nasłuchującym na zdarzenia, na przykład panelu
public void mouseEntered(MouseEvent event)

//metoda wywoływana, gdy kursor opuszcza obszar nasłuchujący zdarzenia
public void mouseExited(MouseEvent event)

Na szczęście ich nazwy są intuicyjne i zazwyczaj wiadomo której użyć. W przypadku mouseClicked należy jedynie pamiętać, że za kliknięcie uważamy wciśnięcie i zwolnienie przycisku w tym samym miejscu, jeśli przycisk wciśniemy, przesuniemy kursor i dopiero zwolnimy przycisk, wtedy zostaną wywołane zarówno mousePressed() oraz mouseReleased, ale nie mouseClicked().

Podobnie sprawa wygląda w przypadku interfejsu MouseMotionListener, jednak tutaj są tylko dwie metody:

//metoda wywoływana, gdy wciśniemy przycisk i przeciągniemy kursor
public void mouseDragged(MouseEvent event)

//metoda wywoływana, gdy poruszamy kursor w obszarze nasłuchującym zdarzenia
public void mouseMoved(MouseEvent event)

Jak widać wszystkie z metod otrzymują jako parametr obiekt klasy MouseEvent. Przechowuje on bardzo dużo ciekawych informacji na temat zdarzenia, które nastąpiło. Możemy z niego odczytać położenie kursora zarówno względem punktu (0,0) naszego panelu, lub innego komponentu nasłuchującego, a także położenie na ekranie. Przycisk, który został wciśnięty, oraz wiele więcej, Dodatkowo klasa MouseEvent posiada szereg zdefiniowanych stałych, z którymi możemy porównywać dane otrzymane od systemu. Aby w ogóle móc przechwytywać zdarzenia myszy musimy najpierw dodać do komponentu nasłuchującego odpowiedniego słuchacza, za pomocą metod addMouseListener, lub addMouseMotionListener.

Na początek napiszmy prościutki program, który zawiera panel implementujący oba wspomniane interfejsy, a wszystko umieścimy w ramce. Informację o zdarzeniach będziemy wyświetlali w konsoli przy pomocy standardowego strumienia wyjścia.

Klasa panelu implementującego interfejsy:

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import javax.swing.JPanel;

public class MouseTestPanel extends JPanel implements MouseListener, MouseMotionListener {

	public MouseTestPanel() {
		addMouseListener(this);
		addMouseMotionListener(this);
	}

	@Override
	public void mouseDragged(MouseEvent arg0) {
		System.out.println("mouseDragged");
	}

	@Override
	public void mouseMoved(MouseEvent arg0) {
		System.out.println("mouseMoved");
	}

	@Override
	public void mouseClicked(MouseEvent e) {
		System.out.println("mouseClicked");
	}

	@Override
	public void mouseEntered(MouseEvent e) {
		System.out.println("mouseEntered");
	}

	@Override
	public void mouseExited(MouseEvent e) {
		System.out.println("mouseExited");
	}

	@Override
	public void mousePressed(MouseEvent e) {
		System.out.println("mousePressed");
	}

	@Override
	public void mouseReleased(MouseEvent e) {
		System.out.println("mouseReleased");
	}

}

Klasa ramki i jednocześnie klasa testowa:

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Frame extends JFrame {
	public Frame() {
		super("MouseTest");

		add(new MouseTestPanel());
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		setVisible(true);
	}

	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			@Override
			public void run() {
				new Frame();
			}
		});
	}
}

Należy oczywiście najpierw rozciągnąć sobie okienko, wjeżdżając i wychodząc z obszaru panelu, a także klikając w jego obszarze można zaobserwować w konsoli, które metody zostają wywoływane.

Przejdźmy jednak do tego o czym wspomniałem na początku. Napiszmy program, który w miejscu, w którym klikniemy narysuje jakieś kształty, np kwadraty. Nie ma tutaj znaczenia, czy użyjemy do tego celu metody mousePressed, mouseReleased, czy mouseClicked, wszystko zależy od naszej inwencji, ale przyjmijmy, że będzie to mousePressed.

Panel z rysowaniem kwadracików 10x10 pikseli:

import java.awt.*;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

import javax.swing.JPanel;

public class MouseTestPanel extends JPanel implements MouseListener {

	private static final int DEFAULT_WIDTH = 200;
	private static final int DEFAULT_HEIGHT = 200;

	private int x, y;

	ArrayList points = new ArrayList();

	public MouseTestPanel() {
		addMouseListener(this);
		setPreferredSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
	}

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
		x = e.getX();
		y = e.getY();
		points.add(new Point(x, y));
		repaint();
	}

	@Override
	public void mouseReleased(MouseEvent e) {
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setColor(Color.WHITE);
		g2d.fillRect(0, 0, this.getWidth(), this.getHeight());
		g2d.setColor(Color.BLACK);
		drawRectangles(g2d);
	}

	private void drawRectangles(Graphics2D g2d) {
		int x, y;
		for (Point p : points) {
			x = (int) p.getX();
			y = (int) p.getY();
			g2d.fillRect(x, y, 10, 10);
		}
	}
}

W skrócie. Posiadamy dwie zmienne x, y, które będą przechowywały miejsce, w których kliknął użytkownik. Dane do tego potrzebne otrzymamy w obiekcie MouseEvent, który jest parametrem wykorzystywanej przez nas metody mousePressed. Położenie kursora w momencie zajścia zdarzenia uzyskujemy dzięki metodom getX() oraz getY(). Dodatkowo stworzyliśmy zmienną typu boolean, dzięki której kwadracik nie jest rysowany po starcie programu w punkcie (0,0), czyli domyślnych wartości x i y. W metodzie mousePressed za każdym razem modyfikujemy zmienne x i y oraz wywołujemy metodę repaint(). Rysowanie polega na wypełnieniu tła białym kolorem i narysowaniu czarnego kwadracika o wymiarach 10x10 pikseli.

import java.awt.EventQueue;
import javax.swing.JFrame;

public class Frame extends JFrame {
	public Frame() {
		super("MouseTest");

		add(new MouseTestPanel());
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}

	public static void main(String[] args) {
		EventQueue.invokeLater(new Runnable() {
			@Override
			public void run() {
				new Frame();
			}
		});
	}
}

W klasie ramki dodaliśmy znaną nam metodę pack(), która dostosowała rozmiar ramki do naszego panelu.

Jesteśmy już blisko końca, ale zapewne chcielibyśmy, aby na planszy pojawiały się wszystkie kwadraciki, które narysowaliśmy, a nie tylko jeden. Jak to najprościej uzyskać? Oczywiście musimy przechowywać gdzieś wartości punktów, w których zaszły kliknięcia. Mogłaby to być tablica, ale nie jest to dobre rozwiązanie, bo musielibyśmy z góry zakładać, jaka ilość kwadracików może pojawić się na planszy. Znacznie lepiej będzie wykorzystać jakąś strukturę danych - najlepiej listę. My wybierzemy listę tablicową ArrayList, ale mogłaby to być równie dobrze LinkedList. Dodatkowo dodamy do niej parametr, który określi, konkretny typ obiektów, co chroni nas przed rzutowaniem. Jedyne zmiany zachodzą w klasie panelu, więc jedynie jego kod umieszczono poniżej:

import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;

import javax.swing.JPanel;

public class MouseTestPanel extends JPanel implements MouseListener {

	private static final int WIDTH = 200;
	private static final int HEIGHT = 200;

	private int x, y;

	ArrayList<Point> points = new ArrayList<Point>();

	public MouseTestPanel() {
		addMouseListener(this);
		setPreferredSize(new Dimension(WIDTH, HEIGHT));
	}

	@Override
	public void mouseClicked(MouseEvent e) {
	}

	@Override
	public void mouseEntered(MouseEvent e) {
	}

	@Override
	public void mouseExited(MouseEvent e) {
	}

	@Override
	public void mousePressed(MouseEvent e) {
		x = e.getX();
		y = e.getY();
		points.add(new Point(x, y));
		repaint();
	}

	@Override
	public void mouseReleased(MouseEvent e) {
	}

	@Override
	protected void paintComponent(Graphics g) {
		super.paintComponent(g);
		Graphics2D g2d = (Graphics2D) g;
		g2d.setColor(Color.WHITE);
		g2d.fillRect(0, 0, WIDTH, HEIGHT);
		g2d.setColor(Color.BLACK);
		drawRectangles(g2d);
	}

	private void drawRectangles(Graphics2D g2d) {
		int x, y;
		for(Point p: points) {
			x = (int)p.getX();
			y = (int)p.getY();
			g2d.fillRect(x, y, 10, 10);
		}
	}
}

Ponieważ kolekcje nie były jeszcze szczegółowo omawiane, a o typach generycznych było jedynie kilka wzmianek jestem winny wam pewne objaśnienie co do części kodu:

ArrayList<Point> points = new ArrayList<Point>();

Tworzymy tutaj dynamiczną strukturę danych. Najprościej rzecz ujmując jest to tablica, która potrafi sama zwiększać swój rozmiar, gdy zajdzie taka potrzeba. Klasa ArrayList jest częścią frameworka Collections, zawierającego kilka innych struktur.

Dzięki temu, że dodaliśmy jej konkretny typ (tzw typ generyczny), wiemy jakie obiekty będziemy przechowywać - w naszym przypadku Point. Więcej na ten temat możecie przeczytać w tej lekcji.

Wracając do naszego panelu, zmiana polega na tym, że teraz po każdym kliknięciu myszy prze użytkownika dodajemy do listy points nowy obiekt typu Point (położenie punktu na płaszczyźnie), a nie tak jak wcześniej od razu go rysujemy. Stworzyliśmy pomocniczą metodę drawRectangles(), w której przy pomocy pętli for each rysujemy wszystkie nasze kwadraty na podstawie punktów znajdujących się w kolekcji (ArrayList implementuje interfejs Iterable, dzięki czemu jest to możliwe). W metodzie paintComponent() przekazujemy jej obiekt typu Graphics2D związany z danym kontekstem, dzięki któremu możemy rysować. Wyrzuciliśmy z kodu również zmienną blokującą rysowanie kwadracika po starcie systemu, ponieważ teraz rysujemy obiekty z kolekcji, która na początku jest domyślnie pusta.

Zadania do samodzielnego wykonania:

4.4 Dodaj możliwość usuwania narysowanych kształtów za pomocą prawego przycisku myszy. Dodaj także możliwość przesuwania wcześniej narysowanych kształtów.

Podpowiedź 1: Klasa MouseEvent posiada metodę getButton(), która zwraca kod wciśniętego klawisza. Są w niej również zdefiniowane odpowiednie stałe, z którymi można porównywać wynik zwrócony przez metodę getButton(), np BUTTON1 ...

Podpowiedź 2: Przy przesuwaniu elementów wykorzystaj interfejs MouseMotionListener i metodę mouseDragged().

Odpowiedź.

<- Poprzednia Lekcja | Następna Lekcja ->

Komentarze

Seb

A u mnie występuje problem z pętlą:
for (Point p : points) {
i wyświetla komunikat:
Exception in thread "AWT-EventQueue-0" java.lang.Error: Unresolved compilation problem:
Type mismatch: cannot convert from element type Object to Point

Po uruchomieniu programu wyświetla się forma ale z przezroczystym tłem i na tym koniec

Seb

OK. Nieuważnie przeczytałem opis. W listingach faktycznie jest błąd:
ArrayList points = new ArrayList();
ale potem jest opisany w artykule:
ArrayList points = new ArrayList();

Slawek

Można wiedzieć, gdzie jest ten błąd, bo nie mogę się dopatrzeć?
Dzięki :)

kris_IV

Ja też nie widzę, ale przy kopiowaniu nie działało :)

Seb

Eclipse wywala mi blędy przy:
ArrayList points = new ArrayList();
a dzała dopiero tak:
ArrayList <Point> points = new ArrayList <Point>();

Slawek

ah ok, w sumie moje niedopatrzenie, bo w swoim eclipse wyłączyłem sprawdzanie takich błędów. Dzięki jeszcze raz.

E30Tomas

15 ArrayList points = new ArrayList(); <-- point jest z małej litery, a działa dopiero przy dużej.
ArrayList points = new ArrayList();

64 } <--- działa bez ""

(Z pierwszego panelu z rysowaniem)

E30Tomas

Edytor tekstu wycina wstawkę z "Point" jak są ostre nawiasy.
Powinno być tak (zamiast " ostry nawias)
ArrayList"Point" points = new ArrayList"Point"();

marcin

Rozumiem kod i pojmuję, że metoda "paintComponent(Graphics g)" służy nam, w skrócie, do rysowania. Jednak nie mogę się dopatrzeć i zrozumieć jak to jest, że program działa, mimo że ta metoda nie jest nigdzie wywoływana.. może mi to ktoś wytłumaczyć?

Hekkaryk

Główna klasa rozszerza JPanel: "extends JPanel implements MouseListener".
W konstruktorze tworzysz nowy JPanel:
"public MouseTestPanel() {
addMouseListener(this);
setPreferredSize(new Dimension(DEFAULT_WIDTH, DEFAULT_HEIGHT));
}"
...który automatycznie wywołuję metodę paintComponent(Graphics g). Przynajmniej takie jest moje zrozumienie tegoż :P

folcrum

Witam serdecznie i prosze o pomoc. Napisalem sobie programik obsługujący zdarzenia myszy. Problem polega na tym, że pisany w NetBeansie i tam uruchamiany dzieła poprawnie. Ale gdy staram się odpalić go z poza NetBeansa lub na innym komputerze wyskakuje mi błąd : "a java exception has occurred" i za nic na świecie nie wiem o co biega.
Oto kod programu:

import java.awt.event.*;
import java.awt.event.MouseEvent;
import javax.swing.JApplet;
import java.awt.*;

public class Oczko extends JApplet implements MouseListener, MouseMotionListener
{
boolean otwarte;
int x,y;
int srx;
int sry;

@Override
public void mouseClicked(MouseEvent e) {
}

@Override
public void mousePressed(MouseEvent e) {
}

@Override
public void mouseReleased(MouseEvent e) {
}

@Override
public void mouseDragged(MouseEvent e) {
}

@Override
public void mouseEntered(MouseEvent e){
this.otwarte = true;
this.repaint();
}

@Override
public void mouseExited(MouseEvent e){
this.otwarte = false;
this.repaint();
}

@Override
public void mouseMoved(MouseEvent e){
this.x = e.getX();
this.y = e.getY();
this.repaint();
}

@Override
public void init(){

setSize(500,500);
this.addMouseListener(this); // ta metoda wlacza "nasluch" na przyciski myszki
this.addMouseMotionListener(this); // ta metoda wlacza "nasluch" na ruchy myszki
otwarte = false;
}

@Override
public void paint(Graphics g){

int wspx = 250;
int wspy = 250;
double promienoka = 100;
double promienmyszy;
double c;

g.setColor(Color.red);
g.fillOval(100, 100, 300, 300);
promienmyszy = Math.sqrt(Math.pow(wspx-x,2)+Math.pow(wspy-y, 2));

if (otwarte==true){
g.setColor(Color.yellow);
g.fillOval(150, 150, 200, 200);
g.setColor(Color.black);
g.drawOval(150, 150, 200, 200);

if (promienokapromienmyszy+13) {

g.setColor(Color.green);
g.fillOval(x-13, y-13, 26, 26);
}

}

if (otwarte == false){
g.setColor(Color.red);
g.fillOval(150, 150, 200, 200);
g.setColor(Color.black);
g.drawOval(150, 150, 200, 200);
}
}
}

Biko

A ja sobie zrobiłem tak:
@Override
public void mousePressed(MouseEvent e) {
x = e.getX();
y = e.getY();
paintPoint();
}
private void paintPoint() {
Graphics2D g2d = (Graphics2D) getComponentGraphics(getGraphics());
g2d.setColor(Color.black);
g2d.fillRect(x, y, 5, 5);
}
żeby nie rysować po każdym punkcie wszystkiego od nowa i nie tworzyć listy.
W związku z tym mam pytanie. Czy jest jakieś uzasadnienie (best practice czy coś w tym stylu) do stosowania metody repaint()?

Slawek

Co do repaint() - wywołujemy ją tylko wtedy, gdy to niezbędne. Nie jest wskazane wywoływać ją na przykład w każdym przebiegu jakiejś pętli (chyba, że to uzasadnione). Nie wywołujemy jej też wewnątrz metody paintComponent, bo się oczywiście zapętlimy.

Co do Twojego rozwiązania ma jedną kluczową wadę - nie przechowujesz nigdzie punktów. Spróbuj rozszerzyć okienko - wszystkie punkty znikną :) Dodatkowo gdybyś chciał dorobić opcję usuwania narysowanych kształtów - byłby problem.
W rozwiązaniu zaprezentowanym na stronie można by wprowadzić oczywiście usprawnienie, żeby nie odrysowywać wszystkich punktów za każdy nowym kliknięciem - podwójne buforowanie. Czyli zapamiętujemy stan kanwy i dorysowujemy na niej tylko nowy kształt.

javalamer

Czyli najlepiej byłoby zostawić paintComponent bez zmian, ale pod mousePressed podpiąć rysowanie pojedynczego punktu zamiast repaint.

Wiki

Napisałeś, że między ostatnimi dwoma klasami MouseTestPanel zaszła jakaś zmiana.. Ale oprócz ucięcia pierwszej linijki, która była niepotrzebna i zmiany nazwy dwóch zmiennych to nie widzę miedzy nimi różnicy..

Problem

Mam pytanie-jak sprawić, by coś działo się po określonym czasie?

Solo

WitamRobiłem tak jak napisałeś, ale efekt jest taki sam jak opisałem wyżej, nadal nie moge usunąć kwraadtu. Może nie w tym miejsu wklejam tą linijkę kodu co podałeś. Nie bardzo rozumię napisz np: nr. 4 . , ale gdzie mam to wpisać bo poprostu nie wiem, możesz mi ty wytłumaczyć tak łopatologicznie, będę wdzięczny. Kod jest taki sam jak w zadanmiu.Pozdrawiam

krzysiek

Z tą myszką można też zrobić troszkę inaczej. Zamiast implementować interfejsy MouseListener i MouseMotionListener i potem nadpisywać łącznie 7 metod (czy są nam one potrzebne czy nie), to można poprostu stworzyć wewnętrzną klasę dziedziczącą z klasy MouseInputAdapter i nadpisać tylko te metody które nas interesują. Przykładowo potrzebujemy tylko metodę mousePressed:

class Mouse extends MouseInputAdapter{
public void mousePressed(MouseEvent e){
System.out.println("Metoda \"mousePresssed\"");
}
}

addMouseListener(new Mouse());

Rafał

W trzecim bloku kodu (licząc od góry) jest błąd.
ArrayList points = new ArrayList();
słowo "point" powinno być z dużej litery. Dla początkujących - takich jak ja znalezienie tego błędu może być problemem.

Borys

Witam!
Próbuję napisać program, który będzie niejako wyzwalał obrazek kliknięciem myszki, tzn. kliknięcie na panelu ma powodować pojawienie się w tym miejscu obrazka pobranego z dysku.
Logicznym wydaje się zaimplementowanie MouseListenera i umieszczenie w nim metody wczytującej i wyświetlającej dany obrazek, jednakże próbuję i próbuję na różne sposoby i nic nie działa. Może coś robię źle, może mam zupełnie błędne podejście... W każdym razie proszę o pomoc - Może ktos umiałby napisać taki kod?
Pozdrawiam!

Robert

Witam.
Mam 2 pytania:
1. Cytat z lekcji powyżej:

"Dodatkowo stworzyliśmy zmienną typu boolean, dzięki której kwadracik nie jest rysowany po starcie programu w punkcie (0,0), czyli domyślnych wartości x i y."

W którym miejscu jest ta zmienna?

2. Fragment kodu:
for (Point p : points) {
...
}
Jak mam to rozumieć (konkretnie to 'p: points'? Domyślam się mniej więcej jak to działa, tylko czy można to jakoś bardziej "łopatologicznie" rozpisać?

Pozdrawiam

Sławek Ludwiczak

Hej, booleana mogło wciąć z powodu małej awarii formatowania kodu na stronie, będę musiał zweryfikować przykład.
for(Point p: points)...
możesz rozumieć identycznie jak:


Point p = null;
for(int i=0; i<points.size(); i++) {
p = points.get(i);
...
}
tochur

Jeślichodzi o ten artuykuł to dwarazy pojawił się ten sam kod ( w drugim przykładzie jest od razu podany sposób z przechowywaniem punktów w liście Array) A nie ma natomiast kodu z przykładu w którym to po każdym kliknięciu miał się pokazywać jeden kwadracik i znikać).
Co do kursu, jest on prowadzony na naprawdę dobrym poziomie, dziękuję za poświęcony czas Panie Sławku.