Szkolenia programowania we Wrocławiu
Kurs Java Podstawy - rozszerzony

Pola tekstowe

Wstęp

Programy, które piszemy prawie zawsze muszą przyjmować od użytkownika jakieś dane. Najprościej wprowadzać je oczywiście z klawiatury do jakiegoś pola tekstowego. Do tej pory nauczyliśmy się w jaki sposób odczytywać dane z klawiatury, ale tylko w konsoli. W przypadku programów z interfejsem graficznym będzie to wyglądało zupełnie inaczej - czy trudniej, czy łatwiej trudno ocenić, wszystko zależy od zastosowania. W tej lekcji dowiesz się jakie istnieją komponenty tekstowe i jak ich używać w swoich programach.

Przegląd komponentów tekstowych

Komponenty tekstowe można podzielić zasadniczo na dwa główne typy:

  • Komponenty, do których można wprowadzać jedynie "czysty" tekst: JTextField, JFormattedTextField, JPasswordField, JTextArea
  • Komponenty, które dodatkowo obsługują różne style oraz można w nich wyświetlać obrazki: JTextPane, JEditorPane

Pierwsza grupa jest najczęściej stosowana, a konkretnie JTextField i JTextArea. Odróżnia je to, że pierwszy z nich pozwala odczytywać dane w jednej linii tekstu (przykładowo imię, nazwisko itp.), JTextArea z kolei może mieć większe rozmiary, więc świetnie nada się do odbierania większych porcji tekstu. Wymieniono tu również JPasswordField - pole do wprowadzania hasła, które automatycznie zamienia wpisywane znaki na kropi, lub ustalone przez nas znaki. JFormattedField pozwoli z kolei odczytywać dane w ustalonej przez nas formie.

W drugiej grupie znajdują się elementy, które obsługują dodatkowo podstawowe znaczniki języka HTML. Można z dzięki nim zrobić w bardzo prosty sposób przeglądarkę prostych stron. Należy jednak uważać, ponieważ obsługiwane są znaczniki jedynie do wersji HTML 3.2 - nie poszalejemy więc za bardzo.

Każdy z komponentów możemy utworzyć za pomocą podstawowego konstruktora, niektóre z nich posiadają natomiast kilka dodatkowych wersji - przykładowo wyświetlających jakiś domyślny tekst.

Pobieranie danych z komponentu

Przypomnimy, że w przypadku, gdy chcieliśmy odbierać dane od użytkownika z konsoli korzystaliśmy z klasy Scanner, lub mogliśmy skorzystać z klasy BufferedReader. W przypadku komponentów tekstowych sprawa jest nieco łatwiejsza - wystarczy wpisać w ich obszarze odpowiedni tekst, który następnie możemy w dowolnym momencie pobrać za pomocą metody getText(). Oczywiście później należy go zazwyczaj i tak w odpowiedni sposób podzielić, i wyciągnąć to co nas interesuje i tu także będziemy musieli wykorzystać klasę Scanner, lub BufferedReader. Przykładowy fragment kodu przedstawiający w jaki sposób to zrobić:

JTextField nameField = new JTextField("Imię");
//tutaj coś robimy
String imie = nameField.getText();

Jak widać jest to dosyć banalne, czas więc na przykład, który nie ukrywam - będzie nieco skomplikowany.

Praktyczny przykład

Napiszemy tym razem program, który będzie bardzo prostym edytorem kodu języka HTML z możliwością podglądu rezultatów. Żeby zobaczyć działanie kilku komponentów dodamy prosty moduł logowania z wykorzystaniem JTextField i JPasswordField.

Zacznijmy od stworzenia modułu logowania - będzie to po prostu oddzielny panel z dwoma polami do wprowadzania tekstu, jedno typu JTextField, drugie JPasswordField, a do tego przycisk, który pozwoli na weryfikację danych i zalogowanie do dalszej części programu.

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;

import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;

public class LoginPanel extends JPanel {
	private JTextField nameField; //pole na nazwę
	private JPasswordField passField; //pole na hasło
	private JButton loginButton; //przycisk logowania
	private LoginListener listener; //słuchacz przycisku

	/**
	 * @return wprowadzona nazwa użytkownika
	 */
	public String getName() {
		return nameField.getText();
	}

	/**
	 * @return wprowadzone przez użytkownika hasło
	 */
	public String getPassword() {
		String password = "";
		char[] pass = passField.getPassword();
		for(int i=0; i<pass.length; i++) {
			password += pass[i];
		}
		return password;
	}

	public LoginPanel(LoginListener listener) {
		super();
		// ustawiamy layout
		GridBagLayout gridBag = new GridBagLayout();
		GridBagConstraints constraints = new GridBagConstraints();
		constraints.fill = GridBagConstraints.CENTER;
		gridBag.setConstraints(this, constraints);
		setLayout(gridBag);
		// tworzymy komponenty logowania
		this.listener = listener;
		this.listener.setPanel(this);
		createComponents();
	}

	/**
	 * Metoda, która tworzy etykiety i pola do wprowadzania danych.
	 */
	private void createComponents() {
		JLabel name = new JLabel("Name: ");
		JLabel password = new JLabel("Password: ");
		nameField = new JTextField();
		passField = new JPasswordField();

		//pomocniczy panel do wprowadzania danych
		JPanel inputPanel = new JPanel();
		inputPanel.setLayout(new GridLayout(2, 2));
		inputPanel.add(name);
		inputPanel.add(nameField);
		inputPanel.add(password);
		inputPanel.add(passField);
		//tworzymy przycisk logowania
		loginButton = new JButton("Zaloguj");
		loginButton.addActionListener(listener);

		//pomocniczy panel do wyśrodkowania elementów
		JPanel parentPanel = new JPanel();
		parentPanel.setLayout(new BorderLayout());
		parentPanel.add(inputPanel, BorderLayout.CENTER);
		parentPanel.add(loginButton, BorderLayout.SOUTH);

		// dodajemy do głównego panelu
		this.add(parentPanel);
	}
}

Z ciekawszych rzeczy w tej klasie warto zauważyć, że przy klasie JPasswordField metoda getText() oznaczona jest jako deprecated - zamiast jej powinniśmy używać metody getPassword() zwracającej tablicę znaków - niestety musimy z tego później skleić Stringa. Ponieważ chciałem, aby wszystkie elementy znalazły się dokładnie po środku posłużyłem się LayoutManagerem - GridBagLayout. Niestety to pierwsze co przyszło mi do głowy, co niestety nie jest piękne. Jeżeli ktoś ma ciekawszy pomysł na rozwiązanie tego "problemu" to chętnie zobaczę i skorzystam.

W następnej kolejności stwórzmy prostą klasę do autoryzacji, udostępnia ona jedynie prostą metodę, która sprawdza, czy wprowadzono poprawną nazwę użytkownika i hasło. W przypadku powyższego panelu mogliśmy pokusić się o wyświetlanie komunikatów o błędach - zostawiam to jednak wam.

public class UserValidator {
	private static final String name = "tajniak";
	private static final String password = "1234";

	public static boolean authenticate(String name, String password) {
		if(UserValidator.name.equals(name) & UserValidator.password.equals(password))
			return true;
		else
			return false;
	}
}

Teraz utwórzmy panel do obsługi kodu html. Koncepcja jest taka, żeby po lewej stronie wpisywać kod, a po prawej stronie otrzymać jego podgląd po wciśnięciu przycisku.

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

import javax.swing.JButton;
import javax.swing.JEditorPane;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;

public class HTMLPanel extends JPanel {
	//pole do wpisywania kodu html
	private final JTextArea textArea = new JTextArea();
	//pole z wygenerowanym kodem
	private final JEditorPane editorPane = new JEditorPane();

	public HTMLPanel() {
		super();
		setLayout(new BorderLayout());
		createPanels();
	}

	private void createPanels() {
		//nie chcemy, aby można było edytować wygenerowany html
		editorPane.setEditable(false);
		//ustawiamy nasz editorPane, aby rozpoznawa znaczniki html
		editorPane.setContentType("text/html");
		//przycisk generowania podglądu
		JButton actionButton = new JButton("Podgląd");
		actionButton.addActionListener(new ConvertListener());
		//panel pomocniczy do rozkładu elementów
		JPanel helpPanel = new JPanel();
		helpPanel.setLayout(new GridLayout(1, 2));
		textArea.setBackground(Color.lightGray);
		//dodajemy komponenty tekstowe do pomocniczego panelu
		helpPanel.add(textArea);
		helpPanel.add(editorPane);
		//dodajemy wszystko do głównego panelu
		this.add(helpPanel, BorderLayout.CENTER);
		this.add(actionButton, BorderLayout.SOUTH);
	}

	class ConvertListener implements ActionListener {
		@Override
		public void actionPerformed(ActionEvent event) {
			//zmiany wyglądu wywołujemy w wątku dystrybucji zdarzeń
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					String text = textArea.getText();
					editorPane.setText(text);
					editorPane.revalidate();
				}
			});
		}
	}
}

Używamy dwóch komponentów:

  • JTextField - pole, w którym będziemy wprowadzać nasz kod html w formie czystego tekstu
  • JEditorPane - panel, który pozwala na wyświetlanie kodu zawierającego znaczniki HTML 3.2, a także obrazki. Wyłączyliśmy możliwość edytowania po tej stronie za pomocą metody sedEditable(false), oraz ustawiliśmy rodzaj contentu jako kod html (text/html).

Ciekawie wygląda również sama obsługa zdarzenia przycisku. Pojawia się tutaj konstrukcja podobna do tej, której zawsze używamy do tworzenia głównej ramki programu. W zasadzie jest to dokładnie to samo, zmiany w komponentach powinniśmy wykonywać w wątku dystrybucji zdarzeń. Jest to spowodowane tym, że powinniśmy się kierować zasadą "nigdy nie blokuj dostępu do GUI" - przecież nikt z nas nie lubi, gdy program nie odpowiada na kompletnie żadne działanie. W tym wypadku nie będzie to miało może specjalnie widzialnego efektu, ale gdy po wciśnięciu przycisku wykonujemy jakieś obliczenia to powinny one być wykonywane w oddzielnym wątku, aby nie blokować interfejsu graficznego.

Zbliżając się do końca potrzebujemy jeszcze klasy nasłuchującej na wciśnięcie przycisku logowania. Podobnie jak przed chwilą przebudowę interfejsu obsłużymy w wątku dystrybucji zdarzeń.

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

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

public class LoginListener implements ActionListener {
	//Główna ramka programu
	private final JFrame frame;
	//Panel logowania, potrzebny do pobrania loginu i hasła
	private LoginPanel loginPanel;

	public void setPanel(LoginPanel loginPanel) {
		this.loginPanel = loginPanel;
	}

	public LoginListener(JFrame frame) {
		this.frame = frame;
	}

	@Override
	public void actionPerformed(ActionEvent event) {
		String name = loginPanel.getName();
		String password = loginPanel.getPassword();
		if (UserValidator.authenticate(name, password)) {
			SwingUtilities.invokeLater(new Runnable() {
				@Override
				public void run() {
					// panel z edytorem html
					JPanel htmlPanel = new HTMLPanel();
					// usuwamy panel logowania
					frame.getContentPane().removeAll();
					// dodajemy panel html i odświeżamy widok
					frame.add(htmlPanel);
					frame.validate();
				}
			});
		}
	}
}

W klasie tej musimy przechowywać referencję do ramki, aby usunąć z niej panel logowania, utworzony na początku naszego przykładu. Robimy to jednak dopiero po zweryfikowaniu loginu i hasła - dlatego w klasie LoginPanel przekazaliśmy referencję this do klasy LoginListener za pomocą metody setPanel(). Po zweryfikowaniu loginu i hasła usuwamy z ramki panel logowania i dodajemy nowy panel obsługi html. Dodatkowo podczas tej operacji praktycznie wszystkie komponenty tracą swoją ważność, należy je więc odświeżyć za pomocą metody validate(). Dzięki wywołaniu jej na obiekcie frame odnowimy praktycznie wszystko co nas interesuje, ponieważ odnawia ona również komponenty podrzędne (podobny efekt uzyskuje się na przykład rozszerzając okno programu).

Ostatnia rzecz jaka nam została to stworzenie klasy ramki programu i klasy startowej - nie ma w nich już nic nowego. Po prostu tworzymy LoginListener, który przekazujemy następnie w konstruktorze do obiektu LoginPanel.

import java.awt.Dimension;

import javax.swing.JFrame;
import javax.swing.JPanel;

public class Frame extends JFrame {
	public Frame() {
		super("Komponenty tekstowe");
		LoginListener listener = new LoginListener(this);
		JPanel loginPanel = new LoginPanel(listener);
		add(loginPanel);

		setPreferredSize(new Dimension(600, 400));
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		pack();
		setVisible(true);
	}
}

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

Ostatecznie powinniśmy uzyskać efekt jak na poniższym obrazku:

wykorzystanie komponentów tekstowych, jtextfield, jpasswordfield, jtextarea i jeditedfield do prostego edytora kodu HTML

4.5 Napisz program, który będzie potrafił stare strony HTML, na przykład kultową stronę kosmicznego meczu. Podpowiedź: Wykorzystaj odpowiedni konstruktor klasy JEditorPane i zapoznaj się z klasą URL.

4.6 Do przykładu z lekcji dodaj funkcjonalność polegającą na tym, że podgląd będzie generowany na bieżąco wraz z wprowadzanym przez użytkownika tekstem.

Rozwiązanie

Komentarze

Komentarze zamknięte. Zapraszamy do grupy na Facebooku
Ype

taki szczegół, w ostatniej klasie zapomniałeś wpisać importować eventquque, a w przedostatniej pojawiło się jakieś name="code"
w public name="code" class Frame extends JFrame {

Steve

Mam mały problem, próbuję napisać program który posiada 3 przyciski, jedno pole tekstowe i jedną etykietę. Do pola ma zostać wprowadzona liczba, przycisk pierwszy ma ją zapisać od końca(1234->4321), drugi ma ją zamienić na system ósemkowy, a trzeci czyscic pole tekstowe.
Jak najlepiej zczytać liczbę z pola tekstowego i przeprowadzić na niej operacje? Funkcje getTxt() i setTxt operują tylko na stringach.
Po użyciu Integer.valueOf() program się sypie.

Steve

Literówki przy get.Text() i setText() - nie przez to program nie działa :D

znik

hmmm..... coś takiego jak char[] pass = passField.getPassword();
chyba ma swoją przyczynę. zwykły get.Test umieszcza hasło w String, czyli w jednym miejscu. i można próbować szukać hasła przez skanowanie przestrzeni procesu.
a tablica znaków, to nigdy nie wiadomo gdzie który znak jest. Możliwe że getPassword rozrzuca znaki po różnych obiektach aby uprzykrzyć życie hackerom.

Ziolo

Od początku nie podobał mi się tutaj gridbag layout i szukałem jakby zrobić to inaczej. W końcu znalazłem, z użyciem Box layout i CENTER_ALIGNMENT:

public LoginPanel(LoginListener listener){
super();
setLayout(new BorderLayout());
this.listener = listener;
this.listener.setPanel(this);
JPanel parentPanel = ParentPanel();
add(parentPanel, BorderLayout.CENTER);

}
JPanel ParentPanel = new JPanel();
JPanel ParentPanel() {
Dimension dimension = new Dimension (100,30);
JLabel name = new JLabel("Name:");
JLabel password = new JLabel("Password: ");
nameField = new JTextField();
passField = new JPasswordField();

JPanel inputPanel = new JPanel();
inputPanel.setLayout(new BoxLayout(inputPanel ,BoxLayout.Y_AXIS));
inputPanel.add(name);
name.setPreferredSize(dimension);
name.setAlignmentX(CENTER_ALIGNMENT);
inputPanel.add(nameField);
nameField.setMaximumSize(dimension);
inputPanel.add(password);
password.setAlignmentX(CENTER_ALIGNMENT);
inputPanel.add(passField);
passField.setMaximumSize(dimension);
inputPanel.setMaximumSize(new Dimension(100,120));

Jack

O co właściwie chodzi z tym panelem pomocniczym?

tam

Brakuje rozwiązania 4.5 i tekst zadania jest niejasny.

Kowalgp

Poproszę o rozwiązanie 4.5, bo próbuje i wciąż mi nie wychodzi.

gunner

Witam, zrobiłem program aplikacje okienkowa która posiada 4 pola ( imie, nazwisko ..) i dwa przyciski Zapisz i Odczyt, i nie mogę dojść jak zrobić, aby przy kliknięciu Zapisz zapisały się wpisane dane do pliku txt. Ma ktoś jakiś pomysł?

Damian

We wstępie pisałeś że pokażesz jak zrobić prostą grę... będą dalsze artykuły pokazujące jak stworzyć grę w javie?

Seba

Zamotany kod tutaj jest nieprzeciętnie, nie da sie w niczym polapac, jak można tak krótki kod rozbijać na tyle klas, UserValidator to juz w ogole jest jakas masakra jesli chodzi o brak poszanowania dla OOP.
Dla osob ktore zaczynaja programowac polecam stworzenie samemu panelu odpowiadajacego za login - gdy login sie zgadza wtedy czyscisz caly frame i dodajesz inny, z htmlem.