Kurs Java Podstawy - rozszerzony

Obsługa zdarzeń - Przyciski

Poznaliśmy bardzo podstawowy schemat tworzenia aplikacji graficznych w języku Java. Wiemy jak narysować  proste kształty, jak dodać przyciski i mniej więcej na czym polegają zarządcy rozkładu. Od naszego programu oczekujemy jednak zapewne czegoś więcej niż tylko przycisków, które nic nie robią i możliwości narysowania kilku kwadracików, z którymi także nic nie da się zrobić. W bibliotece AWT istnieje jednak bardzo przyjemny mechanizm obsługi zdarzeń, który pozwoli nam pisać programy odpowiadające na czynności wykonywane przez użytkownika.

Zdarzenia generowane podczas działania programów pozwolą nam między innymi na:

  • obsługę wciśnięcia przycisku
  • możliwość odczytywania położenia kursora
  • odczytywania jaki przycisk został wciśnięty na klawiaturze, lub myszy
  • i wiele innych rzeczy, które można sobie wyobrazić w programie

W naszej aplikacji napisanej w Javie musimy stworzyć w tym celu kilka rzeczy. Nie wystarczy wywołać jakiejś metody na rzecz konkretnego obiektu (na przykład przycisku), bo przecież musimy wiedzieć, że ten przycisk może być wciśnięty, musimy też wiedzieć, co ma się stać, gdy to nastąpi. Pierwszym krokiem będzie więc utworzenie słuchacza, czyli obiektu klasy implementującej interfejs ActionListener. Najprostsza z możliwych klas przycisku, który pozwoli nam na jakiekolwiek działanie będzie więc wyglądała tak:

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;

public class Przycisk extends JButton implements ActionListener{
	@Override
	public void actionPerformed(ActionEvent arg0) {
		// TODO Auto-generated method stub
	}
}

Jak widać w związku z implementowaniem interfejsu ActionListener musieliśmy również utworzyć metodę actionPerformed(). Przyjmuje ona jakiś parametr, jednak skąd go wziąć i jak taką metodę wywołać? No więc najciekawsze jest to, że my w zasadzie nie musimy się tym przejmować. Metoda ta jest wywoływana, kiedy zostanie wygenerowane zdarzenie na obiekcie powiązanym z danym słuchaczem. Może to być przycisk, pole tekstowe, checkbox, czy suwak. Przekazywany argument ActionEvent przechowuje wiele informacji w tym źródło, które możemy pobrać przy pomocy metody getSource(). Żeby zobaczyć to na jakimś życiowym przykładzie poniżej zamieszczono klasy programu, który zmienia tło po wciśnięciu jednego z trzech przycisków.

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JPanel;

public class ButtonPanel extends JPanel implements ActionListener{

	public static final int HEIGHT = 100;
	public static final int WIDTH = 300;
	private JButton greenButton;
	private JButton blueButton;
	private JButton redButton;

	public ButtonPanel() {
		greenButton = new JButton("Green");
		blueButton = new JButton("Blue");
		redButton = new JButton("Red");

		greenButton.addActionListener(this);
		blueButton.addActionListener(this);
		redButton.addActionListener(this);

		setLayout(new FlowLayout());
		setPreferredSize(new Dimension(WIDTH, HEIGHT));
		add(greenButton);
		add(blueButton);
		add(redButton);
	}

	@Override
	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();

		if(source == greenButton)
			setBackground(Color.GREEN);

		else if(source == blueButton)
			setBackground(Color.BLUE);

		else if(source == redButton)
			setBackground(Color.RED);
	}
}

import javax.swing.*;

public class ActionFrame extends JFrame {

	public ActionFrame() {
		super("Test akcji");

		JPanel buttonPanel = new ButtonPanel();
		add(buttonPanel);

		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}
}

import java.awt.EventQueue;

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

Jak widać ciekawa jest tylko pierwsza klasa, w drugiej i trzeciej nie odnajdujemy nic nowego. Co więc się kolejno dzieje. Utworzyliśmy dwie stałe oznaczające wysokość i szerokość naszego panelu, a także trzy przyciski, odpowiedzialne odpowiednio za zmianę koloru na, zielony(greenButton), niebieski(blueButton) i czerwony(redButton). Jak widać rozwiązanie jest jednak nieco inne niż prezentowane na początku lekcji. Tym razem słuchaczem jest cały kontener komponentów (w naszym przypadku panel, a może nim być także obiekt JComponent, czy nawet ramka dziedzicząca po klasie JFrame), natomiast źródłami trzy przyciski. Dzięki dodaniu słuchacza do każdego z przycisków (w naszym przypadku słuchaczem jest panel, więc przekazujemy this):

	greenButton.addActionListener(this);
	blueButton.addActionListener(this);
	redButton.addActionListener(this);

nasz panel będzie teraz powiadamiany za każdym razem kiedy będzie wciśnięty jeden z przycisków i zostanie wtedy wywołana jego metoda actionPerformed(). Można sobie to najprościej wyobrazić jako krótką konwersację:

-Ej wy przyciski, wiem, że możecie być wciskane i powiadamiać o tym innych

-Jasne podaj nam tylko jakiś obiekt implementujący interfejs ActionListener, a gdy ktoś nas wciśnie to damy mu znać. Oczywiście może być ich wiele, wystarczy, że kilka razy wywołasz naszą metodę addActionListener()

-Ok przekazuję wam więc referencję klasy ButtonPanel, w której się znajdujecie. Ona będzie wiedziała co ma dalej robić, bo przeciążyłem w niej metodę actionPerformed().

W tej z kolei sprawdzamy źródło zdarzenia, pobrane metodą getSource() otrzymanego argumentu ActionEvent. Jak widać wystarczyło nam zwykłe porównanie za pomocą ==, ponieważ interesuje nas referencja, a nie to co obiekt przechowuje. Nie dalej nic innego jak zmiana koloru panelu poprzez wywołanie metody setBackground() z odpowiednią stałą koloru Color.GREEN, Color,BLUE, albo Color.RED.

Można by się zastanowić, czy nie da się w naszym przypadku całkowicie odłączyć przycisków od naszej klasy panelu. Oczywiście, że się da, jednak w tak prostym przykładzie nieco mija się to z celem. Wiąże się to, albo z utworzeniem klasy wewnętrznej do każdego przycisku, lub nawet klas zewnętrznych, którym i tak musimy przekazać wtedy referencję do panelu. Poniżej zamieszczona jest klasa panelu, który wykorzystuje rozwiązanie z klasami wewnętrznymi (działanie jest identyczne, jak programu powyżej):

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JPanel;

public class ButtonPanel extends JPanel{

	public static final int HEIGHT = 100;
	public static final int WIDTH = 300;
	private JButton greenButton;
	private JButton blueButton;
	private JButton redButton;

	private JPanel buttonPanel;

	public ButtonPanel() {
		greenButton = new GreenButton();
		blueButton = new BlueButton();
		redButton = new RedButton();

		buttonPanel = this;

		setLayout(new FlowLayout());
		setPreferredSize(new Dimension(WIDTH, HEIGHT));
		add(greenButton);
		add(blueButton);
		add(redButton);
	}

	class GreenButton extends JButton implements ActionListener {

		GreenButton() {
			super("Green");
			addActionListener(this);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			buttonPanel.setBackground(Color.GREEN);
		}
	}

	class BlueButton extends JButton implements ActionListener {

		BlueButton() {
			super("Blue");
			addActionListener(this);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			buttonPanel.setBackground(Color.BLUE);
		}
	}

	class RedButton extends JButton implements ActionListener {

		RedButton() {
			super("Red");
			addActionListener(this);
		}

		@Override
		public void actionPerformed(ActionEvent e) {
			buttonPanel.setBackground(Color.RED);
		}
	}

}

Zadanie do samodzielnego wykonania:

4.2 Napisz aplikację wykorzystującą elementy AWT oraz Swing, która zawiera trzy przyciski:

  • Pierwszy zmienia kolor tła panelu na losowy kolor: zielony, niebieski, lub czerwony.
  • Drugi przycisk zmienia własne tło na dowolny kolor zdefiniowany przez ciebie. (Wykorzystaj metodę setBackground(Color) klasy JButton)
  • Trzeci przycisk zmienia swoją etykietę na "Wciśnięty", po wciśnięciu. (Wykorzystaj metodę setText(String) klasy JButton)

Wszystkie elementy zdefiniuj w oddzielnych klasach (zewnętrznych) i skorzystaj z jednego z zarządców rozkładem.

Rozwiązanie.

<- Poprzednia LekcjaNastępna Lekcja ->

Komentarze

Tomek

Kiedy będzie można liczyć na rozwiązane, bo mam pewien problem z pierwszym punktem zadania...

Na początek chciałem zrobić zmianę tła na wyznaczony przeze mnie kolor i moja klasa wygląda tak:

import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;

class GreenButton extends JButton implements ActionListener {

GreenButton() {
super("Green");
addActionListener(this);
}

@Override
public void actionPerformed(ActionEvent e) {
obrazekPanel.setBackground(Color.GREEN);
}
}

i problem występuje z linijką
obrazekPanel.setBackground(Color.GREEN);
tak jakby nie widziało tego obrazekPanel, dodatkowo po kropce nie mam możliwości wybrania z listy setBackground, a gdy zostawiam same setBackground to zmienia mi kolor tła przycisku [trzeci punkt].

panelObrazek mam identycznie jak w programie wyżej zadeklarowane
private JPanel obrazekPanel;
i przypisane
obrazekPanel = this;

Aha no i w drugim punkcie przez setColor coś mi nie szło wiec użyłem setBackground i działa... przycisk zmienia swój kolor. Tylko dlaczego nie działa mi setColor?

Slawek

W podejściu, które wybrałeś, klasy przycisków muszą być klasami wewnętrznymi klasy panelu. Nie umieszczaj klas w oddzielnych plikach, tylko w jednym, tak jak w przykładzie. U Ciebie, klasa przycisku po prostu nic nie wie o panelu z przyciskami, musiałbyś dodatkowo przechowywać referencję do panelu, coś w stylu
JPanel buttonPanel;
void setPanel(JPanel panel) {
buttonPanel = panel;
}

z setColor to moja pomyłka, z rozpędu jakoś pisałem i coś mi się pomyliło.
Rozwiązanie niestety dopiero w piątek i wtedy też jakaś kolejna lekcja, strasznie dużo mam do zrobienia teraz.

Paweł

Po co w klasie ButtonPanel jest przypisanie do zmiennej buttonPanel = this; ?

rt

dołaczam sie do pytania :)?

garay

Witam, może ktoś będzie mi w stanie pomóc.
Problem polega na tym, iż mam trzy przyciski JButton i potrzebuje rozróżniać, że JButton1 został naciśniety jako pierwszy, potem nacisnięto JButton2, a na końcu JButton3... Wie ktoś jak to zrobić?

Slawek

Stwórz własną klasę ActionListenera, w której będziesz miał 3 pola przechowujące informację o kolejności wciśniętych przycisków (ich etykiety, numery, lub cokolwiek). Utwórz 1 obiekt takiego typu i przekaż go wszystkim 3 przyciskom w metodzie addActionListener().
Nie wiem co dalej z tym chcesz zrobić, bo może lepiej wykorzystać klasy wewnętrzne, anonimowe, a może nie ...

garay

mam 3 przyciski JButton z wczytanymi ikonami liter, przykładowo k, o, t. Chce z nich ułożyć wyraz kot w poprawnej kolejności,tzn. tylko jeśli jako pierwsze nacisnę na k to ustawiam na setVisible(false) zeby zniknął i tak z pozostałymi. Tylko wszystko musi być w odpowiedniej kolejności w innym przypadku zwracany komunikat np.to nie jest pierwsza litera wyrazu bądź to nie jest druga litera wyrazu. Nie potrafie poradzić sobie od dwóch nocy. Próbowałem normalnie z ActionListener, ActionPerformed i pobierałem getSource() ale to błędna droga jak się okazało. A Twoja podpowiedz ze stworzeniem własnej klasy ActionListener jest troche chyba ponad moje siły...

Slawek

http://javastart.pl/up/KotPanel.txt
http://javastart.pl/up/Test.txt
sprawdź :) Same przyciski, z resztą raczej sobie poradzisz.

znik

w tym przykładzie brakuje mi pewnej uniwersalności, zwłaszcza w konfrontacji z przykładem "K-O-T".
Czy nie lepiej stworzyć w TYM przypadku uniwersalną klasę której argumentem byłaby litera/kolor? w końcu te wszystkie przyciski robią to samo, więc nie wiem czemu dla każdego jest tworzona osobna klasa. jakoś mi się to kłóci z ideą programowania obiektowego.
Idąc za przykładem "K-O-T", można by tu pójść dalej i do konstruktora przekazać słowo które obskoczą przyciski :)

Slawek

Jak najbardziej bardzo dobry pomysł, wystarczyłoby wrzucić switcha w actionPerformed, a najlepiej ustawiać pole z kolorem także w konstruktorze :) Powiedziałbym, że w TYM przypadku akurat te 3 klasy nie robią wielkiej krzywdy, gdyby było ich więcej, akceptowalne byłoby tylko Twoje rozwiązanie.
Dobrze widzieć, że niektórzy czytają moje wypociny naprawdę przy tym myśląc :)

70stick

...
mam nadzieje, że dobrze wyświetli mi ten kod.
Dlaczego to nie działa? Wyskakuje błąd:
=====================================================
Exception in thread "main" java.lang.Error: Unresolved compilation problem:
The method addActionListener(ActionListener) in the type AbstractButton is not applicable for the arguments (swing2.Action)

at swing2.main(swing2.java:20)
=====================================================
Bardzo cię proszę o pomoc. (wziąłem z poradników z neta)

Slawek

brak importu ActionListenera, literówka przy metodzie actionPerformed. Wywal też ten static przy klasie wewnętrznej.

Asia

hmm... mnie rowniez wyskakuje ten blad a zupelnie nie widze zadnej literowki, co wiecej kod przekopiowalam z gotowych materialow do head first java.. pomocy ;/ bo w sieci jako powod podawaja brak importu interfejsu, ale u mnie nie jest to przyczyna...

Bede bardzo wdzieczna za pomoc ;)

Slawek

wklej kod na forum, bez tego ciężko pomóc.

Asia

w koncu wymyslilam co bylo nie tak - sama stworzylam sobie interfejs ActionListener ;)

Anonim

Jak usunąć przycisk??? Żeby nie wadził dalej w programie???

Artur

Próbuję pisać pierwsze programy z zastosowaniem modelu MVC i mam następujący problem z przyciskami. W widoku mam klasę MainView, w której to znajduje się struktura mojego okienka czyli powiedzmy trzy przyciski i jakiś tam label. W modelu siedzi sobie jedna klasa - MyDataBase, z metodami dodaj(), usun() itp. Zgodnie z zasadami MVC, chciałbym uniknąć pisania logiki w Widoku i wykorzystać do tego Controler. Owy kontroler powinien w takim razie określić zachowanie programu po naciśnięciu przycisków (np przycisk pierwszy wykonywałby metodę dodaj na obiekcie klasy MyDateBase, drugi przycisk usuń itd). Tylko że zupełnie nie mam pomysłu jak obsłużyć przyciski poza Widokiem. A może moje założenie, że tak to powinno w MVC wyglądać jest błędne?

Sławek Ludwiczak

Masz widok
Masz model
Tworzysz kontroler, w którym tworzysz instancję modelu i widoku
W kontrolerze możesz utworzyć klasę wewnętrzną implementującą ActionListener i twój konstruktor w kontrolerze powinien być np taki:


Controller() {
model = new Model();
widok = new Widok();
widok.addListener(new ButtonListener());
}

Oczywiście w widoku musisz albo stworzyć metodę addListener, która doda naszego Listenera przekazanego jako argument do przycisków, albo odwołać się do przycisków w kontrolerze i dodać je tam bezpośrednio.
(klasa Listenera nie musi być klasą wewnętrzną, wtedy przekazujesz jej w kontrolerze jako parametry widok i model, albo cały kontroler, który musi wtedy posiadać geter i seter do modelu i widoku).

Obsługa wciśnięcia przycisku w klasie implementującej ActionListenera wygląda wtedy np tak, że:

public void actionPerformed(ActionEvent action) {
if(action.getActionCommand().equals("dodaj")){
String data = widok.getUserInput();
model.dodaj(data);
} else if(...) {
...
}
}

Oczywiście możesz w jednym listenerze sobie rozpoznawać, czy wciśnięto Dodaj, czy Usuń np. ustawiając im a następnie pobierając actionCommand.
W razie dalszych problemów proponuję wkleić przynajmniej częściowy kod na forum, będzie łatwiej.
Tomek

Jedna klasa Przycisk druga ButtonPanel, raczej się powinno używać nazw albo po angielsku albo po polsku a nie jedne takie drugie takie

Sławek Ludwiczak

Pierwsza klasa to przykład i schemat, więc nie, nie powinno się jej nazywać po angielsku :)

KUX

JPanel buttonPanel = new ButtonPanel(); a nie raczej newJPanel()?

Sławek Ludwiczak

Nie, ponieważ ButtonPanel rozszerza klasę JPanel, więc jest to jak najbardziej poprawne.

gunner

Witam, mam zrobić program okienkowy który ma dwa przyciski i z czego jeden wyświetla " Kliknięto x razy" gdzie x jest zmienną zapisaną i zmienia się wraz z kliknięciem. Napisałem już tyle że wypisuje to co trzeba tylko nie zmienia tego x mimo kliknięć. Na końcu utworzyłem metodę public void paintComponent(Graphics g) wypisująca dany tekst, przy słuchaczu dla tego przycisku dodałem x++ i nie działa....

Sławek Ludwiczak

spróbuj dać repaint() w actionPerformed po zwiększeniu licznika.

gunner

Dzięki wielkie :)

Dawid

JPanel buttonPanel = new ButtonPanel();
czemu tutaj stosujemy referencje do typu JPanel, nie lepiej użyć ButtonPanel, skoro rozszerza JPanel ?

Dodatkowo czemu ma służyć takie podejście? chodzi o zaznaczenie że dany obiekt jest 'IS-A' / wywodzi się od JPanel i posiada wszystkie jego własności ?

Piotrek

Jak zrobić żeby po wyciśnięciu przycisku green on znikną?
I analogicznie po wciśnięci red i blue.

aleksanderwiel

A odnośnie mojego pytania dwa komentarze wcześniej to ktoś pomoże? Dodatkowo chciałem się dopytać o to, jak ustawić jakiś obrazek jako tło przycisku? Powiedzmy, że w gimpie przygotowałem sobie obraz przycisku i chcę go teraz dodać do przycisku.

Sławek Ludwiczak

Spróbuj poniższego:


ImageIcon image = new ImageIcon(sciezka_do_obrazka);
Button button = new JButton("",image);
aleksanderwiel

Możesz posłużyć się metodą setVisible(Tu wpisz "true" lub "false"); Metoda ta ustawia widoczność danego obiektu w aplikacji.

aleksanderwiel

Jak zrobić, aby przycisk, który jest klasą zewnętrzną mógł - gdy się go wciśnie i przytrzyma - zmienić kolor na wybrany, np. niebieski, ale po puszczeniu go z powrotem zmienić na domyślny i po najechaniu go kursorem zaznaczyć swój obszar, który potem zniknie po zdjęciu kursora (tak, jak to jest w przypadku ikon na pulpicie)? Często w różnych programach tak też jest i chciałbym umieć przypisać przyciskom podobne funkcje. Proszę o pomoc, za odpowiedź będę wdzięczny :)