Ako v Jave otvoriť a zatvoriť okno

23.1.2012

Ak nechceme programovať hru v príkazovom riadku, potrebujeme vytvoriť na obrazovke okno, v ktorom sa bude zobrazovať. (To platí aj v prípade, že sa hra zobrazuje na celú obrazovku -- z technického hľadiska je to stále okno; iba o čosi väčšie než zvyčajne.) Nové okno dokážeme otvoriť dvoma príkazmi; prvým ho vytvoríme v pamäti a druhým ho zobrazíme na obrazovke. Prečo dva príkazy? Nuž aby sme medzi nimi mohli vytvorené okno rôznymi spôsobmi upraviť, napríklad zadať mu veľkosť, nadpis, grafickú ikonu atď.

Avšak skôr než toto okno vytvoríme, zamyslime sa, čo s ním ďalej. Asi nechceme, aby bolo otvorené navždy. To je tiež v pohode, okno zatvoríme jedným príkazom. Kľúčová otázka je, kedy sa toto okno má zatvoriť? Odpovede typu "ihneď" alebo "o desať sekúnd" sa na tvorbu hier príliš nehodia. Správna odpoveď znie: "keď sa používateľ rozhodne hru ukončiť."

No hej, ale podľa čoho to zistíme?

Na rozdiel od klasického procedurálneho programovania "sprav toto, sprav tamto, teraz si vypýtaj vstup, teraz zobraz výstup" totiž programovanie v multitaskingovom prostredí funguje tak, že síce môžeme s programom spraviť čo chceme a kedy chceme, ale aj používateľ môže s myšou a klávesnicou robiť čo chce a kedy chce, bez ohľadu na to, či sa to nášmu programu hodí alebo nehodí. Operačný systém ho v tom ešte podporuje, dovoľuje mu pootvárať si ďalšie programy, hore-dole prepínať, minimalizovať; skrátka kadejako ho rozmaznáva.

Zrátajme si, koľko možností má používateľ Windowsu na ukončenie bežného programu. Môže kliknúť červenú ikonu "x" v pravom hornom rohu okna. Môže urobiť dvojklik na ikonu v ľavom hornom rohu okna. Môže pravým tlačidlom myši kliknúť na panel úloh a zvoliť možnosť "zavrieť". Môže spustiť správcu úloh, vybrať bežiacu aplikáciu a zvoliť "ukončiť úlohu". Alebo môže stlačiť kombináciu kláves Alt+F4. Nedal by som ruku do ohňa za to, že som tu vymenoval všetky možnosti. A toto všetko máme v našom skromnom programe riešiť, len aby vedel okno otvoriť a na požiadanie zase pekne zatvoriť?

Takéto veci sú už našťastie dávno vyriešené. Nech sa používateľ pokúsi zatvoriť okno ľubovoľným spôsobom, operačný systém to celé zabalí do jednej udalosti s názvom "snaha zatvoriť okno". Ak zistíme, že takáto udalosť nastala, môžeme zatvoriť okno a ukončiť program.

A prečo to vlastne Java nerobí automaticky? Nuž, niekedy je lepšie to okno hneď nezatvárať, ale chvíľu počkať. Ak pracujeme s nejakými údajmi, zrejme ich chceme pred skončením programu uložiť na disk. Možno by sme sa mali používateľa opýtať, či chce a) uložiť údaje a ukončiť program; b) iba ukončiť program a údaje zahodiť; alebo c) nič z uvedeného, lebo na tlačidlo zatvorenia okna klikol omylom a v skutočnosti chce pokračovať v načatej práci. Preto zavretie okna a ukončenie programu nie je automatickou reakciou na danú udalosť, ale najprv ju musí spracovať program.

Aha, ale ako sa o tejto udalosti vôbec dozvieme?

Dlhšia odpoveď je taká, že zájdeme do knižnice a prečítame si sedem hrubých kníh pojednávajúcich o filozofii udalosťami riadeného programovania. Krátka odpoveď je taká, že operačný systém (alebo v našom prípade skôr Java runtime) nám informácie v reálnom čase odovzdá tak, že zavolá nejakú funkciu nášho programu, ktorej ako parameter odovzdá objekt obsahujúci informácie o tom, čo presne sa stalo. Najprv však musíme príslušnú funkciu naprogramovať, a to v takom tvare, aký po nás daný systém požaduje. A potom systému oznámiť, že si želáme, aby túto našu funkciu zahrnul do svojho spravodajského systému.

A teraz sa poďme venovať technickým detailom. Po prvé, v jazyku Java nevieme systému poskytnúť doslova funkciu, pretože funkcia sa v jazyku Java nedá uložiť do premennej a poslať ďalej. (Niektoré iné jazyky takúto možnosť majú.) Musíme teda vytvoriť objekt, ktorý bude mať príslušnú funkciu; tento objekt potom zaregistrujeme ako odoberateľa udalostí, a keď daná udalosť nastane, systém zavolá príslušnú funkciu tohto objektu.

Po druhé, keď v jazyku Java chceme upresniť, ako má nejaká funkcia vyzerať, zvyčajne na to používame rozhranie. V tomto prípade ide o rozhranie "WindowListener"; kto chce dostávať udalosti týkajúce sa okien, musí toto rozhranie implementovať. Toto rozhranie má sedem funkcií (otvorenie okna, aktivovanie okna, deaktivovanie okna, minimalizácia okna, deminimalizácia okna, pokus zatvoriť okno, zatvorenie okna) a náš objekt ich musí obsahovať všetky. Ak sme leniví ich písať, pomôže nám pomocná trieda "WindowAdapter"; táto trieda obsahuje všetkých sedem funkcií a v jej verzii tieto funkcie nerobia nič; objekt sa teda dozvie o všetkých udalostiach a nijako na ne nereaguje. [Asi ako naša generálna prokuratúra na korupčné kauzy.] Náš objekt môžeme odvodiť od tejto triedy a zmeniť iba tú funkciu, ktorú naozaj chceme spracovať; v tomto prípade to bude funkcia "windowClosing" (pozor, nepomýliť si s funkciou "windowClosed"). Čo náš objekt pri volaní danej funkcie urobí? No predsa zavrie okno; o to sa tu celý čas pokúšame.

Po tretie, ak chceme zaregistrovať náš objekt ako odberateľa správ konkrétneho okna, urobíme to pomocou metódy "addWindowListener" príslušného okna. Bolo by dobré, keby si náš objekt toto okno zapamätal, nech neskôr vie, ktoré okno má zavrieť. Celý program bude teda vyzerať takto:

import java.awt.*;
import java.awt.event.*;

class PrijemcaUdalostiOkna extends WindowAdapter {
	private Frame okno;

	public PrijemcaUdalostiOkna(Frame okno) {
		this.okno = okno;
	}

	@Override
	public void windowClosing(WindowEvent e) {
		okno.dispose();
	}

}

public class Program {

	public static void main(String[] args) {
		Frame okno = new Frame();
		okno.setTitle("Pokusne okno");
		okno.setSize(400, 300);
		okno.setVisible(true);
		okno.addWindowListener(new PrijemcaUdalostiOkna(okno));
	}

}

Trieda "Frame" znamená okno, presnejšie hlavné okno aplikácie, ktoré sa zobrazuje v paneli úloh. (Ak v programe otvárame menšie okná, tie sa robia pomocou triedy "Dialog". Okrem toho ešte existuje knižnica Swing na zvlášť vymakané okná.) Trieda "PrijemcaUdalostiOkna" slúži ako príjemca udalostí; ako parameter konštruktora dostane odkaz na okno, tento odkaz si uloží do svojej premennej a pri zavolaní funkcie "windowClosing" ho zavrie. Hlavný program, príznačne nazvaný "Program", vytvorí okno, nastaví mu pár vlastností; z nich najdôležitejšia je "setVisible(true)", lebo až vtedy sa okno naozaj zjaví na obrazovke. V poslednom riadku vytvoríme pomocný objekt typu "PrijemcaUdalostiOkna" a zaregistrujeme ho ako odberateľa udalostí z tohto okna. Tým funkcia "main" končí, ale samotný program tým ešte neskončil, pretože v jazyku Java program končí až vtedy, keď sa ukončila hlavná funkcia a nie je otvorené žiadne okno. Až po zatvorení okna sa program naozaj ukončí.

Keby na tomto mieste článok skončil, asi si pomyslíte, že pri programovaní hier v Jave je pomer "úsilie : výsledok" dosť slabý. Toľko teórie a toľko kódu na obyčajné otvorenie a zatvorenie okna! V skutočnosti sme však dosiahli viac, pretože na rovnakom princípe funguje aj spracovávanie iných udalostí, napríklad udalostí klávesnice a myši. Opäť, máme nejaké rozhrania, v tomto prípade "KeyListener" (stlačenie klávesy, pustenie klávesy, napísanie písmena), "MouseListener" (stlačenie tlačidla myši, pustenie tlačidla myši, kliknutie, príchod myši nad okno, odchod myši sponad okna), "MouseMotionListener" (pohyb myši, ťahanie myšou) a "MouseWheelListener" (otočenie kolieska na myši); k nim máme pre lenivcov predpripravené triedy "KeyAdapter" a "MouseAdapter"; a vytvorené objekty pridáme pomocou funkcií "addKeyListener", "addMouseListener", "addMouseMotionListener" a "addMouseWheelListener". Tieto funkcie dostávajú ako parameter objekt, z ktorého môžete zistiť, ktorá klávesa bola stlačená, prípadne kam sa tá myš pohla.

Vyskúšajme si to na jednoduchom príklade. Informácie budeme zatiaľ vypisovať do príkazového riadku. (To sa nám môže zísť ako pomocná informácia aj počas vývoja skutočnej hry.) Tu sú niektoré funkcie; zvyšné si vyskúšajte sami:

import java.awt.*;
import java.awt.event.*;

class PrijemcaUdalostiOkna extends WindowAdapter {
	private Frame okno;

	public PrijemcaUdalostiOkna(Frame okno) {
		this.okno = okno;
	}

	@Override
	public void windowClosing(WindowEvent e) {
		System.out.println("Ideme zatvorit okno");
		okno.dispose();
	}

	@Override
	public void windowIconified(WindowEvent e) {
		System.out.println("Okno sa minimalizovalo");
	}

}

class PrijemcaUdalostiKlaves extends KeyAdapter {

	@Override
	public void keyPressed(KeyEvent e) {
		if (KeyEvent.VK_ESCAPE == e.getKeyCode()) {
			System.out.println("Stlacene ESC");
		}
	}

}

class PrijemcaUdalostiMysi extends MouseAdapter {

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

}

public class Program {

	public static void main(String[] args) {
		Frame okno = new Frame();
		okno.setTitle("Pokusne okno");
		okno.setSize(400, 300);
		okno.setVisible(true);
		okno.addWindowListener(new PrijemcaUdalostiOkna(okno));
		okno.addKeyListener(new PrijemcaUdalostiKlaves());
		okno.addMouseListener(new PrijemcaUdalostiMysi());
	}

}

Poznámka: Pri práci v prostredí Eclipse si nemusíte pamätať názvy jednotlivých rozhraní a ich funkcií. Prostredie vám napovedá názvy, napríklad napíšete "okno.add", stlačíte Ctrl+medzera a zobrazia sa všetky funkcie okna, začínajúce na "add". Keď vytvoríte triedu odvodenú napríklad od "WindowAdapter" a chcete zmeniť nejakú funkciu, no nepamätáte si jej názov, umiestnite kurzor do kódu triedy, kliknite pravým tlačidlom myši, vyberte možnosť "Source | Override/Implement Methods" a zobrazia sa funkcie, ktoré môžete zmeniť. Prostredie Eclipse má veľa takýchto pomôcok, na ktoré si časom zvyknete a veľmi vám uľahčia prácu.

Udalosti klávesnice a myši sa spracovávajú dvoma spôsobmi. Udalosti "keyTyped" a "mouseClicked" nás informujú o nejakej dokončenej aktivite: vypísanie písmena, kliknutie myši. Udalosti "keyPressed", "keyReleased", "mousePressed" a "mouseReleased" nás informujú o priebehu: kedy sa čo stlačilo, kedy sa čo pustilo. Rozdiel si vysvetlíme na príklad: ak na klávesnici napíšeme veľké písmeno "A", môžeme to vnímať ako jednu udalosť (napísanie písmena "A") alebo ako štyri udalosti (stlačenie klávesy "Shift", stlačenie klávesy "a", pustenie klávesy "a", pustenie klávesy "Shift"). Zvyčajne nepotrebujeme zachytávať oba druhy udalostí; vyberieme si jeden podľa typu programu, ktorý robíme. V prípade akčnej hry, ktorá musí reagovať na stláčanie aj púšťanie kláves, potrebujeme udalosti "keyPressed" a "keyReleased", ale keby sme robili napríklad vlastný textový editor, stačilo by reagovať na "keyTyped".

viliam@bur.sk